mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into panelbase
This commit is contained in:
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,19 +1,23 @@
|
||||
# 3.0.0 (unrelased master branch)
|
||||
|
||||
### New Features ###
|
||||
### New Features
|
||||
* **Playlists**: Playlists can now be persisted and started from urls, closes [#3655](https://github.com/grafana/grafana/pull/3655)
|
||||
* **Metadata**: Settings panel now shows dashboard metadata, closes [#3304](https://github.com/grafana/grafana/issues/3304)
|
||||
* **InfluxDB**: Support for policy selection in query editor, closes [#2018](https://github.com/grafana/grafana/issues/2018)
|
||||
|
||||
### Breaking changes
|
||||
**Plugin API**: Both datasource and panel plugin api (and plugin.json schema) as been updated, requiring a minor update to plugins. See [plugin api](https://github.com/grafana/grafana/blob/master/public/app/plugins/plugin_api.md) for more info.
|
||||
**InfluxDB 0.8.x** The data source for the old version of influxdb (0.8.x) is no longer included in default builds. Can easily be installed via improved plugin system, closes #3523
|
||||
**KairosDB** The data source is no longer included in default builds. Can easily be installed via improved plugin system, closes #3524
|
||||
* **Plugin API**: Both datasource and panel plugin api (and plugin.json schema) have been updated, requiring a minor update to plugins. See [plugin api](https://github.com/grafana/grafana/blob/master/public/app/plugins/plugin_api.md) for more info.
|
||||
* **InfluxDB 0.8.x** The data source for the old version of influxdb (0.8.x) is no longer included in default builds, but can easily be installed via improved plugin system, closes [#3523](https://github.com/grafana/grafana/issues/3523)
|
||||
* **KairosDB** The data source is no longer included in default builds, but can easily be installed via improved plugin system, closes [#3524](https://github.com/grafana/grafana/issues/3524)
|
||||
|
||||
### Enhancements ###
|
||||
### Enhancements
|
||||
* **Sessions**: Support for memcached as session storage, closes [#3458](https://github.com/grafana/grafana/pull/3458)
|
||||
* **mysql**: Grafana now supports ssl for mysql, closes [#3584](https://github.com/grafana/grafana/pull/3584)
|
||||
* **snapshot**: Annotations are now included in snapshots, closes [#3635](https://github.com/grafana/grafana/pull/3635)
|
||||
* **Admin**: Admin can now have global overview of Grafana setup, closes [#3812](https://github.com/grafana/grafana/issues/3812)
|
||||
|
||||
### Bug fixes
|
||||
* **Playlist**: Fix for memory leak when running a playlist, closes [#3794](https://github.com/grafana/grafana/pull/3794)
|
||||
|
||||
# 2.6.1 (unrelased, 2.6.x branch)
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ dependencies:
|
||||
- mkdir -p ${GOPATH}/src/${ORG_PATH}
|
||||
- ln -s ~/grafana ${GOPATH}/src/${ORG_PATH}
|
||||
- go get github.com/tools/godep
|
||||
- rm -rf node_modules
|
||||
- npm install -g npm
|
||||
- npm install
|
||||
|
||||
test:
|
||||
@@ -25,3 +27,10 @@ test:
|
||||
# js tests
|
||||
- ./node_modules/grunt-cli/bin/grunt test
|
||||
- npm run coveralls
|
||||
|
||||
deployment:
|
||||
master:
|
||||
branch: master
|
||||
owner: grafana
|
||||
commands:
|
||||
- ./trigger_grafana_packer.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN}
|
||||
|
||||
1
docker/blocks/elastic/elasticsearch/config/.placeholder
Normal file
1
docker/blocks/elastic/elasticsearch/config/.placeholder
Normal file
@@ -0,0 +1 @@
|
||||
Ensure the existence of the parent folder.
|
||||
6
docker/blocks/elastic/fig
Normal file
6
docker/blocks/elastic/fig
Normal file
@@ -0,0 +1,6 @@
|
||||
elasticsearch:
|
||||
image: elasticsearch:latest
|
||||
command: elasticsearch -Des.network.host=0.0.0.0
|
||||
ports:
|
||||
- "9200:9200"
|
||||
- "9300:9300"
|
||||
@@ -1,68 +1,50 @@
|
||||
from ubuntu:14.10
|
||||
from ubuntu:14.04
|
||||
|
||||
run apt-get -y update
|
||||
run apt-get -y update
|
||||
|
||||
run apt-get -y install software-properties-common
|
||||
|
||||
run apt-get -y install python-software-properties &&\
|
||||
add-apt-repository ppa:chris-lea/node.js &&\
|
||||
apt-get -y update
|
||||
|
||||
run apt-get -y install python-django-tagging python-simplejson python-memcache \
|
||||
python-ldap python-cairo python-django python-twisted \
|
||||
python-pysqlite2 python-support python-pip gunicorn \
|
||||
supervisor nginx-light nodejs git wget curl
|
||||
|
||||
# Install statsd
|
||||
run mkdir /src && git clone https://github.com/etsy/statsd.git /src/statsd
|
||||
run apt-get -y install libcairo2-dev libffi-dev pkg-config python-dev python-pip fontconfig apache2 libapache2-mod-wsgi git-core collectd memcached gcc g++ make supervisor nginx-light gunicorn
|
||||
|
||||
run cd /usr/local/src && git clone https://github.com/graphite-project/graphite-web.git
|
||||
run cd /usr/local/src && git clone https://github.com/graphite-project/carbon.git
|
||||
run cd /usr/local/src && git clone https://github.com/graphite-project/whisper.git
|
||||
|
||||
run cd /usr/local/src/whisper && git checkout master && python setup.py install
|
||||
run cd /usr/local/src/carbon && git checkout 0.9.x && python setup.py install
|
||||
run cd /usr/local/src/graphite-web && git checkout 0.9.x && python check-dependencies.py; python setup.py install
|
||||
|
||||
# statsd
|
||||
add ./files/statsd_config.js /src/statsd/config.js
|
||||
run cd /usr/local/src/carbon && git checkout 0.9.x && pip install -r requirements.txt; python setup.py install
|
||||
run cd /usr/local/src/graphite-web && git checkout 0.9.x && pip install -r requirements.txt; python check-dependencies.py; python setup.py install
|
||||
|
||||
# Add graphite config
|
||||
add ./files/initial_data.json /opt/graphite/webapp/graphite/initial_data.json
|
||||
add ./files/local_settings.py /opt/graphite/webapp/graphite/local_settings.py
|
||||
add ./files/carbon.conf /opt/graphite/conf/carbon.conf
|
||||
add ./files/storage-schemas.conf /opt/graphite/conf/storage-schemas.conf
|
||||
add ./files/storage-aggregation.conf /opt/graphite/conf/storage-aggregation.conf
|
||||
add ./files/events_views.py /opt/graphite/webapp/graphite/events/views.py
|
||||
add ./files/initial_data.json /opt/graphite/webapp/graphite/initial_data.json
|
||||
add ./files/local_settings.py /opt/graphite/webapp/graphite/local_settings.py
|
||||
add ./files/carbon.conf /opt/graphite/conf/carbon.conf
|
||||
add ./files/storage-schemas.conf /opt/graphite/conf/storage-schemas.conf
|
||||
add ./files/storage-aggregation.conf /opt/graphite/conf/storage-aggregation.conf
|
||||
add ./files/events_views.py /opt/graphite/webapp/graphite/events/views.py
|
||||
|
||||
run mkdir -p /opt/graphite/storage/whisper
|
||||
run touch /opt/graphite/storage/graphite.db /opt/graphite/storage/index
|
||||
run chown -R www-data /opt/graphite/storage
|
||||
run chmod 0775 /opt/graphite/storage /opt/graphite/storage/whisper
|
||||
run chmod 0664 /opt/graphite/storage/graphite.db
|
||||
run cd /opt/graphite/webapp/graphite && python manage.py syncdb --noinput
|
||||
run mkdir -p /opt/graphite/storage/whisper
|
||||
run touch /opt/graphite/storage/graphite.db /opt/graphite/storage/index
|
||||
run chown -R www-data /opt/graphite/storage
|
||||
run chmod 0775 /opt/graphite/storage /opt/graphite/storage/whisper
|
||||
run chmod 0664 /opt/graphite/storage/graphite.db
|
||||
run cd /opt/graphite/webapp/graphite && python manage.py syncdb --noinput
|
||||
|
||||
add ./files/my_htpasswd /etc/nginx/.htpasswd
|
||||
|
||||
# Add system service config
|
||||
add ./files/nginx.conf /etc/nginx/nginx.conf
|
||||
add ./files/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
|
||||
add ./files/nginx.conf /etc/nginx/nginx.conf
|
||||
add ./files/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
# Nginx
|
||||
#
|
||||
# graphite
|
||||
expose 80
|
||||
expose 80
|
||||
|
||||
# Carbon line receiver port
|
||||
expose 2003
|
||||
expose 2003
|
||||
# Carbon cache query port
|
||||
expose 7002
|
||||
expose 7002
|
||||
|
||||
# Statsd UDP port
|
||||
expose 8125/udp
|
||||
# Statsd Management port
|
||||
expose 8126
|
||||
|
||||
VOLUME ["/var/lib/elasticsearch"]
|
||||
VOLUME ["/opt/graphite/storage/whisper"]
|
||||
VOLUME ["/var/lib/log/supervisor"]
|
||||
|
||||
cmd ["/usr/bin/supervisord"]
|
||||
cmd ["/usr/bin/supervisord"]
|
||||
|
||||
# vim:ts=8:noet:
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
graphite:
|
||||
build: blocks/graphite
|
||||
ports:
|
||||
- "8776:80"
|
||||
- "8080:80"
|
||||
- "2003:2003"
|
||||
volumes:
|
||||
- /var/docker/gfdev/graphite:/opt/graphite/storage/whisper
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
|
||||
|
||||
1
docker/blocks/graphite/files/my_htpasswd
Normal file
1
docker/blocks/graphite/files/my_htpasswd
Normal file
@@ -0,0 +1 @@
|
||||
grafana:$apr1$4R/20xhC$8t37jPP5dbcLr48btdkU//
|
||||
@@ -24,10 +24,3 @@ stdout_logfile = /var/log/supervisor/%(program_name)s.log
|
||||
stderr_logfile = /var/log/supervisor/%(program_name)s.log
|
||||
autorestart = true
|
||||
|
||||
[program:statsd]
|
||||
;user = www-data
|
||||
command = /usr/bin/node /src/statsd/stats.js /src/statsd/config.js
|
||||
stdout_logfile = /var/log/supervisor/%(program_name)s.log
|
||||
stderr_logfile = /var/log/supervisor/%(program_name)s.log
|
||||
autorestart = true
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# influxdb
|
||||
|
||||
FROM ubuntu
|
||||
|
||||
RUN mkdir -p /opt/influxdb/shared/data
|
||||
|
||||
ADD http://s3.amazonaws.com/influxdb/influxdb_0.8.8_amd64.deb /influx88.deb
|
||||
RUN dpkg -i /influx88.deb
|
||||
RUN rm -rf /opt/influxdb/shared/data
|
||||
|
||||
ADD config.toml /opt/influxdb/shared/config.toml
|
||||
|
||||
EXPOSE 8083 8086 2004
|
||||
|
||||
ENTRYPOINT ["/usr/bin/influxdb"]
|
||||
CMD ["-config=/opt/influxdb/shared/config.toml"]
|
||||
@@ -1,5 +1,5 @@
|
||||
influxdb:
|
||||
build: blocks/influxdb
|
||||
image: tutum/influxdb:latest
|
||||
ports:
|
||||
- "2004:2004"
|
||||
- "8083:8083"
|
||||
|
||||
@@ -51,6 +51,8 @@ Name | Description
|
||||
|
||||
For details of `metric names` & `label names`, and `label values`, please refer to the [Prometheus documentation](http://prometheus.io/docs/concepts/data_model/#metric-names-and-labels).
|
||||
|
||||
> Note: The part of queries is incompatible with the version before 2.6, if you specify like `foo.*`, please change like `metrics(foo.*)`.
|
||||
|
||||
You can create a template variable in Grafana and have that variable filled with values from any Prometheus metric exploration query.
|
||||
You can then use this variable in your Prometheus metric queries.
|
||||
|
||||
|
||||
@@ -1422,6 +1422,34 @@ Keys:
|
||||
}
|
||||
}
|
||||
|
||||
### Grafana Stats
|
||||
|
||||
`GET /api/admin/stats`
|
||||
|
||||
**Example Request**:
|
||||
|
||||
GET /api/admin/stats
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
**Example Response**:
|
||||
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"user_count":2,
|
||||
"org_count":1,
|
||||
"dashboard_count":4,
|
||||
"db_snapshot_count":2,
|
||||
"db_tag_count":6,
|
||||
"data_source_count":1,
|
||||
"playlist_count":1,
|
||||
"starred_db_count":2,
|
||||
"grafana_admin_count":2
|
||||
}
|
||||
|
||||
### Global Users
|
||||
|
||||
`POST /api/admin/users`
|
||||
|
||||
@@ -18,11 +18,11 @@ The Playlist feature can be accessed from Grafana's sidemenu. Click the 'Playlis
|
||||
|
||||
Click on "New Playlist" button to create a new playlist. Firstly, name your playlist and configure a time interval for Grafana to wait on a particular Dashboard before advancing to the next one on the Playlist.
|
||||
|
||||
You can search Dashboards by name (or use a regular expression), and add them to your Playlist. By default, your starred dashboards will appear as candidates for the Playlist.
|
||||
You can search Dashboards by name (or use a regular expression), and add them to your Playlist. Or you could add tags which will include all the dashboards that belongs to a tag when the playlist start playing. By default, your starred dashboards will appear as candidates for the Playlist.
|
||||
|
||||
Be sure to click the "Add to dashboard" button next to the Dashboard name to add it to the Playlist. To remove a dashboard from the playlist click on "Remove[x]" button from the playlist.
|
||||
|
||||
Since the Playlist is basically a list of Dashboards, ensure that all the Dashboards you want to appear in your Playlist are added here.
|
||||
Since the Playlist is basically a list of Dashboards, ensure that all the Dashboards you want to appear in your Playlist are added here.
|
||||
|
||||
## Saving the playlist
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ The coloring options of the Singlestat Panel config allow you to dynamically cha
|
||||
|
||||
1. `Background`: This checkbox applies the configured thresholds and colors to the entirety of the Singlestat Panel background.
|
||||
2. `Value`: This checkbox applies the configured thresholds and colors to the summary stat.
|
||||
3. `Thresholds`: Change the background and value colors dynamically within the panel, depending on the Singlestat value. The threshold field accepts **3 comma-separated** values, corresponding to the three colors directly to the right.
|
||||
3. `Thresholds`: Change the background and value colors dynamically within the panel, depending on the Singlestat value. The threshold field accepts **2 comma-separated** values which represent 3 ranges that correspond to the three colors directly to the right. For example: if the thresholds are 70, 90 then the first color represents < 70, the second color represents between 70 and 90 and the third color represents > 90.
|
||||
4. `Colors`: Select a color and opacity
|
||||
5. `Invert order`: This link toggles the threshold color order.</br>For example: Green, Orange, Red (<img class="no-shadow" src="/img/v1/gyr.png">) will become Red, Orange, Green (<img class="no-shadow" src="/img/v1/ryg.png">).
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ package api
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@@ -27,3 +29,15 @@ func AdminGetSettings(c *middleware.Context) {
|
||||
|
||||
c.JSON(200, settings)
|
||||
}
|
||||
|
||||
func AdminGetStats(c *middleware.Context) {
|
||||
|
||||
statsQuery := m.GetAdminStatsQuery{}
|
||||
|
||||
if err := bus.Dispatch(&statsQuery); err != nil {
|
||||
c.JsonApiErr(500, "Failed to get admin stats from database", err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, statsQuery.Result)
|
||||
}
|
||||
@@ -40,6 +40,7 @@ func Register(r *macaron.Macaron) {
|
||||
r.Get("/admin/users/edit/:id", reqGrafanaAdmin, Index)
|
||||
r.Get("/admin/orgs", reqGrafanaAdmin, Index)
|
||||
r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, Index)
|
||||
r.Get("/admin/stats", reqGrafanaAdmin, Index)
|
||||
|
||||
r.Get("/apps", reqSignedIn, Index)
|
||||
r.Get("/apps/edit/*", reqSignedIn, Index)
|
||||
@@ -210,12 +211,13 @@ func Register(r *macaron.Macaron) {
|
||||
r.Delete("/users/:id", AdminDeleteUser)
|
||||
r.Get("/users/:id/quotas", wrap(GetUserQuotas))
|
||||
r.Put("/users/:id/quotas/:target", bind(m.UpdateUserQuotaCmd{}), wrap(UpdateUserQuota))
|
||||
r.Get("/stats", AdminGetStats)
|
||||
}, reqGrafanaAdmin)
|
||||
|
||||
// rendering
|
||||
r.Get("/render/*", reqSignedIn, RenderToPng)
|
||||
|
||||
InitApiPluginRoutes(r)
|
||||
InitAppPluginRoutes(r)
|
||||
|
||||
r.NotFound(NotFoundHandler)
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
|
||||
"gopkg.in/macaron.v1"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func InitApiPluginRoutes(r *macaron.Macaron) {
|
||||
for _, plugin := range plugins.ApiPlugins {
|
||||
log.Info("Plugin: Adding proxy routes for api plugin")
|
||||
for _, route := range plugin.Routes {
|
||||
url := util.JoinUrlFragments("/api/plugin-proxy/", route.Path)
|
||||
handlers := make([]macaron.Handler, 0)
|
||||
if route.ReqSignedIn {
|
||||
handlers = append(handlers, middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true}))
|
||||
}
|
||||
if route.ReqGrafanaAdmin {
|
||||
handlers = append(handlers, middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true}))
|
||||
}
|
||||
if route.ReqSignedIn && route.ReqRole != "" {
|
||||
if route.ReqRole == m.ROLE_ADMIN {
|
||||
handlers = append(handlers, middleware.RoleAuth(m.ROLE_ADMIN))
|
||||
} else if route.ReqRole == m.ROLE_EDITOR {
|
||||
handlers = append(handlers, middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN))
|
||||
}
|
||||
}
|
||||
handlers = append(handlers, ApiPlugin(route.Url))
|
||||
r.Route(url, route.Method, handlers...)
|
||||
log.Info("Plugin: Adding route %s", url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ApiPlugin(routeUrl string) macaron.Handler {
|
||||
return func(c *middleware.Context) {
|
||||
path := c.Params("*")
|
||||
|
||||
//Create a HTTP header with the context in it.
|
||||
ctx, err := json.Marshal(c.SignedInUser)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "failed to marshal context to json.", err)
|
||||
return
|
||||
}
|
||||
targetUrl, _ := url.Parse(routeUrl)
|
||||
proxy := NewApiPluginProxy(string(ctx), path, targetUrl)
|
||||
proxy.Transport = dataProxyTransport
|
||||
proxy.ServeHTTP(c.Resp, c.Req.Request)
|
||||
}
|
||||
}
|
||||
|
||||
func NewApiPluginProxy(ctx string, proxyPath string, targetUrl *url.URL) *httputil.ReverseProxy {
|
||||
director := func(req *http.Request) {
|
||||
req.URL.Scheme = targetUrl.Scheme
|
||||
req.URL.Host = targetUrl.Host
|
||||
req.Host = targetUrl.Host
|
||||
|
||||
req.URL.Path = util.JoinUrlFragments(targetUrl.Path, proxyPath)
|
||||
|
||||
// clear cookie headers
|
||||
req.Header.Del("Cookie")
|
||||
req.Header.Del("Set-Cookie")
|
||||
req.Header.Add("Grafana-Context", ctx)
|
||||
}
|
||||
|
||||
return &httputil.ReverseProxy{Director: director}
|
||||
}
|
||||
116
pkg/api/app_routes.go
Normal file
116
pkg/api/app_routes.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"text/template"
|
||||
|
||||
"gopkg.in/macaron.v1"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func InitAppPluginRoutes(r *macaron.Macaron) {
|
||||
for _, plugin := range plugins.Apps {
|
||||
for _, route := range plugin.Routes {
|
||||
log.Info("Plugin: Adding proxy route for app plugin")
|
||||
url := util.JoinUrlFragments("/api/plugin-proxy/", route.Path)
|
||||
handlers := make([]macaron.Handler, 0)
|
||||
if route.ReqSignedIn {
|
||||
handlers = append(handlers, middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true}))
|
||||
}
|
||||
if route.ReqGrafanaAdmin {
|
||||
handlers = append(handlers, middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true}))
|
||||
}
|
||||
if route.ReqSignedIn && route.ReqRole != "" {
|
||||
if route.ReqRole == m.ROLE_ADMIN {
|
||||
handlers = append(handlers, middleware.RoleAuth(m.ROLE_ADMIN))
|
||||
} else if route.ReqRole == m.ROLE_EDITOR {
|
||||
handlers = append(handlers, middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN))
|
||||
}
|
||||
}
|
||||
handlers = append(handlers, AppPluginRoute(route, plugin.Id))
|
||||
r.Route(url, route.Method, handlers...)
|
||||
log.Info("Plugin: Adding route %s", url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func AppPluginRoute(route *plugins.AppPluginRoute, appId string) macaron.Handler {
|
||||
return func(c *middleware.Context) {
|
||||
path := c.Params("*")
|
||||
|
||||
proxy := NewApiPluginProxy(c, path, route, appId)
|
||||
proxy.Transport = dataProxyTransport
|
||||
proxy.ServeHTTP(c.Resp, c.Req.Request)
|
||||
}
|
||||
}
|
||||
|
||||
func NewApiPluginProxy(ctx *middleware.Context, proxyPath string, route *plugins.AppPluginRoute, appId string) *httputil.ReverseProxy {
|
||||
targetUrl, _ := url.Parse(route.Url)
|
||||
|
||||
director := func(req *http.Request) {
|
||||
|
||||
req.URL.Scheme = targetUrl.Scheme
|
||||
req.URL.Host = targetUrl.Host
|
||||
req.Host = targetUrl.Host
|
||||
|
||||
req.URL.Path = util.JoinUrlFragments(targetUrl.Path, proxyPath)
|
||||
|
||||
// clear cookie headers
|
||||
req.Header.Del("Cookie")
|
||||
req.Header.Del("Set-Cookie")
|
||||
|
||||
//Create a HTTP header with the context in it.
|
||||
ctxJson, err := json.Marshal(ctx.SignedInUser)
|
||||
if err != nil {
|
||||
ctx.JsonApiErr(500, "failed to marshal context to json.", err)
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Add("Grafana-Context", string(ctxJson))
|
||||
// add custom headers defined in the plugin config.
|
||||
for _, header := range route.Headers {
|
||||
var contentBuf bytes.Buffer
|
||||
t, err := template.New("content").Parse(header.Content)
|
||||
if err != nil {
|
||||
ctx.JsonApiErr(500, fmt.Sprintf("could not parse header content template for header %s.", header.Name), err)
|
||||
return
|
||||
}
|
||||
|
||||
//lookup appSettings
|
||||
query := m.GetAppSettingByAppIdQuery{OrgId: ctx.OrgId, AppId: appId}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
ctx.JsonApiErr(500, "failed to get AppSettings.", err)
|
||||
return
|
||||
}
|
||||
type templateData struct {
|
||||
JsonData map[string]interface{}
|
||||
SecureJsonData map[string]string
|
||||
}
|
||||
data := templateData{
|
||||
JsonData: query.Result.JsonData,
|
||||
SecureJsonData: query.Result.SecureJsonData.Decrypt(),
|
||||
}
|
||||
err = t.Execute(&contentBuf, data)
|
||||
if err != nil {
|
||||
ctx.JsonApiErr(500, fmt.Sprintf("failed to execute header content template for header %s.", header.Name), err)
|
||||
return
|
||||
}
|
||||
log.Debug("Adding header to proxy request. %s: %s", header.Name, contentBuf.String())
|
||||
req.Header.Add(header.Name, contentBuf.String())
|
||||
}
|
||||
}
|
||||
|
||||
return &httputil.ReverseProxy{Director: director}
|
||||
}
|
||||
@@ -103,5 +103,6 @@ func ProxyDataSourceRequest(c *middleware.Context) {
|
||||
proxy := NewReverseProxy(ds, proxyPath, targetUrl)
|
||||
proxy.Transport = dataProxyTransport
|
||||
proxy.ServeHTTP(c.Resp, c.Req.Request)
|
||||
c.Resp.Header().Del("Set-Cookie")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ func NewAppSettingsDto(def *plugins.AppPlugin, data *models.AppSettings) *AppSet
|
||||
dto.Enabled = data.Enabled
|
||||
dto.Pinned = data.Pinned
|
||||
dto.Info = &def.Info
|
||||
dto.JsonData = data.JsonData
|
||||
}
|
||||
|
||||
return dto
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
_ "github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
@@ -101,39 +98,6 @@ func LoadPlaylistItems(id int64) ([]m.PlaylistItem, error) {
|
||||
return *itemQuery.Result, nil
|
||||
}
|
||||
|
||||
func LoadPlaylistDashboards(id int64) ([]m.PlaylistDashboardDto, error) {
|
||||
playlistItems, _ := LoadPlaylistItems(id)
|
||||
|
||||
dashboardIds := make([]int64, 0)
|
||||
|
||||
for _, i := range playlistItems {
|
||||
dashboardId, _ := strconv.ParseInt(i.Value, 10, 64)
|
||||
dashboardIds = append(dashboardIds, dashboardId)
|
||||
}
|
||||
|
||||
if len(dashboardIds) == 0 {
|
||||
return make([]m.PlaylistDashboardDto, 0), nil
|
||||
}
|
||||
|
||||
dashboardQuery := m.GetPlaylistDashboardsQuery{DashboardIds: dashboardIds}
|
||||
if err := bus.Dispatch(&dashboardQuery); err != nil {
|
||||
log.Warn("dashboardquery failed: %v", err)
|
||||
return nil, errors.New("Playlist not found")
|
||||
}
|
||||
|
||||
dtos := make([]m.PlaylistDashboardDto, 0)
|
||||
for _, item := range *dashboardQuery.Result {
|
||||
dtos = append(dtos, m.PlaylistDashboardDto{
|
||||
Id: item.Id,
|
||||
Slug: item.Slug,
|
||||
Title: item.Title,
|
||||
Uri: "db/" + item.Slug,
|
||||
})
|
||||
}
|
||||
|
||||
return dtos, nil
|
||||
}
|
||||
|
||||
func GetPlaylistItems(c *middleware.Context) Response {
|
||||
id := c.ParamsInt64(":id")
|
||||
|
||||
@@ -147,9 +111,9 @@ func GetPlaylistItems(c *middleware.Context) Response {
|
||||
}
|
||||
|
||||
func GetPlaylistDashboards(c *middleware.Context) Response {
|
||||
id := c.ParamsInt64(":id")
|
||||
playlistId := c.ParamsInt64(":id")
|
||||
|
||||
playlists, err := LoadPlaylistDashboards(id)
|
||||
playlists, err := LoadPlaylistDashboards(c.OrgId, c.UserId, playlistId)
|
||||
if err != nil {
|
||||
return ApiError(500, "Could not load dashboards", err)
|
||||
}
|
||||
|
||||
88
pkg/api/playlist_play.go
Normal file
88
pkg/api/playlist_play.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
_ "github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
)
|
||||
|
||||
func populateDashboardsById(dashboardByIds []int64) ([]m.PlaylistDashboardDto, error) {
|
||||
result := make([]m.PlaylistDashboardDto, 0)
|
||||
|
||||
if len(dashboardByIds) > 0 {
|
||||
dashboardQuery := m.GetDashboardsQuery{DashboardIds: dashboardByIds}
|
||||
if err := bus.Dispatch(&dashboardQuery); err != nil {
|
||||
return result, errors.New("Playlist not found") //TODO: dont swallow error
|
||||
}
|
||||
|
||||
for _, item := range *dashboardQuery.Result {
|
||||
result = append(result, m.PlaylistDashboardDto{
|
||||
Id: item.Id,
|
||||
Slug: item.Slug,
|
||||
Title: item.Title,
|
||||
Uri: "db/" + item.Slug,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string) []m.PlaylistDashboardDto {
|
||||
result := make([]m.PlaylistDashboardDto, 0)
|
||||
|
||||
if len(dashboardByTag) > 0 {
|
||||
for _, tag := range dashboardByTag {
|
||||
searchQuery := search.Query{
|
||||
Title: "",
|
||||
Tags: []string{tag},
|
||||
UserId: userId,
|
||||
Limit: 100,
|
||||
IsStarred: false,
|
||||
OrgId: orgId,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&searchQuery); err == nil {
|
||||
for _, item := range searchQuery.Result {
|
||||
result = append(result, m.PlaylistDashboardDto{
|
||||
Id: item.Id,
|
||||
Title: item.Title,
|
||||
Uri: item.Uri,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func LoadPlaylistDashboards(orgId, userId, playlistId int64) ([]m.PlaylistDashboardDto, error) {
|
||||
playlistItems, _ := LoadPlaylistItems(playlistId)
|
||||
|
||||
dashboardByIds := make([]int64, 0)
|
||||
dashboardByTag := make([]string, 0)
|
||||
|
||||
for _, i := range playlistItems {
|
||||
if i.Type == "dashboard_by_id" {
|
||||
dashboardId, _ := strconv.ParseInt(i.Value, 10, 64)
|
||||
dashboardByIds = append(dashboardByIds, dashboardId)
|
||||
}
|
||||
|
||||
if i.Type == "dashboard_by_tag" {
|
||||
dashboardByTag = append(dashboardByTag, i.Value)
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]m.PlaylistDashboardDto, 0)
|
||||
|
||||
var k, _ = populateDashboardsById(dashboardByIds)
|
||||
result = append(result, k...)
|
||||
result = append(result, populateDashboardsByTag(orgId, userId, dashboardByTag)...)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -55,6 +55,7 @@ func sendUsageStats() {
|
||||
metrics["stats.dashboards.count"] = statsQuery.Result.DashboardCount
|
||||
metrics["stats.users.count"] = statsQuery.Result.UserCount
|
||||
metrics["stats.orgs.count"] = statsQuery.Result.OrgCount
|
||||
metrics["stats.playlist.count"] = statsQuery.Result.PlaylistCount
|
||||
|
||||
dsStats := m.GetDataSourceStatsQuery{}
|
||||
if err := bus.Dispatch(&dsStats); err != nil {
|
||||
|
||||
@@ -1,27 +1,49 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrAppSettingNotFound = errors.New("AppSetting not found")
|
||||
)
|
||||
|
||||
type AppSettings struct {
|
||||
Id int64
|
||||
AppId string
|
||||
OrgId int64
|
||||
Enabled bool
|
||||
Pinned bool
|
||||
JsonData map[string]interface{}
|
||||
Id int64
|
||||
AppId string
|
||||
OrgId int64
|
||||
Enabled bool
|
||||
Pinned bool
|
||||
JsonData map[string]interface{}
|
||||
SecureJsonData SecureJsonData
|
||||
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
}
|
||||
|
||||
type SecureJsonData map[string][]byte
|
||||
|
||||
func (s SecureJsonData) Decrypt() map[string]string {
|
||||
decrypted := make(map[string]string)
|
||||
for key, data := range s {
|
||||
decrypted[key] = string(util.Decrypt(data, setting.SecretKey))
|
||||
}
|
||||
return decrypted
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// COMMANDS
|
||||
|
||||
// Also acts as api DTO
|
||||
type UpdateAppSettingsCmd struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Pinned bool `json:"pinned"`
|
||||
JsonData map[string]interface{} `json:"jsonData"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Pinned bool `json:"pinned"`
|
||||
JsonData map[string]interface{} `json:"jsonData"`
|
||||
SecureJsonData map[string]string `json:"secureJsonData"`
|
||||
|
||||
AppId string `json:"-"`
|
||||
OrgId int64 `json:"-"`
|
||||
@@ -33,3 +55,9 @@ type GetAppSettingsQuery struct {
|
||||
OrgId int64
|
||||
Result []*AppSettings
|
||||
}
|
||||
|
||||
type GetAppSettingByAppIdQuery struct {
|
||||
AppId string
|
||||
OrgId int64
|
||||
Result *AppSettings
|
||||
}
|
||||
|
||||
@@ -146,3 +146,8 @@ type GetDashboardTagsQuery struct {
|
||||
OrgId int64
|
||||
Result []*DashboardTagCloudItem
|
||||
}
|
||||
|
||||
type GetDashboardsQuery struct {
|
||||
DashboardIds []int64
|
||||
Result *[]Dashboard
|
||||
}
|
||||
|
||||
@@ -76,9 +76,7 @@ type UpdatePlaylistCommand struct {
|
||||
OrgId int64 `json:"-"`
|
||||
Id int64 `json:"id" binding:"Required"`
|
||||
Name string `json:"name" binding:"Required"`
|
||||
Type string `json:"type"`
|
||||
Interval string `json:"interval"`
|
||||
Data []int64 `json:"data"`
|
||||
Items []PlaylistItemDTO `json:"items"`
|
||||
|
||||
Result *PlaylistDTO
|
||||
@@ -86,9 +84,7 @@ type UpdatePlaylistCommand struct {
|
||||
|
||||
type CreatePlaylistCommand struct {
|
||||
Name string `json:"name" binding:"Required"`
|
||||
Type string `json:"type"`
|
||||
Interval string `json:"interval"`
|
||||
Data []int64 `json:"data"`
|
||||
Items []PlaylistItemDTO `json:"items"`
|
||||
|
||||
OrgId int64 `json:"-"`
|
||||
@@ -121,8 +117,3 @@ type GetPlaylistItemsByIdQuery struct {
|
||||
PlaylistId int64
|
||||
Result *[]PlaylistItem
|
||||
}
|
||||
|
||||
type GetPlaylistDashboardsQuery struct {
|
||||
DashboardIds []int64
|
||||
Result *PlaylistDashboards
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ type SystemStats struct {
|
||||
DashboardCount int
|
||||
UserCount int
|
||||
OrgCount int
|
||||
PlaylistCount int
|
||||
}
|
||||
|
||||
type DataSourceStats struct {
|
||||
@@ -18,3 +19,19 @@ type GetSystemStatsQuery struct {
|
||||
type GetDataSourceStatsQuery struct {
|
||||
Result []*DataSourceStats
|
||||
}
|
||||
|
||||
type AdminStats struct {
|
||||
UserCount int `json:"user_count"`
|
||||
OrgCount int `json:"org_count"`
|
||||
DashboardCount int `json:"dashboard_count"`
|
||||
DbSnapshotCount int `json:"db_snapshot_count"`
|
||||
DbTagCount int `json:"db_tag_count"`
|
||||
DataSourceCount int `json:"data_source_count"`
|
||||
PlaylistCount int `json:"playlist_count"`
|
||||
StarredDbCount int `json:"starred_db_count"`
|
||||
GrafanaAdminCount int `json:"grafana_admin_count"`
|
||||
}
|
||||
|
||||
type GetAdminStatsQuery struct {
|
||||
Result *AdminStats
|
||||
}
|
||||
|
||||
@@ -26,14 +26,30 @@ type AppIncludeInfo struct {
|
||||
|
||||
type AppPlugin struct {
|
||||
FrontendPluginBase
|
||||
Css *AppPluginCss `json:"css"`
|
||||
Pages []AppPluginPage `json:"pages"`
|
||||
Includes []AppIncludeInfo `json:"-"`
|
||||
Css *AppPluginCss `json:"css"`
|
||||
Pages []AppPluginPage `json:"pages"`
|
||||
Routes []*AppPluginRoute `json:"routes"`
|
||||
Includes []AppIncludeInfo `json:"-"`
|
||||
|
||||
Pinned bool `json:"-"`
|
||||
Enabled bool `json:"-"`
|
||||
}
|
||||
|
||||
type AppPluginRoute struct {
|
||||
Path string `json:"path"`
|
||||
Method string `json:"method"`
|
||||
ReqSignedIn bool `json:"reqSignedIn"`
|
||||
ReqGrafanaAdmin bool `json:"reqGrafanaAdmin"`
|
||||
ReqRole models.RoleType `json:"reqRole"`
|
||||
Url string `json:"url"`
|
||||
Headers []AppPluginRouteHeader `json:"headers"`
|
||||
}
|
||||
|
||||
type AppPluginRouteHeader struct {
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func (app *AppPlugin) Load(decoder *json.Decoder, pluginDir string) error {
|
||||
if err := decoder.Decode(&app); err != nil {
|
||||
return err
|
||||
|
||||
@@ -2,8 +2,6 @@ package plugins
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
type PluginLoader interface {
|
||||
@@ -44,24 +42,9 @@ type PluginStaticRoute struct {
|
||||
PluginId string
|
||||
}
|
||||
|
||||
type ApiPluginRoute struct {
|
||||
Path string `json:"path"`
|
||||
Method string `json:"method"`
|
||||
ReqSignedIn bool `json:"reqSignedIn"`
|
||||
ReqGrafanaAdmin bool `json:"reqGrafanaAdmin"`
|
||||
ReqRole models.RoleType `json:"reqRole"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
type ApiPlugin struct {
|
||||
PluginBase
|
||||
Routes []*ApiPluginRoute `json:"routes"`
|
||||
}
|
||||
|
||||
type EnabledPlugins struct {
|
||||
Panels []*PanelPlugin
|
||||
DataSources map[string]*DataSourcePlugin
|
||||
ApiList []*ApiPlugin
|
||||
Apps []*AppPlugin
|
||||
}
|
||||
|
||||
@@ -69,7 +52,6 @@ func NewEnabledPlugins() EnabledPlugins {
|
||||
return EnabledPlugins{
|
||||
Panels: make([]*PanelPlugin, 0),
|
||||
DataSources: make(map[string]*DataSourcePlugin),
|
||||
ApiList: make([]*ApiPlugin, 0),
|
||||
Apps: make([]*AppPlugin, 0),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
var (
|
||||
DataSources map[string]*DataSourcePlugin
|
||||
Panels map[string]*PanelPlugin
|
||||
ApiPlugins map[string]*ApiPlugin
|
||||
StaticRoutes []*PluginStaticRoute
|
||||
Apps map[string]*AppPlugin
|
||||
PluginTypes map[string]interface{}
|
||||
@@ -30,14 +29,12 @@ type PluginScanner struct {
|
||||
|
||||
func Init() error {
|
||||
DataSources = make(map[string]*DataSourcePlugin)
|
||||
ApiPlugins = make(map[string]*ApiPlugin)
|
||||
StaticRoutes = make([]*PluginStaticRoute, 0)
|
||||
Panels = make(map[string]*PanelPlugin)
|
||||
Apps = make(map[string]*AppPlugin)
|
||||
PluginTypes = map[string]interface{}{
|
||||
"panel": PanelPlugin{},
|
||||
"datasource": DataSourcePlugin{},
|
||||
"api": ApiPlugin{},
|
||||
"app": AppPlugin{},
|
||||
}
|
||||
|
||||
|
||||
@@ -68,11 +68,5 @@ func GetEnabledPlugins(orgId int64) (*EnabledPlugins, error) {
|
||||
}
|
||||
}
|
||||
|
||||
for _, api := range ApiPlugins {
|
||||
if isPluginEnabled(api.IncludedInAppId) {
|
||||
enabledPlugins.ApiList = append(enabledPlugins.ApiList, api)
|
||||
}
|
||||
}
|
||||
|
||||
return &enabledPlugins, nil
|
||||
}
|
||||
|
||||
@@ -5,10 +5,13 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
bus.AddHandler("sql", GetAppSettings)
|
||||
bus.AddHandler("sql", GetAppSettingByAppId)
|
||||
bus.AddHandler("sql", UpdateAppSettings)
|
||||
}
|
||||
|
||||
@@ -19,6 +22,18 @@ func GetAppSettings(query *m.GetAppSettingsQuery) error {
|
||||
return sess.Find(&query.Result)
|
||||
}
|
||||
|
||||
func GetAppSettingByAppId(query *m.GetAppSettingByAppIdQuery) error {
|
||||
appSetting := m.AppSettings{OrgId: query.OrgId, AppId: query.AppId}
|
||||
has, err := x.Get(&appSetting)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has == false {
|
||||
return m.ErrAppSettingNotFound
|
||||
}
|
||||
query.Result = &appSetting
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateAppSettings(cmd *m.UpdateAppSettingsCmd) error {
|
||||
return inTransaction2(func(sess *session) error {
|
||||
var app m.AppSettings
|
||||
@@ -27,18 +42,27 @@ func UpdateAppSettings(cmd *m.UpdateAppSettingsCmd) error {
|
||||
sess.UseBool("enabled")
|
||||
sess.UseBool("pinned")
|
||||
if !exists {
|
||||
// encrypt secureJsonData
|
||||
secureJsonData := make(map[string][]byte)
|
||||
for key, data := range cmd.SecureJsonData {
|
||||
secureJsonData[key] = util.Encrypt([]byte(data), setting.SecretKey)
|
||||
}
|
||||
app = m.AppSettings{
|
||||
AppId: cmd.AppId,
|
||||
OrgId: cmd.OrgId,
|
||||
Enabled: cmd.Enabled,
|
||||
Pinned: cmd.Pinned,
|
||||
JsonData: cmd.JsonData,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
AppId: cmd.AppId,
|
||||
OrgId: cmd.OrgId,
|
||||
Enabled: cmd.Enabled,
|
||||
Pinned: cmd.Pinned,
|
||||
JsonData: cmd.JsonData,
|
||||
SecureJsonData: secureJsonData,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
_, err = sess.Insert(&app)
|
||||
return err
|
||||
} else {
|
||||
for key, data := range cmd.SecureJsonData {
|
||||
app.SecureJsonData[key] = util.Encrypt([]byte(data), setting.SecretKey)
|
||||
}
|
||||
app.Updated = time.Now()
|
||||
app.Enabled = cmd.Enabled
|
||||
app.JsonData = cmd.JsonData
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
func init() {
|
||||
bus.AddHandler("sql", SaveDashboard)
|
||||
bus.AddHandler("sql", GetDashboard)
|
||||
bus.AddHandler("sql", GetDashboards)
|
||||
bus.AddHandler("sql", DeleteDashboard)
|
||||
bus.AddHandler("sql", SearchDashboards)
|
||||
bus.AddHandler("sql", GetDashboardTags)
|
||||
@@ -223,3 +224,20 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func GetDashboards(query *m.GetDashboardsQuery) error {
|
||||
if len(query.DashboardIds) == 0 {
|
||||
return m.ErrCommandValidationFailed
|
||||
}
|
||||
|
||||
var dashboards = make([]m.Dashboard, 0)
|
||||
|
||||
err := x.In("id", query.DashboardIds).Find(&dashboards)
|
||||
query.Result = &dashboards
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
|
||||
func addAppSettingsMigration(mg *Migrator) {
|
||||
|
||||
appSettingsV1 := Table{
|
||||
appSettingsV2 := Table{
|
||||
Name: "app_settings",
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
@@ -13,6 +13,7 @@ func addAppSettingsMigration(mg *Migrator) {
|
||||
{Name: "enabled", Type: DB_Bool, Nullable: false},
|
||||
{Name: "pinned", Type: DB_Bool, Nullable: false},
|
||||
{Name: "json_data", Type: DB_Text, Nullable: true},
|
||||
{Name: "secure_json_data", Type: DB_Text, Nullable: true},
|
||||
{Name: "created", Type: DB_DateTime, Nullable: false},
|
||||
{Name: "updated", Type: DB_DateTime, Nullable: false},
|
||||
},
|
||||
@@ -21,8 +22,10 @@ func addAppSettingsMigration(mg *Migrator) {
|
||||
},
|
||||
}
|
||||
|
||||
mg.AddMigration("create app_settings table v1", NewAddTableMigration(appSettingsV1))
|
||||
mg.AddMigration("Drop old table app_settings v1", NewDropTableMigration("app_settings"))
|
||||
|
||||
mg.AddMigration("create app_settings table v2", NewAddTableMigration(appSettingsV2))
|
||||
|
||||
//------- indexes ------------------
|
||||
addTableIndicesMigrations(mg, "v3", appSettingsV1)
|
||||
addTableIndicesMigrations(mg, "v3", appSettingsV2)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ func init() {
|
||||
bus.AddHandler("sql", DeletePlaylist)
|
||||
bus.AddHandler("sql", SearchPlaylists)
|
||||
bus.AddHandler("sql", GetPlaylist)
|
||||
bus.AddHandler("sql", GetPlaylistDashboards)
|
||||
bus.AddHandler("sql", GetPlaylistItem)
|
||||
}
|
||||
|
||||
@@ -162,20 +161,3 @@ func GetPlaylistItem(query *m.GetPlaylistItemsByIdQuery) error {
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func GetPlaylistDashboards(query *m.GetPlaylistDashboardsQuery) error {
|
||||
if len(query.DashboardIds) == 0 {
|
||||
return m.ErrCommandValidationFailed
|
||||
}
|
||||
|
||||
var dashboards = make(m.PlaylistDashboards, 0)
|
||||
|
||||
err := x.In("id", query.DashboardIds).Find(&dashboards)
|
||||
query.Result = &dashboards
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
44
pkg/services/sqlstore/playlist_test.go
Normal file
44
pkg/services/sqlstore/playlist_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func TestPlaylistDataAccess(t *testing.T) {
|
||||
|
||||
Convey("Testing Playlist data access", t, func() {
|
||||
InitTestDB(t)
|
||||
|
||||
Convey("Can create playlist", func() {
|
||||
items := []m.PlaylistItemDTO{
|
||||
{Title: "graphite", Value: "graphite", Type: "dashboard_by_tag"},
|
||||
{Title: "Backend response times", Value: "3", Type: "dashboard_by_id"},
|
||||
}
|
||||
cmd := m.CreatePlaylistCommand{Name: "NYC office", Interval: "10m", OrgId: 1, Items: items}
|
||||
err := CreatePlaylist(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("can update playlist", func() {
|
||||
items := []m.PlaylistItemDTO{
|
||||
{Title: "influxdb", Value: "influxdb", Type: "dashboard_by_tag"},
|
||||
{Title: "Backend response times", Value: "2", Type: "dashboard_by_id"},
|
||||
}
|
||||
query := m.UpdatePlaylistCommand{Name: "NYC office ", OrgId: 1, Id: 1, Interval: "10s", Items: items}
|
||||
err = UpdatePlaylist(&query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("can remove playlist", func() {
|
||||
query := m.DeletePlaylistCommand{Id: 1}
|
||||
err = DeletePlaylist(&query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
func init() {
|
||||
bus.AddHandler("sql", GetSystemStats)
|
||||
bus.AddHandler("sql", GetDataSourceStats)
|
||||
bus.AddHandler("sql", GetAdminStats)
|
||||
}
|
||||
|
||||
func GetDataSourceStats(query *m.GetDataSourceStatsQuery) error {
|
||||
@@ -34,7 +35,11 @@ func GetSystemStats(query *m.GetSystemStatsQuery) error {
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM ` + dialect.Quote("dashboard") + `
|
||||
) AS dashboard_count
|
||||
) AS dashboard_count,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM ` + dialect.Quote("playlist") + `
|
||||
) AS playlist_count
|
||||
`
|
||||
|
||||
var stats m.SystemStats
|
||||
@@ -46,3 +51,54 @@ func GetSystemStats(query *m.GetSystemStatsQuery) error {
|
||||
query.Result = &stats
|
||||
return err
|
||||
}
|
||||
|
||||
func GetAdminStats(query *m.GetAdminStatsQuery) error {
|
||||
var rawSql = `SELECT
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM ` + dialect.Quote("user") + `
|
||||
) AS user_count,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM ` + dialect.Quote("org") + `
|
||||
) AS org_count,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM ` + dialect.Quote("dashboard") + `
|
||||
) AS dashboard_count,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM ` + dialect.Quote("dashboard_snapshot") + `
|
||||
) AS db_snapshot_count,
|
||||
(
|
||||
SELECT COUNT( DISTINCT ( ` + dialect.Quote("term") + ` ))
|
||||
FROM ` + dialect.Quote("dashboard_tag") + `
|
||||
) AS db_tag_count,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM ` + dialect.Quote("data_source") + `
|
||||
) AS data_source_count,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM ` + dialect.Quote("playlist") + `
|
||||
) AS playlist_count,
|
||||
(
|
||||
SELECT COUNT(DISTINCT ` + dialect.Quote("dashboard_id") + ` )
|
||||
FROM ` + dialect.Quote("star") + `
|
||||
) AS starred_db_count,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM ` + dialect.Quote("user") + `
|
||||
WHERE ` + dialect.Quote("is_admin") + ` = 1
|
||||
) AS grafana_admin_count
|
||||
`
|
||||
|
||||
var stats m.AdminStats
|
||||
_, err := x.Sql(rawSql).Get(&stats)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query.Result = &stats
|
||||
return err
|
||||
}
|
||||
|
||||
66
pkg/util/encryption.go
Normal file
66
pkg/util/encryption.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"io"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
)
|
||||
|
||||
const saltLength = 8
|
||||
|
||||
func Decrypt(payload []byte, secret string) []byte {
|
||||
salt := payload[:saltLength]
|
||||
key := encryptionKeyToBytes(secret, string(salt))
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
log.Fatal(4, err.Error())
|
||||
}
|
||||
|
||||
// The IV needs to be unique, but not secure. Therefore it's common to
|
||||
// include it at the beginning of the ciphertext.
|
||||
if len(payload) < aes.BlockSize {
|
||||
log.Fatal(4, "payload too short")
|
||||
}
|
||||
iv := payload[saltLength : saltLength+aes.BlockSize]
|
||||
payload = payload[saltLength+aes.BlockSize:]
|
||||
|
||||
stream := cipher.NewCFBDecrypter(block, iv)
|
||||
|
||||
// XORKeyStream can work in-place if the two arguments are the same.
|
||||
stream.XORKeyStream(payload, payload)
|
||||
return payload
|
||||
}
|
||||
|
||||
func Encrypt(payload []byte, secret string) []byte {
|
||||
salt := GetRandomString(saltLength)
|
||||
|
||||
key := encryptionKeyToBytes(secret, salt)
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
log.Fatal(4, err.Error())
|
||||
}
|
||||
|
||||
// The IV needs to be unique, but not secure. Therefore it's common to
|
||||
// include it at the beginning of the ciphertext.
|
||||
ciphertext := make([]byte, saltLength+aes.BlockSize+len(payload))
|
||||
copy(ciphertext[:saltLength], []byte(salt))
|
||||
iv := ciphertext[saltLength : saltLength+aes.BlockSize]
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
log.Fatal(4, err.Error())
|
||||
}
|
||||
|
||||
stream := cipher.NewCFBEncrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext[saltLength+aes.BlockSize:], payload)
|
||||
|
||||
return ciphertext
|
||||
}
|
||||
|
||||
// Key needs to be 32bytes
|
||||
func encryptionKeyToBytes(secret, salt string) []byte {
|
||||
return PBKDF2([]byte(secret), []byte(salt), 10000, 32, sha256.New)
|
||||
}
|
||||
27
pkg/util/encryption_test.go
Normal file
27
pkg/util/encryption_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestEncryption(t *testing.T) {
|
||||
|
||||
Convey("When getting encryption key", t, func() {
|
||||
|
||||
key := encryptionKeyToBytes("secret", "salt")
|
||||
So(len(key), ShouldEqual, 32)
|
||||
|
||||
key = encryptionKeyToBytes("a very long secret key that is larger then 32bytes", "salt")
|
||||
So(len(key), ShouldEqual, 32)
|
||||
})
|
||||
|
||||
Convey("When decrypting basic payload", t, func() {
|
||||
encrypted := Encrypt([]byte("grafana"), "1234")
|
||||
decrypted := Decrypt(encrypted, "1234")
|
||||
|
||||
So(string(decrypted), ShouldEqual, "grafana")
|
||||
})
|
||||
|
||||
}
|
||||
@@ -27,6 +27,11 @@ func (r *UrlQueryReader) Get(name string, def string) string {
|
||||
func JoinUrlFragments(a, b string) string {
|
||||
aslash := strings.HasSuffix(a, "/")
|
||||
bslash := strings.HasPrefix(b, "/")
|
||||
|
||||
if len(b) == 0 {
|
||||
return a
|
||||
}
|
||||
|
||||
switch {
|
||||
case aslash && bslash:
|
||||
return a + b[1:]
|
||||
|
||||
46
pkg/util/url_test.go
Normal file
46
pkg/util/url_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestUrl(t *testing.T) {
|
||||
|
||||
Convey("When joining two urls where right hand side is empty", t, func() {
|
||||
result := JoinUrlFragments("http://localhost:8080", "")
|
||||
|
||||
So(result, ShouldEqual, "http://localhost:8080")
|
||||
})
|
||||
|
||||
Convey("When joining two urls where right hand side is empty and lefthand side has a trailing slash", t, func() {
|
||||
result := JoinUrlFragments("http://localhost:8080/", "")
|
||||
|
||||
So(result, ShouldEqual, "http://localhost:8080/")
|
||||
})
|
||||
|
||||
Convey("When joining two urls where neither has a trailing slash", t, func() {
|
||||
result := JoinUrlFragments("http://localhost:8080", "api")
|
||||
|
||||
So(result, ShouldEqual, "http://localhost:8080/api")
|
||||
})
|
||||
|
||||
Convey("When joining two urls where lefthand side has a trailing slash", t, func() {
|
||||
result := JoinUrlFragments("http://localhost:8080/", "api")
|
||||
|
||||
So(result, ShouldEqual, "http://localhost:8080/api")
|
||||
})
|
||||
|
||||
Convey("When joining two urls where righthand side has preceding slash", t, func() {
|
||||
result := JoinUrlFragments("http://localhost:8080", "/api")
|
||||
|
||||
So(result, ShouldEqual, "http://localhost:8080/api")
|
||||
})
|
||||
|
||||
Convey("When joining two urls where righthand side has trailing slash", t, func() {
|
||||
result := JoinUrlFragments("http://localhost:8080", "api/")
|
||||
|
||||
So(result, ShouldEqual, "http://localhost:8080/api/")
|
||||
})
|
||||
}
|
||||
@@ -150,6 +150,9 @@ export function grafanaAppDirective(playlistSrv) {
|
||||
scope.$watch('contextSrv.sidemenu', newVal => {
|
||||
if (newVal !== undefined) {
|
||||
elem.toggleClass('sidemenu-open', scope.contextSrv.sidemenu);
|
||||
if (!newVal) {
|
||||
scope.contextSrv.setPinnedState(false);
|
||||
}
|
||||
}
|
||||
if (scope.contextSrv.sidemenu) {
|
||||
ignoreSideMenuHide = true;
|
||||
@@ -159,6 +162,12 @@ export function grafanaAppDirective(playlistSrv) {
|
||||
}
|
||||
});
|
||||
|
||||
scope.$watch('contextSrv.pinned', newVal => {
|
||||
if (newVal !== undefined) {
|
||||
elem.toggleClass('sidemenu-pinned', newVal);
|
||||
}
|
||||
});
|
||||
|
||||
// tooltip removal fix
|
||||
scope.$on("$routeChangeSuccess", function() {
|
||||
$("#tooltip, .tooltip").remove();
|
||||
@@ -182,7 +191,7 @@ export function grafanaAppDirective(playlistSrv) {
|
||||
}
|
||||
}
|
||||
// hide sidemenu
|
||||
if (!ignoreSideMenuHide && elem.find('.sidemenu').length > 0) {
|
||||
if (!ignoreSideMenuHide && !scope.contextSrv.pinned && elem.find('.sidemenu').length > 0) {
|
||||
if (target.parents('.sidemenu').length === 0) {
|
||||
scope.$apply(() => scope.contextSrv.toggleSideMenu());
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<div class="navbar navbar-static-top">
|
||||
<div class="navbar">
|
||||
<div class="navbar-inner"><div class="container-fluid">
|
||||
<div class="top-nav-btn top-nav-menu-btn">
|
||||
<a class="pointer" ng-click="ctrl.contextSrv.toggleSideMenu()">
|
||||
<span class="top-nav-logo-background">
|
||||
<img class="logo-icon" src="img/fav32.png"></img>
|
||||
<img class="logo-icon" src="img/grafana_icon.svg"></img>
|
||||
</span>
|
||||
<i class="icon-gf icon-gf-grafana_wordmark"></i>
|
||||
<i class="fa fa-caret-down"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
<div ng-controller="SearchCtrl" ng-init="init()" class="search-box">
|
||||
|
||||
<div class="search-field-wrapper">
|
||||
<span style="position: relative;">
|
||||
<input type="text" placeholder="Find dashboards by name" give-focus="giveSearchFocus" tabindex="1"
|
||||
ng-keydown="keyDown($event)" ng-model="query.query" ng-model-options="{ debounce: 500 }" spellcheck='false' ng-change="search()" />
|
||||
<input type="text" placeholder="Find dashboards by name" give-focus="ctrl.giveSearchFocus" tabindex="1"
|
||||
ng-keydown="ctrl.keyDown($event)" ng-model="ctrl.query.query" ng-model-options="{ debounce: 500 }" spellcheck='false' ng-change="ctrl.search()" />
|
||||
</span>
|
||||
<div class="search-switches">
|
||||
<i class="fa fa-filter"></i>
|
||||
<a class="pointer" href="javascript:void 0;" ng-click="showStarred()" tabindex="2">
|
||||
<i class="fa fa-remove" ng-show="query.starred"></i>
|
||||
<a class="pointer" href="javascript:void 0;" ng-click="ctrl.showStarred()" tabindex="2">
|
||||
<i class="fa fa-remove" ng-show="ctrl.query.starred"></i>
|
||||
starred
|
||||
</a> |
|
||||
<a class="pointer" href="javascript:void 0;" ng-click="getTags()" tabindex="3">
|
||||
<i class="fa fa-remove" ng-show="tagsMode"></i>
|
||||
<a class="pointer" href="javascript:void 0;" ng-click="ctrl.getTags()" tabindex="3">
|
||||
<i class="fa fa-remove" ng-show="ctrl.tagsMode"></i>
|
||||
tags
|
||||
</a>
|
||||
<span ng-if="query.tag.length">
|
||||
<span ng-if="ctrl.query.tag.length">
|
||||
|
|
||||
<span ng-repeat="tagName in query.tag">
|
||||
<a ng-click="removeTag(tagName, $event)" tag-color-from-name="tagName" class="label label-tag">
|
||||
<span ng-repeat="tagName in ctrl.query.tag">
|
||||
<a ng-click="ctrl.removeTag(tagName, $event)" tag-color-from-name="tagName" class="label label-tag">
|
||||
<i class="fa fa-remove"></i>
|
||||
{{tagName}}
|
||||
</a>
|
||||
@@ -27,12 +25,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-results-container" ng-if="tagsMode">
|
||||
<div class="search-results-container" ng-if="ctrl.tagsMode">
|
||||
<div class="row">
|
||||
<div class="span6 offset1">
|
||||
<div ng-repeat="tag in results" class="pointer" style="width: 180px; float: left;"
|
||||
<div ng-repeat="tag in ctrl.results" class="pointer" style="width: 180px; float: left;"
|
||||
ng-class="{'selected': $index === selectedIndex }"
|
||||
ng-click="filterByTag(tag.term, $event)">
|
||||
ng-click="ctrl.filterByTag(tag.term, $event)">
|
||||
<a class="search-result-tag label label-tag" tag-color-from-name="tag.term">
|
||||
<i class="fa fa-tag"></i>
|
||||
<span>{{tag.term}} ({{tag.count}})</span>
|
||||
@@ -42,14 +40,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-results-container" ng-if="!tagsMode">
|
||||
<h6 ng-hide="results.length">No dashboards matching your query were found.</h6>
|
||||
<div class="search-results-container" ng-if="!ctrl.tagsMode">
|
||||
<h6 ng-hide="ctrl.results.length">No dashboards matching your query were found.</h6>
|
||||
|
||||
<a class="search-item pointer search-item-{{row.type}}" bindonce ng-repeat="row in results"
|
||||
<a class="search-item pointer search-item-{{row.type}}" bindonce ng-repeat="row in ctrl.results"
|
||||
ng-class="{'selected': $index == selectedIndex}" ng-href="{{row.url}}">
|
||||
|
||||
<span class="search-result-tags">
|
||||
<span ng-click="filterByTag(tag, $event)" ng-repeat="tag in row.tags" tag-color-from-name="tag" class="label label-tag">
|
||||
<span ng-click="ctrl.filterByTag(tag, $event)" ng-repeat="tag in row.tags" tag-color-from-name="tag" class="label label-tag">
|
||||
{{tag}}
|
||||
</span>
|
||||
<i class="fa" ng-class="{'fa-star': row.isStarred, 'fa-star-o': !row.isStarred}"></i>
|
||||
@@ -63,15 +61,14 @@
|
||||
</div>
|
||||
|
||||
<div class="search-button-row">
|
||||
<button class="btn btn-inverse pull-left" ng-click="newDashboard()" ng-show="contextSrv.isEditor">
|
||||
<button class="btn btn-inverse pull-left" ng-click="ctrl.newDashboard()" ng-show="ctrl.contextSrv.isEditor">
|
||||
<i class="fa fa-plus"></i>
|
||||
New
|
||||
</button>
|
||||
<a class="btn btn-inverse pull-left" href="import/dashboard" ng-show="contextSrv.isEditor">
|
||||
<a class="btn btn-inverse pull-left" href="import/dashboard" ng-show="ctrl.contextSrv.isEditor">
|
||||
<i class="fa fa-download"></i>
|
||||
Import
|
||||
</a>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
150
public/app/core/components/search/search.ts
Normal file
150
public/app/core/components/search/search.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import angular from 'angular';
|
||||
import config from 'app/core/config';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import coreModule from '../../core_module';
|
||||
|
||||
export class SearchCtrl {
|
||||
query: any;
|
||||
giveSearchFocus: number;
|
||||
selectedIndex: number;
|
||||
results: any;
|
||||
currentSearchId: number;
|
||||
tagsMode: boolean;
|
||||
showImport: boolean;
|
||||
dismiss: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $scope, private $location, private $timeout, private backendSrv, private contextSrv) {
|
||||
this.giveSearchFocus = 0;
|
||||
this.selectedIndex = -1;
|
||||
this.results = [];
|
||||
this.query = { query: '', tag: [], starred: false };
|
||||
this.currentSearchId = 0;
|
||||
|
||||
$timeout(() => {
|
||||
this.giveSearchFocus = this.giveSearchFocus + 1;
|
||||
this.query.query = '';
|
||||
this.search();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
keyDown(evt) {
|
||||
if (evt.keyCode === 27) {
|
||||
this.dismiss();
|
||||
}
|
||||
if (evt.keyCode === 40) {
|
||||
this.moveSelection(1);
|
||||
}
|
||||
if (evt.keyCode === 38) {
|
||||
this.moveSelection(-1);
|
||||
}
|
||||
if (evt.keyCode === 13) {
|
||||
if (this.$scope.tagMode) {
|
||||
var tag = this.results[this.selectedIndex];
|
||||
if (tag) {
|
||||
this.filterByTag(tag.term, null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedDash = this.results[this.selectedIndex];
|
||||
if (selectedDash) {
|
||||
this.$location.search({});
|
||||
this.$location.path(selectedDash.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
moveSelection(direction) {
|
||||
var max = (this.results || []).length;
|
||||
var newIndex = this.selectedIndex + direction;
|
||||
this.selectedIndex = ((newIndex %= max) < 0) ? newIndex + max : newIndex;
|
||||
}
|
||||
|
||||
searchDashboards() {
|
||||
this.tagsMode = false;
|
||||
this.currentSearchId = this.currentSearchId + 1;
|
||||
var localSearchId = this.currentSearchId;
|
||||
|
||||
return this.backendSrv.search(this.query).then((results) => {
|
||||
if (localSearchId < this.currentSearchId) { return; }
|
||||
|
||||
this.results = _.map(results, function(dash) {
|
||||
dash.url = 'dashboard/' + dash.uri;
|
||||
return dash;
|
||||
});
|
||||
|
||||
if (this.queryHasNoFilters()) {
|
||||
this.results.unshift({ title: 'Home', url: config.appSubUrl + '/', type: 'dash-home' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
queryHasNoFilters() {
|
||||
var query = this.query;
|
||||
return query.query === '' && query.starred === false && query.tag.length === 0;
|
||||
};
|
||||
|
||||
filterByTag(tag, evt) {
|
||||
this.query.tag.push(tag);
|
||||
this.search();
|
||||
this.giveSearchFocus = this.giveSearchFocus + 1;
|
||||
if (evt) {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
removeTag(tag, evt) {
|
||||
this.query.tag = _.without(this.query.tag, tag);
|
||||
this.search();
|
||||
this.giveSearchFocus = this.giveSearchFocus + 1;
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
};
|
||||
|
||||
getTags() {
|
||||
return this.backendSrv.get('/api/dashboards/tags').then((results) => {
|
||||
this.tagsMode = !this.tagsMode;
|
||||
this.results = results;
|
||||
this.giveSearchFocus = this.giveSearchFocus + 1;
|
||||
if ( !this.tagsMode ) {
|
||||
this.search();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
showStarred() {
|
||||
this.query.starred = !this.query.starred;
|
||||
this.giveSearchFocus = this.giveSearchFocus + 1;
|
||||
this.search();
|
||||
};
|
||||
|
||||
search() {
|
||||
this.showImport = false;
|
||||
this.selectedIndex = 0;
|
||||
this.searchDashboards();
|
||||
};
|
||||
|
||||
newDashboard() {
|
||||
this.$location.url('dashboard/new');
|
||||
};
|
||||
}
|
||||
|
||||
export function searchDirective() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'app/core/components/search/search.html',
|
||||
controller: SearchCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
scope: {
|
||||
dismiss: '&'
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('search', searchDirective);
|
||||
@@ -62,5 +62,11 @@
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a class="sidemenu-item" target="_self" ng-hide="ctrl.contextSrv.pinned" ng-click="ctrl.contextSrv.setPinnedState(true)">
|
||||
<span class="icon-circle sidemenu-icon"><i class="fa fa-fw fa-thumb-tack"></i></span>
|
||||
<span class="sidemenu-item-text">Pin</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
@@ -22,8 +22,12 @@ export class SideMenuCtrl {
|
||||
this.appSubUrl = config.appSubUrl;
|
||||
this.showSignout = this.contextSrv.isSignedIn && !config['authProxyEnabled'];
|
||||
this.updateMenu();
|
||||
|
||||
this.$scope.$on('$routeChangeSuccess', () => {
|
||||
this.contextSrv.sidemenu = false;
|
||||
this.updateMenu();
|
||||
if (!this.contextSrv.pinned) {
|
||||
this.contextSrv.sidemenu = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -83,11 +87,11 @@ export class SideMenuCtrl {
|
||||
this.switchOrg(org.orgId);
|
||||
}
|
||||
});
|
||||
|
||||
if (config.allowOrgCreate) {
|
||||
this.orgMenu.push({text: "New organization", icon: "fa fa-fw fa-plus", url: this.getUrl('/org/new')});
|
||||
}
|
||||
});
|
||||
|
||||
if (config.allowOrgCreate) {
|
||||
this.orgMenu.push({text: "New organization", icon: "fa fa-fw fa-plus", url: this.getUrl('/org/new')});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -108,16 +112,23 @@ export class SideMenuCtrl {
|
||||
});
|
||||
|
||||
this.mainLinks.push({
|
||||
text: "Global Users",
|
||||
text: "Stats",
|
||||
icon: "fa fa-fw fa-bar-chart",
|
||||
url: this.getUrl("/admin/stats"),
|
||||
});
|
||||
|
||||
this.mainLinks.push({
|
||||
text: "Users",
|
||||
icon: "fa fa-fw fa-user",
|
||||
url: this.getUrl("/admin/users"),
|
||||
});
|
||||
|
||||
this.mainLinks.push({
|
||||
text: "Global Orgs",
|
||||
text: "Organizations",
|
||||
icon: "fa fa-fw fa-users",
|
||||
url: this.getUrl("/admin/orgs"),
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
updateMenu() {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
define([
|
||||
'./search_ctrl',
|
||||
'./inspect_ctrl',
|
||||
'./json_editor_ctrl',
|
||||
'./login_ctrl',
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'../core_module',
|
||||
'app/core/config',
|
||||
],
|
||||
function (angular, _, coreModule, config) {
|
||||
'use strict';
|
||||
|
||||
coreModule.default.controller('SearchCtrl', function($scope, $location, $timeout, backendSrv) {
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.giveSearchFocus = 0;
|
||||
$scope.selectedIndex = -1;
|
||||
$scope.results = [];
|
||||
$scope.query = { query: '', tag: [], starred: false };
|
||||
$scope.currentSearchId = 0;
|
||||
|
||||
$timeout(function() {
|
||||
$scope.giveSearchFocus = $scope.giveSearchFocus + 1;
|
||||
$scope.query.query = '';
|
||||
$scope.search();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
$scope.keyDown = function (evt) {
|
||||
if (evt.keyCode === 27) {
|
||||
$scope.dismiss();
|
||||
}
|
||||
if (evt.keyCode === 40) {
|
||||
$scope.moveSelection(1);
|
||||
}
|
||||
if (evt.keyCode === 38) {
|
||||
$scope.moveSelection(-1);
|
||||
}
|
||||
if (evt.keyCode === 13) {
|
||||
if ($scope.tagMode) {
|
||||
var tag = $scope.results[$scope.selectedIndex];
|
||||
if (tag) {
|
||||
$scope.filterByTag(tag.term);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedDash = $scope.results[$scope.selectedIndex];
|
||||
if (selectedDash) {
|
||||
$location.search({});
|
||||
$location.path(selectedDash.url);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$scope.moveSelection = function(direction) {
|
||||
var max = ($scope.results || []).length;
|
||||
var newIndex = $scope.selectedIndex + direction;
|
||||
$scope.selectedIndex = ((newIndex %= max) < 0) ? newIndex + max : newIndex;
|
||||
};
|
||||
|
||||
$scope.searchDashboards = function() {
|
||||
$scope.tagsMode = false;
|
||||
$scope.currentSearchId = $scope.currentSearchId + 1;
|
||||
var localSearchId = $scope.currentSearchId;
|
||||
|
||||
return backendSrv.search($scope.query).then(function(results) {
|
||||
if (localSearchId < $scope.currentSearchId) { return; }
|
||||
|
||||
$scope.results = _.map(results, function(dash) {
|
||||
dash.url = 'dashboard/' + dash.uri;
|
||||
return dash;
|
||||
});
|
||||
|
||||
if ($scope.queryHasNoFilters()) {
|
||||
$scope.results.unshift({ title: 'Home', url: config.appSubUrl + '/', type: 'dash-home' });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.queryHasNoFilters = function() {
|
||||
var query = $scope.query;
|
||||
return query.query === '' && query.starred === false && query.tag.length === 0;
|
||||
};
|
||||
|
||||
$scope.filterByTag = function(tag, evt) {
|
||||
$scope.query.tag.push(tag);
|
||||
$scope.search();
|
||||
$scope.giveSearchFocus = $scope.giveSearchFocus + 1;
|
||||
if (evt) {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.removeTag = function(tag, evt) {
|
||||
$scope.query.tag = _.without($scope.query.tag, tag);
|
||||
$scope.search();
|
||||
$scope.giveSearchFocus = $scope.giveSearchFocus + 1;
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
};
|
||||
|
||||
$scope.getTags = function() {
|
||||
return backendSrv.get('/api/dashboards/tags').then(function(results) {
|
||||
$scope.tagsMode = true;
|
||||
$scope.results = results;
|
||||
$scope.giveSearchFocus = $scope.giveSearchFocus + 1;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showStarred = function() {
|
||||
$scope.query.starred = !$scope.query.starred;
|
||||
$scope.giveSearchFocus = $scope.giveSearchFocus + 1;
|
||||
$scope.search();
|
||||
};
|
||||
|
||||
$scope.search = function() {
|
||||
$scope.showImport = false;
|
||||
$scope.selectedIndex = 0;
|
||||
$scope.searchDashboards();
|
||||
};
|
||||
|
||||
$scope.newDashboard = function() {
|
||||
$location.url('dashboard/new');
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -15,7 +15,6 @@ import "./directives/ng_model_on_blur";
|
||||
import "./directives/password_strenght";
|
||||
import "./directives/spectrum_picker";
|
||||
import "./directives/tags";
|
||||
import "./directives/topnav";
|
||||
import "./directives/value_select_dropdown";
|
||||
import "./directives/give_focus";
|
||||
import './jquery_extended';
|
||||
@@ -23,6 +22,7 @@ import './partials';
|
||||
|
||||
import {grafanaAppDirective} from './components/grafana_app';
|
||||
import {sideMenuDirective} from './components/sidemenu/sidemenu';
|
||||
import {searchDirective} from './components/search/search';
|
||||
import {navbarDirective} from './components/navbar/navbar';
|
||||
import {arrayJoin} from './directives/array_join';
|
||||
import 'app/core/controllers/all';
|
||||
@@ -31,4 +31,4 @@ import 'app/core/routes/all';
|
||||
import './filters/filters';
|
||||
import coreModule from './core_module';
|
||||
|
||||
export {arrayJoin, coreModule, grafanaAppDirective, sideMenuDirective, navbarDirective};
|
||||
export {arrayJoin, coreModule, grafanaAppDirective, sideMenuDirective, navbarDirective, searchDirective};
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
define([
|
||||
'../core_module',
|
||||
],
|
||||
function (coreModule) {
|
||||
'use strict';
|
||||
|
||||
coreModule.default.directive('topnav', function($rootScope, contextSrv) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
transclude: true,
|
||||
scope: {
|
||||
title: "@",
|
||||
section: "@",
|
||||
titleUrl: "@",
|
||||
subnav: "=",
|
||||
},
|
||||
template:
|
||||
'<div class="navbar navbar-static-top"><div class="navbar-inner"><div class="container-fluid">' +
|
||||
'<div class="top-nav">' +
|
||||
'<div class="top-nav-btn top-nav-menu-btn">' +
|
||||
'<a class="pointer" ng-click="contextSrv.toggleSideMenu()">' +
|
||||
'<span class="top-nav-logo-background">' +
|
||||
'<img class="logo-icon" src="img/fav32.png"></img>' +
|
||||
'</span>' +
|
||||
'<i class="fa fa-caret-down"></i>' +
|
||||
'</a>' +
|
||||
'</div>' +
|
||||
|
||||
'<span class="icon-circle top-nav-icon">' +
|
||||
'<i ng-class="icon"></i>' +
|
||||
'</span>' +
|
||||
|
||||
'<span ng-show="section">' +
|
||||
'<span class="top-nav-title">{{section}}</span>' +
|
||||
'<i class="top-nav-breadcrumb-icon fa fa-angle-right"></i>' +
|
||||
'</span>' +
|
||||
|
||||
'<a ng-href="{{titleUrl}}" class="top-nav-title">' +
|
||||
'{{title}}' +
|
||||
'</a>' +
|
||||
'<i ng-show="subnav" class="top-nav-breadcrumb-icon fa fa-angle-right"></i>' +
|
||||
'</div><div ng-transclude></div></div></div></div>',
|
||||
link: function(scope, elem, attrs) {
|
||||
scope.icon = attrs.icon;
|
||||
scope.contextSrv = contextSrv;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
@@ -112,6 +112,11 @@ define([
|
||||
templateUrl: 'app/features/admin/partials/edit_org.html',
|
||||
controller : 'AdminEditOrgCtrl',
|
||||
})
|
||||
.when('/admin/stats', {
|
||||
templateUrl: 'app/features/admin/partials/stats.html',
|
||||
controller : 'AdminStatsCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
})
|
||||
.when('/login', {
|
||||
templateUrl: 'app/partials/login.html',
|
||||
controller : 'LoginCtrl',
|
||||
|
||||
@@ -20,10 +20,23 @@ function (angular, _, coreModule, store, config) {
|
||||
return this.user.orgRole === role;
|
||||
};
|
||||
|
||||
this.setPinnedState = function(val) {
|
||||
this.pinned = val;
|
||||
store.set('grafana.sidemenu.pinned', val);
|
||||
};
|
||||
|
||||
this.toggleSideMenu = function() {
|
||||
this.sidemenu = !this.sidemenu;
|
||||
if (!this.sidemenu) {
|
||||
this.setPinnedState(false);
|
||||
}
|
||||
};
|
||||
|
||||
this.pinned = store.getBool('grafana.sidemenu.pinned', false);
|
||||
if (this.pinned) {
|
||||
this.sidemenu = true;
|
||||
}
|
||||
|
||||
this.version = config.buildInfo.version;
|
||||
this.lightTheme = false;
|
||||
this.user = new User();
|
||||
|
||||
@@ -41,6 +41,7 @@ export default class TimeSeries {
|
||||
nullPointMode: any;
|
||||
fillBelowTo: any;
|
||||
transform: any;
|
||||
flotpairs: any;
|
||||
|
||||
constructor(opts) {
|
||||
this.datapoints = opts.datapoints;
|
||||
|
||||
37
public/app/core/utils/file_export.ts
Normal file
37
public/app/core/utils/file_export.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
declare var window: any;
|
||||
|
||||
export function exportSeriesListToCsv(seriesList) {
|
||||
var text = 'Series;Time;Value\n';
|
||||
_.each(seriesList, function(series) {
|
||||
_.each(series.datapoints, function(dp) {
|
||||
text += series.alias + ';' + new Date(dp[1]).toISOString() + ';' + dp[0] + '\n';
|
||||
});
|
||||
});
|
||||
saveSaveBlob(text, 'grafana_data_export.csv');
|
||||
};
|
||||
|
||||
export function exportTableDataToCsv(table) {
|
||||
var text = '';
|
||||
// add header
|
||||
_.each(table.columns, function(column) {
|
||||
text += column.text + ';';
|
||||
});
|
||||
text += '\n';
|
||||
// process data
|
||||
_.each(table.rows, function(row) {
|
||||
_.each(row, function(value) {
|
||||
text += value + ';';
|
||||
});
|
||||
text += '\n';
|
||||
});
|
||||
saveSaveBlob(text, 'grafana_data_export.csv');
|
||||
};
|
||||
|
||||
export function saveSaveBlob(payload, fname) {
|
||||
var blob = new Blob([payload], { type: "text/csv;charset=utf-8" });
|
||||
window.saveAs(blob, fname);
|
||||
};
|
||||
@@ -179,17 +179,6 @@ function($, _) {
|
||||
.replace(/ +/g,'-');
|
||||
};
|
||||
|
||||
kbn.exportSeriesListToCsv = function(seriesList) {
|
||||
var text = 'Series;Time;Value\n';
|
||||
_.each(seriesList, function(series) {
|
||||
_.each(series.datapoints, function(dp) {
|
||||
text += series.alias + ';' + new Date(dp[1]).toISOString() + ';' + dp[0] + '\n';
|
||||
});
|
||||
});
|
||||
var blob = new Blob([text], { type: "text/csv;charset=utf-8" });
|
||||
window.saveAs(blob, 'grafana_data_export.csv');
|
||||
};
|
||||
|
||||
kbn.stringToJsRegex = function(str) {
|
||||
if (str[0] !== '/') {
|
||||
return new RegExp('^' + str + '$');
|
||||
|
||||
18
public/app/features/admin/adminStatsCtrl.ts
Normal file
18
public/app/features/admin/adminStatsCtrl.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import angular from 'angular';
|
||||
|
||||
export class AdminStatsCtrl {
|
||||
stats: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv: any) {}
|
||||
|
||||
init() {
|
||||
this.backendSrv.get('/api/admin/stats').then(stats => {
|
||||
this.stats = stats;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('grafana.controllers').controller('AdminStatsCtrl', AdminStatsCtrl);
|
||||
@@ -4,4 +4,5 @@ define([
|
||||
'./adminEditOrgCtrl',
|
||||
'./adminEditUserCtrl',
|
||||
'./adminSettingsCtrl',
|
||||
'./adminStatsCtrl',
|
||||
], function () {});
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<topnav icon="fa fa-fw fa-user" title="Global Users" subnav="true">
|
||||
<navbar icon="fa fa-fw fa-user" title="Organizations" title-url="admin/orgs" subnav="true">
|
||||
<ul class="nav">
|
||||
<li><a href="admin/orgs">List</a></li>
|
||||
<li class="active"><a href="admin/orgs/edit/{{org.id}}">Edit Org</a></li>
|
||||
<li class="active"><a href="admin/orgs/edit/{{org.id}}">{{org.name}}</a></li>
|
||||
</ul>
|
||||
</topnav>
|
||||
</navbar>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<topnav icon="fa fa-fw fa-user" title="Global Users" subnav="true">
|
||||
<navbar icon="fa fa-fw fa-user" title="Users" title-url="admin/users" subnav="true">
|
||||
<ul class="nav">
|
||||
<li><a href="admin/users">Users</a></li>
|
||||
<li><a href="admin/users/create">Create user</a></li>
|
||||
<li class="active"><a href="admin/users/edit/{{user_id}}">Edit user</a></li>
|
||||
</ul>
|
||||
</topnav>
|
||||
</navbar>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
<topnav icon="fa fa-fw fa-cogs" title="Global Users" subnav="true">
|
||||
<navbar icon="fa fa-fw fa-user" title="Users" title-url="admin/users" subnav="true">
|
||||
<ul class="nav">
|
||||
<li><a href="admin/users">Users</a></li>
|
||||
<li class="active"><a href="admin/users/create">Create user</a></li>
|
||||
<li class="active"><a href="admin/users/create">Add user</a></li>
|
||||
</ul>
|
||||
</topnav>
|
||||
</navbar>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
<h2>
|
||||
Create a new user
|
||||
Add new user
|
||||
</h2>
|
||||
|
||||
<form name="userForm">
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
<topnav icon="fa fa-fw fa-users" title="Global Orgs" subnav="true">
|
||||
<ul class="nav">
|
||||
<li class="active"><a href="admin/orgs">List</a></li>
|
||||
</ul>
|
||||
</topnav>
|
||||
<navbar icon="fa fa-fw fa-users" title="Organizations">
|
||||
</navbar>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page-wide">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<topnav icon="fa fa-fw fa-info" title="System info">
|
||||
</topnav>
|
||||
<navbar icon="fa fa-fw fa-info" title="System info">
|
||||
</navbar>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
|
||||
57
public/app/features/admin/partials/stats.html
Normal file
57
public/app/features/admin/partials/stats.html
Normal file
@@ -0,0 +1,57 @@
|
||||
<navbar icon="fa fa-fw fa-bar-chart" title="Stats">
|
||||
</navbar>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page-wide" ng-init="ctrl.init()">
|
||||
<h1>
|
||||
Overview
|
||||
</h1>
|
||||
|
||||
<table class="filter-table form-inline">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Total dashboards</td>
|
||||
<td>{{ctrl.stats.dashboard_count}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total users</td>
|
||||
<td>{{ctrl.stats.user_count}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total grafana admins</td>
|
||||
<td>{{ctrl.stats.grafana_admin_count}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total organizations</td>
|
||||
<td>{{ctrl.stats.org_count}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total datasources</td>
|
||||
<td>{{ctrl.stats.data_source_count}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total playlists</td>
|
||||
<td>{{ctrl.stats.playlist_count}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total snapshots</td>
|
||||
<td>{{ctrl.stats.db_snapshot_count}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total dashboard tags</td>
|
||||
<td>{{ctrl.stats.db_tag_count}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total starred dashboards</td>
|
||||
<td>{{ctrl.stats.starred_db_count}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,15 +1,14 @@
|
||||
<topnav icon="fa fa-fw fa-user" title="Global Users" subnav="true">
|
||||
<ul class="nav">
|
||||
<li class="active"><a href="admin/users">List</a></li>
|
||||
<li><a href="admin/users/create">Create user</a></li>
|
||||
</ul>
|
||||
</topnav>
|
||||
<navbar icon="fa fa-fw fa-user" title="Users" title-url="admin/users">
|
||||
</navbar>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page-wide">
|
||||
<h1>
|
||||
Users
|
||||
</h1>
|
||||
<a class="btn btn-inverse pull-right" href="admin/users/create">
|
||||
<i class="fa fa-plus"></i>
|
||||
Add new user
|
||||
</a>
|
||||
|
||||
<h1>Users</h1>
|
||||
|
||||
<table class="filter-table form-inline">
|
||||
<thead>
|
||||
|
||||
@@ -24,6 +24,7 @@ export class AppEditCtrl {
|
||||
enabled: this.appModel.enabled,
|
||||
pinned: this.appModel.pinned,
|
||||
jsonData: this.appModel.jsonData,
|
||||
secureJsonData: this.appModel.secureJsonData,
|
||||
}, options);
|
||||
|
||||
this.backendSrv.post(`/api/org/apps/${this.$routeParams.appId}/settings`, updateCmd).then(function() {
|
||||
|
||||
@@ -98,6 +98,8 @@
|
||||
<div class="simple-box-body">
|
||||
<div ng-if="ctrl.appModel.appId">
|
||||
<app-config-view app-model="ctrl.appModel"></app-config-view>
|
||||
<div class="clearfix"></div>
|
||||
<button type="submit" class="btn btn-success" ng-click="ctrl.update()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
<topnav title="Apps" icon="fa fa-fw fa-cubes" subnav="true">
|
||||
<ul class="nav">
|
||||
<li class="active" ><a href="org/apps">Overview</a></li>
|
||||
</ul>
|
||||
</topnav>
|
||||
<navbar title="Apps" icon="fa fa-fw fa-cubes">
|
||||
</navbar>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page-wide" ng-init="ctrl.init()">
|
||||
|
||||
@@ -234,9 +234,9 @@ function (angular, $, _, moment) {
|
||||
var i, j, k;
|
||||
var oldVersion = this.schemaVersion;
|
||||
var panelUpgrades = [];
|
||||
this.schemaVersion = 8;
|
||||
this.schemaVersion = 9;
|
||||
|
||||
if (oldVersion === 8) {
|
||||
if (oldVersion === this.schemaVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -390,6 +390,23 @@ function (angular, $, _, moment) {
|
||||
});
|
||||
}
|
||||
|
||||
// schema version 9 changes
|
||||
if (oldVersion < 9) {
|
||||
// move aliasYAxis changes
|
||||
panelUpgrades.push(function(panel) {
|
||||
if (panel.type !== 'singlestat' && panel.thresholds !== "") { return; }
|
||||
|
||||
if (panel.thresholds) {
|
||||
var k = panel.thresholds.split(",");
|
||||
|
||||
if (k.length >= 3) {
|
||||
k.shift();
|
||||
panel.thresholds = k.join(",");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (panelUpgrades.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<div class="top-nav-snapshot-title" ng-if="dashboardMeta.isSnapshot">
|
||||
<a class="pointer" bs-tooltip="titleTooltip" data-placement="bottom">
|
||||
<i class="gf-icon gf-icon-snap-multi"></i>
|
||||
<i class="icon-gf icon-gf-snapshot"></i>
|
||||
<span class="dashboard-title">
|
||||
{{dashboard.title}}
|
||||
<em class="small"> (snapshot)</em>
|
||||
@@ -24,8 +24,16 @@
|
||||
<i class="fa" ng-class="{'fa-star-o': !dashboardMeta.isStarred, 'fa-star': dashboardMeta.isStarred}" style="color: orange;"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li ng-show="dashboardMeta.canShare">
|
||||
<a class="pointer" ng-click="shareDashboard()" bs-tooltip="'Share dashboard'" data-placement="bottom"><i class="fa fa-share-square-o"></i></a>
|
||||
<li ng-show="dashboardMeta.canShare" class="dropdown">
|
||||
<a class="pointer" ng-click="hideTooltip($event)" bs-tooltip="'Share dashboard'" data-placement="bottom" data-toggle="dropdown"><i class="fa fa-share-square-o"></i></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="shareDashboard(0)">
|
||||
<i class="fa fa-link"></i>
|
||||
Link to Dashboard</a></li>
|
||||
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="shareDashboard(1)">
|
||||
<i class="gf-icon gf-icon-snap-multi"></i>
|
||||
Snapshot sharing</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li ng-show="dashboardMeta.canSave">
|
||||
<a ng-click="saveDashboard()" bs-tooltip="'Save dashboard'" data-placement="bottom"><i class="fa fa-save"></i></a>
|
||||
|
||||
@@ -42,10 +42,13 @@ export class DashNavCtrl {
|
||||
}
|
||||
};
|
||||
|
||||
$scope.shareDashboard = function() {
|
||||
$scope.shareDashboard = function(tabIndex) {
|
||||
var modalScope = $scope.$new();
|
||||
modalScope.tabIndex = tabIndex;
|
||||
|
||||
$scope.appEvent('show-modal', {
|
||||
src: './app/features/dashboard/partials/shareModal.html',
|
||||
scope: $scope.$new(),
|
||||
scope: modalScope
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ function (angular, $) {
|
||||
editorScope = null;
|
||||
};
|
||||
|
||||
var view = $('<div class="search-container" ng-include="\'app/partials/search.html\'"></div>');
|
||||
var view = $('<search class="search-container" dismiss="dismiss()"></search>');
|
||||
|
||||
elem.append(view);
|
||||
$compile(elem.contents())(editorScope);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<topnav icon="fa fa-th-large" title="Dashboards" subnav="true">
|
||||
<navbar icon="fa fa-th-large" title="Dashboards" subnav="true">
|
||||
<ul class="nav">
|
||||
<li class="active"><a href="import/dashboard">Import</a></li>
|
||||
</ul>
|
||||
</topnav>
|
||||
</navbar>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
Title
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" class="input-xlarge tight-form-input" ng-model='dashboard.title'></input>
|
||||
<input type="text" class="input-large tight-form-input" ng-model='dashboard.title'></input>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Tags
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
|
||||
<script type="text/ng-template" id="shareLink.html">
|
||||
<div class="share-modal-big-icon">
|
||||
<i class="fa fa-external-link"></i>
|
||||
<i class="fa fa-link"></i>
|
||||
</div>
|
||||
|
||||
<div ng-include src="'shareLinkOptions.html'"></div>
|
||||
@@ -110,7 +110,7 @@
|
||||
<div class="ng-cloak" ng-cloak ng-controller="ShareSnapshotCtrl" ng-init="init()">
|
||||
<div class="share-modal-big-icon">
|
||||
<i ng-if="loading" class="fa fa-spinner fa-spin"></i>
|
||||
<i ng-if="!loading" class="gf-icon gf-icon-snap-multi"></i>
|
||||
<i ng-if="!loading" class="icon-gf icon-gf-snapshot"></i>
|
||||
</div>
|
||||
|
||||
<div class="share-snapshot-header" ng-if="step === 1">
|
||||
|
||||
@@ -12,7 +12,7 @@ function (angular, _, require, config) {
|
||||
module.controller('ShareModalCtrl', function($scope, $rootScope, $location, $timeout, timeSrv, $element, templateSrv, linkSrv) {
|
||||
|
||||
$scope.options = { forCurrent: true, includeTemplateVars: true, theme: 'current' };
|
||||
$scope.editor = { index: 0 };
|
||||
$scope.editor = { index: $scope.tabIndex || 0};
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.modeSharePanel = $scope.panel ? true : false;
|
||||
|
||||
@@ -7,6 +7,7 @@ export class SubmenuCtrl {
|
||||
variables: any;
|
||||
dashboard: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $rootScope, private templateValuesSrv, private dynamicDashboardSrv) {
|
||||
this.annotations = this.dashboard.templating.list;
|
||||
this.variables = this.dashboard.templating.list;
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
<div class="gf-box-header">
|
||||
<div class="gf-box-title">
|
||||
<i class="fa fa-clock-o"></i>
|
||||
Custom time range
|
||||
</div>
|
||||
<button class="gf-box-header-close-btn" ng-click="dismiss();">
|
||||
<i class="fa fa-remove"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="gf-box-body">
|
||||
<div class="timepicker form-horizontal">
|
||||
<form name="timeForm" style="margin-bottom: 0">
|
||||
|
||||
<div class="timepicker-from-column">
|
||||
<label class="small">From</label>
|
||||
<div class="fake-input timepicker-input">
|
||||
<input class="timepicker-date" type="text" ng-change="validate(temptime)" ng-model="temptime.from.date" data-date-format="yyyy-mm-dd" required bs-datepicker />@
|
||||
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.hour" required ng-pattern="patterns.hour" onClick="this.select();"/>:
|
||||
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.minute" required ng-pattern="patterns.minute" onClick="this.select();"/>:
|
||||
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.second" required ng-pattern="patterns.second" onClick="this.select();"/>.
|
||||
<input class="timepicker-ms" type="text" maxlength="3" ng-change="validate(temptime)" ng-model="temptime.from.millisecond" required ng-pattern="patterns.millisecond" onClick="this.select();"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timepicker-to-column">
|
||||
|
||||
<label class="small">To (<a class="link" ng-class="{'strong':temptime.now}" ng-click="ctrl.setNow();temptime.now=true">set now</a>)</label>
|
||||
|
||||
<div class="fake-input timepicker-input">
|
||||
<div ng-hide="temptime.now">
|
||||
<input class="timepicker-date" type="text" ng-change="validate(temptime)" ng-model="temptime.to.date" data-date-format="yyyy-mm-dd" required bs-datepicker />@
|
||||
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.hour" required ng-pattern="patterns.hour" onClick="this.select();"/>:
|
||||
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.minute" required ng-pattern="patterns.minute" onClick="this.select();"/>:
|
||||
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.second" required ng-pattern="patterns.second" onClick="this.select();"/>.
|
||||
<input class="timepicker-ms" type="text" maxlength="3" ng-change="validate(temptime)" ng-model="temptime.to.millisecond" required ng-pattern="patterns.millisecond" onClick="this.select();"/>
|
||||
</div>
|
||||
<span type="text" ng-show="temptime.now" ng-disabled="temptime.now">  <i class="pointer fa fa-remove" ng-click="ctrl.setNow();temptime.now=false;"></i> Right Now <input type="text" name="dummy" style="visibility:hidden" /></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<button ng-click="ctrl.setAbsoluteTimeFilter(ctrl.validate(temptime));dismiss();" ng-disabled="!timeForm.$valid" class="btn btn-success">Apply</button>
|
||||
<span class="" ng-hide="input.$valid">Invalid date or range</span>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="row pull-right">
|
||||
<div class="gf-timepicker-absolute-section">
|
||||
<form name="timeForm" class="gf-timepicker-absolute-section">
|
||||
<h3>Time range</h3>
|
||||
<label class="small">From:</label>
|
||||
<div class="input-prepend">
|
||||
@@ -29,10 +29,10 @@
|
||||
<select ng-model="ctrl.refresh.value" class='input-medium' ng-options="f.value as f.text for f in ctrl.refresh.options">
|
||||
</select>
|
||||
|
||||
<button class="btn btn-inverse gf-timepicker-btn-apply" type="button" ng-click="ctrl.applyCustom()">
|
||||
<button type="submit" class="btn btn-primary" ng-click="ctrl.applyCustom();" ng-disabled="!timeForm.$valid">
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="gf-timepicker-relative-section">
|
||||
<h3>Quick ranges</h3>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import moment from 'moment';
|
||||
import * as dateMath from 'app/core/utils/datemath';
|
||||
|
||||
export function inputDateDirective() {
|
||||
return {
|
||||
@@ -11,8 +12,14 @@ export function inputDateDirective() {
|
||||
|
||||
var fromUser = function (text) {
|
||||
if (text.indexOf('now') !== -1) {
|
||||
if (!dateMath.isValid(text)) {
|
||||
ngModel.$setValidity("error", false);
|
||||
return undefined;
|
||||
}
|
||||
ngModel.$setValidity("error", true);
|
||||
return text;
|
||||
}
|
||||
|
||||
var parsed;
|
||||
if ($scope.ctrl.isUtc) {
|
||||
parsed = moment.utc(text, format);
|
||||
@@ -20,7 +27,13 @@ export function inputDateDirective() {
|
||||
parsed = moment(text, format);
|
||||
}
|
||||
|
||||
return parsed.isValid() ? parsed : undefined;
|
||||
if (!parsed.isValid()) {
|
||||
ngModel.$setValidity("error", false);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
ngModel.$setValidity("error", true);
|
||||
return parsed;
|
||||
};
|
||||
|
||||
var toUser = function (currentValue) {
|
||||
|
||||
@@ -159,7 +159,7 @@ function (angular, _) {
|
||||
};
|
||||
|
||||
updateDashLinks();
|
||||
$rootScope.onAppEvent('dash-links-updated', updateDashLinks, $rootScope);
|
||||
$rootScope.onAppEvent('dash-links-updated', updateDashLinks, $scope);
|
||||
});
|
||||
|
||||
module.controller('DashLinkEditorCtrl', function($scope, $rootScope) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<topnav title="Data sources" title-url="datasources" icon="fa fa-fw fa-database" subnav="true">
|
||||
<navbar title="Data sources" title-url="datasources" icon="fa fa-fw fa-database" subnav="true">
|
||||
<ul class="nav">
|
||||
<li ng-class="{active: isNew}" ng-show="isNew"><a href="datasources/new">Add new</a></li>
|
||||
<li class="active" ng-show="!isNew"><a href="datasources/edit/{{current.name}}">{{current.name}}</a></li>
|
||||
</ul>
|
||||
</topnav>
|
||||
</navbar>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<topnav title="Data sources" icon="fa fa-fw fa-database" subnav="false">
|
||||
</topnav>
|
||||
<navbar title="Data sources" icon="fa fa-fw fa-database">
|
||||
</navbar>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page-wide">
|
||||
|
||||
<a type="submit" class="btn btn-inverse pull-right" href="datasources/new">
|
||||
<a class="btn btn-inverse pull-right" href="datasources/new">
|
||||
<i class="fa fa-plus"></i>
|
||||
Add data source
|
||||
</a>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<topnav title="Organization" icon="fa fa-fw fa-users" subnav="true">
|
||||
<navbar title="Organization" icon="fa fa-fw fa-users" subnav="true">
|
||||
<ul class="nav">
|
||||
<li class="active"><a href="org/new">New organization</a></li>
|
||||
</ul>
|
||||
</topnav>
|
||||
</navbar>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<topnav icon="fa fa-fw fa-users" title="Organization" subnav="true">
|
||||
<navbar icon="fa fa-fw fa-users" title="Organization" subnav="true">
|
||||
<ul class="nav">
|
||||
<li class="active"><a href="org/apikeys">API Keys</a></li>
|
||||
</ul>
|
||||
</topnav>
|
||||
</navbar>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page-wide">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<topnav icon="fa fa-fw fa-users" title="Organization" subnav="true">
|
||||
<navbar icon="fa fa-fw fa-users" title="Organization">
|
||||
<ul class="nav">
|
||||
<li class="active"><a href="org">Preferences</a></li>
|
||||
</ul>
|
||||
</topnav>
|
||||
</navbar>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<topnav title="Organization" icon="fa fa-fw fa-users" subnav="true">
|
||||
<navbar title="Organization" icon="fa fa-fw fa-users" subnav="true">
|
||||
<ul class="nav">
|
||||
<li class="active"><a href="org/users">Users</a></li>
|
||||
</ul>
|
||||
</topnav>
|
||||
</navbar>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page-wide">
|
||||
|
||||
@@ -123,7 +123,7 @@ function (angular, $, _) {
|
||||
}
|
||||
|
||||
var menuTemplate;
|
||||
if ($(e.target).hasClass('fa-external-link')) {
|
||||
if ($(e.target).hasClass('fa-link')) {
|
||||
menuTemplate = createExternalLinkMenu($scope);
|
||||
} else {
|
||||
menuTemplate = createMenuTemplate($scope);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
define([
|
||||
'./playlists_ctrl',
|
||||
'./playlist_search',
|
||||
'./playlist_srv',
|
||||
'./playlist_edit_ctrl',
|
||||
'./playlist_routes'
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<p class="text-center">Are you sure want to delete "{{playlist.title}}" playlist?</p>
|
||||
<p class="text-center">
|
||||
<button type="button" class="btn btn-danger" ng-click="removePlaylist()">Yes</button>
|
||||
<button type="button" class="btn btn-default" ng-click="dismiss()">No</button>
|
||||
</p>
|
||||
@@ -1,14 +1,14 @@
|
||||
<navbar title="Playlists" title-url="playlists" icon="fa fa-fw fa-list" subnav="true">
|
||||
<ul class="nav">
|
||||
<li ng-class="{active: isNew()}" ng-show="isNew()"><a href="datasources/create">New</a></li>
|
||||
<li class="active" ng-show="!isNew()"><a href="playlists/edit/{{playlist.id}}">{{playlist.name}}</a></li>
|
||||
<li ng-class="{active: ctrl.isNew()}" ng-show="ctrl.isNew()"><a href="datasources/create">New</a></li>
|
||||
<li class="active" ng-show="!ctrl.isNew()"><a href="playlists/edit/{{ctrl.playlist.id}}">{{ctrl.playlist.name}}</a></li>
|
||||
</ul>
|
||||
</navbar>
|
||||
|
||||
<div class="page-container" ng-form="playlistEditForm">
|
||||
<div class="page">
|
||||
<h2 ng-show="isNew()">New playlist</h2>
|
||||
<h2 ng-show="!isNew()">Edit playlist</h2>
|
||||
<h2 ng-show="ctrl.isNew()">New playlist</h2>
|
||||
<h2 ng-show="!ctrl.isNew()">Edit playlist</h2>
|
||||
|
||||
<h4>Name and interval</h4>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
Name
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" required ng-model="playlist.name" class="input-xlarge tight-form-input">
|
||||
<input type="text" required ng-model="ctrl.playlist.name" class="input-xlarge tight-form-input">
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
@@ -31,7 +31,7 @@
|
||||
Interval
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" required ng-model="playlist.interval" placeholder="5m" class="input-xlarge tight-form-input">
|
||||
<input type="text" required ng-model="ctrl.playlist.interval" placeholder="5m" class="input-xlarge tight-form-input">
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
@@ -39,66 +39,72 @@
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<h4>Add dashboards</h4>
|
||||
|
||||
<div style="display: inline-block">
|
||||
<div class="tight-form last">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item">
|
||||
Search
|
||||
</li>
|
||||
<li>
|
||||
<input type="text"
|
||||
class="tight-form-input input-xlarge last"
|
||||
ng-model="searchQuery"
|
||||
placeholder="dashboard search term"
|
||||
ng-trim="true"
|
||||
ng-change="search()">
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="span5 pull-left">
|
||||
<h5>Search results ({{filteredPlaylistItems.length}})</h5>
|
||||
<h5>Add dashboards</h5>
|
||||
<div style="">
|
||||
<playlist-search class="playlist-search-container" search-started="ctrl.searchStarted(promise)"></playlist-search>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="span5 pull-left" ng-if="ctrl.filteredDashboards.length > 0">
|
||||
<h5>Search results ({{ctrl.filteredDashboards.length}})</h5>
|
||||
<table class="grafana-options-table">
|
||||
<tr ng-repeat="playlistItem in filteredPlaylistItems">
|
||||
<tr ng-repeat="playlistItem in ctrl.filteredDashboards">
|
||||
<td style="white-space: nowrap;">
|
||||
{{playlistItem.title}}
|
||||
</td>
|
||||
<td style="text-align: center">
|
||||
<button class="btn btn-inverse btn-mini pull-right" ng-click="addPlaylistItem(playlistItem)">
|
||||
<button class="btn btn-inverse btn-mini pull-right" ng-click="ctrl.addPlaylistItem(playlistItem)">
|
||||
<i class="fa fa-plus"></i>
|
||||
Add to playlist
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="isSearchResultsEmpty()">
|
||||
<td colspan="2">
|
||||
<i class="fa fa-warning"></i> Search results empty
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="playlist-search-results-container" ng-if="ctrl.filteredTags.length > 0">
|
||||
<div class="row">
|
||||
<div class="span6 offset1">
|
||||
<div ng-repeat="tag in ctrl.filteredTags" class="pointer" style="width: 180px; float: left;"
|
||||
ng-class="{'selected': $index === selectedIndex }"
|
||||
ng-click="ctrl.addTagPlaylistItem(tag, $event)">
|
||||
<a class="search-result-tag label label-tag" tag-color-from-name="tag.term">
|
||||
<i class="fa fa-tag"></i>
|
||||
<span>{{tag.term}} ({{tag.count}})</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="span5 pull-left">
|
||||
<h5>Added dashboards</h5>
|
||||
<table class="grafana-options-table">
|
||||
<tr ng-repeat="playlistItem in playlistItems">
|
||||
<td style="white-space: nowrap;">
|
||||
<tr ng-repeat="playlistItem in ctrl.playlistItems">
|
||||
<td style="white-space: nowrap;" ng-if="playlistItem.type === 'dashboard_by_id'">
|
||||
{{playlistItem.title}}
|
||||
</td>
|
||||
<td style="white-space: nowrap;" ng-if="playlistItem.type === 'dashboard_by_tag'">
|
||||
<a class="search-result-tag label label-tag" tag-color-from-name="playlistItem.title">
|
||||
<i class="fa fa-tag"></i>
|
||||
<span>{{playlistItem.title}}</span>
|
||||
</a>
|
||||
</td>
|
||||
|
||||
<td style="text-align: right">
|
||||
<button class="btn btn-inverse btn-mini" ng-hide="$first" ng-click="movePlaylistItemUp(playlistItem)">
|
||||
<button class="btn btn-inverse btn-mini" ng-hide="$first" ng-click="ctrl.movePlaylistItemUp(playlistItem)">
|
||||
<i class="fa fa-arrow-up"></i>
|
||||
</button>
|
||||
<button class="btn btn-inverse btn-mini" ng-hide="$last" ng-click="movePlaylistItemDown(playlistItem)">
|
||||
<button class="btn btn-inverse btn-mini" ng-hide="$last" ng-click="ctrl.movePlaylistItemDown(playlistItem)">
|
||||
<i class="fa fa-arrow-down"></i>
|
||||
</button>
|
||||
<button class="btn btn-inverse btn-mini" ng-click="removePlaylistItem(playlistItem)">
|
||||
<button class="btn btn-inverse btn-mini" ng-click="ctrl.removePlaylistItem(playlistItem)">
|
||||
<i class="fa fa-remove"></i>
|
||||
</button>
|
||||
</td>
|
||||
@@ -113,11 +119,11 @@
|
||||
<!-- <div class="tight-form"> -->
|
||||
<button type="button"
|
||||
class="btn btn-success"
|
||||
ng-disabled="playlistEditForm.$invalid || isPlaylistEmpty()"
|
||||
ng-click="savePlaylist(playlist, playlistItems)">Save</button>
|
||||
ng-disabled="ctrl.playlistEditForm.$invalid || ctrl.isPlaylistEmpty()"
|
||||
ng-click="ctrl.savePlaylist(ctrl.playlist, ctrl.playlistItems)">Save</button>
|
||||
<button type="button"
|
||||
class="btn btn-inverse"
|
||||
ng-click="backToList()">Cancel</button>
|
||||
ng-click="ctrl.backToList()">Cancel</button>
|
||||
<!-- </div> -->
|
||||
</div>
|
||||
|
||||
|
||||
26
public/app/features/playlist/partials/playlist_search.html
Normal file
26
public/app/features/playlist/partials/playlist_search.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<div class="playlist-search-field-wrapper">
|
||||
<span style="position: relative;">
|
||||
<input type="text" placeholder="Find dashboards by name" tabindex="1"
|
||||
ng-keydown="ctrl.keyDown($event)" ng-model="ctrl.query.query" ng-model-options="{ debounce: 500 }" spellcheck='false' ng-change="ctrl.searchDashboards()" />
|
||||
</span>
|
||||
<div class="playlist-search-switches">
|
||||
<i class="fa fa-filter"></i>
|
||||
<a class="pointer" href="javascript:void 0;" ng-click="ctrl.showStarred()" tabindex="2">
|
||||
<i class="fa fa-remove" ng-show="ctrl.query.starred"></i>
|
||||
starred
|
||||
</a> |
|
||||
<a class="pointer" href="javascript:void 0;" ng-click="ctrl.getTags()" tabindex="3">
|
||||
<i class="fa fa-remove" ng-show="ctrl.tagsMode"></i>
|
||||
tags
|
||||
</a>
|
||||
<span ng-if="ctrl.query.tag.length">
|
||||
|
|
||||
<span ng-repeat="tagName in ctrl.query.tag">
|
||||
<a ng-click="ctrl.removeTag(tagName, $event)" tag-color-from-name="ctrl.tagName" class="label label-tag">
|
||||
<i class="fa fa-remove"></i>
|
||||
{{tagName}}
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -19,7 +19,7 @@
|
||||
<th style="width: 25px"></th>
|
||||
|
||||
</thead>
|
||||
<tr ng-repeat="playlist in playlists">
|
||||
<tr ng-repeat="playlist in ctrl.playlists">
|
||||
<td>
|
||||
<a href="playlists/edit/{{playlist.id}}">{{playlist.name}}</a>
|
||||
</td>
|
||||
@@ -39,7 +39,7 @@
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a ng-click="removePlaylist(playlist)" class="btn btn-danger btn-mini">
|
||||
<a ng-click="ctrl.removePlaylist(playlist)" class="btn btn-danger btn-mini">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
define([
|
||||
'angular',
|
||||
'app/core/config',
|
||||
'lodash'
|
||||
],
|
||||
function (angular, config, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('PlaylistEditCtrl', function($scope, playlistSrv, backendSrv, $location, $route) {
|
||||
$scope.filteredPlaylistItems = [];
|
||||
$scope.foundPlaylistItems = [];
|
||||
$scope.searchQuery = '';
|
||||
$scope.loading = false;
|
||||
$scope.playlist = {
|
||||
interval: '10m',
|
||||
};
|
||||
$scope.playlistItems = [];
|
||||
|
||||
$scope.init = function() {
|
||||
if ($route.current.params.id) {
|
||||
var playlistId = $route.current.params.id;
|
||||
|
||||
backendSrv.get('/api/playlists/' + playlistId)
|
||||
.then(function(result) {
|
||||
$scope.playlist = result;
|
||||
});
|
||||
|
||||
backendSrv.get('/api/playlists/' + playlistId + '/items')
|
||||
.then(function(result) {
|
||||
$scope.playlistItems = result;
|
||||
});
|
||||
}
|
||||
|
||||
$scope.search();
|
||||
};
|
||||
|
||||
$scope.search = function() {
|
||||
var query = {limit: 10};
|
||||
|
||||
if ($scope.searchQuery) {
|
||||
query.query = $scope.searchQuery;
|
||||
}
|
||||
|
||||
$scope.loading = true;
|
||||
|
||||
backendSrv.search(query)
|
||||
.then(function(results) {
|
||||
$scope.foundPlaylistItems = results;
|
||||
$scope.filterFoundPlaylistItems();
|
||||
})
|
||||
.finally(function() {
|
||||
$scope.loading = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.filterFoundPlaylistItems = function() {
|
||||
$scope.filteredPlaylistItems = _.reject($scope.foundPlaylistItems, function(playlistItem) {
|
||||
return _.findWhere($scope.playlistItems, function(listPlaylistItem) {
|
||||
return parseInt(listPlaylistItem.value) === playlistItem.id;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.addPlaylistItem = function(playlistItem) {
|
||||
playlistItem.value = playlistItem.id.toString();
|
||||
playlistItem.type = 'dashboard_by_id';
|
||||
playlistItem.order = $scope.playlistItems.length + 1;
|
||||
|
||||
$scope.playlistItems.push(playlistItem);
|
||||
$scope.filterFoundPlaylistItems();
|
||||
};
|
||||
|
||||
$scope.removePlaylistItem = function(playlistItem) {
|
||||
_.remove($scope.playlistItems, function(listedPlaylistItem) {
|
||||
return playlistItem === listedPlaylistItem;
|
||||
});
|
||||
$scope.filterFoundPlaylistItems();
|
||||
};
|
||||
|
||||
$scope.savePlaylist = function(playlist, playlistItems) {
|
||||
var savePromise;
|
||||
|
||||
playlist.items = playlistItems;
|
||||
|
||||
savePromise = playlist.id
|
||||
? backendSrv.put('/api/playlists/' + playlist.id, playlist)
|
||||
: backendSrv.post('/api/playlists', playlist);
|
||||
|
||||
savePromise
|
||||
.then(function() {
|
||||
$scope.appEvent('alert-success', ['Playlist saved', '']);
|
||||
$location.path('/playlists');
|
||||
}, function() {
|
||||
$scope.appEvent('alert-error', ['Unable to save playlist', '']);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.isNew = function() {
|
||||
return !$scope.playlist.id;
|
||||
};
|
||||
|
||||
$scope.isPlaylistEmpty = function() {
|
||||
return !$scope.playlistItems.length;
|
||||
};
|
||||
|
||||
$scope.isSearchResultsEmpty = function() {
|
||||
return !$scope.foundPlaylistItems.length;
|
||||
};
|
||||
|
||||
$scope.isSearchQueryEmpty = function() {
|
||||
return $scope.searchQuery === '';
|
||||
};
|
||||
|
||||
$scope.backToList = function() {
|
||||
$location.path('/playlists');
|
||||
};
|
||||
|
||||
$scope.isLoading = function() {
|
||||
return $scope.loading;
|
||||
};
|
||||
|
||||
$scope.movePlaylistItem = function(playlistItem, offset) {
|
||||
var currentPosition = $scope.playlistItems.indexOf(playlistItem);
|
||||
var newPosition = currentPosition + offset;
|
||||
|
||||
if (newPosition >= 0 && newPosition < $scope.playlistItems.length) {
|
||||
$scope.playlistItems.splice(currentPosition, 1);
|
||||
$scope.playlistItems.splice(newPosition, 0, playlistItem);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.movePlaylistItemUp = function(playlistItem) {
|
||||
$scope.moveDashboard(playlistItem, -1);
|
||||
};
|
||||
|
||||
$scope.movePlaylistItemDown = function(playlistItem) {
|
||||
$scope.moveDashboard(playlistItem, 1);
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
});
|
||||
});
|
||||
136
public/app/features/playlist/playlist_edit_ctrl.ts
Normal file
136
public/app/features/playlist/playlist_edit_ctrl.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import coreModule from '../../core/core_module';
|
||||
import config from 'app/core/config';
|
||||
|
||||
export class PlaylistEditCtrl {
|
||||
filteredDashboards: any = [];
|
||||
filteredTags: any = [];
|
||||
searchQuery: string = '';
|
||||
loading: boolean = false;
|
||||
playlist: any = {
|
||||
interval: '10m',
|
||||
};
|
||||
playlistItems: any = [];
|
||||
dashboardresult: any = [];
|
||||
tagresult: any = [];
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $scope, private playlistSrv, private backendSrv, private $location, private $route) {
|
||||
if ($route.current.params.id) {
|
||||
var playlistId = $route.current.params.id;
|
||||
|
||||
backendSrv.get('/api/playlists/' + playlistId)
|
||||
.then((result) => {
|
||||
this.playlist = result;
|
||||
});
|
||||
|
||||
backendSrv.get('/api/playlists/' + playlistId + '/items')
|
||||
.then((result) => {
|
||||
this.playlistItems = result;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
filterFoundPlaylistItems() {
|
||||
this.filteredDashboards = _.reject(this.dashboardresult, (playlistItem) => {
|
||||
return _.findWhere(this.playlistItems, (listPlaylistItem) => {
|
||||
return parseInt(listPlaylistItem.value) === playlistItem.id;
|
||||
});
|
||||
});
|
||||
|
||||
this.filteredTags = _.reject(this.tagresult, (tag) => {
|
||||
return _.findWhere(this.playlistItems, (listPlaylistItem) => {
|
||||
return listPlaylistItem.value === tag.term;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
addPlaylistItem(playlistItem) {
|
||||
playlistItem.value = playlistItem.id.toString();
|
||||
playlistItem.type = 'dashboard_by_id';
|
||||
playlistItem.order = this.playlistItems.length + 1;
|
||||
|
||||
this.playlistItems.push(playlistItem);
|
||||
this.filterFoundPlaylistItems();
|
||||
}
|
||||
|
||||
addTagPlaylistItem(tag) {
|
||||
var playlistItem: any = {
|
||||
value: tag.term,
|
||||
type: 'dashboard_by_tag',
|
||||
order: this.playlistItems.length + 1,
|
||||
title: tag.term
|
||||
};
|
||||
|
||||
this.playlistItems.push(playlistItem);
|
||||
this.filterFoundPlaylistItems();
|
||||
}
|
||||
|
||||
removePlaylistItem(playlistItem) {
|
||||
_.remove(this.playlistItems, (listedPlaylistItem) => {
|
||||
return playlistItem === listedPlaylistItem;
|
||||
});
|
||||
this.filterFoundPlaylistItems();
|
||||
};
|
||||
|
||||
savePlaylist(playlist, playlistItems) {
|
||||
var savePromise;
|
||||
|
||||
playlist.items = playlistItems;
|
||||
|
||||
savePromise = playlist.id
|
||||
? this.backendSrv.put('/api/playlists/' + playlist.id, playlist)
|
||||
: this.backendSrv.post('/api/playlists', playlist);
|
||||
|
||||
savePromise
|
||||
.then(() => {
|
||||
this.$scope.appEvent('alert-success', ['Playlist saved', '']);
|
||||
this.$location.path('/playlists');
|
||||
}, () => {
|
||||
this.$scope.appEvent('alert-error', ['Unable to save playlist', '']);
|
||||
});
|
||||
}
|
||||
|
||||
isNew() {
|
||||
return !this.playlist.id;
|
||||
}
|
||||
|
||||
isPlaylistEmpty() {
|
||||
return !this.playlistItems.length;
|
||||
}
|
||||
|
||||
backToList() {
|
||||
this.$location.path('/playlists');
|
||||
}
|
||||
|
||||
searchStarted(promise) {
|
||||
promise.then((data) => {
|
||||
this.dashboardresult = data.dashboardResult;
|
||||
this.tagresult = data.tagResult;
|
||||
this.filterFoundPlaylistItems();
|
||||
});
|
||||
}
|
||||
|
||||
movePlaylistItem(playlistItem, offset) {
|
||||
var currentPosition = this.playlistItems.indexOf(playlistItem);
|
||||
var newPosition = currentPosition + offset;
|
||||
|
||||
if (newPosition >= 0 && newPosition < this.playlistItems.length) {
|
||||
this.playlistItems.splice(currentPosition, 1);
|
||||
this.playlistItems.splice(newPosition, 0, playlistItem);
|
||||
}
|
||||
}
|
||||
|
||||
movePlaylistItemUp(playlistItem) {
|
||||
this.movePlaylistItem(playlistItem, -1);
|
||||
}
|
||||
|
||||
movePlaylistItemDown(playlistItem) {
|
||||
this.movePlaylistItem(playlistItem, 1);
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.controller('PlaylistEditCtrl', PlaylistEditCtrl);
|
||||
@@ -1,4 +1,4 @@
|
||||
define([
|
||||
define([
|
||||
'angular',
|
||||
'app/core/config',
|
||||
'lodash'
|
||||
@@ -12,14 +12,17 @@ function (angular) {
|
||||
$routeProvider
|
||||
.when('/playlists', {
|
||||
templateUrl: 'app/features/playlist/partials/playlists.html',
|
||||
controllerAs: 'ctrl',
|
||||
controller : 'PlaylistsCtrl'
|
||||
})
|
||||
.when('/playlists/create', {
|
||||
templateUrl: 'app/features/playlist/partials/playlist.html',
|
||||
controllerAs: 'ctrl',
|
||||
controller : 'PlaylistEditCtrl'
|
||||
})
|
||||
.when('/playlists/edit/:id', {
|
||||
templateUrl: 'app/features/playlist/partials/playlist.html',
|
||||
controllerAs: 'ctrl',
|
||||
controller : 'PlaylistEditCtrl'
|
||||
})
|
||||
.when('/playlists/play/:id', {
|
||||
|
||||
83
public/app/features/playlist/playlist_search.ts
Normal file
83
public/app/features/playlist/playlist_search.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import angular from 'angular';
|
||||
import config from 'app/core/config';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import coreModule from '../../core/core_module';
|
||||
|
||||
export class PlaylistSearchCtrl {
|
||||
query: any;
|
||||
tagsMode: boolean;
|
||||
|
||||
searchStarted: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $scope, private $location, private $timeout, private backendSrv, private contextSrv) {
|
||||
this.query = { query: '', tag: [], starred: false };
|
||||
|
||||
$timeout(() => {
|
||||
this.query.query = '';
|
||||
this.searchDashboards();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
searchDashboards() {
|
||||
this.tagsMode = false;
|
||||
var prom: any = {};
|
||||
|
||||
prom.promise = this.backendSrv.search(this.query).then((result) => {
|
||||
return {
|
||||
dashboardResult: result,
|
||||
tagResult: []
|
||||
};
|
||||
});
|
||||
|
||||
this.searchStarted(prom);
|
||||
}
|
||||
|
||||
showStarred() {
|
||||
this.query.starred = !this.query.starred;
|
||||
this.searchDashboards();
|
||||
}
|
||||
|
||||
queryHasNoFilters() {
|
||||
return this.query.query === '' && this.query.starred === false && this.query.tag.length === 0;
|
||||
}
|
||||
|
||||
filterByTag(tag, evt) {
|
||||
this.query.tag.push(tag);
|
||||
this.searchDashboards();
|
||||
if (evt) {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
getTags() {
|
||||
var prom: any = {};
|
||||
prom.promise = this.backendSrv.get('/api/dashboards/tags').then((result) => {
|
||||
return {
|
||||
dashboardResult: [],
|
||||
tagResult: result
|
||||
};
|
||||
});
|
||||
|
||||
this.searchStarted(prom);
|
||||
}
|
||||
}
|
||||
|
||||
export function playlistSearchDirective() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'app/features/playlist/partials/playlist_search.html',
|
||||
controller: PlaylistSearchCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
scope: {
|
||||
searchStarted: '&'
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('playlistSearch', playlistSearchDirective);
|
||||
@@ -1,6 +1,7 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import angular from 'angular';
|
||||
import config from 'app/core/config';
|
||||
import coreModule from '../../core/core_module';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
|
||||
@@ -20,10 +21,9 @@ class PlaylistSrv {
|
||||
var playedAllDashboards = this.index > this.dashboards.length - 1;
|
||||
|
||||
if (playedAllDashboards) {
|
||||
this.start(this.playlistId);
|
||||
window.location.href = `${config.appSubUrl}/playlists/play/${this.playlistId}`;
|
||||
} else {
|
||||
var dash = this.dashboards[this.index];
|
||||
|
||||
this.$location.url('dashboard/' + dash.uri);
|
||||
|
||||
this.index++;
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash'
|
||||
],
|
||||
function (angular, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('PlaylistsCtrl', function($scope, $location, backendSrv) {
|
||||
backendSrv.get('/api/playlists')
|
||||
.then(function(result) {
|
||||
$scope.playlists = result;
|
||||
});
|
||||
|
||||
$scope.removePlaylistConfirmed = function(playlist) {
|
||||
_.remove($scope.playlists, {id: playlist.id});
|
||||
|
||||
backendSrv.delete('/api/playlists/' + playlist.id)
|
||||
.then(function() {
|
||||
$scope.appEvent('alert-success', ['Playlist deleted', '']);
|
||||
}, function() {
|
||||
$scope.appEvent('alert-error', ['Unable to delete playlist', '']);
|
||||
$scope.playlists.push(playlist);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removePlaylist = function(playlist) {
|
||||
|
||||
$scope.appEvent('confirm-modal', {
|
||||
title: 'Confirm delete playlist',
|
||||
text: 'Are you sure you want to delete playlist ' + playlist.name + '?',
|
||||
yesText: "Delete",
|
||||
icon: "fa-warning",
|
||||
onConfirm: function() {
|
||||
$scope.removePlaylistConfirmed(playlist);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
});
|
||||
});
|
||||
44
public/app/features/playlist/playlists_ctrl.ts
Normal file
44
public/app/features/playlist/playlists_ctrl.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import coreModule from '../../core/core_module';
|
||||
|
||||
export class PlaylistsCtrl {
|
||||
playlists: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $scope, private $location, private backendSrv) {
|
||||
backendSrv.get('/api/playlists')
|
||||
.then((result) => {
|
||||
this.playlists = result;
|
||||
});
|
||||
}
|
||||
|
||||
removePlaylistConfirmed(playlist) {
|
||||
_.remove(this.playlists, { id: playlist.id });
|
||||
|
||||
this.backendSrv.delete('/api/playlists/' + playlist.id)
|
||||
.then(() => {
|
||||
this.$scope.appEvent('alert-success', ['Playlist deleted', '']);
|
||||
}, () => {
|
||||
this.$scope.appEvent('alert-error', ['Unable to delete playlist', '']);
|
||||
this.playlists.push(playlist);
|
||||
});
|
||||
}
|
||||
|
||||
removePlaylist(playlist) {
|
||||
|
||||
this.$scope.appEvent('confirm-modal', {
|
||||
title: 'Confirm delete playlist',
|
||||
text: 'Are you sure you want to delete playlist ' + playlist.name + '?',
|
||||
yesText: "Delete",
|
||||
icon: "fa-warning",
|
||||
onConfirm: () => {
|
||||
this.removePlaylistConfirmed(playlist);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.controller('PlaylistsCtrl', PlaylistsCtrl);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user