Commit 8154b4f6 by Torkel Ödegaard

search and save are 'working', barely

parents
node_modules
coverage/
.aws-config.json
dist
public
gin-bin
# locally required config files
web.config
config.js
# Editor junk
*.sublime-workspace
*.swp
.idea/
[submodule "grafana"]
path = grafana
url = git@github.com:torkelo/grafana-private.git
{
"browser": true,
"bitwise":false,
"curly": true,
"eqnull": true,
"globalstrict": true,
"devel": true,
"eqeqeq": true,
"forin": false,
"immed": true,
"supernew": true,
"expr": true,
"indent": 2,
"latedef": true,
"newcap": true,
"noarg": true,
"noempty": true,
"undef": true,
"boss": true,
"trailing": true,
"laxbreak": true,
"laxcomma": true,
"sub": true,
"unused": true,
"maxdepth": 5,
"maxlen": 140,
"globals": {
"define": true,
"require": true,
"Chromath": false,
"setImmediate": true
}
}
\ No newline at end of file
language: node_js
node_js:
- "0.10"
before_script:
- npm install -g grunt-cli
\ No newline at end of file
/* jshint node:true */
'use strict';
module.exports = function (grunt) {
var config = {
pkg: grunt.file.readJSON('package.json'),
baseDir: '.',
srcDir: 'public',
destDir: 'dist',
tempDir: 'tmp',
docsDir: 'docs/'
};
// load plugins
require('load-grunt-tasks')(grunt);
// load task definitions
grunt.loadTasks('tasks');
// Utility function to load plugin settings into config
function loadConfig(config,path) {
require('glob').sync('*', {cwd: path}).forEach(function(option) {
var key = option.replace(/\.js$/,'');
// If key already exists, extend it. It is your responsibility to avoid naming collisions
config[key] = config[key] || {};
grunt.util._.extend(config[key], require(path + option)(config,grunt));
});
// technically not required
return config;
}
// Merge that object with what with whatever we have here
loadConfig(config,'./tasks/options/');
// pass the config to grunt
grunt.initConfig(config);
};
\ No newline at end of file
package configuration
type Cfg struct {
httpPort string
DashboardSource DashboardSourceCfg
}
type DashboardSourceCfg struct {
sourceType string
path string
}
package httpApi
import (
"html/template"
log "github.com/alecthomas/log4go"
"github.com/gin-gonic/gin"
"github.com/torkelo/grafana-pro/backend/models"
"github.com/torkelo/grafana-pro/backend/stores"
)
type HttpServer struct {
port string
shutdown chan bool
store stores.Store
}
func NewHttpServer(port string, store stores.Store) *HttpServer {
self := &HttpServer{}
self.port = port
self.store = store
return self
}
func CacheHeadersMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Add("Cache-Control", "max-age=0, public, must-revalidate, proxy-revalidate")
}
}
func (self *HttpServer) ListenAndServe() {
log.Info("Starting Http Listener on port %v", self.port)
defer func() { self.shutdown <- true }()
r := gin.Default()
r.Use(CacheHeadersMiddleware())
templates := template.New("templates")
templates.Delims("[[", "]]")
templates.ParseFiles("./views/index.html")
r.SetHTMLTemplate(templates)
r.GET("/", self.index)
r.GET("/api/dashboards/:id", self.getDashboard)
r.GET("/api/search/", self.search)
r.POST("/api/dashboard", self.postDashboard)
r.Static("/public", "./public")
r.Static("/app", "./public/app")
r.Static("/img", "./public/img")
r.Run(":" + self.port)
}
type IndexViewModel struct {
Title string
}
func (self *HttpServer) index(c *gin.Context) {
c.HTML(200, "index.html", &IndexViewModel{Title: "hello from go"})
}
type ErrorRsp struct {
Message string `json:"message"`
}
func (self *HttpServer) getDashboard(c *gin.Context) {
id := c.Params.ByName("id")
dash, err := self.store.GetById(id)
if err != nil {
c.JSON(404, &ErrorRsp{Message: "Dashboard not found"})
return
}
c.JSON(200, dash.Data)
}
func (self *HttpServer) search(c *gin.Context) {
query := c.Params.ByName("q")
results, err := self.store.Query(query)
if err != nil {
c.JSON(500, &ErrorRsp{Message: "Search error"})
return
}
c.JSON(200, results)
}
func (self *HttpServer) postDashboard(c *gin.Context) {
var command saveDashboardCommand
if c.EnsureBody(&command) {
err := self.store.Save(&models.Dashboard{Data: command.Dashboard})
if err == nil {
c.JSON(200, gin.H{"status": "saved"})
return
}
}
c.JSON(500, gin.H{"error": "bad request"})
}
package httpApi
type saveDashboardCommand struct {
Id string `json:"id"`
Title string `json:"title"`
Dashboard map[string]interface{}
}
package models
import (
"encoding/json"
"io"
)
type Dashboard struct {
Data map[string]interface{}
}
type SearchResult struct {
Type string `json:"title"`
Id string `json:"id"`
Title string `json:"title"`
}
func NewDashboard(title string) *Dashboard {
dash := &Dashboard{}
dash.Data = make(map[string]interface{})
dash.Data["title"] = title
return dash
}
func NewFromJson(reader io.Reader) (*Dashboard, error) {
dash := NewDashboard("temp")
jsonParser := json.NewDecoder(reader)
if err := jsonParser.Decode(&dash.Data); err != nil {
return nil, err
}
return dash, nil
}
/*type DashboardServices struct {
}
type DashboardServicesFilter struct {
}
type DashboardServicesFilterTime struct {
From string To string
}*/
func (dash *Dashboard) GetString(prop string) string {
return dash.Data[prop].(string)
}
func (dash *Dashboard) Title() string {
return dash.GetString("title")
}
package server
import (
"github.com/torkelo/grafana-pro/backend/httpApi"
"github.com/torkelo/grafana-pro/backend/stores"
)
type Server struct {
HttpServer *httpApi.HttpServer
Store stores.Store
}
func NewServer(port string) (*Server, error) {
store := stores.New()
httpServer := httpApi.NewHttpServer(port, store)
return &Server{
HttpServer: httpServer,
Store: store,
}, nil
}
func (self *Server) ListenAndServe() error {
self.HttpServer.ListenAndServe()
return nil
}
package stores
import (
"encoding/json"
"io"
"os"
"path/filepath"
"strings"
log "github.com/alecthomas/log4go"
"github.com/torkelo/grafana-pro/backend/models"
)
type fileStore struct {
dataDir string
dashDir string
cache map[string]*models.Dashboard
}
func NewFileStore(dataDir string) *fileStore {
if dirDoesNotExist(dataDir) {
log.Crashf("FileStore failed to initialize, dataDir does not exist %v", dataDir)
}
dashDir := filepath.Join(dataDir, "dashboards")
if dirDoesNotExist(dashDir) {
log.Debug("Did not find dashboard dir, creating...")
err := os.Mkdir(dashDir, 0777)
if err != nil {
log.Crashf("FileStore failed to initialize, could not create directory %v, error: %v", dashDir, err)
}
}
store := &fileStore{}
store.dataDir = dataDir
store.dashDir = dashDir
store.cache = make(map[string]*models.Dashboard)
go store.scanFiles()
return store
}
func (store *fileStore) scanFiles() {
visitor := func(path string, f os.FileInfo, err error) error {
if err != nil {
return err
}
if f.IsDir() {
return nil
}
if strings.HasSuffix(f.Name(), ".json") {
err = store.loadDashboardIntoCache(path)
if err != nil {
return err
}
}
return nil
}
err := filepath.Walk(store.dashDir, visitor)
if err != nil {
log.Error("FileStore::updateCache failed %v", err)
}
}
func (store fileStore) loadDashboardIntoCache(filename string) error {
log.Info("Loading dashboard file %v into cache", filename)
dash, err := loadDashboardFromFile(filename)
if err != nil {
return err
}
store.cache[dash.Title()] = dash
return nil
}
func (store *fileStore) Close() {
}
func (store *fileStore) GetById(id string) (*models.Dashboard, error) {
log.Debug("FileStore::GetById id = %v", id)
filename := store.getFilePathForDashboard(id)
return loadDashboardFromFile(filename)
}
func (store *fileStore) Save(dash *models.Dashboard) error {
filename := store.getFilePathForDashboard(dash.Title())
log.Debug("Saving dashboard %v to %v", dash.Title(), filename)
var err error
var data []byte
if data, err = json.Marshal(dash.Data); err != nil {
return err
}
return writeFile(filename, data)
}
func (store *fileStore) Query(query string) ([]*models.SearchResult, error) {
results := make([]*models.SearchResult, 0, 50)
for _, dash := range store.cache {
item := &models.SearchResult{
Id: dash.Title(),
Type: "dashboard",
}
results = append(results, item)
}
return results, nil
}
func loadDashboardFromFile(filename string) (*models.Dashboard, error) {
log.Debug("FileStore::loading dashboard from file %v", filename)
configFile, err := os.Open(filename)
if err != nil {
return nil, err
}
return models.NewFromJson(configFile)
}
func (store *fileStore) getFilePathForDashboard(id string) string {
id = strings.ToLower(id)
id = strings.Replace(id, " ", "-", -1)
return filepath.Join(store.dashDir, id) + ".json"
}
func dirDoesNotExist(dir string) bool {
_, err := os.Stat(dir)
return os.IsNotExist(err)
}
func writeFile(filename string, data []byte) error {
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
return err
}
n, err := f.Write(data)
if err == nil && n < len(data) {
err = io.ErrShortWrite
}
if err1 := f.Close(); err == nil {
err = err1
}
return err
}
package stores
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"testing"
. "github.com/smartystreets/goconvey/convey"
"github.com/torkelo/grafana-pro/backend/models"
)
func TestFileStore(t *testing.T) {
GivenFileStore("When saving a dashboard", t, func(store *fileStore) {
dashboard := models.NewDashboard("hello")
err := store.Save(dashboard)
Convey("should be saved to disk", func() {
So(err, ShouldBeNil)
_, err = os.Stat(store.getFilePathForDashboard("hello"))
So(err, ShouldBeNil)
})
})
GivenFileStore("When getting a saved dashboard", t, func(store *fileStore) {
copyDashboardToTempData("default.json", "", store.dashDir)
dash, err := store.GetById("default")
Convey("should be read from disk", func() {
So(err, ShouldBeNil)
So(dash, ShouldNotBeNil)
So(dash.Title(), ShouldEqual, "Grafana Play Home")
})
})
GivenFileStore("when getting dashboard with capital letters", t, func(store *fileStore) {
copyDashboardToTempData("annotations.json", "", store.dashDir)
dash, err := store.GetById("AnnoTations")
Convey("should be read from disk", func() {
So(err, ShouldBeNil)
So(dash, ShouldNotBeNil)
So(dash.Title(), ShouldEqual, "Annotations")
})
})
GivenFileStore("When copying dashboards into data dir", t, func(store *fileStore) {
copyDashboardToTempData("annotations.json", "", store.dashDir)
copyDashboardToTempData("default.json", "", store.dashDir)
copyDashboardToTempData("graph-styles.json", "", store.dashDir)
store.scanFiles()
Convey("scan should generate index of all dashboards", func() {
result, err := store.Query("*")
So(err, ShouldBeNil)
So(len(result), ShouldEqual, 3)
})
})
}
func copyDashboardToTempData(name string, destName string, dir string) {
if destName == "" {
destName = name
}
source, _ := filepath.Abs("../../data/dashboards/" + name)
dest := filepath.Join(dir, destName)
err := copyFile(dest, source)
if err != nil {
panic(fmt.Sprintf("failed to copy file %v", name))
}
}
func GivenFileStore(desc string, t *testing.T, f func(store *fileStore)) {
Convey(desc, t, func() {
tempDir, _ := ioutil.TempDir("", "store")
store := NewFileStore(tempDir)
f(store)
Reset(func() {
os.RemoveAll(tempDir)
})
})
}
func copyFile(dst, src string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, in)
cerr := out.Close()
if err != nil {
return err
}
return cerr
}
package stores
import (
"github.com/torkelo/grafana-pro/backend/models"
)
type Store interface {
GetById(id string) (*models.Dashboard, error)
Save(dash *models.Dashboard) error
Query(query string) ([]*models.SearchResult, error)
Close()
}
func New() Store {
return NewFileStore("data")
}
{
"title": "Annotations",
"services": {
"filter": {
"list": [],
"time": {
"from": "now-1h",
"to": "now"
}
}
},
"rows": [
{
"title": "Welcome to Grafana",
"height": "350px",
"editable": true,
"collapse": false,
"collapsable": true,
"panels": [
{
"span": 12,
"editable": true,
"type": "graphite",
"x-axis": true,
"y-axis": true,
"scale": 1,
"y_formats": [
"short",
"short"
],
"grid": {
"max": null,
"min": 0,
"threshold1": null,
"threshold2": null,
"threshold1Color": "rgba(216, 200, 27, 0.27)",
"threshold2Color": "rgba(234, 112, 112, 0.22)"
},
"resolution": 100,
"lines": true,
"fill": 0,
"linewidth": 1,
"points": false,
"pointradius": 5,
"bars": false,
"stack": true,
"spyable": true,
"options": false,
"legend": {
"show": true,
"values": false,
"min": false,
"max": false,
"current": false,
"total": false,
"avg": false
},
"interactive": true,
"legend_counts": true,
"timezone": "browser",
"percentage": false,
"zerofill": true,
"nullPointMode": "connected",
"steppedLine": false,
"tooltip": {
"value_type": "cumulative",
"query_as_alias": true
},
"targets": [
{
"target": "aliasByNode(apps.fakesite.web_server_02.counters.requests.count,2)"
},
{
"target": "aliasByNode(apps.fakesite.web_server_01.counters.requests.count,2)"
}
],
"aliasColors": {},
"aliasYAxis": {},
"title": "Amnotations example",
"datasource": null,
"renderer": "flot",
"annotate": {
"enable": false
}
}
],
"notice": false
},
{
"title": "test",
"height": "350px",
"editable": true,
"collapse": false,
"collapsable": true,
"panels": [
{
"span": 6,
"editable": true,
"type": "graphite",
"x-axis": true,
"y-axis": true,
"scale": 1,
"y_formats": [
"short",
"short"
],
"grid": {
"max": null,
"min": 0,
"threshold1": null,
"threshold2": null,
"threshold1Color": "rgba(216, 200, 27, 0.27)",
"threshold2Color": "rgba(234, 112, 112, 0.22)"
},
"resolution": 100,
"lines": true,
"fill": 1,
"linewidth": 1,
"points": false,
"pointradius": 5,
"bars": false,
"stack": true,
"spyable": true,
"options": false,
"legend": {
"show": true,
"values": false,
"min": false,
"max": false,
"current": false,
"total": false,
"avg": false
},
"interactive": true,
"legend_counts": true,
"timezone": "browser",
"percentage": false,
"zerofill": true,
"nullPointMode": "connected",
"steppedLine": false,
"tooltip": {
"value_type": "cumulative",
"query_as_alias": true
},
"targets": [
{
"target": "aliasByNode(apps.fakesite.web_server_02.counters.request_status.code_304.count,5)"
}
],
"aliasColors": {
"web_server_01": "#1F78C1",
"web_server_02": "#6ED0E0"
},
"aliasYAxis": {},
"title": "Annotations example",
"datasource": null,
"renderer": "flot",
"annotate": {
"enable": false
}
},
{
"error": false,
"span": 6,
"editable": true,
"type": "text",
"loadingEditor": false,
"mode": "markdown",
"content": "### Annotations\n\n- Annotation is a feature that must be enabled in the dashboards settings / Controls tab / Feature toggles\n- Annotation bar is then visible at the top. \n- Click on the cog to open the Annotations dialog \n- In this dialog you can add or edit annotations \n- Currently only Graphite metrics and Graphite events are supported sources of annotations\n- More datasource options for annotations will be added \n- Click on the annotation name in the bar to toggle the annotation on or off\n\n",
"style": {},
"title": "Description"
}
],
"notice": false
}
],
"editable": true,
"failover": false,
"panel_hints": true,
"style": "dark",
"pulldowns": [
{
"type": "filtering",
"collapse": false,
"notice": false,
"enable": false
},
{
"type": "annotations",
"enable": true,
"annotations": [
{
"name": "deploys",
"type": "graphite metric",
"showLine": true,
"iconColor": "#C0C6BE",
"lineColor": "rgba(253, 54, 54, 0.77)",
"iconSize": 13,
"enable": true,
"target": "alias(apps.fakesite.web_server_01.counters.request_status.code_500.count, 'deployed v1.3')"
},
{
"name": "puppet apply",
"type": "graphite metric",
"showLine": true,
"iconColor": "#C0C6BE",
"lineColor": "rgba(255, 96, 96, 0.592157)",
"iconSize": 13,
"enable": false,
"target": "alias(apps.fakesite.web_server_02.counters.request_status.code_403.count,'puppet apply')"
}
]
}
],
"nav": [
{
"type": "timepicker",
"collapse": false,
"notice": false,
"enable": true,
"status": "Stable",
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
],
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"now": true
}
],
"loader": {
"save_gist": false,
"save_elasticsearch": true,
"save_local": true,
"save_default": true,
"save_temp": true,
"save_temp_ttl_enable": true,
"save_temp_ttl": "30d",
"load_gist": false,
"load_elasticsearch": true,
"load_elasticsearch_size": 20,
"load_local": false,
"hide": false
},
"refresh": false,
"tags": [
"annotations",
"graphite",
"showcase"
],
"timezone": "browser"
}
\ No newline at end of file
{
"title": "Templated Graphs Nested",
"services": {
"filter": {
"list": [
{
"type": "filter",
"name": "app",
"query": "apps.*",
"includeAll": true,
"options": [
{
"text": "All",
"value": "{backend,fakesite}"
},
{
"text": "backend",
"value": "backend"
},
{
"text": "fakesite",
"value": "fakesite"
}
],
"current": {
"text": "backend",
"value": "backend"
}
},
{
"type": "filter",
"name": "server",
"query": "apps.[[app]].*",
"includeAll": true,
"options": [
{
"text": "All",
"value": "{backend_01,backend_02,backend_03,backend_04}"
},
{
"text": "backend_01",
"value": "backend_01"
},
{
"text": "backend_02",
"value": "backend_02"
},
{
"text": "backend_03",
"value": "backend_03"
},
{
"text": "backend_04",
"value": "backend_04"
}
],
"current": {
"text": "All",
"value": "{backend_01,backend_02,backend_03,backend_04}"
}
}
],
"time": {
"from": "now-6h",
"to": "now"
}
}
},
"rows": [
{
"title": "Row1",
"height": "350px",
"editable": true,
"collapse": false,
"collapsable": true,
"panels": [
{
"span": 12,
"editable": true,
"type": "graphite",
"loadingEditor": false,
"datasource": null,
"renderer": "flot",
"x-axis": true,
"y-axis": true,
"scale": 1,
"y_formats": [
"short",
"short"
],
"grid": {
"max": null,
"min": 0,
"threshold1": null,
"threshold2": null,
"threshold1Color": "rgba(216, 200, 27, 0.27)",
"threshold2Color": "rgba(234, 112, 112, 0.22)"
},
"annotate": {
"enable": false
},
"resolution": 100,
"lines": true,
"fill": 1,
"linewidth": 1,
"points": false,
"pointradius": 5,
"bars": false,
"stack": true,
"legend": {
"show": true,
"values": false,
"min": false,
"max": false,
"current": false,
"total": false,
"avg": false
},
"percentage": false,
"zerofill": true,
"nullPointMode": "connected",
"steppedLine": false,
"tooltip": {
"value_type": "cumulative",
"query_as_alias": true
},
"targets": [
{
"target": "groupByNode(apps.[[app]].[[server]].counters.requests.count,2,'sum')"
}
],
"aliasColors": {
"highres.test": "#1F78C1",
"scale(highres.test,3)": "#6ED0E0",
"mobile": "#6ED0E0",
"tablet": "#EAB839"
},
"aliasYAxis": {},
"title": "Traffic"
}
],
"notice": false
},
{
"title": "Row1",
"height": "350px",
"editable": true,
"collapse": false,
"collapsable": true,
"panels": [
{
"span": 12,
"editable": true,
"type": "graphite",
"loadingEditor": false,
"datasource": null,
"renderer": "flot",
"x-axis": true,
"y-axis": true,
"scale": 1,
"y_formats": [
"short",
"short"
],
"grid": {
"max": null,
"min": 0,
"threshold1": null,
"threshold2": null,
"threshold1Color": "rgba(216, 200, 27, 0.27)",
"threshold2Color": "rgba(234, 112, 112, 0.22)"
},
"annotate": {
"enable": false
},
"resolution": 100,
"lines": true,
"fill": 0,
"linewidth": 1,
"points": false,
"pointradius": 5,
"bars": false,
"stack": false,
"legend": {
"show": true,
"values": false,
"min": false,
"max": false,
"current": false,
"total": false,
"avg": false
},
"percentage": false,
"zerofill": true,
"nullPointMode": "connected",
"steppedLine": false,
"tooltip": {
"value_type": "cumulative",
"query_as_alias": true
},
"targets": [
{
"target": "aliasByNode(movingAverage(scaleToSeconds(apps.[[app]].[[server]].counters.requests.count,60),10),1,2)"
}
],
"aliasColors": {
"highres.test": "#1F78C1",
"scale(highres.test,3)": "#6ED0E0",
"mobile": "#6ED0E0",
"tablet": "#EAB839"
},
"aliasYAxis": {},
"title": "Sessions / min"
}
],
"notice": false
}
],
"editable": true,
"failover": false,
"panel_hints": true,
"style": "dark",
"pulldowns": [
{
"type": "filtering",
"collapse": false,
"notice": false,
"enable": true
},
{
"type": "annotations",
"enable": false
}
],
"nav": [
{
"type": "timepicker",
"collapse": false,
"notice": false,
"enable": true,
"status": "Stable",
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
],
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"now": true
}
],
"loader": {
"save_gist": false,
"save_elasticsearch": true,
"save_local": true,
"save_default": true,
"save_temp": true,
"save_temp_ttl_enable": true,
"save_temp_ttl": "30d",
"load_gist": false,
"load_elasticsearch": true,
"load_elasticsearch_size": 20,
"load_local": false,
"hide": false
},
"refresh": false,
"tags": [
"showcase",
"templated"
],
"timezone": "browser"
}
\ No newline at end of file
{
"title": "Templated Graphs",
"services": {
"filter": {
"list": [
{
"type": "filter",
"name": "root",
"query": "statsd.fakesite.counters.session_start.*",
"includeAll": true,
"options": [
{
"text": "All",
"value": "{desktop,mobile,tablet}"
},
{
"text": "desktop",
"value": "desktop"
},
{
"text": "mobile",
"value": "mobile"
},
{
"text": "tablet",
"value": "tablet"
}
],
"current": {
"text": "All",
"value": "{desktop,mobile,tablet}"
}
}
],
"time": {
"from": "now-6h",
"to": "now"
}
}
},
"rows": [
{
"title": "Row1",
"height": "350px",
"editable": true,
"collapse": false,
"collapsable": true,
"panels": [
{
"span": 12,
"editable": true,
"type": "graphite",
"loadingEditor": false,
"datasource": null,
"renderer": "flot",
"x-axis": true,
"y-axis": true,
"scale": 1,
"y_formats": [
"short",
"short"
],
"grid": {
"max": null,
"min": 0,
"threshold1": null,
"threshold2": null,
"threshold1Color": "rgba(216, 200, 27, 0.27)",
"threshold2Color": "rgba(234, 112, 112, 0.22)"
},
"annotate": {
"enable": false
},
"resolution": 100,
"lines": true,
"fill": 1,
"linewidth": 1,
"points": false,
"pointradius": 5,
"bars": false,
"stack": false,
"legend": {
"show": true,
"values": false,
"min": false,
"max": false,
"current": false,
"total": false,
"avg": false
},
"percentage": false,
"zerofill": true,
"nullPointMode": "connected",
"steppedLine": false,
"tooltip": {
"value_type": "cumulative",
"query_as_alias": true
},
"targets": [
{
"target": "aliasByNode(summarize(statsd.fakesite.counters.session_start.[[root]].count,'1min','sum'),4)"
}
],
"aliasColors": {
"highres.test": "#1F78C1",
"scale(highres.test,3)": "#6ED0E0",
"mobile": "#6ED0E0",
"tablet": "#EAB839"
},
"aliasYAxis": {},
"title": "Device sessions"
}
],
"notice": false
},
{
"title": "Row1",
"height": "350px",
"editable": true,
"collapse": false,
"collapsable": true,
"panels": [
{
"span": 6,
"editable": true,
"type": "graphite",
"loadingEditor": false,
"datasource": null,
"renderer": "flot",
"x-axis": true,
"y-axis": true,
"scale": 1,
"y_formats": [
"short",
"short"
],
"grid": {
"max": null,
"min": 0,
"threshold1": null,
"threshold2": null,
"threshold1Color": "rgba(216, 200, 27, 0.27)",
"threshold2Color": "rgba(234, 112, 112, 0.22)"
},
"annotate": {
"enable": false
},
"resolution": 100,
"lines": false,
"fill": 1,
"linewidth": 2,
"points": false,
"pointradius": 5,
"bars": true,
"stack": true,
"legend": {
"show": true,
"values": false,
"min": false,
"max": false,
"current": false,
"total": false,
"avg": false
},
"percentage": false,
"zerofill": true,
"nullPointMode": "connected",
"steppedLine": false,
"tooltip": {
"value_type": "cumulative",
"query_as_alias": true
},
"targets": [
{
"target": "aliasByNode(summarize(statsd.fakesite.counters.session_start.[[root]].count,'1h','sum'),4)"
}
],
"aliasColors": {
"highres.test": "#1F78C1",
"scale(highres.test,3)": "#6ED0E0",
"tablet": "#EAB839",
"desktop": "#7EB26D",
"mobile": "#6ED0E0"
},
"aliasYAxis": {},
"title": "Device sessions (1h)"
},
{
"error": false,
"span": 6,
"editable": true,
"type": "text",
"loadingEditor": false,
"mode": "markdown",
"content": "#### Templated metric queries / graphs \n- In dashboard settings, in the Controls tab / Feature toggles. You can enable 'Filtering' \n- This feature when enabled will show you a bar bellow the menu.\n- In this bar you can add filters, or what should be named templated metric segments. \n- A filter is a query for a specific metric segment\n- Open any graph in this dashboard and edit mode and you can see that the [[device]] filter is used instead of a wildcard.\n- Try clicking the All link in the filter menu at the top, change device and see that all graphs change to only show values for that device. ",
"style": {},
"title": "Description"
}
],
"notice": false
}
],
"editable": true,
"failover": false,
"panel_hints": true,
"style": "dark",
"pulldowns": [
{
"type": "filtering",
"collapse": false,
"notice": false,
"enable": true
},
{
"type": "annotations",
"enable": false
}
],
"nav": [
{
"type": "timepicker",
"collapse": false,
"notice": false,
"enable": true,
"status": "Stable",
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
],
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"now": true
}
],
"loader": {
"save_gist": false,
"save_elasticsearch": true,
"save_local": true,
"save_default": true,
"save_temp": true,
"save_temp_ttl_enable": true,
"save_temp_ttl": "30d",
"load_gist": false,
"load_elasticsearch": true,
"load_elasticsearch_size": 20,
"load_local": false,
"hide": false
},
"refresh": false,
"tags": [
"showcase",
"templated"
],
"timezone": "browser"
}
\ No newline at end of file
Subproject commit 91a6ae756f30744afe82dabbb5caa7f43d6f7e5a
File added
{
"folders":
[
{
"follow_symlinks": true,
"path": ".",
"folder_exclude_patterns": [
"node_modules",
"grafana"
]
}
],
"settings":
{
"tab_size": 2,
"trim_trailing_white_space_on_save": true
}
}
package main
import (
"os"
"time"
log "github.com/alecthomas/log4go"
"github.com/torkelo/grafana-pro/backend/server"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "3838"
}
log.Info("Starting Grafana-Pro v.1-alpha")
server, err := server.NewServer(port)
if err != nil {
time.Sleep(time.Second)
panic(err)
}
err = server.ListenAndServe()
if err != nil {
log.Error("ListenAndServe failed: ", err)
}
time.Sleep(time.Millisecond * 2000)
}
{
"author": {
"name": "Torkel Ödegaard",
"company": "Coding Instinct AB"
},
"name": "grafana",
"version": "1.7.0-rc1",
"repository": {
"type": "git",
"url": "http://github.com/torkelo/grafana.git"
},
"devDependencies": {
"expect.js": "~0.2.0",
"glob": "~3.2.7",
"grunt": "~0.4.0",
"grunt-angular-templates": "^0.5.5",
"grunt-cli": "~0.1.13",
"grunt-contrib-clean": "~0.5.0",
"grunt-contrib-compress": "~0.5.2",
"grunt-contrib-concat": "^0.4.0",
"grunt-contrib-connect": "~0.5.0",
"grunt-contrib-copy": "~0.5.0",
"grunt-contrib-cssmin": "~0.6.1",
"grunt-contrib-htmlmin": "~0.1.3",
"grunt-contrib-jshint": "~0.10.0",
"grunt-contrib-less": "~0.7.0",
"grunt-contrib-requirejs": "~0.4.1",
"grunt-contrib-uglify": "~0.2.4",
"grunt-filerev": "^0.2.1",
"grunt-git-describe": "~2.3.2",
"grunt-karma": "~0.8.3",
"grunt-ngmin": "0.0.3",
"grunt-string-replace": "~0.2.4",
"grunt-usemin": "^2.1.1",
"jshint-stylish": "~0.1.5",
"karma": "~0.12.16",
"karma-chrome-launcher": "~0.1.4",
"karma-coffee-preprocessor": "~0.1.2",
"karma-coverage": "^0.2.5",
"karma-coveralls": "^0.1.4",
"karma-expect": "~1.1.0",
"karma-firefox-launcher": "~0.1.3",
"karma-html2js-preprocessor": "~0.1.0",
"karma-jasmine": "~0.2.2",
"karma-mocha": "~0.1.4",
"karma-phantomjs-launcher": "~0.1.1",
"karma-requirejs": "~0.2.1",
"karma-script-launcher": "~0.1.0",
"load-grunt-tasks": "~0.2.0",
"mocha": "~1.16.1",
"requirejs": "~2.1.14",
"rjs-build-analysis": "0.0.3"
},
"engines": {
"node": "0.10.x",
"npm": "1.2.x"
},
"scripts": {
"test": "grunt test",
"coveralls": "grunt karma:coveralls && rm -rf ./coverage"
},
"license": "Apache License",
"dependencies": {
"grunt-jscs-checker": "^0.4.4",
"karma-sinon": "^1.0.3",
"sinon": "^1.10.3"
}
}
module.exports = function(grunt) {
// Concat and Minify the src directory into dist
grunt.registerTask('build', [
'jshint:source',
'jshint:tests',
'clean:on_start',
'less:src',
'concat:css',
'copy:everything_but_less_to_temp',
'htmlmin:build',
'ngtemplates',
'cssmin:build',
'build:grafanaVersion',
'ngmin:build',
'requirejs:build',
'concat:js',
'filerev',
'usemin',
'clean:temp',
'uglify:dest'
]);
grunt.registerTask('build:grafanaVersion', function() {
grunt.config('string-replace.config', {
files: {
'<%= tempDir %>/app/app.js': '<%= tempDir %>/app/app.js'
},
options: {
replacements: [{ pattern: /@grafanaVersion@/g, replacement: '<%= pkg.version %>' }]
}
});
grunt.task.run('string-replace:config');
});
};
// Lint and build CSS
module.exports = function(grunt) {
grunt.registerTask('default', ['jscs', 'jshint', 'less:src', 'concat:css']);
grunt.registerTask('test', ['default', 'karma:test']);
};
module.exports = function(grunt) {
// build, then zip and upload to s3
grunt.registerTask('distribute', [
'distribute:load_s3_config',
'build',
'compress:zip',
'compress:tgz',
's3:dist',
'clean:temp'
]);
// build, then zip and upload to s3
grunt.registerTask('release', [
// 'distribute:load_s3_config',
'build',
'compress:zip_release',
'compress:tgz_release',
//'s3:release',
//'clean:temp'
]);
// collect the key and secret from the .aws-config.json file, finish configuring the s3 task
grunt.registerTask('distribute:load_s3_config', function () {
var config = grunt.file.readJSON('.aws-config.json');
grunt.config('s3.options', {
key: config.key,
secret: config.secret
});
});
}
\ No newline at end of file
module.exports = function(config) {
return {
on_start: ['<%= destDir %>', '<%= tempDir %>'],
temp: ['<%= tempDir %>'],
docs: ['<%= docsDir %>']
};
};
\ No newline at end of file
module.exports = function(config) {
return {
zip: {
options: {
archive: '<%= tempDir %>/<%= pkg.name %>-latest.zip'
},
files : [
{
expand: true,
cwd: '<%= destDir %>',
src: ['**/*'],
dest: '<%= pkg.name %>/',
},
{
expand: true,
dest: '<%= pkg.name %>/',
src: ['LICENSE.md', 'README.md', 'NOTICE.md'],
}
]
},
tgz: {
options: {
archive: '<%= tempDir %>/<%= pkg.name %>-latest.tar.gz'
},
files : [
{
expand: true,
cwd: '<%= destDir %>',
src: ['**/*'],
dest: '<%= pkg.name %>/',
},
{
expand: true,
src: ['LICENSE.md', 'README.md', 'NOTICE.md'],
dest: '<%= pkg.name %>/',
}
]
},
zip_release: {
options: {
archive: '<%= tempDir %>/<%= pkg.name %>-<%= pkg.version %>.zip'
},
files : [
{
expand: true,
cwd: '<%= destDir %>',
src: ['**/*'],
dest: '<%= pkg.name %>-<%= pkg.version %>/',
},
{
expand: true,
src: ['LICENSE.md', 'README.md', 'NOTICE.md'],
dest: '<%= pkg.name %>-<%= pkg.version %>/',
}
]
},
tgz_release: {
options: {
archive: '<%= tempDir %>/<%= pkg.name %>-<%= pkg.version %>.tar.gz'
},
files : [
{
expand: true,
cwd: '<%= destDir %>',
src: ['**/*'],
dest: '<%= pkg.name %>-<%= pkg.version %>/',
},
{
expand: true,
src: ['LICENSE.md', 'README.md', 'NOTICE.md'],
dest: '<%= pkg.name %>-<%= pkg.version %>/',
}
]
}
};
};
\ No newline at end of file
module.exports = function(config) {
return {
css: {
src: [
'<%= srcDir %>/css/normalize.min.css',
'<%= srcDir %>/css/timepicker.css',
'<%= srcDir %>/css/spectrum.css',
'<%= srcDir %>/css/animate.min.css',
'<%= srcDir %>/css/bootstrap.dark.min.css'
],
dest: '<%= srcDir %>/css/default.min.css'
},
js: {
src: [
'<%= destDir %>/vendor/require/require.js',
'<%= destDir %>/app/components/require.config.js',
'<%= destDir %>/app/app.js',
],
dest: '<%= destDir %>/app/app.js'
},
};
};
module.exports = function(config) {
return {
dev: {
options: {
port: 5601,
hostname: '*',
base: config.srcDir,
keepalive: true
}
},
dist: {
options: {
port: 5605,
hostname: '*',
base: config.destDir,
keepalive: true
}
},
}
};
module.exports = function(config) {
return {
// copy source to temp, we will minify in place for the dist build
everything_but_less_to_temp: {
cwd: '<%= srcDir %>',
expand: true,
src: ['**/*', '!**/*.less', '!config.js'],
dest: '<%= tempDir %>'
}
};
};
\ No newline at end of file
module.exports = function(config) {
return {
build: {
expand: true,
cwd: '<%= tempDir %>',
src: '**/*.css',
dest: '<%= tempDir %>'
}
};
};
\ No newline at end of file
module.exports = function(config) {
return {
options: {
encoding: 'utf8',
algorithm: 'md5',
length: 8,
},
css: {
src: '<%= destDir %>/css/default.min.css',
dest: '<%= destDir %>/css'
},
js: {
src: '<%= destDir %>/app/app.js',
dest: '<%= destDir %>/app'
}
};
};
module.exports = function(config) {
return {
me: {
// Target-specific file lists and/or options go here.
}
};
};
\ No newline at end of file
module.exports = function(config) {
return {
build: {
options:{
removeComments: true,
collapseWhitespace: true
},
expand: true,
cwd: '<%= tempDir %>',
src: [
//'index.html',
'app/panels/**/*.html',
'app/partials/**/*.html'
],
dest: '<%= tempDir %>'
}
};
};
\ No newline at end of file
module.exports = function(config) {
return {
src: [
'Gruntfile.js',
'<%= srcDir %>/app/**/*.js',
'!<%= srcDir %>/app/panels/*/{lib,leaflet}/*',
'!<%= srcDir %>/app/dashboards/*'
],
options: {
config: ".jscs.json",
},
};
};
/*
"requireCurlyBraces": ["if", "else", "for", "while", "do", "try", "catch"],
"requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"],
"disallowLeftStickedOperators": ["?", "+", "-", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="],
"disallowRightStickedOperators": ["?", "+", "/", "*", ":", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="],
"requireRightStickedOperators": ["!"],
"requireLeftStickedOperators": [","],
*/
\ No newline at end of file
module.exports = function(config) {
return {
source: {
files: {
src: ['Gruntfile.js', '<%= srcDir %>/app/**/*.js'],
}
},
tests: {
files: {
src: ['<%= srcDir %>/test/**/*.js'],
}
},
options: {
jshintrc: true,
reporter: require('jshint-stylish'),
ignores: [
'node_modules/*',
'dist/*',
'sample/*',
'<%= srcDir %>/vendor/*',
'<%= srcDir %>/app/panels/*/{lib,leaflet}/*',
'<%= srcDir %>/app/dashboards/*'
]
}
};
};
\ No newline at end of file
module.exports = function(config) {
return {
dev: {
configFile: 'src/test/karma.conf.js',
singleRun: false,
},
debug: {
configFile: 'src/test/karma.conf.js',
singleRun: false,
browsers: ['Chrome']
},
test: {
configFile: 'src/test/karma.conf.js',
},
coveralls: {
configFile: 'src/test/karma.conf.js',
reporters: ['dots','coverage','coveralls'],
preprocessors: {
'src/app/**/*.js': ['coverage']
},
coverageReporter: {
type: 'lcov',
dir: 'coverage/'
}
}
};
};
module.exports = function(config) {
return {
// this is the only task, other than copy, that runs on the src directory, since we don't really need
// the less files in the dist. Everything else runs from on temp, and require copys everything
// from temp -> dist
dist:{
expand: true,
cwd:'<%= srcDir %>/vendor/bootstrap/less/',
src: ['bootstrap.dark.less', 'bootstrap.light.less'],
dest: '<%= tempDir %>/css/',
},
// Compile in place when not building
src:{
options: {
paths: ["<%= srcDir %>/vendor/bootstrap/less", "<%= srcDir %>/css/less"],
yuicompress:true
},
files: {
"<%= srcDir %>/css/bootstrap.dark.min.css": "<%= srcDir %>/css/less/bootstrap.dark.less",
"<%= srcDir %>/css/bootstrap.light.min.css": "<%= srcDir %>/css/less/bootstrap.light.less",
"<%= srcDir %>/css/bootstrap-responsive.min.css": "<%= srcDir %>/css/less/grafana-responsive.less"
}
}
};
};
\ No newline at end of file
module.exports = function(config) {
return {
banner: '/*! <%= pkg.name %> - v<%= pkg.version %> - ' +
'<%= grunt.template.today("yyyy-mm-dd") %>\n' +
'<%= pkg.homepage ? " * " + pkg.homepage + "\\n" : "" %>' +
' * Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' +
' Licensed <%= pkg.license %> */\n\n'
};
};
\ No newline at end of file
module.exports = function(config) {
return {
build: {
expand:true,
cwd:'<%= tempDir %>',
src: [
'app/controllers/**/*.js',
'app/directives/**/*.js',
'app/services/**/*.js',
'app/filters/**/*.js',
'app/panels/**/*.js',
'app/routes/**/*.js',
'app/app.js',
'vendor/angular/**/*.js',
],
dest: '<%= tempDir %>'
}
};
};
module.exports = function(config) {
return {
grafana: {
cwd: '<%= tempDir %>',
src: ['app/**/*.html', '!app/panels/*/module.html'],
dest: '<%= tempDir %>/app/components/partials.js',
options: {
bootstrap: function(module, script) {
return "define('components/partials', ['angular'], function(angular) { \n" +
"angular.module('grafana').run(['$templateCache', function($templateCache) { \n" +
script +
'\n}]);' +
'\n});';
}
}
}
};
};
\ No newline at end of file
module.exports = function(config,grunt) {
var _c = {
build: {
options: {
appDir: '<%= tempDir %>',
dir: '<%= destDir %>',
mainConfigFile: '<%= tempDir %>/app/components/require.config.js',
modules: [], // populated below
optimize: 'none',
optimizeCss: 'none',
optimizeAllPluginResources: false,
paths: { config: '../config.sample' }, // fix, fallbacks need to be specified
removeCombined: true,
findNestedDependencies: true,
normalizeDirDefines: 'all',
inlineText: true,
skipPragmas: true,
done: function (done, output) {
var duplicates = require('rjs-build-analysis').duplicates(output);
if (duplicates.length > 0) {
grunt.log.subhead('Duplicates found in requirejs build:');
grunt.log.warn(duplicates);
done(new Error('r.js built duplicate modules, please check the excludes option.'));
}
done();
}
}
}
};
// setup the modules require will build
var requireModules = _c.build.options.modules = [
{
// main/common module
name: 'app',
include: [
'css',
'kbn',
'text',
'jquery',
'angular',
'settings',
'bootstrap',
'modernizr',
'timepicker',
'datepicker',
'underscore',
'filters/all',
'jquery.flot',
'services/all',
'angular-strap',
'directives/all',
'jquery.flot.pie',
'angular-dragdrop',
]
}
];
var fs = require('fs');
var panelPath = config.srcDir+'/app/panels'
// create a module for each directory in src/app/panels/
fs.readdirSync(panelPath).forEach(function (panelName) {
requireModules[0].include.push('panels/'+panelName+'/module');
requireModules[0].include.push('text!panels/'+panelName+'/module.html');
});
// exclude the literal config definition from all modules
requireModules
.forEach(function (module) {
module.excludeShallow = module.excludeShallow || [];
module.excludeShallow.push('config');
});
return _c;
};
module.exports = function(config) {
return {
dest: {
expand: true,
src: ['**/*.js', '!config.sample.js', '!app/dashboards/*.js', '!app/dashboards/**/*.js',],
dest: '<%= destDir %>',
cwd: '<%= destDir %>',
options: {
quite: true,
compress: true,
preserveComments: false,
banner: '<%= meta.banner %>'
}
}
};
};
\ No newline at end of file
module.exports = function(config) {
return {
html: '<%= destDir %>/index.html',
};
};
module.exports = function(config) {
return {
html: 'tmp/index.html',
};
};
module.exports = function(grunt) {
grunt.registerTask('server', ['connect:dev']);
};
\ No newline at end of file
<!DOCTYPE html>
<!--[if IE 8]> <html class="no-js lt-ie9" lang="en"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width">
<title>Grafana</title>
<link rel="stylesheet" href="public/css/default.min.css" title="Dark">
<!-- build:js app/app.js -->
<script src="public/vendor/require/require.js"></script>
<script src="public/app/components/require.config.js"></script>
<!-- endbuild -->
<script>require(['app'], function (app) { app.boot(); })</script>
</head>
<body ng-cloak ng-controller="GrafanaCtrl">
<link rel="stylesheet" href="public/css/bootstrap.light.min.css" ng-if="grafana.style === 'light'">
<link rel="stylesheet" href="public/css/bootstrap-responsive.min.css">
<link rel="stylesheet" href="public/css/font-awesome.min.css">
<div ng-repeat='alert in dashAlerts.list' class="alert-{{alert.severity}} dashboard-notice" ng-show="$last">
<button type="button" class="close" ng-click="dashAlerts.clear(alert)" style="padding-right:50px">&times;</button>
<strong>{{alert.title}}</strong> <span ng-bind-html='alert.text'></span> <div style="padding-right:10px" class='pull-right small'> {{$index + 1}} alert(s) </div>
</div>
<div ng-view ng-class="{'dashboard-fullscreen': fullscreen}"></div>
</body>
</html>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment