mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into 12918-only-arrow-functions
This commit is contained in:
commit
151e950e2c
@ -5,6 +5,7 @@
|
|||||||
* **Alerting**: Notification reminders [#7330](https://github.com/grafana/grafana/issues/7330), thx [@jbaublitz](https://github.com/jbaublitz)
|
* **Alerting**: Notification reminders [#7330](https://github.com/grafana/grafana/issues/7330), thx [@jbaublitz](https://github.com/jbaublitz)
|
||||||
* **Dashboard**: TV & Kiosk mode changes, new cycle view mode button in dashboard toolbar [#13025](https://github.com/grafana/grafana/pull/13025)
|
* **Dashboard**: TV & Kiosk mode changes, new cycle view mode button in dashboard toolbar [#13025](https://github.com/grafana/grafana/pull/13025)
|
||||||
* **OAuth**: Gitlab OAuth with support for filter by groups [#5623](https://github.com/grafana/grafana/issues/5623), thx [@BenoitKnecht](https://github.com/BenoitKnecht)
|
* **OAuth**: Gitlab OAuth with support for filter by groups [#5623](https://github.com/grafana/grafana/issues/5623), thx [@BenoitKnecht](https://github.com/BenoitKnecht)
|
||||||
|
* **Postgres**: Graphical query builder [#10095](https://github.com/grafana/grafana/issues/10095), thx [svenklemm](https://github.com/svenklemm)
|
||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
|
|
||||||
@ -19,7 +20,9 @@
|
|||||||
|
|
||||||
### Minor
|
### Minor
|
||||||
|
|
||||||
|
* **Units**: Adds bitcoin axes unit. [#13125](https://github.com/grafana/grafana/pull/13125)
|
||||||
* **GrafanaCli**: Fixed issue with grafana-cli install plugin resulting in corrupt http response from source error. Fixes [#13079](https://github.com/grafana/grafana/issues/13079)
|
* **GrafanaCli**: Fixed issue with grafana-cli install plugin resulting in corrupt http response from source error. Fixes [#13079](https://github.com/grafana/grafana/issues/13079)
|
||||||
|
* **Logging**: Reopen log files after receiving a SIGHUP signal [#13112](https://github.com/grafana/grafana/pull/13112), thx [@filewalkwithme](https://github.com/filewalkwithme)
|
||||||
* **Api**: Delete nonexistent datasource should return 404 [#12313](https://github.com/grafana/grafana/issues/12313), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
|
* **Api**: Delete nonexistent datasource should return 404 [#12313](https://github.com/grafana/grafana/issues/12313), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
|
||||||
* **Dashboard**: Fix selecting current dashboard from search should not reload dashboard [#12248](https://github.com/grafana/grafana/issues/12248)
|
* **Dashboard**: Fix selecting current dashboard from search should not reload dashboard [#12248](https://github.com/grafana/grafana/issues/12248)
|
||||||
* **Dashboard**: Use uid when linking to dashboards internally in a dashboard [#10705](https://github.com/grafana/grafana/issues/10705)
|
* **Dashboard**: Use uid when linking to dashboards internally in a dashboard [#10705](https://github.com/grafana/grafana/issues/10705)
|
||||||
|
@ -538,3 +538,8 @@ container_name =
|
|||||||
|
|
||||||
[external_image_storage.local]
|
[external_image_storage.local]
|
||||||
# does not require any configuration
|
# does not require any configuration
|
||||||
|
|
||||||
|
[rendering]
|
||||||
|
# Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer
|
||||||
|
server_url =
|
||||||
|
callback_url =
|
||||||
|
@ -460,3 +460,8 @@ log_queries =
|
|||||||
|
|
||||||
[external_image_storage.local]
|
[external_image_storage.local]
|
||||||
# does not require any configuration
|
# does not require any configuration
|
||||||
|
|
||||||
|
[rendering]
|
||||||
|
# Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer
|
||||||
|
;server_url =
|
||||||
|
;callback_url =
|
||||||
|
@ -155,7 +155,7 @@ Since not all datasources have the same configuration settings we only have the
|
|||||||
| tlsSkipVerify | boolean | *All* | Controls whether a client verifies the server's certificate chain and host name. |
|
| tlsSkipVerify | boolean | *All* | Controls whether a client verifies the server's certificate chain and host name. |
|
||||||
| graphiteVersion | string | Graphite | Graphite version |
|
| graphiteVersion | string | Graphite | Graphite version |
|
||||||
| timeInterval | string | Elastic, InfluxDB & Prometheus | Lowest interval/step value that should be used for this data source |
|
| timeInterval | string | Elastic, InfluxDB & Prometheus | Lowest interval/step value that should be used for this data source |
|
||||||
| esVersion | number | Elastic | Elasticsearch version as an number (2/5/56) |
|
| esVersion | number | Elastic | Elasticsearch version as a number (2/5/56) |
|
||||||
| timeField | string | Elastic | Which field that should be used as timestamp |
|
| timeField | string | Elastic | Which field that should be used as timestamp |
|
||||||
| interval | string | Elastic | Index date time format |
|
| interval | string | Elastic | Index date time format |
|
||||||
| authType | string | Cloudwatch | Auth provider. keys/credentials/arn |
|
| authType | string | Cloudwatch | Auth provider. keys/credentials/arn |
|
||||||
@ -165,6 +165,8 @@ Since not all datasources have the same configuration settings we only have the
|
|||||||
| tsdbVersion | string | OpenTSDB | Version |
|
| tsdbVersion | string | OpenTSDB | Version |
|
||||||
| tsdbResolution | string | OpenTSDB | Resolution |
|
| tsdbResolution | string | OpenTSDB | Resolution |
|
||||||
| sslmode | string | PostgreSQL | SSLmode. 'disable', 'require', 'verify-ca' or 'verify-full' |
|
| sslmode | string | PostgreSQL | SSLmode. 'disable', 'require', 'verify-ca' or 'verify-full' |
|
||||||
|
| postgresVersion | number | PostgreSQL | Postgres version as a number (903/904/905/906/1000) meaning v9.3, v9.4, ..., v10 |
|
||||||
|
| timescaledb | boolean | PostgreSQL | Enable usage of TimescaleDB extension |
|
||||||
|
|
||||||
#### Secure Json Data
|
#### Secure Json Data
|
||||||
|
|
||||||
|
@ -31,7 +31,9 @@ Name | Description
|
|||||||
*User* | Database user's login/username
|
*User* | Database user's login/username
|
||||||
*Password* | Database user's password
|
*Password* | Database user's password
|
||||||
*SSL Mode* | This option determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server.
|
*SSL Mode* | This option determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server.
|
||||||
*TimescaleDB* | With this option enabled Grafana will use TimescaleDB features, e.g. use ```time_bucket``` for grouping by time (only available in Grafana 5.3+).
|
*Version* | This option determines which functions are available in the query builder (only available in Grafana 5.3+).
|
||||||
|
*TimescaleDB* | TimescaleDB is a time-series database built as a PostgreSQL extension. If enabled, Grafana will use `time_bucket` in the `$__timeGroup` macro and display TimescaleDB specific aggregate functions in the query builder (only available in Grafana 5.3+).
|
||||||
|
|
||||||
|
|
||||||
### Database User Permissions (Important!)
|
### Database User Permissions (Important!)
|
||||||
|
|
||||||
@ -292,5 +294,6 @@ datasources:
|
|||||||
password: "Password!"
|
password: "Password!"
|
||||||
jsonData:
|
jsonData:
|
||||||
sslmode: "disable" # disable/require/verify-ca/verify-full
|
sslmode: "disable" # disable/require/verify-ca/verify-full
|
||||||
|
postgresVersion: 903 # 903=9.3, 904=9.4, 905=9.5, 906=9.6, 1000=10
|
||||||
timescaledb: false
|
timescaledb: false
|
||||||
```
|
```
|
||||||
|
@ -96,13 +96,17 @@ func main() {
|
|||||||
|
|
||||||
func listenToSystemSignals(server *GrafanaServerImpl) {
|
func listenToSystemSignals(server *GrafanaServerImpl) {
|
||||||
signalChan := make(chan os.Signal, 1)
|
signalChan := make(chan os.Signal, 1)
|
||||||
ignoreChan := make(chan os.Signal, 1)
|
sighupChan := make(chan os.Signal, 1)
|
||||||
|
|
||||||
signal.Notify(ignoreChan, syscall.SIGHUP)
|
signal.Notify(sighupChan, syscall.SIGHUP)
|
||||||
signal.Notify(signalChan, os.Interrupt, os.Kill, syscall.SIGTERM)
|
signal.Notify(signalChan, os.Interrupt, os.Kill, syscall.SIGTERM)
|
||||||
|
|
||||||
select {
|
for {
|
||||||
case sig := <-signalChan:
|
select {
|
||||||
server.Shutdown(fmt.Sprintf("System signal: %s", sig))
|
case _ = <-sighupChan:
|
||||||
|
log.Reload()
|
||||||
|
case sig := <-signalChan:
|
||||||
|
server.Shutdown(fmt.Sprintf("System signal: %s", sig))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -236,3 +236,20 @@ func (w *FileLogWriter) Close() {
|
|||||||
func (w *FileLogWriter) Flush() {
|
func (w *FileLogWriter) Flush() {
|
||||||
w.mw.fd.Sync()
|
w.mw.fd.Sync()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reload file logger
|
||||||
|
func (w *FileLogWriter) Reload() {
|
||||||
|
// block Logger's io.Writer
|
||||||
|
w.mw.Lock()
|
||||||
|
defer w.mw.Unlock()
|
||||||
|
|
||||||
|
// Close
|
||||||
|
fd := w.mw.fd
|
||||||
|
fd.Close()
|
||||||
|
|
||||||
|
// Open again
|
||||||
|
err := w.StartLogger()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Reload StartLogger: %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -3,3 +3,7 @@ package log
|
|||||||
type DisposableHandler interface {
|
type DisposableHandler interface {
|
||||||
Close()
|
Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ReloadableHandler interface {
|
||||||
|
Reload()
|
||||||
|
}
|
||||||
|
@ -21,10 +21,12 @@ import (
|
|||||||
|
|
||||||
var Root log15.Logger
|
var Root log15.Logger
|
||||||
var loggersToClose []DisposableHandler
|
var loggersToClose []DisposableHandler
|
||||||
|
var loggersToReload []ReloadableHandler
|
||||||
var filters map[string]log15.Lvl
|
var filters map[string]log15.Lvl
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
loggersToClose = make([]DisposableHandler, 0)
|
loggersToClose = make([]DisposableHandler, 0)
|
||||||
|
loggersToReload = make([]ReloadableHandler, 0)
|
||||||
Root = log15.Root()
|
Root = log15.Root()
|
||||||
Root.SetHandler(log15.DiscardHandler())
|
Root.SetHandler(log15.DiscardHandler())
|
||||||
}
|
}
|
||||||
@ -115,6 +117,12 @@ func Close() {
|
|||||||
loggersToClose = make([]DisposableHandler, 0)
|
loggersToClose = make([]DisposableHandler, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Reload() {
|
||||||
|
for _, logger := range loggersToReload {
|
||||||
|
logger.Reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func GetLogLevelFor(name string) Lvl {
|
func GetLogLevelFor(name string) Lvl {
|
||||||
if level, ok := filters[name]; ok {
|
if level, ok := filters[name]; ok {
|
||||||
switch level {
|
switch level {
|
||||||
@ -230,6 +238,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) {
|
|||||||
fileHandler.Init()
|
fileHandler.Init()
|
||||||
|
|
||||||
loggersToClose = append(loggersToClose, fileHandler)
|
loggersToClose = append(loggersToClose, fileHandler)
|
||||||
|
loggersToReload = append(loggersToReload, fileHandler)
|
||||||
handler = fileHandler
|
handler = fileHandler
|
||||||
case "syslog":
|
case "syslog":
|
||||||
sysLogHandler := NewSyslog(sec, format)
|
sysLogHandler := NewSyslog(sec, format)
|
||||||
|
@ -2,6 +2,7 @@ package rendering
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -20,14 +21,13 @@ var netTransport = &http.Transport{
|
|||||||
TLSHandshakeTimeout: 5 * time.Second,
|
TLSHandshakeTimeout: 5 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var netClient = &http.Client{
|
||||||
|
Transport: netTransport,
|
||||||
|
}
|
||||||
|
|
||||||
func (rs *RenderingService) renderViaHttp(ctx context.Context, opts Opts) (*RenderResult, error) {
|
func (rs *RenderingService) renderViaHttp(ctx context.Context, opts Opts) (*RenderResult, error) {
|
||||||
filePath := rs.getFilePathForNewImage()
|
filePath := rs.getFilePathForNewImage()
|
||||||
|
|
||||||
var netClient = &http.Client{
|
|
||||||
Timeout: opts.Timeout,
|
|
||||||
Transport: netTransport,
|
|
||||||
}
|
|
||||||
|
|
||||||
rendererUrl, err := url.Parse(rs.Cfg.RendererUrl)
|
rendererUrl, err := url.Parse(rs.Cfg.RendererUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -35,10 +35,10 @@ func (rs *RenderingService) renderViaHttp(ctx context.Context, opts Opts) (*Rend
|
|||||||
|
|
||||||
queryParams := rendererUrl.Query()
|
queryParams := rendererUrl.Query()
|
||||||
queryParams.Add("url", rs.getURL(opts.Path))
|
queryParams.Add("url", rs.getURL(opts.Path))
|
||||||
queryParams.Add("renderKey", rs.getRenderKey(opts.UserId, opts.OrgId, opts.OrgRole))
|
queryParams.Add("renderKey", rs.getRenderKey(opts.OrgId, opts.UserId, opts.OrgRole))
|
||||||
queryParams.Add("width", strconv.Itoa(opts.Width))
|
queryParams.Add("width", strconv.Itoa(opts.Width))
|
||||||
queryParams.Add("height", strconv.Itoa(opts.Height))
|
queryParams.Add("height", strconv.Itoa(opts.Height))
|
||||||
queryParams.Add("domain", rs.getLocalDomain())
|
queryParams.Add("domain", rs.domain)
|
||||||
queryParams.Add("timezone", isoTimeOffsetToPosixTz(opts.Timezone))
|
queryParams.Add("timezone", isoTimeOffsetToPosixTz(opts.Timezone))
|
||||||
queryParams.Add("encoding", opts.Encoding)
|
queryParams.Add("encoding", opts.Encoding)
|
||||||
queryParams.Add("timeout", strconv.Itoa(int(opts.Timeout.Seconds())))
|
queryParams.Add("timeout", strconv.Itoa(int(opts.Timeout.Seconds())))
|
||||||
@ -49,20 +49,48 @@ func (rs *RenderingService) renderViaHttp(ctx context.Context, opts Opts) (*Rend
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reqContext, cancel := context.WithTimeout(ctx, opts.Timeout+time.Second*2)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
req = req.WithContext(reqContext)
|
||||||
|
|
||||||
// make request to renderer server
|
// make request to renderer server
|
||||||
resp, err := netClient.Do(req)
|
resp, err := netClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
rs.log.Error("Failed to send request to remote rendering service.", "error", err)
|
||||||
|
return nil, fmt.Errorf("Failed to send request to remote rendering service. %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// save response to file
|
// save response to file
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// check for timeout first
|
||||||
|
if reqContext.Err() == context.DeadlineExceeded {
|
||||||
|
rs.log.Info("Rendering timed out")
|
||||||
|
return nil, ErrTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we didnt get a 200 response, something went wrong.
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
rs.log.Error("Remote rendering request failed", "error", resp.Status)
|
||||||
|
return nil, fmt.Errorf("Remote rendering request failed. %d: %s", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
out, err := os.Create(filePath)
|
out, err := os.Create(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer out.Close()
|
defer out.Close()
|
||||||
io.Copy(out, resp.Body)
|
_, err = io.Copy(out, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
// check that we didnt timeout while receiving the response.
|
||||||
|
if reqContext.Err() == context.DeadlineExceeded {
|
||||||
|
rs.log.Info("Rendering timed out")
|
||||||
|
return nil, ErrTimeout
|
||||||
|
}
|
||||||
|
rs.log.Error("Remote rendering request failed", "error", err)
|
||||||
|
return nil, fmt.Errorf("Remote rendering request failed. %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
return &RenderResult{FilePath: filePath}, err
|
return &RenderResult{FilePath: filePath}, err
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) (
|
|||||||
fmt.Sprintf("width=%v", opts.Width),
|
fmt.Sprintf("width=%v", opts.Width),
|
||||||
fmt.Sprintf("height=%v", opts.Height),
|
fmt.Sprintf("height=%v", opts.Height),
|
||||||
fmt.Sprintf("png=%v", pngPath),
|
fmt.Sprintf("png=%v", pngPath),
|
||||||
fmt.Sprintf("domain=%v", rs.getLocalDomain()),
|
fmt.Sprintf("domain=%v", rs.domain),
|
||||||
fmt.Sprintf("timeout=%v", opts.Timeout.Seconds()),
|
fmt.Sprintf("timeout=%v", opts.Timeout.Seconds()),
|
||||||
fmt.Sprintf("renderKey=%v", renderKey),
|
fmt.Sprintf("renderKey=%v", renderKey),
|
||||||
}
|
}
|
||||||
|
@ -77,10 +77,10 @@ func (rs *RenderingService) renderViaPlugin(ctx context.Context, opts Opts) (*Re
|
|||||||
Height: int32(opts.Height),
|
Height: int32(opts.Height),
|
||||||
FilePath: pngPath,
|
FilePath: pngPath,
|
||||||
Timeout: int32(opts.Timeout.Seconds()),
|
Timeout: int32(opts.Timeout.Seconds()),
|
||||||
RenderKey: rs.getRenderKey(opts.UserId, opts.OrgId, opts.OrgRole),
|
RenderKey: rs.getRenderKey(opts.OrgId, opts.UserId, opts.OrgRole),
|
||||||
Encoding: opts.Encoding,
|
Encoding: opts.Encoding,
|
||||||
Timezone: isoTimeOffsetToPosixTz(opts.Timezone),
|
Timezone: isoTimeOffsetToPosixTz(opts.Timezone),
|
||||||
Domain: rs.getLocalDomain(),
|
Domain: rs.domain,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -3,6 +3,8 @@ package rendering
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
plugin "github.com/hashicorp/go-plugin"
|
plugin "github.com/hashicorp/go-plugin"
|
||||||
@ -27,12 +29,31 @@ type RenderingService struct {
|
|||||||
grpcPlugin pluginModel.RendererPlugin
|
grpcPlugin pluginModel.RendererPlugin
|
||||||
pluginInfo *plugins.RendererPlugin
|
pluginInfo *plugins.RendererPlugin
|
||||||
renderAction renderFunc
|
renderAction renderFunc
|
||||||
|
domain string
|
||||||
|
|
||||||
Cfg *setting.Cfg `inject:""`
|
Cfg *setting.Cfg `inject:""`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *RenderingService) Init() error {
|
func (rs *RenderingService) Init() error {
|
||||||
rs.log = log.New("rendering")
|
rs.log = log.New("rendering")
|
||||||
|
|
||||||
|
// ensure ImagesDir exists
|
||||||
|
err := os.MkdirAll(rs.Cfg.ImagesDir, 0700)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// set value used for domain attribute of renderKey cookie
|
||||||
|
if rs.Cfg.RendererUrl != "" {
|
||||||
|
// RendererCallbackUrl has already been passed, it wont generate an error.
|
||||||
|
u, _ := url.Parse(rs.Cfg.RendererCallbackUrl)
|
||||||
|
rs.domain = u.Hostname()
|
||||||
|
} else if setting.HttpAddr != setting.DEFAULT_HTTP_ADDR {
|
||||||
|
rs.domain = setting.HttpAddr
|
||||||
|
} else {
|
||||||
|
rs.domain = "localhost"
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,16 +103,17 @@ func (rs *RenderingService) getFilePathForNewImage() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (rs *RenderingService) getURL(path string) string {
|
func (rs *RenderingService) getURL(path string) string {
|
||||||
// &render=1 signals to the legacy redirect layer to
|
if rs.Cfg.RendererUrl != "" {
|
||||||
return fmt.Sprintf("%s://%s:%s/%s&render=1", setting.Protocol, rs.getLocalDomain(), setting.HttpPort, path)
|
// The backend rendering service can potentially be remote.
|
||||||
}
|
// So we need to use the root_url to ensure the rendering service
|
||||||
|
// can reach this Grafana instance.
|
||||||
|
|
||||||
|
// &render=1 signals to the legacy redirect layer to
|
||||||
|
return fmt.Sprintf("%s%s&render=1", rs.Cfg.RendererCallbackUrl, path)
|
||||||
|
|
||||||
func (rs *RenderingService) getLocalDomain() string {
|
|
||||||
if setting.HttpAddr != setting.DEFAULT_HTTP_ADDR {
|
|
||||||
return setting.HttpAddr
|
|
||||||
}
|
}
|
||||||
|
// &render=1 signals to the legacy redirect layer to
|
||||||
return "localhost"
|
return fmt.Sprintf("%s://%s:%s/%s&render=1", setting.Protocol, rs.domain, setting.HttpPort, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *RenderingService) getRenderKey(orgId, userId int64, orgRole models.RoleType) string {
|
func (rs *RenderingService) getRenderKey(orgId, userId int64, orgRole models.RoleType) string {
|
||||||
|
@ -197,6 +197,7 @@ type Cfg struct {
|
|||||||
ImagesDir string
|
ImagesDir string
|
||||||
PhantomDir string
|
PhantomDir string
|
||||||
RendererUrl string
|
RendererUrl string
|
||||||
|
RendererCallbackUrl string
|
||||||
DisableBruteForceLoginProtection bool
|
DisableBruteForceLoginProtection bool
|
||||||
|
|
||||||
TempDataLifetime time.Duration
|
TempDataLifetime time.Duration
|
||||||
@ -641,6 +642,18 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
|||||||
// Rendering
|
// Rendering
|
||||||
renderSec := iniFile.Section("rendering")
|
renderSec := iniFile.Section("rendering")
|
||||||
cfg.RendererUrl = renderSec.Key("server_url").String()
|
cfg.RendererUrl = renderSec.Key("server_url").String()
|
||||||
|
cfg.RendererCallbackUrl = renderSec.Key("callback_url").String()
|
||||||
|
if cfg.RendererCallbackUrl == "" {
|
||||||
|
cfg.RendererCallbackUrl = AppUrl
|
||||||
|
} else {
|
||||||
|
if cfg.RendererCallbackUrl[len(cfg.RendererCallbackUrl)-1] != '/' {
|
||||||
|
cfg.RendererCallbackUrl += "/"
|
||||||
|
}
|
||||||
|
_, err := url.Parse(cfg.RendererCallbackUrl)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(4, "Invalid callback_url(%s): %s", cfg.RendererCallbackUrl, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
cfg.ImagesDir = filepath.Join(DataPath, "png")
|
cfg.ImagesDir = filepath.Join(DataPath, "png")
|
||||||
cfg.PhantomDir = filepath.Join(HomePath, "tools/phantomjs")
|
cfg.PhantomDir = filepath.Join(HomePath, "tools/phantomjs")
|
||||||
cfg.TempDataLifetime = iniFile.Section("paths").Key("temp_data_lifetime").MustDuration(time.Second * 3600 * 24)
|
cfg.TempDataLifetime = iniFile.Section("paths").Key("temp_data_lifetime").MustDuration(time.Second * 3600 * 24)
|
||||||
|
@ -20,6 +20,7 @@ func TestLoadingSettings(t *testing.T) {
|
|||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
So(AdminUser, ShouldEqual, "admin")
|
So(AdminUser, ShouldEqual, "admin")
|
||||||
|
So(cfg.RendererCallbackUrl, ShouldEqual, "http://localhost:3000/")
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("Should be able to override via environment variables", func() {
|
Convey("Should be able to override via environment variables", func() {
|
||||||
@ -178,5 +179,15 @@ func TestLoadingSettings(t *testing.T) {
|
|||||||
So(InstanceName, ShouldEqual, hostname)
|
So(InstanceName, ShouldEqual, hostname)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("Reading callback_url should add trailing slash", func() {
|
||||||
|
cfg := NewCfg()
|
||||||
|
cfg.Load(&CommandLineArgs{
|
||||||
|
HomePath: "../../",
|
||||||
|
Args: []string{"cfg:rendering.callback_url=http://myserver/renderer"},
|
||||||
|
})
|
||||||
|
|
||||||
|
So(cfg.RendererCallbackUrl, ShouldEqual, "http://myserver/renderer/")
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ import _ from 'lodash';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
// add move to lodash for backward compatabiltiy
|
// add move to lodash for backward compatabiltiy
|
||||||
_.move = function(array, fromIndex, toIndex) {
|
_.move = (array, fromIndex, toIndex) => {
|
||||||
array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]);
|
array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]);
|
||||||
return array;
|
return array;
|
||||||
};
|
};
|
||||||
@ -76,9 +76,9 @@ export class GrafanaApp {
|
|||||||
$provide.decorator('$http', [
|
$provide.decorator('$http', [
|
||||||
'$delegate',
|
'$delegate',
|
||||||
'$templateCache',
|
'$templateCache',
|
||||||
function($delegate, $templateCache) {
|
($delegate, $templateCache) => {
|
||||||
const get = $delegate.get;
|
const get = $delegate.get;
|
||||||
$delegate.get = function(url, config) {
|
$delegate.get = (url, config) => {
|
||||||
if (url.match(/\.html$/)) {
|
if (url.match(/\.html$/)) {
|
||||||
// some template's already exist in the cache
|
// some template's already exist in the cache
|
||||||
if (!$templateCache.get(url)) {
|
if (!$templateCache.get(url)) {
|
||||||
@ -135,7 +135,7 @@ export class GrafanaApp {
|
|||||||
this.preBootModules = null;
|
this.preBootModules = null;
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(function(err) {
|
.catch(err => {
|
||||||
console.log('Application boot failed:', err);
|
console.log('Application boot failed:', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ export default function debounce(func, wait) {
|
|||||||
return function(this: any) {
|
return function(this: any) {
|
||||||
const context = this;
|
const context = this;
|
||||||
const args = arguments;
|
const args = arguments;
|
||||||
const later = function() {
|
const later = () => {
|
||||||
timeout = null;
|
timeout = null;
|
||||||
func.apply(context, args);
|
func.apply(context, args);
|
||||||
};
|
};
|
||||||
|
@ -6,6 +6,7 @@ import { NavStore } from 'app/stores/NavStore/NavStore';
|
|||||||
import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore';
|
import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore';
|
||||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||||
import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
|
import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
|
||||||
|
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
nav: typeof NavStore.Type;
|
nav: typeof NavStore.Type;
|
||||||
@ -61,48 +62,81 @@ export class TeamList extends React.Component<Props, any> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderTeamList(teams) {
|
||||||
|
return (
|
||||||
|
<div className="page-container page-body">
|
||||||
|
<div className="page-action-bar">
|
||||||
|
<div className="gf-form gf-form--grow">
|
||||||
|
<label className="gf-form--has-input-icon gf-form--grow">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="gf-form-input"
|
||||||
|
placeholder="Search teams"
|
||||||
|
value={teams.search}
|
||||||
|
onChange={this.onSearchQueryChange}
|
||||||
|
/>
|
||||||
|
<i className="gf-form-input-icon fa fa-search" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="page-action-bar__spacer" />
|
||||||
|
|
||||||
|
<a className="btn btn-success" href="org/teams/new">
|
||||||
|
<i className="fa fa-plus" /> New team
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="admin-list-table">
|
||||||
|
<table className="filter-table filter-table--hover form-inline">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th />
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Members</th>
|
||||||
|
<th style={{ width: '1%' }} />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>{teams.filteredTeams.map(team => this.renderTeamMember(team))}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEmptyList() {
|
||||||
|
return (
|
||||||
|
<div className="page-container page-body">
|
||||||
|
<EmptyListCTA
|
||||||
|
model={{
|
||||||
|
title: "You haven't created any teams yet.",
|
||||||
|
buttonIcon: 'fa fa-plus',
|
||||||
|
buttonLink: 'org/teams/new',
|
||||||
|
buttonTitle: ' New team',
|
||||||
|
proTip: 'Assign folder and dashboard permissions to teams instead of users to ease administration.',
|
||||||
|
proTipLink: '',
|
||||||
|
proTipLinkTitle: '',
|
||||||
|
proTipTarget: '_blank',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { nav, teams } = this.props;
|
const { nav, teams } = this.props;
|
||||||
|
let view;
|
||||||
|
|
||||||
|
if (teams.filteredTeams.length > 0) {
|
||||||
|
view = this.renderTeamList(teams);
|
||||||
|
} else {
|
||||||
|
view = this.renderEmptyList();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<PageHeader model={nav as any} />
|
<PageHeader model={nav as any} />
|
||||||
<div className="page-container page-body">
|
{view}
|
||||||
<div className="page-action-bar">
|
|
||||||
<div className="gf-form gf-form--grow">
|
|
||||||
<label className="gf-form--has-input-icon gf-form--grow">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="gf-form-input"
|
|
||||||
placeholder="Search teams"
|
|
||||||
value={teams.search}
|
|
||||||
onChange={this.onSearchQueryChange}
|
|
||||||
/>
|
|
||||||
<i className="gf-form-input-icon fa fa-search" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="page-action-bar__spacer" />
|
|
||||||
|
|
||||||
<a className="btn btn-success" href="org/teams/new">
|
|
||||||
<i className="fa fa-plus" /> New team
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="admin-list-table">
|
|
||||||
<table className="filter-table filter-table--hover form-inline">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th />
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Email</th>
|
|
||||||
<th>Members</th>
|
|
||||||
<th style={{ width: '1%' }} />
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>{teams.filteredTeams.map(team => this.renderTeamMember(team))}</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,12 @@ import PageHeader from './components/PageHeader/PageHeader';
|
|||||||
import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
|
import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
|
||||||
import { SearchResult } from './components/search/SearchResult';
|
import { SearchResult } from './components/search/SearchResult';
|
||||||
import { TagFilter } from './components/TagFilter/TagFilter';
|
import { TagFilter } from './components/TagFilter/TagFilter';
|
||||||
|
import { SideMenu } from './components/sidemenu/SideMenu';
|
||||||
import DashboardPermissions from './components/Permissions/DashboardPermissions';
|
import DashboardPermissions from './components/Permissions/DashboardPermissions';
|
||||||
|
|
||||||
export function registerAngularDirectives() {
|
export function registerAngularDirectives() {
|
||||||
react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
|
react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
|
||||||
|
react2AngularDirective('sidemenu', SideMenu, []);
|
||||||
react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
|
react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
|
||||||
react2AngularDirective('emptyListCta', EmptyListCTA, ['model']);
|
react2AngularDirective('emptyListCta', EmptyListCTA, ['model']);
|
||||||
react2AngularDirective('searchResult', SearchResult, []);
|
react2AngularDirective('searchResult', SearchResult, []);
|
||||||
|
@ -31,7 +31,7 @@ export function gfPageDirective() {
|
|||||||
header: '?gfPageHeader',
|
header: '?gfPageHeader',
|
||||||
body: 'gfPageBody',
|
body: 'gfPageBody',
|
||||||
},
|
},
|
||||||
link: function(scope, elem, attrs) {
|
link: (scope, elem, attrs) => {
|
||||||
console.log(scope);
|
console.log(scope);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -4,7 +4,7 @@ import appEvents from 'app/core/app_events';
|
|||||||
export function pageScrollbar() {
|
export function pageScrollbar() {
|
||||||
return {
|
return {
|
||||||
restrict: 'A',
|
restrict: 'A',
|
||||||
link: function(scope, elem, attrs) {
|
link: (scope, elem, attrs) => {
|
||||||
let lastPos = 0;
|
let lastPos = 0;
|
||||||
|
|
||||||
appEvents.on(
|
appEvents.on(
|
||||||
|
@ -14,7 +14,7 @@ const scrollerClass = 'baron__scroller';
|
|||||||
export function geminiScrollbar() {
|
export function geminiScrollbar() {
|
||||||
return {
|
return {
|
||||||
restrict: 'A',
|
restrict: 'A',
|
||||||
link: function(scope, elem, attrs) {
|
link: (scope, elem, attrs) => {
|
||||||
let scrollRoot = elem.parent();
|
let scrollRoot = elem.parent();
|
||||||
const scroller = elem;
|
const scroller = elem;
|
||||||
|
|
||||||
|
96
public/app/core/components/sidemenu/BottomNavLinks.test.tsx
Normal file
96
public/app/core/components/sidemenu/BottomNavLinks.test.tsx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import BottomNavLinks from './BottomNavLinks';
|
||||||
|
import appEvents from '../../app_events';
|
||||||
|
|
||||||
|
jest.mock('../../app_events', () => ({
|
||||||
|
emit: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const setup = (propOverrides?: object) => {
|
||||||
|
const props = Object.assign(
|
||||||
|
{
|
||||||
|
link: {},
|
||||||
|
user: {
|
||||||
|
isGrafanaAdmin: false,
|
||||||
|
isSignedIn: false,
|
||||||
|
orgCount: 2,
|
||||||
|
orgRole: '',
|
||||||
|
orgId: 1,
|
||||||
|
orgName: 'Grafana',
|
||||||
|
timezone: 'UTC',
|
||||||
|
helpFlags1: 1,
|
||||||
|
lightTheme: false,
|
||||||
|
hasEditPermissionInFolders: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
propOverrides
|
||||||
|
);
|
||||||
|
return shallow(<BottomNavLinks {...props} />);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Render', () => {
|
||||||
|
it('should render component', () => {
|
||||||
|
const wrapper = setup();
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render organisation switcher', () => {
|
||||||
|
const wrapper = setup({
|
||||||
|
link: {
|
||||||
|
showOrgSwitcher: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render subtitle', () => {
|
||||||
|
const wrapper = setup({
|
||||||
|
link: {
|
||||||
|
subTitle: 'subtitle',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render children', () => {
|
||||||
|
const wrapper = setup({
|
||||||
|
link: {
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
hideFromMenu: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Functions', () => {
|
||||||
|
describe('item clicked', () => {
|
||||||
|
const wrapper = setup();
|
||||||
|
const mockEvent = { preventDefault: jest.fn() };
|
||||||
|
it('should emit show modal event if url matches shortcut', () => {
|
||||||
|
const child = { url: '/shortcuts' };
|
||||||
|
const instance = wrapper.instance() as BottomNavLinks;
|
||||||
|
instance.itemClicked(mockEvent, child);
|
||||||
|
|
||||||
|
expect(appEvents.emit).toHaveBeenCalledWith('show-modal', { templateHtml: '<help-modal></help-modal>' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
78
public/app/core/components/sidemenu/BottomNavLinks.tsx
Normal file
78
public/app/core/components/sidemenu/BottomNavLinks.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import appEvents from '../../app_events';
|
||||||
|
import { User } from '../../services/context_srv';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
link: any;
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
class BottomNavLinks extends PureComponent<Props> {
|
||||||
|
itemClicked = (event, child) => {
|
||||||
|
if (child.url === '/shortcuts') {
|
||||||
|
event.preventDefault();
|
||||||
|
appEvents.emit('show-modal', {
|
||||||
|
templateHtml: '<help-modal></help-modal>',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
switchOrg = () => {
|
||||||
|
appEvents.emit('show-modal', {
|
||||||
|
templateHtml: '<org-switcher dismiss="dismiss()"></org-switcher>',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { link, user } = this.props;
|
||||||
|
return (
|
||||||
|
<div className="sidemenu-item dropdown dropup">
|
||||||
|
<a href={link.url} className="sidemenu-link" target={link.target}>
|
||||||
|
<span className="icon-circle sidemenu-icon">
|
||||||
|
{link.icon && <i className={link.icon} />}
|
||||||
|
{link.img && <img src={link.img} />}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<ul className="dropdown-menu dropdown-menu--sidemenu" role="menu">
|
||||||
|
{link.subTitle && (
|
||||||
|
<li className="sidemenu-subtitle">
|
||||||
|
<span className="sidemenu-item-text">{link.subTitle}</span>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{link.showOrgSwitcher && (
|
||||||
|
<li className="sidemenu-org-switcher">
|
||||||
|
<a onClick={this.switchOrg}>
|
||||||
|
<div>
|
||||||
|
<div className="sidemenu-org-switcher__org-name">{user.orgName}</div>
|
||||||
|
<div className="sidemenu-org-switcher__org-current">Current Org:</div>
|
||||||
|
</div>
|
||||||
|
<div className="sidemenu-org-switcher__switch">
|
||||||
|
<i className="fa fa-fw fa-random" />Switch
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{link.children &&
|
||||||
|
link.children.map((child, index) => {
|
||||||
|
if (!child.hideFromMenu) {
|
||||||
|
return (
|
||||||
|
<li className={child.divider} key={`${child.text}-${index}`}>
|
||||||
|
<a href={child.url} target={child.target} onClick={event => this.itemClicked(event, child)}>
|
||||||
|
{child.icon && <i className={child.icon} />}
|
||||||
|
{child.text}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
|
<li className="side-menu-header">
|
||||||
|
<span className="sidemenu-item-text">{link.text}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BottomNavLinks;
|
44
public/app/core/components/sidemenu/BottomSection.test.tsx
Normal file
44
public/app/core/components/sidemenu/BottomSection.test.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import BottomSection from './BottomSection';
|
||||||
|
|
||||||
|
jest.mock('../../config', () => ({
|
||||||
|
bootData: {
|
||||||
|
navTree: [
|
||||||
|
{
|
||||||
|
id: 'profile',
|
||||||
|
hideFromMenu: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hideFromMenu: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hideFromMenu: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hideFromMenu: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
orgCount: 5,
|
||||||
|
orgName: 'Grafana',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('app/core/services/context_srv', () => ({
|
||||||
|
contextSrv: {
|
||||||
|
sidemenu: true,
|
||||||
|
isSignedIn: false,
|
||||||
|
isGrafanaAdmin: false,
|
||||||
|
hasEditPermissionFolders: false,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Render', () => {
|
||||||
|
it('should render component', () => {
|
||||||
|
const wrapper = shallow(<BottomSection />);
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
29
public/app/core/components/sidemenu/BottomSection.tsx
Normal file
29
public/app/core/components/sidemenu/BottomSection.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import SignIn from './SignIn';
|
||||||
|
import BottomNavLinks from './BottomNavLinks';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
|
import config from '../../config';
|
||||||
|
|
||||||
|
export default function BottomSection() {
|
||||||
|
const navTree = _.cloneDeep(config.bootData.navTree);
|
||||||
|
const bottomNav = _.filter(navTree, item => item.hideFromMenu);
|
||||||
|
const isSignedIn = contextSrv.isSignedIn;
|
||||||
|
const user = contextSrv.user;
|
||||||
|
|
||||||
|
if (user && user.orgCount > 1) {
|
||||||
|
const profileNode = _.find(bottomNav, { id: 'profile' });
|
||||||
|
if (profileNode) {
|
||||||
|
profileNode.showOrgSwitcher = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sidemenu__bottom">
|
||||||
|
{!isSignedIn && <SignIn />}
|
||||||
|
{bottomNav.map((link, index) => {
|
||||||
|
return <BottomNavLinks link={link} user={user} key={`${link.url}-${index}`} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
35
public/app/core/components/sidemenu/DropDownChild.test.tsx
Normal file
35
public/app/core/components/sidemenu/DropDownChild.test.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import DropDownChild from './DropDownChild';
|
||||||
|
|
||||||
|
const setup = (propOverrides?: object) => {
|
||||||
|
const props = Object.assign(
|
||||||
|
{
|
||||||
|
child: {
|
||||||
|
divider: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
propOverrides
|
||||||
|
);
|
||||||
|
|
||||||
|
return shallow(<DropDownChild {...props} />);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Render', () => {
|
||||||
|
it('should render component', () => {
|
||||||
|
const wrapper = setup();
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render icon if exists', () => {
|
||||||
|
const wrapper = setup({
|
||||||
|
child: {
|
||||||
|
divider: false,
|
||||||
|
icon: 'icon-test',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
21
public/app/core/components/sidemenu/DropDownChild.tsx
Normal file
21
public/app/core/components/sidemenu/DropDownChild.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import React, { SFC } from 'react';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
child: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DropDownChild: SFC<Props> = props => {
|
||||||
|
const { child } = props;
|
||||||
|
const listItemClassName = child.divider ? 'divider' : '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className={listItemClassName}>
|
||||||
|
<a href={child.url}>
|
||||||
|
{child.icon && <i className={child.icon} />}
|
||||||
|
{child.text}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DropDownChild;
|
70
public/app/core/components/sidemenu/SideMenu.test.tsx
Normal file
70
public/app/core/components/sidemenu/SideMenu.test.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import { SideMenu } from './SideMenu';
|
||||||
|
import appEvents from '../../app_events';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
|
|
||||||
|
jest.mock('../../app_events', () => ({
|
||||||
|
emit: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('app/core/services/context_srv', () => ({
|
||||||
|
contextSrv: {
|
||||||
|
sidemenu: true,
|
||||||
|
user: {},
|
||||||
|
isSignedIn: false,
|
||||||
|
isGrafanaAdmin: false,
|
||||||
|
isEditor: false,
|
||||||
|
hasEditPermissionFolders: false,
|
||||||
|
toggleSideMenu: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const setup = (propOverrides?: object) => {
|
||||||
|
const props = Object.assign(
|
||||||
|
{
|
||||||
|
loginUrl: '',
|
||||||
|
user: {},
|
||||||
|
mainLinks: [],
|
||||||
|
bottomeLinks: [],
|
||||||
|
isSignedIn: false,
|
||||||
|
},
|
||||||
|
propOverrides
|
||||||
|
);
|
||||||
|
|
||||||
|
return shallow(<SideMenu {...props} />);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Render', () => {
|
||||||
|
it('should render component', () => {
|
||||||
|
const wrapper = setup();
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Functions', () => {
|
||||||
|
describe('toggle side menu', () => {
|
||||||
|
const wrapper = setup();
|
||||||
|
const instance = wrapper.instance() as SideMenu;
|
||||||
|
instance.toggleSideMenu();
|
||||||
|
|
||||||
|
it('should call contextSrv.toggleSideMenu', () => {
|
||||||
|
expect(contextSrv.toggleSideMenu).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit toggle sidemenu event', () => {
|
||||||
|
expect(appEvents.emit).toHaveBeenCalledWith('toggle-sidemenu');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toggle side menu on mobile', () => {
|
||||||
|
const wrapper = setup();
|
||||||
|
const instance = wrapper.instance() as SideMenu;
|
||||||
|
instance.toggleSideMenuSmallBreakpoint();
|
||||||
|
|
||||||
|
it('should emit toggle sidemenu event', () => {
|
||||||
|
expect(appEvents.emit).toHaveBeenCalledWith('toggle-sidemenu-mobile');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
32
public/app/core/components/sidemenu/SideMenu.tsx
Normal file
32
public/app/core/components/sidemenu/SideMenu.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import appEvents from '../../app_events';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
|
import TopSection from './TopSection';
|
||||||
|
import BottomSection from './BottomSection';
|
||||||
|
|
||||||
|
export class SideMenu extends PureComponent {
|
||||||
|
toggleSideMenu = () => {
|
||||||
|
contextSrv.toggleSideMenu();
|
||||||
|
appEvents.emit('toggle-sidemenu');
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleSideMenuSmallBreakpoint = () => {
|
||||||
|
appEvents.emit('toggle-sidemenu-mobile');
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return [
|
||||||
|
<div className="sidemenu__logo" onClick={this.toggleSideMenu} key="logo">
|
||||||
|
<img src="public/img/grafana_icon.svg" alt="graphana_logo" />
|
||||||
|
</div>,
|
||||||
|
<div className="sidemenu__logo_small_breakpoint" onClick={this.toggleSideMenuSmallBreakpoint} key="hamburger">
|
||||||
|
<i className="fa fa-bars" />
|
||||||
|
<span className="sidemenu__close">
|
||||||
|
<i className="fa fa-times" /> Close
|
||||||
|
</span>
|
||||||
|
</div>,
|
||||||
|
<TopSection key="topsection" />,
|
||||||
|
<BottomSection key="bottomsection" />,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import SideMenuDropDown from './SideMenuDropDown';
|
||||||
|
|
||||||
|
const setup = (propOverrides?: object) => {
|
||||||
|
const props = Object.assign(
|
||||||
|
{
|
||||||
|
link: {
|
||||||
|
text: 'link',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
propOverrides
|
||||||
|
);
|
||||||
|
|
||||||
|
return shallow(<SideMenuDropDown {...props} />);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Render', () => {
|
||||||
|
it('should render component', () => {
|
||||||
|
const wrapper = setup();
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render children', () => {
|
||||||
|
const wrapper = setup({
|
||||||
|
link: {
|
||||||
|
text: 'link',
|
||||||
|
children: [{ id: 1 }, { id: 2 }, { id: 3 }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
23
public/app/core/components/sidemenu/SideMenuDropDown.tsx
Normal file
23
public/app/core/components/sidemenu/SideMenuDropDown.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React, { SFC } from 'react';
|
||||||
|
import DropDownChild from './DropDownChild';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
link: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SideMenuDropDown: SFC<Props> = props => {
|
||||||
|
const { link } = props;
|
||||||
|
return (
|
||||||
|
<ul className="dropdown-menu dropdown-menu--sidemenu" role="menu">
|
||||||
|
<li className="side-menu-header">
|
||||||
|
<span className="sidemenu-item-text">{link.text}</span>
|
||||||
|
</li>
|
||||||
|
{link.children &&
|
||||||
|
link.children.map((child, index) => {
|
||||||
|
return <DropDownChild child={child} key={`${child.url}-${index}`} />;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SideMenuDropDown;
|
11
public/app/core/components/sidemenu/SignIn.test.tsx
Normal file
11
public/app/core/components/sidemenu/SignIn.test.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import SignIn from './SignIn';
|
||||||
|
|
||||||
|
describe('Render', () => {
|
||||||
|
it('should render component', () => {
|
||||||
|
const wrapper = shallow(<SignIn />);
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
23
public/app/core/components/sidemenu/SignIn.tsx
Normal file
23
public/app/core/components/sidemenu/SignIn.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React, { SFC } from 'react';
|
||||||
|
|
||||||
|
const SignIn: SFC<any> = () => {
|
||||||
|
const loginUrl = `login?redirect=${encodeURIComponent(window.location.pathname)}`;
|
||||||
|
return (
|
||||||
|
<div className="sidemenu-item">
|
||||||
|
<a href={loginUrl} className="sidemenu-link" target="_self">
|
||||||
|
<span className="icon-circle sidemenu-icon">
|
||||||
|
<i className="fa fa-fw fa-sign-in" />
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<a href={loginUrl} target="_self">
|
||||||
|
<ul className="dropdown-menu dropdown-menu--sidemenu" role="menu">
|
||||||
|
<li className="side-menu-header">
|
||||||
|
<span className="sidemenu-item-text">Sign In</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SignIn;
|
41
public/app/core/components/sidemenu/TopSection.test.tsx
Normal file
41
public/app/core/components/sidemenu/TopSection.test.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import TopSection from './TopSection';
|
||||||
|
|
||||||
|
jest.mock('../../config', () => ({
|
||||||
|
bootData: {
|
||||||
|
navTree: [
|
||||||
|
{ id: '1', hideFromMenu: true },
|
||||||
|
{ id: '2', hideFromMenu: true },
|
||||||
|
{ id: '3', hideFromMenu: false },
|
||||||
|
{ id: '4', hideFromMenu: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const setup = (propOverrides?: object) => {
|
||||||
|
const props = Object.assign(
|
||||||
|
{
|
||||||
|
mainLinks: [],
|
||||||
|
},
|
||||||
|
propOverrides
|
||||||
|
);
|
||||||
|
|
||||||
|
return shallow(<TopSection {...props} />);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Render', () => {
|
||||||
|
it('should render component', () => {
|
||||||
|
const wrapper = setup();
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render items', () => {
|
||||||
|
const wrapper = setup({
|
||||||
|
mainLinks: [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
19
public/app/core/components/sidemenu/TopSection.tsx
Normal file
19
public/app/core/components/sidemenu/TopSection.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React, { SFC } from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import TopSectionItem from './TopSectionItem';
|
||||||
|
import config from '../../config';
|
||||||
|
|
||||||
|
const TopSection: SFC<any> = () => {
|
||||||
|
const navTree = _.cloneDeep(config.bootData.navTree);
|
||||||
|
const mainLinks = _.filter(navTree, item => !item.hideFromMenu);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sidemenu__top">
|
||||||
|
{mainLinks.map((link, index) => {
|
||||||
|
return <TopSectionItem link={link} key={`${link.id}-${index}`} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TopSection;
|
22
public/app/core/components/sidemenu/TopSectionItem.test.tsx
Normal file
22
public/app/core/components/sidemenu/TopSectionItem.test.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import TopSectionItem from './TopSectionItem';
|
||||||
|
|
||||||
|
const setup = (propOverrides?: object) => {
|
||||||
|
const props = Object.assign(
|
||||||
|
{
|
||||||
|
link: {},
|
||||||
|
},
|
||||||
|
propOverrides
|
||||||
|
);
|
||||||
|
|
||||||
|
return shallow(<TopSectionItem {...props} />);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Render', () => {
|
||||||
|
it('should render component', () => {
|
||||||
|
const wrapper = setup();
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
23
public/app/core/components/sidemenu/TopSectionItem.tsx
Normal file
23
public/app/core/components/sidemenu/TopSectionItem.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React, { SFC } from 'react';
|
||||||
|
import SideMenuDropDown from './SideMenuDropDown';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
link: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TopSectionItem: SFC<Props> = props => {
|
||||||
|
const { link } = props;
|
||||||
|
return (
|
||||||
|
<div className="sidemenu-item dropdown">
|
||||||
|
<a className="sidemenu-link" href={link.url} target={link.target}>
|
||||||
|
<span className="icon-circle sidemenu-icon">
|
||||||
|
<i className={link.icon} />
|
||||||
|
{link.img && <img src={link.img} />}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{link.children && <SideMenuDropDown link={link} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TopSectionItem;
|
@ -0,0 +1,163 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Render should render children 1`] = `
|
||||||
|
<div
|
||||||
|
className="sidemenu-item dropdown dropup"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className="sidemenu-link"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="icon-circle sidemenu-icon"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<ul
|
||||||
|
className="dropdown-menu dropdown-menu--sidemenu"
|
||||||
|
role="menu"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
key="undefined-0"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
onClick={[Function]}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
key="undefined-1"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
onClick={[Function]}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
key="undefined-2"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
onClick={[Function]}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
className="side-menu-header"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="sidemenu-item-text"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Render should render component 1`] = `
|
||||||
|
<div
|
||||||
|
className="sidemenu-item dropdown dropup"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className="sidemenu-link"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="icon-circle sidemenu-icon"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<ul
|
||||||
|
className="dropdown-menu dropdown-menu--sidemenu"
|
||||||
|
role="menu"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
className="side-menu-header"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="sidemenu-item-text"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Render should render organisation switcher 1`] = `
|
||||||
|
<div
|
||||||
|
className="sidemenu-item dropdown dropup"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className="sidemenu-link"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="icon-circle sidemenu-icon"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<ul
|
||||||
|
className="dropdown-menu dropdown-menu--sidemenu"
|
||||||
|
role="menu"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
className="sidemenu-org-switcher"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="sidemenu-org-switcher__org-name"
|
||||||
|
>
|
||||||
|
Grafana
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="sidemenu-org-switcher__org-current"
|
||||||
|
>
|
||||||
|
Current Org:
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="sidemenu-org-switcher__switch"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="fa fa-fw fa-random"
|
||||||
|
/>
|
||||||
|
Switch
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
className="side-menu-header"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="sidemenu-item-text"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Render should render subtitle 1`] = `
|
||||||
|
<div
|
||||||
|
className="sidemenu-item dropdown dropup"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className="sidemenu-link"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="icon-circle sidemenu-icon"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<ul
|
||||||
|
className="dropdown-menu dropdown-menu--sidemenu"
|
||||||
|
role="menu"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
className="sidemenu-subtitle"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="sidemenu-item-text"
|
||||||
|
>
|
||||||
|
subtitle
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
className="side-menu-header"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="sidemenu-item-text"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`;
|
@ -0,0 +1,34 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Render should render component 1`] = `
|
||||||
|
<div
|
||||||
|
className="sidemenu__bottom"
|
||||||
|
>
|
||||||
|
<SignIn />
|
||||||
|
<BottomNavLinks
|
||||||
|
key="undefined-0"
|
||||||
|
link={
|
||||||
|
Object {
|
||||||
|
"hideFromMenu": true,
|
||||||
|
"id": "profile",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<BottomNavLinks
|
||||||
|
key="undefined-1"
|
||||||
|
link={
|
||||||
|
Object {
|
||||||
|
"hideFromMenu": true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<BottomNavLinks
|
||||||
|
key="undefined-2"
|
||||||
|
link={
|
||||||
|
Object {
|
||||||
|
"hideFromMenu": true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`;
|
@ -0,0 +1,21 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Render should render component 1`] = `
|
||||||
|
<li
|
||||||
|
className="divider"
|
||||||
|
>
|
||||||
|
<a />
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Render should render icon if exists 1`] = `
|
||||||
|
<li
|
||||||
|
className=""
|
||||||
|
>
|
||||||
|
<a>
|
||||||
|
<i
|
||||||
|
className="icon-test"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
`;
|
@ -0,0 +1,22 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Render should render component 1`] = `
|
||||||
|
Array [
|
||||||
|
<div
|
||||||
|
className="sidemenu__logo"
|
||||||
|
key="logo"
|
||||||
|
onClick={[Function]}
|
||||||
|
/>,
|
||||||
|
<div
|
||||||
|
className="sidemenu__logo_small_breakpoint"
|
||||||
|
key="hamburger"
|
||||||
|
onClick={[Function]}
|
||||||
|
/>,
|
||||||
|
<TopSection
|
||||||
|
key="topsection"
|
||||||
|
/>,
|
||||||
|
<BottomSection
|
||||||
|
key="bottomsection"
|
||||||
|
/>,
|
||||||
|
]
|
||||||
|
`;
|
@ -0,0 +1,59 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Render should render children 1`] = `
|
||||||
|
<ul
|
||||||
|
className="dropdown-menu dropdown-menu--sidemenu"
|
||||||
|
role="menu"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
className="side-menu-header"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="sidemenu-item-text"
|
||||||
|
>
|
||||||
|
link
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<DropDownChild
|
||||||
|
child={
|
||||||
|
Object {
|
||||||
|
"id": 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
key="undefined-0"
|
||||||
|
/>
|
||||||
|
<DropDownChild
|
||||||
|
child={
|
||||||
|
Object {
|
||||||
|
"id": 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
key="undefined-1"
|
||||||
|
/>
|
||||||
|
<DropDownChild
|
||||||
|
child={
|
||||||
|
Object {
|
||||||
|
"id": 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
key="undefined-2"
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Render should render component 1`] = `
|
||||||
|
<ul
|
||||||
|
className="dropdown-menu dropdown-menu--sidemenu"
|
||||||
|
role="menu"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
className="side-menu-header"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="sidemenu-item-text"
|
||||||
|
>
|
||||||
|
link
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
`;
|
@ -0,0 +1,40 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Render should render component 1`] = `
|
||||||
|
<div
|
||||||
|
className="sidemenu-item"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className="sidemenu-link"
|
||||||
|
href="login?redirect=blank"
|
||||||
|
target="_self"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="icon-circle sidemenu-icon"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="fa fa-fw fa-sign-in"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="login?redirect=blank"
|
||||||
|
target="_self"
|
||||||
|
>
|
||||||
|
<ul
|
||||||
|
className="dropdown-menu dropdown-menu--sidemenu"
|
||||||
|
role="menu"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
className="side-menu-header"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="sidemenu-item-text"
|
||||||
|
>
|
||||||
|
Sign In
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
`;
|
@ -0,0 +1,33 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Render should render component 1`] = `
|
||||||
|
<div
|
||||||
|
className="sidemenu__top"
|
||||||
|
>
|
||||||
|
<TopSectionItem
|
||||||
|
key="3-0"
|
||||||
|
link={
|
||||||
|
Object {
|
||||||
|
"hideFromMenu": false,
|
||||||
|
"id": "3",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Render should render items 1`] = `
|
||||||
|
<div
|
||||||
|
className="sidemenu__top"
|
||||||
|
>
|
||||||
|
<TopSectionItem
|
||||||
|
key="3-0"
|
||||||
|
link={
|
||||||
|
Object {
|
||||||
|
"hideFromMenu": false,
|
||||||
|
"id": "3",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`;
|
@ -0,0 +1,17 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Render should render component 1`] = `
|
||||||
|
<div
|
||||||
|
className="sidemenu-item dropdown"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className="sidemenu-link"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="icon-circle sidemenu-icon"
|
||||||
|
>
|
||||||
|
<i />
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
`;
|
@ -1,81 +0,0 @@
|
|||||||
<a class="sidemenu__logo" ng-click="ctrl.toggleSideMenu()">
|
|
||||||
<img src="public/img/grafana_icon.svg"></img>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a class="sidemenu__logo_small_breakpoint" ng-click="ctrl.toggleSideMenuSmallBreakpoint()">
|
|
||||||
<i class="fa fa-bars"></i>
|
|
||||||
<span class="sidemenu__close">
|
|
||||||
<i class="fa fa-times"></i> Close</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="sidemenu__top">
|
|
||||||
<div ng-repeat="item in ::ctrl.mainLinks" class="sidemenu-item dropdown">
|
|
||||||
<a href="{{::item.url}}" class="sidemenu-link" target="{{::item.target}}">
|
|
||||||
<span class="icon-circle sidemenu-icon">
|
|
||||||
<i class="{{::item.icon}}" ng-show="::item.icon"></i>
|
|
||||||
<img ng-src="{{::item.img}}" ng-show="::item.img">
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
<ul class="dropdown-menu dropdown-menu--sidemenu" role="menu" ng-if="::item.children">
|
|
||||||
<li class="side-menu-header">
|
|
||||||
<span class="sidemenu-item-text">{{::item.text}}</span>
|
|
||||||
</li>
|
|
||||||
<li ng-repeat="child in ::item.children" ng-class="{divider: child.divider}">
|
|
||||||
<a href="{{::child.url}}">
|
|
||||||
<i class="{{::child.icon}}" ng-show="::child.icon"></i>
|
|
||||||
{{::child.text}}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sidemenu__bottom">
|
|
||||||
<div ng-show="::!ctrl.isSignedIn" class="sidemenu-item">
|
|
||||||
<a href="{{ctrl.loginUrl}}" class="sidemenu-link" target="_self">
|
|
||||||
<span class="icon-circle sidemenu-icon">
|
|
||||||
<i class="fa fa-fw fa-sign-in"></i>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
<a href="{{ctrl.loginUrl}}" target="_self">
|
|
||||||
<ul class="dropdown-menu dropdown-menu--sidemenu" role="menu">
|
|
||||||
<li class="side-menu-header">
|
|
||||||
<span class="sidemenu-item-text">Sign In</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-repeat="item in ::ctrl.bottomNav" class="sidemenu-item dropdown dropup">
|
|
||||||
<a href="{{::item.url}}" class="sidemenu-link" target="{{::item.target}}">
|
|
||||||
<span class="icon-circle sidemenu-icon">
|
|
||||||
<i class="{{::item.icon}}" ng-show="::item.icon"></i>
|
|
||||||
<img ng-src="{{::item.img}}" ng-show="::item.img">
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
<ul class="dropdown-menu dropdown-menu--sidemenu" role="menu">
|
|
||||||
<li ng-if="item.subTitle" class="sidemenu-subtitle">
|
|
||||||
<span class="sidemenu-item-text">{{::item.subTitle}}</span>
|
|
||||||
</li>
|
|
||||||
<li ng-if="item.showOrgSwitcher" class="sidemenu-org-switcher">
|
|
||||||
<a ng-click="ctrl.switchOrg()">
|
|
||||||
<div>
|
|
||||||
<div class="sidemenu-org-switcher__org-name">{{ctrl.contextSrv.user.orgName}}</div>
|
|
||||||
<div class="sidemenu-org-switcher__org-current">Current Org:</div>
|
|
||||||
</div>
|
|
||||||
<div class="sidemenu-org-switcher__switch">
|
|
||||||
<i class="fa fa-fw fa-random"></i>Switch</div>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li ng-repeat="child in ::item.children" ng-class="{divider: child.divider}" ng-hide="::child.hideFromMenu">
|
|
||||||
<a href="{{::child.url}}" target="{{::child.target}}" ng-click="ctrl.itemClicked(child, $event)">
|
|
||||||
<i class="{{::child.icon}}" ng-show="::child.icon"></i>
|
|
||||||
{{::child.text}}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="side-menu-header">
|
|
||||||
<span class="sidemenu-item-text">{{::item.text}}</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,89 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import config from 'app/core/config';
|
|
||||||
import $ from 'jquery';
|
|
||||||
import coreModule from '../../core_module';
|
|
||||||
import appEvents from 'app/core/app_events';
|
|
||||||
|
|
||||||
export class SideMenuCtrl {
|
|
||||||
user: any;
|
|
||||||
mainLinks: any;
|
|
||||||
bottomNav: any;
|
|
||||||
loginUrl: string;
|
|
||||||
isSignedIn: boolean;
|
|
||||||
isOpenMobile: boolean;
|
|
||||||
|
|
||||||
/** @ngInject */
|
|
||||||
constructor(private $scope, private $rootScope, private $location, private contextSrv, private $timeout) {
|
|
||||||
this.isSignedIn = contextSrv.isSignedIn;
|
|
||||||
this.user = contextSrv.user;
|
|
||||||
|
|
||||||
const navTree = _.cloneDeep(config.bootData.navTree);
|
|
||||||
this.mainLinks = _.filter(navTree, item => !item.hideFromMenu);
|
|
||||||
this.bottomNav = _.filter(navTree, item => item.hideFromMenu);
|
|
||||||
this.loginUrl = 'login?redirect=' + encodeURIComponent(this.$location.path());
|
|
||||||
|
|
||||||
if (contextSrv.user.orgCount > 1) {
|
|
||||||
const profileNode = _.find(this.bottomNav, { id: 'profile' });
|
|
||||||
if (profileNode) {
|
|
||||||
profileNode.showOrgSwitcher = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$scope.$on('$routeChangeSuccess', () => {
|
|
||||||
this.loginUrl = 'login?redirect=' + encodeURIComponent(this.$location.path());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleSideMenu() {
|
|
||||||
this.contextSrv.toggleSideMenu();
|
|
||||||
appEvents.emit('toggle-sidemenu');
|
|
||||||
|
|
||||||
this.$timeout(() => {
|
|
||||||
this.$rootScope.$broadcast('render');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleSideMenuSmallBreakpoint() {
|
|
||||||
appEvents.emit('toggle-sidemenu-mobile');
|
|
||||||
}
|
|
||||||
|
|
||||||
switchOrg() {
|
|
||||||
this.$rootScope.appEvent('show-modal', {
|
|
||||||
templateHtml: '<org-switcher dismiss="dismiss()"></org-switcher>',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
itemClicked(item, evt) {
|
|
||||||
if (item.url === '/shortcuts') {
|
|
||||||
appEvents.emit('show-modal', {
|
|
||||||
templateHtml: '<help-modal></help-modal>',
|
|
||||||
});
|
|
||||||
evt.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sideMenuDirective() {
|
|
||||||
return {
|
|
||||||
restrict: 'E',
|
|
||||||
templateUrl: 'public/app/core/components/sidemenu/sidemenu.html',
|
|
||||||
controller: SideMenuCtrl,
|
|
||||||
bindToController: true,
|
|
||||||
controllerAs: 'ctrl',
|
|
||||||
scope: {},
|
|
||||||
link: function(scope, elem) {
|
|
||||||
// hack to hide dropdown menu
|
|
||||||
elem.on('click.dropdown', '.dropdown-menu a', function(evt) {
|
|
||||||
const menu = $(evt.target).parents('.dropdown-menu');
|
|
||||||
const parent = menu.parent();
|
|
||||||
menu.detach();
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
parent.append(menu);
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
coreModule.directive('sidemenu', sideMenuDirective);
|
|
74
public/app/core/components/sql_part/sql_part.ts
Normal file
74
public/app/core/components/sql_part/sql_part.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
export class SqlPartDef {
|
||||||
|
type: string;
|
||||||
|
style: string;
|
||||||
|
label: string;
|
||||||
|
params: any[];
|
||||||
|
defaultParams: any[];
|
||||||
|
wrapOpen: string;
|
||||||
|
wrapClose: string;
|
||||||
|
separator: string;
|
||||||
|
|
||||||
|
constructor(options: any) {
|
||||||
|
this.type = options.type;
|
||||||
|
if (options.label) {
|
||||||
|
this.label = options.label;
|
||||||
|
} else {
|
||||||
|
this.label = this.type[0].toUpperCase() + this.type.substring(1) + ':';
|
||||||
|
}
|
||||||
|
this.style = options.style;
|
||||||
|
if (this.style === 'function') {
|
||||||
|
this.wrapOpen = '(';
|
||||||
|
this.wrapClose = ')';
|
||||||
|
this.separator = ', ';
|
||||||
|
} else {
|
||||||
|
this.wrapOpen = ' ';
|
||||||
|
this.wrapClose = ' ';
|
||||||
|
this.separator = ' ';
|
||||||
|
}
|
||||||
|
this.params = options.params;
|
||||||
|
this.defaultParams = options.defaultParams;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SqlPart {
|
||||||
|
part: any;
|
||||||
|
def: SqlPartDef;
|
||||||
|
params: any[];
|
||||||
|
label: string;
|
||||||
|
name: string;
|
||||||
|
datatype: string;
|
||||||
|
|
||||||
|
constructor(part: any, def: any) {
|
||||||
|
this.part = part;
|
||||||
|
this.def = def;
|
||||||
|
if (!this.def) {
|
||||||
|
throw { message: 'Could not find sql part ' + part.type };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.datatype = part.datatype;
|
||||||
|
|
||||||
|
if (part.name) {
|
||||||
|
this.name = part.name;
|
||||||
|
this.label = def.label + ' ' + part.name;
|
||||||
|
} else {
|
||||||
|
this.name = '';
|
||||||
|
this.label = def.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
part.params = part.params || _.clone(this.def.defaultParams);
|
||||||
|
this.params = part.params;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateParam(strValue, index) {
|
||||||
|
// handle optional parameters
|
||||||
|
if (strValue === '' && this.def.params[index].optional) {
|
||||||
|
this.params.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
this.params[index] = strValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.part.params = this.params;
|
||||||
|
}
|
||||||
|
}
|
199
public/app/core/components/sql_part/sql_part_editor.ts
Normal file
199
public/app/core/components/sql_part/sql_part_editor.ts
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import $ from 'jquery';
|
||||||
|
import coreModule from 'app/core/core_module';
|
||||||
|
|
||||||
|
const template = `
|
||||||
|
<div class="dropdown cascade-open">
|
||||||
|
<a ng-click="showActionsMenu()" class="query-part-name pointer dropdown-toggle" data-toggle="dropdown">{{part.label}}</a>
|
||||||
|
<span>{{part.def.wrapOpen}}</span><span class="query-part-parameters"></span><span>{{part.def.wrapClose}}</span>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li ng-repeat="action in partActions">
|
||||||
|
<a ng-click="triggerPartAction(action)">{{action.text}}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
|
||||||
|
/** @ngInject */
|
||||||
|
export function sqlPartEditorDirective($compile, templateSrv) {
|
||||||
|
const paramTemplate = '<input type="text" class="hide input-mini"></input>';
|
||||||
|
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
template: template,
|
||||||
|
scope: {
|
||||||
|
part: '=',
|
||||||
|
handleEvent: '&',
|
||||||
|
debounce: '@',
|
||||||
|
},
|
||||||
|
link: function postLink($scope, elem) {
|
||||||
|
const part = $scope.part;
|
||||||
|
const partDef = part.def;
|
||||||
|
const $paramsContainer = elem.find('.query-part-parameters');
|
||||||
|
const debounceLookup = $scope.debounce;
|
||||||
|
let cancelBlur = null;
|
||||||
|
|
||||||
|
$scope.partActions = [];
|
||||||
|
|
||||||
|
function clickFuncParam(this: any, paramIndex) {
|
||||||
|
/*jshint validthis:true */
|
||||||
|
const $link = $(this);
|
||||||
|
const $input = $link.next();
|
||||||
|
|
||||||
|
$input.val(part.params[paramIndex]);
|
||||||
|
$input.css('width', $link.width() + 16 + 'px');
|
||||||
|
|
||||||
|
$link.hide();
|
||||||
|
$input.show();
|
||||||
|
$input.focus();
|
||||||
|
$input.select();
|
||||||
|
|
||||||
|
const typeahead = $input.data('typeahead');
|
||||||
|
if (typeahead) {
|
||||||
|
$input.val('');
|
||||||
|
typeahead.lookup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function inputBlur($input, paramIndex) {
|
||||||
|
cancelBlur = setTimeout(function() {
|
||||||
|
switchToLink($input, paramIndex);
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchToLink($input, paramIndex) {
|
||||||
|
/*jshint validthis:true */
|
||||||
|
const $link = $input.prev();
|
||||||
|
const newValue = $input.val();
|
||||||
|
|
||||||
|
if (newValue !== '' || part.def.params[paramIndex].optional) {
|
||||||
|
$link.html(templateSrv.highlightVariablesAsHtml(newValue));
|
||||||
|
|
||||||
|
part.updateParam($input.val(), paramIndex);
|
||||||
|
$scope.$apply(() => {
|
||||||
|
$scope.handleEvent({ $event: { name: 'part-param-changed' } });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$input.hide();
|
||||||
|
$link.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function inputKeyPress(this: any, paramIndex, e) {
|
||||||
|
/*jshint validthis:true */
|
||||||
|
if (e.which === 13) {
|
||||||
|
switchToLink($(this), paramIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function inputKeyDown(this: any) {
|
||||||
|
/*jshint validthis:true */
|
||||||
|
this.style.width = (3 + this.value.length) * 8 + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTypeahead($input, param, paramIndex) {
|
||||||
|
if (!param.options && !param.dynamicLookup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeaheadSource = function(query, callback) {
|
||||||
|
if (param.options) {
|
||||||
|
let options = param.options;
|
||||||
|
if (param.type === 'int') {
|
||||||
|
options = _.map(options, function(val) {
|
||||||
|
return val.toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.$apply(function() {
|
||||||
|
$scope.handleEvent({ $event: { name: 'get-param-options', param: param } }).then(function(result) {
|
||||||
|
const dynamicOptions = _.map(result, function(op) {
|
||||||
|
return op.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// add current value to dropdown if it's not in dynamicOptions
|
||||||
|
if (_.indexOf(dynamicOptions, part.params[paramIndex]) === -1) {
|
||||||
|
dynamicOptions.unshift(part.params[paramIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(dynamicOptions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$input.attr('data-provide', 'typeahead');
|
||||||
|
|
||||||
|
$input.typeahead({
|
||||||
|
source: typeaheadSource,
|
||||||
|
minLength: 0,
|
||||||
|
items: 1000,
|
||||||
|
updater: function(value) {
|
||||||
|
if (value === part.params[paramIndex]) {
|
||||||
|
clearTimeout(cancelBlur);
|
||||||
|
$input.focus();
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const typeahead = $input.data('typeahead');
|
||||||
|
typeahead.lookup = function() {
|
||||||
|
this.query = this.$element.val() || '';
|
||||||
|
const items = this.source(this.query, $.proxy(this.process, this));
|
||||||
|
return items ? this.process(items) : items;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (debounceLookup) {
|
||||||
|
typeahead.lookup = _.debounce(typeahead.lookup, 500, { leading: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.showActionsMenu = function() {
|
||||||
|
$scope.handleEvent({ $event: { name: 'get-part-actions' } }).then(res => {
|
||||||
|
$scope.partActions = res;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.triggerPartAction = function(action) {
|
||||||
|
$scope.handleEvent({ $event: { name: 'action', action: action } });
|
||||||
|
};
|
||||||
|
|
||||||
|
function addElementsAndCompile() {
|
||||||
|
_.each(partDef.params, function(param, index) {
|
||||||
|
if (param.optional && part.params.length <= index) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index > 0) {
|
||||||
|
$('<span>' + partDef.separator + '</span>').appendTo($paramsContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
const paramValue = templateSrv.highlightVariablesAsHtml(part.params[index]);
|
||||||
|
const $paramLink = $('<a class="graphite-func-param-link pointer">' + paramValue + '</a>');
|
||||||
|
const $input = $(paramTemplate);
|
||||||
|
|
||||||
|
$paramLink.appendTo($paramsContainer);
|
||||||
|
$input.appendTo($paramsContainer);
|
||||||
|
|
||||||
|
$input.blur(_.partial(inputBlur, $input, index));
|
||||||
|
$input.keyup(inputKeyDown);
|
||||||
|
$input.keypress(_.partial(inputKeyPress, index));
|
||||||
|
$paramLink.click(_.partial(clickFuncParam, index));
|
||||||
|
|
||||||
|
addTypeahead($input, param, index);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function relink() {
|
||||||
|
$paramsContainer.empty();
|
||||||
|
addElementsAndCompile();
|
||||||
|
}
|
||||||
|
|
||||||
|
relink();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
coreModule.directive('sqlPartEditor', sqlPartEditorDirective);
|
@ -33,9 +33,9 @@ export class Settings {
|
|||||||
constructor(options) {
|
constructor(options) {
|
||||||
const defaults = {
|
const defaults = {
|
||||||
datasources: {},
|
datasources: {},
|
||||||
window_title_prefix: 'Grafana - ',
|
windowTitlePrefix: 'Grafana - ',
|
||||||
panels: {},
|
panels: {},
|
||||||
new_panel_title: 'Panel Title',
|
newPanelTitle: 'Panel Title',
|
||||||
playlist_timespan: '1m',
|
playlist_timespan: '1m',
|
||||||
unsaved_changes_warning: true,
|
unsaved_changes_warning: true,
|
||||||
appSubUrl: '',
|
appSubUrl: '',
|
||||||
|
@ -16,8 +16,8 @@ export class InvitedCtrl {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.init = function() {
|
$scope.init = () => {
|
||||||
backendSrv.get('/api/user/invite/' + $routeParams.code).then(function(invite) {
|
backendSrv.get('/api/user/invite/' + $routeParams.code).then(invite => {
|
||||||
$scope.formModel.name = invite.name;
|
$scope.formModel.name = invite.name;
|
||||||
$scope.formModel.email = invite.email;
|
$scope.formModel.email = invite.email;
|
||||||
$scope.formModel.username = invite.email;
|
$scope.formModel.username = invite.email;
|
||||||
@ -28,12 +28,12 @@ export class InvitedCtrl {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.submit = function() {
|
$scope.submit = () => {
|
||||||
if (!$scope.inviteForm.$valid) {
|
if (!$scope.inviteForm.$valid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
backendSrv.post('/api/user/invite/complete', $scope.formModel).then(function() {
|
backendSrv.post('/api/user/invite/complete', $scope.formModel).then(() => {
|
||||||
window.location.href = config.appSubUrl + '/';
|
window.location.href = config.appSubUrl + '/';
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -29,7 +29,7 @@ export class LoginCtrl {
|
|||||||
$scope.loginMode = true;
|
$scope.loginMode = true;
|
||||||
$scope.submitBtnText = 'Log in';
|
$scope.submitBtnText = 'Log in';
|
||||||
|
|
||||||
$scope.init = function() {
|
$scope.init = () => {
|
||||||
$scope.$watch('loginMode', $scope.loginModeChanged);
|
$scope.$watch('loginMode', $scope.loginModeChanged);
|
||||||
|
|
||||||
if (config.loginError) {
|
if (config.loginError) {
|
||||||
@ -37,7 +37,7 @@ export class LoginCtrl {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.submit = function() {
|
$scope.submit = () => {
|
||||||
if ($scope.loginMode) {
|
if ($scope.loginMode) {
|
||||||
$scope.login();
|
$scope.login();
|
||||||
} else {
|
} else {
|
||||||
@ -45,7 +45,7 @@ export class LoginCtrl {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.changeView = function() {
|
$scope.changeView = () => {
|
||||||
const loginView = document.querySelector('#login-view');
|
const loginView = document.querySelector('#login-view');
|
||||||
const changePasswordView = document.querySelector('#change-password-view');
|
const changePasswordView = document.querySelector('#change-password-view');
|
||||||
|
|
||||||
@ -65,7 +65,7 @@ export class LoginCtrl {
|
|||||||
}, 400);
|
}, 400);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.changePassword = function() {
|
$scope.changePassword = () => {
|
||||||
$scope.command.oldPassword = 'admin';
|
$scope.command.oldPassword = 'admin';
|
||||||
|
|
||||||
if ($scope.command.newPassword !== $scope.command.confirmNew) {
|
if ($scope.command.newPassword !== $scope.command.confirmNew) {
|
||||||
@ -73,25 +73,25 @@ export class LoginCtrl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
backendSrv.put('/api/user/password', $scope.command).then(function() {
|
backendSrv.put('/api/user/password', $scope.command).then(() => {
|
||||||
$scope.toGrafana();
|
$scope.toGrafana();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.skip = function() {
|
$scope.skip = () => {
|
||||||
$scope.toGrafana();
|
$scope.toGrafana();
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.loginModeChanged = function(newValue) {
|
$scope.loginModeChanged = newValue => {
|
||||||
$scope.submitBtnText = newValue ? 'Log in' : 'Sign up';
|
$scope.submitBtnText = newValue ? 'Log in' : 'Sign up';
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.signUp = function() {
|
$scope.signUp = () => {
|
||||||
if (!$scope.loginForm.$valid) {
|
if (!$scope.loginForm.$valid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
backendSrv.post('/api/user/signup', $scope.formModel).then(function(result) {
|
backendSrv.post('/api/user/signup', $scope.formModel).then(result => {
|
||||||
if (result.status === 'SignUpCreated') {
|
if (result.status === 'SignUpCreated') {
|
||||||
$location.path('/signup').search({ email: $scope.formModel.email });
|
$location.path('/signup').search({ email: $scope.formModel.email });
|
||||||
} else {
|
} else {
|
||||||
@ -100,7 +100,7 @@ export class LoginCtrl {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.login = function() {
|
$scope.login = () => {
|
||||||
delete $scope.loginError;
|
delete $scope.loginError;
|
||||||
|
|
||||||
if (!$scope.loginForm.$valid) {
|
if (!$scope.loginForm.$valid) {
|
||||||
@ -110,7 +110,7 @@ export class LoginCtrl {
|
|||||||
|
|
||||||
backendSrv
|
backendSrv
|
||||||
.post('/login', $scope.formModel)
|
.post('/login', $scope.formModel)
|
||||||
.then(function(result) {
|
.then(result => {
|
||||||
$scope.result = result;
|
$scope.result = result;
|
||||||
|
|
||||||
if ($scope.formModel.password !== 'admin' || $scope.ldapEnabled || $scope.authProxyEnabled) {
|
if ($scope.formModel.password !== 'admin' || $scope.ldapEnabled || $scope.authProxyEnabled) {
|
||||||
@ -125,7 +125,7 @@ export class LoginCtrl {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.toGrafana = function() {
|
$scope.toGrafana = () => {
|
||||||
const params = $location.search();
|
const params = $location.search();
|
||||||
|
|
||||||
if (params.redirect && params.redirect[0] === '/') {
|
if (params.redirect && params.redirect[0] === '/') {
|
||||||
|
@ -22,16 +22,16 @@ export class ResetPasswordCtrl {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.sendResetEmail = function() {
|
$scope.sendResetEmail = () => {
|
||||||
if (!$scope.sendResetForm.$valid) {
|
if (!$scope.sendResetForm.$valid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
backendSrv.post('/api/user/password/send-reset-email', $scope.formModel).then(function() {
|
backendSrv.post('/api/user/password/send-reset-email', $scope.formModel).then(() => {
|
||||||
$scope.mode = 'email-sent';
|
$scope.mode = 'email-sent';
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.submitReset = function() {
|
$scope.submitReset = () => {
|
||||||
if (!$scope.resetForm.$valid) {
|
if (!$scope.resetForm.$valid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -41,7 +41,7 @@ export class ResetPasswordCtrl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
backendSrv.post('/api/user/password/reset', $scope.formModel).then(function() {
|
backendSrv.post('/api/user/password/reset', $scope.formModel).then(() => {
|
||||||
$location.path('login');
|
$location.path('login');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -20,7 +20,6 @@ import './services/search_srv';
|
|||||||
import './services/ng_react';
|
import './services/ng_react';
|
||||||
|
|
||||||
import { grafanaAppDirective } from './components/grafana_app';
|
import { grafanaAppDirective } from './components/grafana_app';
|
||||||
import { sideMenuDirective } from './components/sidemenu/sidemenu';
|
|
||||||
import { searchDirective } from './components/search/search';
|
import { searchDirective } from './components/search/search';
|
||||||
import { infoPopover } from './components/info_popover';
|
import { infoPopover } from './components/info_popover';
|
||||||
import { navbarDirective } from './components/navbar/navbar';
|
import { navbarDirective } from './components/navbar/navbar';
|
||||||
@ -31,6 +30,7 @@ import { layoutSelector } from './components/layout_selector/layout_selector';
|
|||||||
import { switchDirective } from './components/switch';
|
import { switchDirective } from './components/switch';
|
||||||
import { dashboardSelector } from './components/dashboard_selector';
|
import { dashboardSelector } from './components/dashboard_selector';
|
||||||
import { queryPartEditorDirective } from './components/query_part/query_part_editor';
|
import { queryPartEditorDirective } from './components/query_part/query_part_editor';
|
||||||
|
import { sqlPartEditorDirective } from './components/sql_part/sql_part_editor';
|
||||||
import { formDropdownDirective } from './components/form_dropdown/form_dropdown';
|
import { formDropdownDirective } from './components/form_dropdown/form_dropdown';
|
||||||
import 'app/core/controllers/all';
|
import 'app/core/controllers/all';
|
||||||
import 'app/core/services/all';
|
import 'app/core/services/all';
|
||||||
@ -61,7 +61,6 @@ export {
|
|||||||
arrayJoin,
|
arrayJoin,
|
||||||
coreModule,
|
coreModule,
|
||||||
grafanaAppDirective,
|
grafanaAppDirective,
|
||||||
sideMenuDirective,
|
|
||||||
navbarDirective,
|
navbarDirective,
|
||||||
searchDirective,
|
searchDirective,
|
||||||
liveSrv,
|
liveSrv,
|
||||||
@ -72,6 +71,7 @@ export {
|
|||||||
appEvents,
|
appEvents,
|
||||||
dashboardSelector,
|
dashboardSelector,
|
||||||
queryPartEditorDirective,
|
queryPartEditorDirective,
|
||||||
|
sqlPartEditorDirective,
|
||||||
colors,
|
colors,
|
||||||
formDropdownDirective,
|
formDropdownDirective,
|
||||||
assignModelProperties,
|
assignModelProperties,
|
||||||
|
@ -3,22 +3,22 @@ import angular from 'angular';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import coreModule from '../core_module';
|
import coreModule from '../core_module';
|
||||||
|
|
||||||
coreModule.filter('stringSort', function() {
|
coreModule.filter('stringSort', () => {
|
||||||
return function(input) {
|
return input => {
|
||||||
return input.sort();
|
return input.sort();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
coreModule.filter('slice', function() {
|
coreModule.filter('slice', () => {
|
||||||
return function(arr, start, end) {
|
return (arr, start, end) => {
|
||||||
if (!_.isUndefined(arr)) {
|
if (!_.isUndefined(arr)) {
|
||||||
return arr.slice(start, end);
|
return arr.slice(start, end);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
coreModule.filter('stringify', function() {
|
coreModule.filter('stringify', () => {
|
||||||
return function(arr) {
|
return arr => {
|
||||||
if (_.isObject(arr) && !_.isArray(arr)) {
|
if (_.isObject(arr) && !_.isArray(arr)) {
|
||||||
return angular.toJson(arr);
|
return angular.toJson(arr);
|
||||||
} else {
|
} else {
|
||||||
@ -27,8 +27,8 @@ coreModule.filter('stringify', function() {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
coreModule.filter('moment', function() {
|
coreModule.filter('moment', () => {
|
||||||
return function(date, mode) {
|
return (date, mode) => {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case 'ago':
|
case 'ago':
|
||||||
return moment(date).fromNow();
|
return moment(date).fromNow();
|
||||||
@ -37,8 +37,8 @@ coreModule.filter('moment', function() {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
coreModule.filter('noXml', function() {
|
coreModule.filter('noXml', () => {
|
||||||
const noXml = function(text) {
|
const noXml = text => {
|
||||||
return _.isString(text)
|
return _.isString(text)
|
||||||
? text
|
? text
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
@ -48,14 +48,14 @@ coreModule.filter('noXml', function() {
|
|||||||
.replace(/"/g, '"')
|
.replace(/"/g, '"')
|
||||||
: text;
|
: text;
|
||||||
};
|
};
|
||||||
return function(text) {
|
return text => {
|
||||||
return _.isArray(text) ? _.map(text, noXml) : noXml(text);
|
return _.isArray(text) ? _.map(text, noXml) : noXml(text);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
function interpolateTemplateVars(templateSrv) {
|
function interpolateTemplateVars(templateSrv) {
|
||||||
const filterFunc: any = function(text, scope) {
|
const filterFunc: any = (text, scope) => {
|
||||||
let scopedVars;
|
let scopedVars;
|
||||||
if (scope.ctrl) {
|
if (scope.ctrl) {
|
||||||
scopedVars = (scope.ctrl.panel || scope.ctrl.row).scopedVars;
|
scopedVars = (scope.ctrl.panel || scope.ctrl.row).scopedVars;
|
||||||
|
@ -4,7 +4,7 @@ import _ from 'lodash';
|
|||||||
Mixins :)
|
Mixins :)
|
||||||
*/
|
*/
|
||||||
_.mixin({
|
_.mixin({
|
||||||
move: function(array, fromIndex, toIndex) {
|
move: (array, fromIndex, toIndex) => {
|
||||||
array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]);
|
array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]);
|
||||||
return array;
|
return array;
|
||||||
},
|
},
|
||||||
|
@ -70,7 +70,7 @@ export class AlertSrv {
|
|||||||
const newAlertJson = angular.toJson(newAlert);
|
const newAlertJson = angular.toJson(newAlert);
|
||||||
|
|
||||||
// remove same alert if it already exists
|
// remove same alert if it already exists
|
||||||
_.remove(this.list, function(value) {
|
_.remove(this.list, value => {
|
||||||
return angular.toJson(value) === newAlertJson;
|
return angular.toJson(value) === newAlertJson;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -8,6 +8,8 @@ export class User {
|
|||||||
isSignedIn: any;
|
isSignedIn: any;
|
||||||
orgRole: any;
|
orgRole: any;
|
||||||
orgId: number;
|
orgId: number;
|
||||||
|
orgName: string;
|
||||||
|
orgCount: number;
|
||||||
timezone: string;
|
timezone: string;
|
||||||
helpFlags1: number;
|
helpFlags1: number;
|
||||||
lightTheme: boolean;
|
lightTheme: boolean;
|
||||||
|
@ -6,13 +6,13 @@ import Drop from 'tether-drop';
|
|||||||
function popoverSrv(this: any, $compile, $rootScope, $timeout) {
|
function popoverSrv(this: any, $compile, $rootScope, $timeout) {
|
||||||
let openDrop = null;
|
let openDrop = null;
|
||||||
|
|
||||||
this.close = function() {
|
this.close = () => {
|
||||||
if (openDrop) {
|
if (openDrop) {
|
||||||
openDrop.close();
|
openDrop.close();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.show = function(options) {
|
this.show = options => {
|
||||||
if (openDrop) {
|
if (openDrop) {
|
||||||
openDrop.close();
|
openDrop.close();
|
||||||
openDrop = null;
|
openDrop = null;
|
||||||
@ -68,7 +68,7 @@ function popoverSrv(this: any, $compile, $rootScope, $timeout) {
|
|||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
// return close function
|
// return close function
|
||||||
return function() {
|
return () => {
|
||||||
if (drop) {
|
if (drop) {
|
||||||
drop.close();
|
drop.close();
|
||||||
}
|
}
|
||||||
|
@ -42,48 +42,48 @@ export function uiSegmentSrv(this: any, $sce, templateSrv) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.newSelectMeasurement = function() {
|
this.newSelectMeasurement = () => {
|
||||||
return new MetricSegment({ value: 'select measurement', fake: true });
|
return new MetricSegment({ value: 'select measurement', fake: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
this.newFake = function(text, type, cssClass) {
|
this.newFake = (text, type, cssClass) => {
|
||||||
return new MetricSegment({ value: text, fake: true, type: type, cssClass: cssClass });
|
return new MetricSegment({ value: text, fake: true, type: type, cssClass: cssClass });
|
||||||
};
|
};
|
||||||
|
|
||||||
this.newSegment = function(options) {
|
this.newSegment = options => {
|
||||||
return new MetricSegment(options);
|
return new MetricSegment(options);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.newKey = function(key) {
|
this.newKey = key => {
|
||||||
return new MetricSegment({ value: key, type: 'key', cssClass: 'query-segment-key' });
|
return new MetricSegment({ value: key, type: 'key', cssClass: 'query-segment-key' });
|
||||||
};
|
};
|
||||||
|
|
||||||
this.newKeyValue = function(value) {
|
this.newKeyValue = value => {
|
||||||
return new MetricSegment({ value: value, type: 'value', cssClass: 'query-segment-value' });
|
return new MetricSegment({ value: value, type: 'value', cssClass: 'query-segment-value' });
|
||||||
};
|
};
|
||||||
|
|
||||||
this.newCondition = function(condition) {
|
this.newCondition = condition => {
|
||||||
return new MetricSegment({ value: condition, type: 'condition', cssClass: 'query-keyword' });
|
return new MetricSegment({ value: condition, type: 'condition', cssClass: 'query-keyword' });
|
||||||
};
|
};
|
||||||
|
|
||||||
this.newOperator = function(op) {
|
this.newOperator = op => {
|
||||||
return new MetricSegment({ value: op, type: 'operator', cssClass: 'query-segment-operator' });
|
return new MetricSegment({ value: op, type: 'operator', cssClass: 'query-segment-operator' });
|
||||||
};
|
};
|
||||||
|
|
||||||
this.newOperators = function(ops) {
|
this.newOperators = ops => {
|
||||||
return _.map(ops, function(op) {
|
return _.map(ops, op => {
|
||||||
return new MetricSegment({ value: op, type: 'operator', cssClass: 'query-segment-operator' });
|
return new MetricSegment({ value: op, type: 'operator', cssClass: 'query-segment-operator' });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
this.transformToSegments = function(addTemplateVars, variableTypeFilter) {
|
this.transformToSegments = (addTemplateVars, variableTypeFilter) => {
|
||||||
return function(results) {
|
return results => {
|
||||||
const segments = _.map(results, function(segment) {
|
const segments = _.map(results, segment => {
|
||||||
return self.newSegment({ value: segment.text, expandable: segment.expandable });
|
return self.newSegment({ value: segment.text, expandable: segment.expandable });
|
||||||
});
|
});
|
||||||
|
|
||||||
if (addTemplateVars) {
|
if (addTemplateVars) {
|
||||||
_.each(templateSrv.variables, function(variable) {
|
_.each(templateSrv.variables, variable => {
|
||||||
if (variableTypeFilter === void 0 || variableTypeFilter === variable.type) {
|
if (variableTypeFilter === void 0 || variableTypeFilter === variable.type) {
|
||||||
segments.unshift(self.newSegment({ type: 'value', value: '$' + variable.name, expandable: true }));
|
segments.unshift(self.newSegment({ type: 'value', value: '$' + variable.name, expandable: true }));
|
||||||
}
|
}
|
||||||
@ -94,11 +94,11 @@ export function uiSegmentSrv(this: any, $sce, templateSrv) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
this.newSelectMetric = function() {
|
this.newSelectMetric = () => {
|
||||||
return new MetricSegment({ value: 'select metric', fake: true });
|
return new MetricSegment({ value: 'select metric', fake: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
this.newPlusButton = function() {
|
this.newPlusButton = () => {
|
||||||
return new MetricSegment({
|
return new MetricSegment({
|
||||||
fake: true,
|
fake: true,
|
||||||
html: '<i class="fa fa-plus "></i>',
|
html: '<i class="fa fa-plus "></i>',
|
||||||
|
@ -44,7 +44,7 @@ export class UtilSrv {
|
|||||||
backdrop: options.backdrop,
|
backdrop: options.backdrop,
|
||||||
});
|
});
|
||||||
|
|
||||||
Promise.resolve(modal).then(function(modalEl) {
|
Promise.resolve(modal).then(modalEl => {
|
||||||
modalEl.modal('show');
|
modalEl.modal('show');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -52,12 +52,12 @@ export class UtilSrv {
|
|||||||
showConfirmModal(payload) {
|
showConfirmModal(payload) {
|
||||||
const scope = this.$rootScope.$new();
|
const scope = this.$rootScope.$new();
|
||||||
|
|
||||||
scope.onConfirm = function() {
|
scope.onConfirm = () => {
|
||||||
payload.onConfirm();
|
payload.onConfirm();
|
||||||
scope.dismiss();
|
scope.dismiss();
|
||||||
};
|
};
|
||||||
|
|
||||||
scope.updateConfirmText = function(value) {
|
scope.updateConfirmText = value => {
|
||||||
scope.confirmTextValid = payload.confirmText.toLowerCase() === value.toLowerCase();
|
scope.confirmTextValid = payload.confirmText.toLowerCase() === value.toLowerCase();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||||
jest.mock('app/core/store');
|
jest.mock('app/core/store');
|
||||||
|
|
||||||
describe('backend_srv', function() {
|
describe('backend_srv', () => {
|
||||||
const _httpBackend = options => {
|
const _httpBackend = options => {
|
||||||
if (options.url === 'gateway-error') {
|
if (options.url === 'gateway-error') {
|
||||||
return Promise.reject({ status: 502 });
|
return Promise.reject({ status: 502 });
|
||||||
|
@ -91,11 +91,11 @@ describe('DateMath', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
_.each(spans, span => {
|
_.each(spans, span => {
|
||||||
it('should round now to the beginning of the ' + span, function() {
|
it('should round now to the beginning of the ' + span, () => {
|
||||||
expect(dateMath.parse('now/' + span).format(format)).toEqual(now.startOf(span).format(format));
|
expect(dateMath.parse('now/' + span).format(format)).toEqual(now.startOf(span).format(format));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should round now to the end of the ' + span, function() {
|
it('should round now to the end of the ' + span, () => {
|
||||||
expect(dateMath.parse('now/' + span, true).format(format)).toEqual(now.endOf(span).format(format));
|
expect(dateMath.parse('now/' + span, true).format(format)).toEqual(now.endOf(span).format(format));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -114,18 +114,18 @@ describe('DateMath', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('relative time to date parsing', function() {
|
describe('relative time to date parsing', () => {
|
||||||
it('should handle negative time', function() {
|
it('should handle negative time', () => {
|
||||||
const date = dateMath.parseDateMath('-2d', moment([2014, 1, 5]));
|
const date = dateMath.parseDateMath('-2d', moment([2014, 1, 5]));
|
||||||
expect(date.valueOf()).toEqual(moment([2014, 1, 3]).valueOf());
|
expect(date.valueOf()).toEqual(moment([2014, 1, 3]).valueOf());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle multiple math expressions', function() {
|
it('should handle multiple math expressions', () => {
|
||||||
const date = dateMath.parseDateMath('-2d-6h', moment([2014, 1, 5]));
|
const date = dateMath.parseDateMath('-2d-6h', moment([2014, 1, 5]));
|
||||||
expect(date.valueOf()).toEqual(moment([2014, 1, 2, 18]).valueOf());
|
expect(date.valueOf()).toEqual(moment([2014, 1, 2, 18]).valueOf());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false when invalid expression', function() {
|
it('should return false when invalid expression', () => {
|
||||||
const date = dateMath.parseDateMath('2', moment([2014, 1, 5]));
|
const date = dateMath.parseDateMath('2', moment([2014, 1, 5]));
|
||||||
expect(date).toEqual(undefined);
|
expect(date).toEqual(undefined);
|
||||||
});
|
});
|
||||||
|
@ -101,7 +101,7 @@ describe('file_export', () => {
|
|||||||
expect(returnedText).toBe(expectedText);
|
expect(returnedText).toBe(expectedText);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should decode HTML encoded characters', function() {
|
it('should decode HTML encoded characters', () => {
|
||||||
const inputTable = {
|
const inputTable = {
|
||||||
columns: [{ text: 'string_value' }],
|
columns: [{ text: 'string_value' }],
|
||||||
rows: [
|
rows: [
|
||||||
|
@ -2,27 +2,27 @@ import kbn from '../utils/kbn';
|
|||||||
import * as dateMath from '../utils/datemath';
|
import * as dateMath from '../utils/datemath';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
describe('unit format menu', function() {
|
describe('unit format menu', () => {
|
||||||
const menu = kbn.getUnitFormats();
|
const menu = kbn.getUnitFormats();
|
||||||
menu.map(function(submenu) {
|
menu.map(submenu => {
|
||||||
describe('submenu ' + submenu.text, function() {
|
describe('submenu ' + submenu.text, () => {
|
||||||
it('should have a title', function() {
|
it('should have a title', () => {
|
||||||
expect(typeof submenu.text).toBe('string');
|
expect(typeof submenu.text).toBe('string');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have a submenu', function() {
|
it('should have a submenu', () => {
|
||||||
expect(Array.isArray(submenu.submenu)).toBe(true);
|
expect(Array.isArray(submenu.submenu)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
submenu.submenu.map(function(entry) {
|
submenu.submenu.map(entry => {
|
||||||
describe('entry ' + entry.text, function() {
|
describe('entry ' + entry.text, () => {
|
||||||
it('should have a title', function() {
|
it('should have a title', () => {
|
||||||
expect(typeof entry.text).toBe('string');
|
expect(typeof entry.text).toBe('string');
|
||||||
});
|
});
|
||||||
it('should have a format', function() {
|
it('should have a format', () => {
|
||||||
expect(typeof entry.value).toBe('string');
|
expect(typeof entry.value).toBe('string');
|
||||||
});
|
});
|
||||||
it('should have a valid format', function() {
|
it('should have a valid format', () => {
|
||||||
expect(typeof kbn.valueFormats[entry.value]).toBe('function');
|
expect(typeof kbn.valueFormats[entry.value]).toBe('function');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -32,8 +32,8 @@ describe('unit format menu', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function describeValueFormat(desc, value, tickSize, tickDecimals, result) {
|
function describeValueFormat(desc, value, tickSize, tickDecimals, result) {
|
||||||
describe('value format: ' + desc, function() {
|
describe('value format: ' + desc, () => {
|
||||||
it('should translate ' + value + ' as ' + result, function() {
|
it('should translate ' + value + ' as ' + result, () => {
|
||||||
const scaledDecimals = tickDecimals - Math.floor(Math.log(tickSize) / Math.LN10);
|
const scaledDecimals = tickDecimals - Math.floor(Math.log(tickSize) / Math.LN10);
|
||||||
const str = kbn.valueFormats[desc](value, tickDecimals, scaledDecimals);
|
const str = kbn.valueFormats[desc](value, tickDecimals, scaledDecimals);
|
||||||
expect(str).toBe(result);
|
expect(str).toBe(result);
|
||||||
@ -100,85 +100,85 @@ describeValueFormat('d', 3, 1, 0, '3 day');
|
|||||||
describeValueFormat('d', 245, 100, 0, '35 week');
|
describeValueFormat('d', 245, 100, 0, '35 week');
|
||||||
describeValueFormat('d', 2456, 10, 0, '6.73 year');
|
describeValueFormat('d', 2456, 10, 0, '6.73 year');
|
||||||
|
|
||||||
describe('date time formats', function() {
|
describe('date time formats', () => {
|
||||||
const epoch = 1505634997920;
|
const epoch = 1505634997920;
|
||||||
const utcTime = moment.utc(epoch);
|
const utcTime = moment.utc(epoch);
|
||||||
const browserTime = moment(epoch);
|
const browserTime = moment(epoch);
|
||||||
|
|
||||||
it('should format as iso date', function() {
|
it('should format as iso date', () => {
|
||||||
const expected = browserTime.format('YYYY-MM-DD HH:mm:ss');
|
const expected = browserTime.format('YYYY-MM-DD HH:mm:ss');
|
||||||
const actual = kbn.valueFormats.dateTimeAsIso(epoch);
|
const actual = kbn.valueFormats.dateTimeAsIso(epoch);
|
||||||
expect(actual).toBe(expected);
|
expect(actual).toBe(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should format as iso date (in UTC)', function() {
|
it('should format as iso date (in UTC)', () => {
|
||||||
const expected = utcTime.format('YYYY-MM-DD HH:mm:ss');
|
const expected = utcTime.format('YYYY-MM-DD HH:mm:ss');
|
||||||
const actual = kbn.valueFormats.dateTimeAsIso(epoch, true);
|
const actual = kbn.valueFormats.dateTimeAsIso(epoch, true);
|
||||||
expect(actual).toBe(expected);
|
expect(actual).toBe(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should format as iso date and skip date when today', function() {
|
it('should format as iso date and skip date when today', () => {
|
||||||
const now = moment();
|
const now = moment();
|
||||||
const expected = now.format('HH:mm:ss');
|
const expected = now.format('HH:mm:ss');
|
||||||
const actual = kbn.valueFormats.dateTimeAsIso(now.valueOf(), false);
|
const actual = kbn.valueFormats.dateTimeAsIso(now.valueOf(), false);
|
||||||
expect(actual).toBe(expected);
|
expect(actual).toBe(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should format as iso date (in UTC) and skip date when today', function() {
|
it('should format as iso date (in UTC) and skip date when today', () => {
|
||||||
const now = moment.utc();
|
const now = moment.utc();
|
||||||
const expected = now.format('HH:mm:ss');
|
const expected = now.format('HH:mm:ss');
|
||||||
const actual = kbn.valueFormats.dateTimeAsIso(now.valueOf(), true);
|
const actual = kbn.valueFormats.dateTimeAsIso(now.valueOf(), true);
|
||||||
expect(actual).toBe(expected);
|
expect(actual).toBe(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should format as US date', function() {
|
it('should format as US date', () => {
|
||||||
const expected = browserTime.format('MM/DD/YYYY h:mm:ss a');
|
const expected = browserTime.format('MM/DD/YYYY h:mm:ss a');
|
||||||
const actual = kbn.valueFormats.dateTimeAsUS(epoch, false);
|
const actual = kbn.valueFormats.dateTimeAsUS(epoch, false);
|
||||||
expect(actual).toBe(expected);
|
expect(actual).toBe(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should format as US date (in UTC)', function() {
|
it('should format as US date (in UTC)', () => {
|
||||||
const expected = utcTime.format('MM/DD/YYYY h:mm:ss a');
|
const expected = utcTime.format('MM/DD/YYYY h:mm:ss a');
|
||||||
const actual = kbn.valueFormats.dateTimeAsUS(epoch, true);
|
const actual = kbn.valueFormats.dateTimeAsUS(epoch, true);
|
||||||
expect(actual).toBe(expected);
|
expect(actual).toBe(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should format as US date and skip date when today', function() {
|
it('should format as US date and skip date when today', () => {
|
||||||
const now = moment();
|
const now = moment();
|
||||||
const expected = now.format('h:mm:ss a');
|
const expected = now.format('h:mm:ss a');
|
||||||
const actual = kbn.valueFormats.dateTimeAsUS(now.valueOf(), false);
|
const actual = kbn.valueFormats.dateTimeAsUS(now.valueOf(), false);
|
||||||
expect(actual).toBe(expected);
|
expect(actual).toBe(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should format as US date (in UTC) and skip date when today', function() {
|
it('should format as US date (in UTC) and skip date when today', () => {
|
||||||
const now = moment.utc();
|
const now = moment.utc();
|
||||||
const expected = now.format('h:mm:ss a');
|
const expected = now.format('h:mm:ss a');
|
||||||
const actual = kbn.valueFormats.dateTimeAsUS(now.valueOf(), true);
|
const actual = kbn.valueFormats.dateTimeAsUS(now.valueOf(), true);
|
||||||
expect(actual).toBe(expected);
|
expect(actual).toBe(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should format as from now with days', function() {
|
it('should format as from now with days', () => {
|
||||||
const daysAgo = moment().add(-7, 'd');
|
const daysAgo = moment().add(-7, 'd');
|
||||||
const expected = '7 days ago';
|
const expected = '7 days ago';
|
||||||
const actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), false);
|
const actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), false);
|
||||||
expect(actual).toBe(expected);
|
expect(actual).toBe(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should format as from now with days (in UTC)', function() {
|
it('should format as from now with days (in UTC)', () => {
|
||||||
const daysAgo = moment.utc().add(-7, 'd');
|
const daysAgo = moment.utc().add(-7, 'd');
|
||||||
const expected = '7 days ago';
|
const expected = '7 days ago';
|
||||||
const actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), true);
|
const actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), true);
|
||||||
expect(actual).toBe(expected);
|
expect(actual).toBe(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should format as from now with minutes', function() {
|
it('should format as from now with minutes', () => {
|
||||||
const daysAgo = moment().add(-2, 'm');
|
const daysAgo = moment().add(-2, 'm');
|
||||||
const expected = '2 minutes ago';
|
const expected = '2 minutes ago';
|
||||||
const actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), false);
|
const actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), false);
|
||||||
expect(actual).toBe(expected);
|
expect(actual).toBe(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should format as from now with minutes (in UTC)', function() {
|
it('should format as from now with minutes (in UTC)', () => {
|
||||||
const daysAgo = moment.utc().add(-2, 'm');
|
const daysAgo = moment.utc().add(-2, 'm');
|
||||||
const expected = '2 minutes ago';
|
const expected = '2 minutes ago';
|
||||||
const actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), true);
|
const actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), true);
|
||||||
@ -186,92 +186,92 @@ describe('date time formats', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('kbn.toFixed and negative decimals', function() {
|
describe('kbn.toFixed and negative decimals', () => {
|
||||||
it('should treat as zero decimals', function() {
|
it('should treat as zero decimals', () => {
|
||||||
const str = kbn.toFixed(186.123, -2);
|
const str = kbn.toFixed(186.123, -2);
|
||||||
expect(str).toBe('186');
|
expect(str).toBe('186');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('kbn ms format when scaled decimals is null do not use it', function() {
|
describe('kbn ms format when scaled decimals is null do not use it', () => {
|
||||||
it('should use specified decimals', function() {
|
it('should use specified decimals', () => {
|
||||||
const str = kbn.valueFormats['ms'](10000086.123, 1, null);
|
const str = kbn.valueFormats['ms'](10000086.123, 1, null);
|
||||||
expect(str).toBe('2.8 hour');
|
expect(str).toBe('2.8 hour');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('kbn kbytes format when scaled decimals is null do not use it', function() {
|
describe('kbn kbytes format when scaled decimals is null do not use it', () => {
|
||||||
it('should use specified decimals', function() {
|
it('should use specified decimals', () => {
|
||||||
const str = kbn.valueFormats['kbytes'](10000000, 3, null);
|
const str = kbn.valueFormats['kbytes'](10000000, 3, null);
|
||||||
expect(str).toBe('9.537 GiB');
|
expect(str).toBe('9.537 GiB');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('kbn deckbytes format when scaled decimals is null do not use it', function() {
|
describe('kbn deckbytes format when scaled decimals is null do not use it', () => {
|
||||||
it('should use specified decimals', function() {
|
it('should use specified decimals', () => {
|
||||||
const str = kbn.valueFormats['deckbytes'](10000000, 3, null);
|
const str = kbn.valueFormats['deckbytes'](10000000, 3, null);
|
||||||
expect(str).toBe('10.000 GB');
|
expect(str).toBe('10.000 GB');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('kbn roundValue', function() {
|
describe('kbn roundValue', () => {
|
||||||
it('should should handle null value', function() {
|
it('should should handle null value', () => {
|
||||||
const str = kbn.roundValue(null, 2);
|
const str = kbn.roundValue(null, 2);
|
||||||
expect(str).toBe(null);
|
expect(str).toBe(null);
|
||||||
});
|
});
|
||||||
it('should round value', function() {
|
it('should round value', () => {
|
||||||
const str = kbn.roundValue(200.877, 2);
|
const str = kbn.roundValue(200.877, 2);
|
||||||
expect(str).toBe(200.88);
|
expect(str).toBe(200.88);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('calculateInterval', function() {
|
describe('calculateInterval', () => {
|
||||||
it('1h 100 resultion', function() {
|
it('1h 100 resultion', () => {
|
||||||
const range = { from: dateMath.parse('now-1h'), to: dateMath.parse('now') };
|
const range = { from: dateMath.parse('now-1h'), to: dateMath.parse('now') };
|
||||||
const res = kbn.calculateInterval(range, 100, null);
|
const res = kbn.calculateInterval(range, 100, null);
|
||||||
expect(res.interval).toBe('30s');
|
expect(res.interval).toBe('30s');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('10m 1600 resolution', function() {
|
it('10m 1600 resolution', () => {
|
||||||
const range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') };
|
const range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') };
|
||||||
const res = kbn.calculateInterval(range, 1600, null);
|
const res = kbn.calculateInterval(range, 1600, null);
|
||||||
expect(res.interval).toBe('500ms');
|
expect(res.interval).toBe('500ms');
|
||||||
expect(res.intervalMs).toBe(500);
|
expect(res.intervalMs).toBe(500);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fixed user min interval', function() {
|
it('fixed user min interval', () => {
|
||||||
const range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') };
|
const range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') };
|
||||||
const res = kbn.calculateInterval(range, 1600, '10s');
|
const res = kbn.calculateInterval(range, 1600, '10s');
|
||||||
expect(res.interval).toBe('10s');
|
expect(res.interval).toBe('10s');
|
||||||
expect(res.intervalMs).toBe(10000);
|
expect(res.intervalMs).toBe(10000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('short time range and user low limit', function() {
|
it('short time range and user low limit', () => {
|
||||||
const range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') };
|
const range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') };
|
||||||
const res = kbn.calculateInterval(range, 1600, '>10s');
|
const res = kbn.calculateInterval(range, 1600, '>10s');
|
||||||
expect(res.interval).toBe('10s');
|
expect(res.interval).toBe('10s');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('large time range and user low limit', function() {
|
it('large time range and user low limit', () => {
|
||||||
const range = { from: dateMath.parse('now-14d'), to: dateMath.parse('now') };
|
const range = { from: dateMath.parse('now-14d'), to: dateMath.parse('now') };
|
||||||
const res = kbn.calculateInterval(range, 1000, '>10s');
|
const res = kbn.calculateInterval(range, 1000, '>10s');
|
||||||
expect(res.interval).toBe('20m');
|
expect(res.interval).toBe('20m');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('10s 900 resolution and user low limit in ms', function() {
|
it('10s 900 resolution and user low limit in ms', () => {
|
||||||
const range = { from: dateMath.parse('now-10s'), to: dateMath.parse('now') };
|
const range = { from: dateMath.parse('now-10s'), to: dateMath.parse('now') };
|
||||||
const res = kbn.calculateInterval(range, 900, '>15ms');
|
const res = kbn.calculateInterval(range, 900, '>15ms');
|
||||||
expect(res.interval).toBe('15ms');
|
expect(res.interval).toBe('15ms');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('1d 1 resolution', function() {
|
it('1d 1 resolution', () => {
|
||||||
const range = { from: dateMath.parse('now-1d'), to: dateMath.parse('now') };
|
const range = { from: dateMath.parse('now-1d'), to: dateMath.parse('now') };
|
||||||
const res = kbn.calculateInterval(range, 1, null);
|
const res = kbn.calculateInterval(range, 1, null);
|
||||||
expect(res.interval).toBe('1d');
|
expect(res.interval).toBe('1d');
|
||||||
expect(res.intervalMs).toBe(86400000);
|
expect(res.intervalMs).toBe(86400000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('86399s 1 resolution', function() {
|
it('86399s 1 resolution', () => {
|
||||||
const range = {
|
const range = {
|
||||||
from: dateMath.parse('now-86390s'),
|
from: dateMath.parse('now-86390s'),
|
||||||
to: dateMath.parse('now'),
|
to: dateMath.parse('now'),
|
||||||
@ -282,140 +282,140 @@ describe('calculateInterval', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('hex', function() {
|
describe('hex', () => {
|
||||||
it('positive integer', function() {
|
it('positive integer', () => {
|
||||||
const str = kbn.valueFormats.hex(100, 0);
|
const str = kbn.valueFormats.hex(100, 0);
|
||||||
expect(str).toBe('64');
|
expect(str).toBe('64');
|
||||||
});
|
});
|
||||||
it('negative integer', function() {
|
it('negative integer', () => {
|
||||||
const str = kbn.valueFormats.hex(-100, 0);
|
const str = kbn.valueFormats.hex(-100, 0);
|
||||||
expect(str).toBe('-64');
|
expect(str).toBe('-64');
|
||||||
});
|
});
|
||||||
it('null', function() {
|
it('null', () => {
|
||||||
const str = kbn.valueFormats.hex(null, 0);
|
const str = kbn.valueFormats.hex(null, 0);
|
||||||
expect(str).toBe('');
|
expect(str).toBe('');
|
||||||
});
|
});
|
||||||
it('positive float', function() {
|
it('positive float', () => {
|
||||||
const str = kbn.valueFormats.hex(50.52, 1);
|
const str = kbn.valueFormats.hex(50.52, 1);
|
||||||
expect(str).toBe('32.8');
|
expect(str).toBe('32.8');
|
||||||
});
|
});
|
||||||
it('negative float', function() {
|
it('negative float', () => {
|
||||||
const str = kbn.valueFormats.hex(-50.333, 2);
|
const str = kbn.valueFormats.hex(-50.333, 2);
|
||||||
expect(str).toBe('-32.547AE147AE14');
|
expect(str).toBe('-32.547AE147AE14');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('hex 0x', function() {
|
describe('hex 0x', () => {
|
||||||
it('positive integeter', function() {
|
it('positive integeter', () => {
|
||||||
const str = kbn.valueFormats.hex0x(7999, 0);
|
const str = kbn.valueFormats.hex0x(7999, 0);
|
||||||
expect(str).toBe('0x1F3F');
|
expect(str).toBe('0x1F3F');
|
||||||
});
|
});
|
||||||
it('negative integer', function() {
|
it('negative integer', () => {
|
||||||
const str = kbn.valueFormats.hex0x(-584, 0);
|
const str = kbn.valueFormats.hex0x(-584, 0);
|
||||||
expect(str).toBe('-0x248');
|
expect(str).toBe('-0x248');
|
||||||
});
|
});
|
||||||
it('null', function() {
|
it('null', () => {
|
||||||
const str = kbn.valueFormats.hex0x(null, 0);
|
const str = kbn.valueFormats.hex0x(null, 0);
|
||||||
expect(str).toBe('');
|
expect(str).toBe('');
|
||||||
});
|
});
|
||||||
it('positive float', function() {
|
it('positive float', () => {
|
||||||
const str = kbn.valueFormats.hex0x(74.443, 3);
|
const str = kbn.valueFormats.hex0x(74.443, 3);
|
||||||
expect(str).toBe('0x4A.716872B020C4');
|
expect(str).toBe('0x4A.716872B020C4');
|
||||||
});
|
});
|
||||||
it('negative float', function() {
|
it('negative float', () => {
|
||||||
const str = kbn.valueFormats.hex0x(-65.458, 1);
|
const str = kbn.valueFormats.hex0x(-65.458, 1);
|
||||||
expect(str).toBe('-0x41.8');
|
expect(str).toBe('-0x41.8');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('duration', function() {
|
describe('duration', () => {
|
||||||
it('null', function() {
|
it('null', () => {
|
||||||
const str = kbn.toDuration(null, 0, 'millisecond');
|
const str = kbn.toDuration(null, 0, 'millisecond');
|
||||||
expect(str).toBe('');
|
expect(str).toBe('');
|
||||||
});
|
});
|
||||||
it('0 milliseconds', function() {
|
it('0 milliseconds', () => {
|
||||||
const str = kbn.toDuration(0, 0, 'millisecond');
|
const str = kbn.toDuration(0, 0, 'millisecond');
|
||||||
expect(str).toBe('0 milliseconds');
|
expect(str).toBe('0 milliseconds');
|
||||||
});
|
});
|
||||||
it('1 millisecond', function() {
|
it('1 millisecond', () => {
|
||||||
const str = kbn.toDuration(1, 0, 'millisecond');
|
const str = kbn.toDuration(1, 0, 'millisecond');
|
||||||
expect(str).toBe('1 millisecond');
|
expect(str).toBe('1 millisecond');
|
||||||
});
|
});
|
||||||
it('-1 millisecond', function() {
|
it('-1 millisecond', () => {
|
||||||
const str = kbn.toDuration(-1, 0, 'millisecond');
|
const str = kbn.toDuration(-1, 0, 'millisecond');
|
||||||
expect(str).toBe('1 millisecond ago');
|
expect(str).toBe('1 millisecond ago');
|
||||||
});
|
});
|
||||||
it('seconds', function() {
|
it('seconds', () => {
|
||||||
const str = kbn.toDuration(1, 0, 'second');
|
const str = kbn.toDuration(1, 0, 'second');
|
||||||
expect(str).toBe('1 second');
|
expect(str).toBe('1 second');
|
||||||
});
|
});
|
||||||
it('minutes', function() {
|
it('minutes', () => {
|
||||||
const str = kbn.toDuration(1, 0, 'minute');
|
const str = kbn.toDuration(1, 0, 'minute');
|
||||||
expect(str).toBe('1 minute');
|
expect(str).toBe('1 minute');
|
||||||
});
|
});
|
||||||
it('hours', function() {
|
it('hours', () => {
|
||||||
const str = kbn.toDuration(1, 0, 'hour');
|
const str = kbn.toDuration(1, 0, 'hour');
|
||||||
expect(str).toBe('1 hour');
|
expect(str).toBe('1 hour');
|
||||||
});
|
});
|
||||||
it('days', function() {
|
it('days', () => {
|
||||||
const str = kbn.toDuration(1, 0, 'day');
|
const str = kbn.toDuration(1, 0, 'day');
|
||||||
expect(str).toBe('1 day');
|
expect(str).toBe('1 day');
|
||||||
});
|
});
|
||||||
it('weeks', function() {
|
it('weeks', () => {
|
||||||
const str = kbn.toDuration(1, 0, 'week');
|
const str = kbn.toDuration(1, 0, 'week');
|
||||||
expect(str).toBe('1 week');
|
expect(str).toBe('1 week');
|
||||||
});
|
});
|
||||||
it('months', function() {
|
it('months', () => {
|
||||||
const str = kbn.toDuration(1, 0, 'month');
|
const str = kbn.toDuration(1, 0, 'month');
|
||||||
expect(str).toBe('1 month');
|
expect(str).toBe('1 month');
|
||||||
});
|
});
|
||||||
it('years', function() {
|
it('years', () => {
|
||||||
const str = kbn.toDuration(1, 0, 'year');
|
const str = kbn.toDuration(1, 0, 'year');
|
||||||
expect(str).toBe('1 year');
|
expect(str).toBe('1 year');
|
||||||
});
|
});
|
||||||
it('decimal days', function() {
|
it('decimal days', () => {
|
||||||
const str = kbn.toDuration(1.5, 2, 'day');
|
const str = kbn.toDuration(1.5, 2, 'day');
|
||||||
expect(str).toBe('1 day, 12 hours, 0 minutes');
|
expect(str).toBe('1 day, 12 hours, 0 minutes');
|
||||||
});
|
});
|
||||||
it('decimal months', function() {
|
it('decimal months', () => {
|
||||||
const str = kbn.toDuration(1.5, 3, 'month');
|
const str = kbn.toDuration(1.5, 3, 'month');
|
||||||
expect(str).toBe('1 month, 2 weeks, 1 day, 0 hours');
|
expect(str).toBe('1 month, 2 weeks, 1 day, 0 hours');
|
||||||
});
|
});
|
||||||
it('no decimals', function() {
|
it('no decimals', () => {
|
||||||
const str = kbn.toDuration(38898367008, 0, 'millisecond');
|
const str = kbn.toDuration(38898367008, 0, 'millisecond');
|
||||||
expect(str).toBe('1 year');
|
expect(str).toBe('1 year');
|
||||||
});
|
});
|
||||||
it('1 decimal', function() {
|
it('1 decimal', () => {
|
||||||
const str = kbn.toDuration(38898367008, 1, 'millisecond');
|
const str = kbn.toDuration(38898367008, 1, 'millisecond');
|
||||||
expect(str).toBe('1 year, 2 months');
|
expect(str).toBe('1 year, 2 months');
|
||||||
});
|
});
|
||||||
it('too many decimals', function() {
|
it('too many decimals', () => {
|
||||||
const str = kbn.toDuration(38898367008, 20, 'millisecond');
|
const str = kbn.toDuration(38898367008, 20, 'millisecond');
|
||||||
expect(str).toBe('1 year, 2 months, 3 weeks, 4 days, 5 hours, 6 minutes, 7 seconds, 8 milliseconds');
|
expect(str).toBe('1 year, 2 months, 3 weeks, 4 days, 5 hours, 6 minutes, 7 seconds, 8 milliseconds');
|
||||||
});
|
});
|
||||||
it('floating point error', function() {
|
it('floating point error', () => {
|
||||||
const str = kbn.toDuration(36993906007, 8, 'millisecond');
|
const str = kbn.toDuration(36993906007, 8, 'millisecond');
|
||||||
expect(str).toBe('1 year, 2 months, 0 weeks, 3 days, 4 hours, 5 minutes, 6 seconds, 7 milliseconds');
|
expect(str).toBe('1 year, 2 months, 0 weeks, 3 days, 4 hours, 5 minutes, 6 seconds, 7 milliseconds');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('volume', function() {
|
describe('volume', () => {
|
||||||
it('1000m3', function() {
|
it('1000m3', () => {
|
||||||
const str = kbn.valueFormats['m3'](1000, 1, null);
|
const str = kbn.valueFormats['m3'](1000, 1, null);
|
||||||
expect(str).toBe('1000.0 m³');
|
expect(str).toBe('1000.0 m³');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('hh:mm:ss', function() {
|
describe('hh:mm:ss', () => {
|
||||||
it('00:04:06', function() {
|
it('00:04:06', () => {
|
||||||
const str = kbn.valueFormats['dthms'](246, 1);
|
const str = kbn.valueFormats['dthms'](246, 1);
|
||||||
expect(str).toBe('00:04:06');
|
expect(str).toBe('00:04:06');
|
||||||
});
|
});
|
||||||
it('24:00:00', function() {
|
it('24:00:00', () => {
|
||||||
const str = kbn.valueFormats['dthms'](86400, 1);
|
const str = kbn.valueFormats['dthms'](86400, 1);
|
||||||
expect(str).toBe('24:00:00');
|
expect(str).toBe('24:00:00');
|
||||||
});
|
});
|
||||||
it('6824413:53:20', function() {
|
it('6824413:53:20', () => {
|
||||||
const str = kbn.valueFormats['dthms'](24567890000, 1);
|
const str = kbn.valueFormats['dthms'](24567890000, 1);
|
||||||
expect(str).toBe('6824413:53:20');
|
expect(str).toBe('6824413:53:20');
|
||||||
});
|
});
|
||||||
|
@ -1,33 +1,33 @@
|
|||||||
import TimeSeries from 'app/core/time_series2';
|
import TimeSeries from 'app/core/time_series2';
|
||||||
import { updateLegendValues } from 'app/core/time_series2';
|
import { updateLegendValues } from 'app/core/time_series2';
|
||||||
|
|
||||||
describe('TimeSeries', function() {
|
describe('TimeSeries', () => {
|
||||||
let points, series;
|
let points, series;
|
||||||
const yAxisFormats = ['short', 'ms'];
|
const yAxisFormats = ['short', 'ms'];
|
||||||
let testData;
|
let testData;
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(() => {
|
||||||
testData = {
|
testData = {
|
||||||
alias: 'test',
|
alias: 'test',
|
||||||
datapoints: [[1, 2], [null, 3], [10, 4], [8, 5]],
|
datapoints: [[1, 2], [null, 3], [10, 4], [8, 5]],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when getting flot pairs', function() {
|
describe('when getting flot pairs', () => {
|
||||||
it('with connected style, should ignore nulls', function() {
|
it('with connected style, should ignore nulls', () => {
|
||||||
series = new TimeSeries(testData);
|
series = new TimeSeries(testData);
|
||||||
points = series.getFlotPairs('connected', yAxisFormats);
|
points = series.getFlotPairs('connected', yAxisFormats);
|
||||||
expect(points.length).toBe(3);
|
expect(points.length).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with null as zero style, should replace nulls with zero', function() {
|
it('with null as zero style, should replace nulls with zero', () => {
|
||||||
series = new TimeSeries(testData);
|
series = new TimeSeries(testData);
|
||||||
points = series.getFlotPairs('null as zero', yAxisFormats);
|
points = series.getFlotPairs('null as zero', yAxisFormats);
|
||||||
expect(points.length).toBe(4);
|
expect(points.length).toBe(4);
|
||||||
expect(points[1][1]).toBe(0);
|
expect(points[1][1]).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('if last is null current should pick next to last', function() {
|
it('if last is null current should pick next to last', () => {
|
||||||
series = new TimeSeries({
|
series = new TimeSeries({
|
||||||
datapoints: [[10, 1], [null, 2]],
|
datapoints: [[10, 1], [null, 2]],
|
||||||
});
|
});
|
||||||
@ -35,7 +35,7 @@ describe('TimeSeries', function() {
|
|||||||
expect(series.stats.current).toBe(10);
|
expect(series.stats.current).toBe(10);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('max value should work for negative values', function() {
|
it('max value should work for negative values', () => {
|
||||||
series = new TimeSeries({
|
series = new TimeSeries({
|
||||||
datapoints: [[-10, 1], [-4, 2]],
|
datapoints: [[-10, 1], [-4, 2]],
|
||||||
});
|
});
|
||||||
@ -43,13 +43,13 @@ describe('TimeSeries', function() {
|
|||||||
expect(series.stats.max).toBe(-4);
|
expect(series.stats.max).toBe(-4);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('average value should ignore nulls', function() {
|
it('average value should ignore nulls', () => {
|
||||||
series = new TimeSeries(testData);
|
series = new TimeSeries(testData);
|
||||||
series.getFlotPairs('null', yAxisFormats);
|
series.getFlotPairs('null', yAxisFormats);
|
||||||
expect(series.stats.avg).toBe(6.333333333333333);
|
expect(series.stats.avg).toBe(6.333333333333333);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('the delta value should account for nulls', function() {
|
it('the delta value should account for nulls', () => {
|
||||||
series = new TimeSeries({
|
series = new TimeSeries({
|
||||||
datapoints: [[1, 2], [3, 3], [null, 4], [10, 5], [15, 6]],
|
datapoints: [[1, 2], [3, 3], [null, 4], [10, 5], [15, 6]],
|
||||||
});
|
});
|
||||||
@ -57,7 +57,7 @@ describe('TimeSeries', function() {
|
|||||||
expect(series.stats.delta).toBe(14);
|
expect(series.stats.delta).toBe(14);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('the delta value should account for nulls on first', function() {
|
it('the delta value should account for nulls on first', () => {
|
||||||
series = new TimeSeries({
|
series = new TimeSeries({
|
||||||
datapoints: [[null, 2], [1, 3], [10, 4], [15, 5]],
|
datapoints: [[null, 2], [1, 3], [10, 4], [15, 5]],
|
||||||
});
|
});
|
||||||
@ -65,7 +65,7 @@ describe('TimeSeries', function() {
|
|||||||
expect(series.stats.delta).toBe(14);
|
expect(series.stats.delta).toBe(14);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('the delta value should account for nulls on last', function() {
|
it('the delta value should account for nulls on last', () => {
|
||||||
series = new TimeSeries({
|
series = new TimeSeries({
|
||||||
datapoints: [[1, 2], [5, 3], [10, 4], [null, 5]],
|
datapoints: [[1, 2], [5, 3], [10, 4], [null, 5]],
|
||||||
});
|
});
|
||||||
@ -73,7 +73,7 @@ describe('TimeSeries', function() {
|
|||||||
expect(series.stats.delta).toBe(9);
|
expect(series.stats.delta).toBe(9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('the delta value should account for resets', function() {
|
it('the delta value should account for resets', () => {
|
||||||
series = new TimeSeries({
|
series = new TimeSeries({
|
||||||
datapoints: [[1, 2], [5, 3], [10, 4], [0, 5], [10, 6]],
|
datapoints: [[1, 2], [5, 3], [10, 4], [0, 5], [10, 6]],
|
||||||
});
|
});
|
||||||
@ -81,7 +81,7 @@ describe('TimeSeries', function() {
|
|||||||
expect(series.stats.delta).toBe(19);
|
expect(series.stats.delta).toBe(19);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('the delta value should account for resets on last', function() {
|
it('the delta value should account for resets on last', () => {
|
||||||
series = new TimeSeries({
|
series = new TimeSeries({
|
||||||
datapoints: [[1, 2], [2, 3], [10, 4], [8, 5]],
|
datapoints: [[1, 2], [2, 3], [10, 4], [8, 5]],
|
||||||
});
|
});
|
||||||
@ -89,13 +89,13 @@ describe('TimeSeries', function() {
|
|||||||
expect(series.stats.delta).toBe(17);
|
expect(series.stats.delta).toBe(17);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('the range value should be max - min', function() {
|
it('the range value should be max - min', () => {
|
||||||
series = new TimeSeries(testData);
|
series = new TimeSeries(testData);
|
||||||
series.getFlotPairs('null', yAxisFormats);
|
series.getFlotPairs('null', yAxisFormats);
|
||||||
expect(series.stats.range).toBe(9);
|
expect(series.stats.range).toBe(9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('first value should ingone nulls', function() {
|
it('first value should ingone nulls', () => {
|
||||||
series = new TimeSeries(testData);
|
series = new TimeSeries(testData);
|
||||||
series.getFlotPairs('null', yAxisFormats);
|
series.getFlotPairs('null', yAxisFormats);
|
||||||
expect(series.stats.first).toBe(1);
|
expect(series.stats.first).toBe(1);
|
||||||
@ -106,13 +106,13 @@ describe('TimeSeries', function() {
|
|||||||
expect(series.stats.first).toBe(1);
|
expect(series.stats.first).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with null as zero style, average value should treat nulls as 0', function() {
|
it('with null as zero style, average value should treat nulls as 0', () => {
|
||||||
series = new TimeSeries(testData);
|
series = new TimeSeries(testData);
|
||||||
series.getFlotPairs('null as zero', yAxisFormats);
|
series.getFlotPairs('null as zero', yAxisFormats);
|
||||||
expect(series.stats.avg).toBe(4.75);
|
expect(series.stats.avg).toBe(4.75);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('average value should be null if all values is null', function() {
|
it('average value should be null if all values is null', () => {
|
||||||
series = new TimeSeries({
|
series = new TimeSeries({
|
||||||
datapoints: [[null, 2], [null, 3], [null, 4], [null, 5]],
|
datapoints: [[null, 2], [null, 3], [null, 4], [null, 5]],
|
||||||
});
|
});
|
||||||
@ -120,7 +120,7 @@ describe('TimeSeries', function() {
|
|||||||
expect(series.stats.avg).toBe(null);
|
expect(series.stats.avg).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calculates timeStep', function() {
|
it('calculates timeStep', () => {
|
||||||
series = new TimeSeries({
|
series = new TimeSeries({
|
||||||
datapoints: [[null, 1], [null, 2], [null, 3]],
|
datapoints: [[null, 1], [null, 2], [null, 3]],
|
||||||
});
|
});
|
||||||
@ -135,190 +135,190 @@ describe('TimeSeries', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('When checking if ms resolution is needed', function() {
|
describe('When checking if ms resolution is needed', () => {
|
||||||
describe('msResolution with second resolution timestamps', function() {
|
describe('msResolution with second resolution timestamps', () => {
|
||||||
beforeEach(function() {
|
beforeEach(() => {
|
||||||
series = new TimeSeries({
|
series = new TimeSeries({
|
||||||
datapoints: [[45, 1234567890], [60, 1234567899]],
|
datapoints: [[45, 1234567890], [60, 1234567899]],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set hasMsResolution to false', function() {
|
it('should set hasMsResolution to false', () => {
|
||||||
expect(series.hasMsResolution).toBe(false);
|
expect(series.hasMsResolution).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('msResolution with millisecond resolution timestamps', function() {
|
describe('msResolution with millisecond resolution timestamps', () => {
|
||||||
beforeEach(function() {
|
beforeEach(() => {
|
||||||
series = new TimeSeries({
|
series = new TimeSeries({
|
||||||
datapoints: [[55, 1236547890001], [90, 1234456709000]],
|
datapoints: [[55, 1236547890001], [90, 1234456709000]],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show millisecond resolution tooltip', function() {
|
it('should show millisecond resolution tooltip', () => {
|
||||||
expect(series.hasMsResolution).toBe(true);
|
expect(series.hasMsResolution).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('msResolution with millisecond resolution timestamps but with trailing zeroes', function() {
|
describe('msResolution with millisecond resolution timestamps but with trailing zeroes', () => {
|
||||||
beforeEach(function() {
|
beforeEach(() => {
|
||||||
series = new TimeSeries({
|
series = new TimeSeries({
|
||||||
datapoints: [[45, 1234567890000], [60, 1234567899000]],
|
datapoints: [[45, 1234567890000], [60, 1234567899000]],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not show millisecond resolution tooltip', function() {
|
it('should not show millisecond resolution tooltip', () => {
|
||||||
expect(series.hasMsResolution).toBe(false);
|
expect(series.hasMsResolution).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('can detect if series contains ms precision', function() {
|
describe('can detect if series contains ms precision', () => {
|
||||||
let fakedata;
|
let fakedata;
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(() => {
|
||||||
fakedata = testData;
|
fakedata = testData;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('missing datapoint with ms precision', function() {
|
it('missing datapoint with ms precision', () => {
|
||||||
fakedata.datapoints[0] = [1337, 1234567890000];
|
fakedata.datapoints[0] = [1337, 1234567890000];
|
||||||
series = new TimeSeries(fakedata);
|
series = new TimeSeries(fakedata);
|
||||||
expect(series.isMsResolutionNeeded()).toBe(false);
|
expect(series.isMsResolutionNeeded()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('contains datapoint with ms precision', function() {
|
it('contains datapoint with ms precision', () => {
|
||||||
fakedata.datapoints[0] = [1337, 1236547890001];
|
fakedata.datapoints[0] = [1337, 1236547890001];
|
||||||
series = new TimeSeries(fakedata);
|
series = new TimeSeries(fakedata);
|
||||||
expect(series.isMsResolutionNeeded()).toBe(true);
|
expect(series.isMsResolutionNeeded()).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('series overrides', function() {
|
describe('series overrides', () => {
|
||||||
let series;
|
let series;
|
||||||
beforeEach(function() {
|
beforeEach(() => {
|
||||||
series = new TimeSeries(testData);
|
series = new TimeSeries(testData);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('fill & points', function() {
|
describe('fill & points', () => {
|
||||||
beforeEach(function() {
|
beforeEach(() => {
|
||||||
series.alias = 'test';
|
series.alias = 'test';
|
||||||
series.applySeriesOverrides([{ alias: 'test', fill: 0, points: true }]);
|
series.applySeriesOverrides([{ alias: 'test', fill: 0, points: true }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set fill zero, and enable points', function() {
|
it('should set fill zero, and enable points', () => {
|
||||||
expect(series.lines.fill).toBe(0.001);
|
expect(series.lines.fill).toBe(0.001);
|
||||||
expect(series.points.show).toBe(true);
|
expect(series.points.show).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('series option overrides, bars, true & lines false', function() {
|
describe('series option overrides, bars, true & lines false', () => {
|
||||||
beforeEach(function() {
|
beforeEach(() => {
|
||||||
series.alias = 'test';
|
series.alias = 'test';
|
||||||
series.applySeriesOverrides([{ alias: 'test', bars: true, lines: false }]);
|
series.applySeriesOverrides([{ alias: 'test', bars: true, lines: false }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should disable lines, and enable bars', function() {
|
it('should disable lines, and enable bars', () => {
|
||||||
expect(series.lines.show).toBe(false);
|
expect(series.lines.show).toBe(false);
|
||||||
expect(series.bars.show).toBe(true);
|
expect(series.bars.show).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('series option overrides, linewidth, stack', function() {
|
describe('series option overrides, linewidth, stack', () => {
|
||||||
beforeEach(function() {
|
beforeEach(() => {
|
||||||
series.alias = 'test';
|
series.alias = 'test';
|
||||||
series.applySeriesOverrides([{ alias: 'test', linewidth: 5, stack: false }]);
|
series.applySeriesOverrides([{ alias: 'test', linewidth: 5, stack: false }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should disable stack, and set lineWidth', function() {
|
it('should disable stack, and set lineWidth', () => {
|
||||||
expect(series.stack).toBe(false);
|
expect(series.stack).toBe(false);
|
||||||
expect(series.lines.lineWidth).toBe(5);
|
expect(series.lines.lineWidth).toBe(5);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('series option overrides, dashes and lineWidth', function() {
|
describe('series option overrides, dashes and lineWidth', () => {
|
||||||
beforeEach(function() {
|
beforeEach(() => {
|
||||||
series.alias = 'test';
|
series.alias = 'test';
|
||||||
series.applySeriesOverrides([{ alias: 'test', linewidth: 5, dashes: true }]);
|
series.applySeriesOverrides([{ alias: 'test', linewidth: 5, dashes: true }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should enable dashes, set dashes lineWidth to 5 and lines lineWidth to 0', function() {
|
it('should enable dashes, set dashes lineWidth to 5 and lines lineWidth to 0', () => {
|
||||||
expect(series.dashes.show).toBe(true);
|
expect(series.dashes.show).toBe(true);
|
||||||
expect(series.dashes.lineWidth).toBe(5);
|
expect(series.dashes.lineWidth).toBe(5);
|
||||||
expect(series.lines.lineWidth).toBe(0);
|
expect(series.lines.lineWidth).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('series option overrides, fill below to', function() {
|
describe('series option overrides, fill below to', () => {
|
||||||
beforeEach(function() {
|
beforeEach(() => {
|
||||||
series.alias = 'test';
|
series.alias = 'test';
|
||||||
series.applySeriesOverrides([{ alias: 'test', fillBelowTo: 'min' }]);
|
series.applySeriesOverrides([{ alias: 'test', fillBelowTo: 'min' }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should disable line fill and add fillBelowTo', function() {
|
it('should disable line fill and add fillBelowTo', () => {
|
||||||
expect(series.fillBelowTo).toBe('min');
|
expect(series.fillBelowTo).toBe('min');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('series option overrides, pointradius, steppedLine', function() {
|
describe('series option overrides, pointradius, steppedLine', () => {
|
||||||
beforeEach(function() {
|
beforeEach(() => {
|
||||||
series.alias = 'test';
|
series.alias = 'test';
|
||||||
series.applySeriesOverrides([{ alias: 'test', pointradius: 5, steppedLine: true }]);
|
series.applySeriesOverrides([{ alias: 'test', pointradius: 5, steppedLine: true }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set pointradius, and set steppedLine', function() {
|
it('should set pointradius, and set steppedLine', () => {
|
||||||
expect(series.points.radius).toBe(5);
|
expect(series.points.radius).toBe(5);
|
||||||
expect(series.lines.steps).toBe(true);
|
expect(series.lines.steps).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('override match on regex', function() {
|
describe('override match on regex', () => {
|
||||||
beforeEach(function() {
|
beforeEach(() => {
|
||||||
series.alias = 'test_01';
|
series.alias = 'test_01';
|
||||||
series.applySeriesOverrides([{ alias: '/.*01/', lines: false }]);
|
series.applySeriesOverrides([{ alias: '/.*01/', lines: false }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should match second series', function() {
|
it('should match second series', () => {
|
||||||
expect(series.lines.show).toBe(false);
|
expect(series.lines.show).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('override series y-axis, and z-index', function() {
|
describe('override series y-axis, and z-index', () => {
|
||||||
beforeEach(function() {
|
beforeEach(() => {
|
||||||
series.alias = 'test';
|
series.alias = 'test';
|
||||||
series.applySeriesOverrides([{ alias: 'test', yaxis: 2, zindex: 2 }]);
|
series.applySeriesOverrides([{ alias: 'test', yaxis: 2, zindex: 2 }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set yaxis', function() {
|
it('should set yaxis', () => {
|
||||||
expect(series.yaxis).toBe(2);
|
expect(series.yaxis).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set zindex', function() {
|
it('should set zindex', () => {
|
||||||
expect(series.zindex).toBe(2);
|
expect(series.zindex).toBe(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('override color', function() {
|
describe('override color', () => {
|
||||||
beforeEach(function() {
|
beforeEach(() => {
|
||||||
series.applySeriesOverrides([{ alias: 'test', color: '#112233' }]);
|
series.applySeriesOverrides([{ alias: 'test', color: '#112233' }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set color', function() {
|
it('should set color', () => {
|
||||||
expect(series.color).toBe('#112233');
|
expect(series.color).toBe('#112233');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set bars.fillColor', function() {
|
it('should set bars.fillColor', () => {
|
||||||
expect(series.bars.fillColor).toBe('#112233');
|
expect(series.bars.fillColor).toBe('#112233');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('value formatter', function() {
|
describe('value formatter', () => {
|
||||||
let series;
|
let series;
|
||||||
beforeEach(function() {
|
beforeEach(() => {
|
||||||
series = new TimeSeries(testData);
|
series = new TimeSeries(testData);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should format non-numeric values as empty string', function() {
|
it('should format non-numeric values as empty string', () => {
|
||||||
expect(series.formatValue(null)).toBe('');
|
expect(series.formatValue(null)).toBe('');
|
||||||
expect(series.formatValue(undefined)).toBe('');
|
expect(series.formatValue(undefined)).toBe('');
|
||||||
expect(series.formatValue(NaN)).toBe('');
|
expect(series.formatValue(NaN)).toBe('');
|
||||||
@ -327,10 +327,10 @@ describe('TimeSeries', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('legend decimals', function() {
|
describe('legend decimals', () => {
|
||||||
let series, panel;
|
let series, panel;
|
||||||
const height = 200;
|
const height = 200;
|
||||||
beforeEach(function() {
|
beforeEach(() => {
|
||||||
testData = {
|
testData = {
|
||||||
alias: 'test',
|
alias: 'test',
|
||||||
datapoints: [[1, 2], [0, 3], [10, 4], [8, 5]],
|
datapoints: [[1, 2], [0, 3], [10, 4], [8, 5]],
|
||||||
@ -347,14 +347,14 @@ describe('TimeSeries', function() {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set decimals based on Y axis (expect calculated decimals = 1)', function() {
|
it('should set decimals based on Y axis (expect calculated decimals = 1)', () => {
|
||||||
const data = [series];
|
const data = [series];
|
||||||
// Expect ticks with this data will have decimals = 1
|
// Expect ticks with this data will have decimals = 1
|
||||||
updateLegendValues(data, panel, height);
|
updateLegendValues(data, panel, height);
|
||||||
expect(data[0].decimals).toBe(2);
|
expect(data[0].decimals).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set decimals based on Y axis to 0 if calculated decimals = 0)', function() {
|
it('should set decimals based on Y axis to 0 if calculated decimals = 0)', () => {
|
||||||
testData.datapoints = [[10, 2], [0, 3], [100, 4], [80, 5]];
|
testData.datapoints = [[10, 2], [0, 3], [100, 4], [80, 5]];
|
||||||
series = new TimeSeries(testData);
|
series = new TimeSeries(testData);
|
||||||
series.getFlotPairs();
|
series.getFlotPairs();
|
||||||
@ -363,14 +363,14 @@ describe('TimeSeries', function() {
|
|||||||
expect(data[0].decimals).toBe(0);
|
expect(data[0].decimals).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set decimals to Y axis decimals + 1', function() {
|
it('should set decimals to Y axis decimals + 1', () => {
|
||||||
panel.yaxes[0].decimals = 2;
|
panel.yaxes[0].decimals = 2;
|
||||||
const data = [series];
|
const data = [series];
|
||||||
updateLegendValues(data, panel, height);
|
updateLegendValues(data, panel, height);
|
||||||
expect(data[0].decimals).toBe(3);
|
expect(data[0].decimals).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set decimals to legend decimals value if it was set explicitly', function() {
|
it('should set decimals to legend decimals value if it was set explicitly', () => {
|
||||||
panel.decimals = 3;
|
panel.decimals = 3;
|
||||||
const data = [series];
|
const data = [series];
|
||||||
updateLegendValues(data, panel, height);
|
updateLegendValues(data, panel, height);
|
||||||
|
@ -9,8 +9,8 @@ for (let i = 0; i < links.length; i++) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isWebkit = !!window.navigator.userAgent.match(/AppleWebKit\/([^ ;]*)/);
|
const isWebkit = !!window.navigator.userAgent.match(/AppleWebKit\/([^ ;]*)/);
|
||||||
const webkitLoadCheck = function(link, callback) {
|
const webkitLoadCheck = (link, callback) => {
|
||||||
setTimeout(function() {
|
setTimeout(() => {
|
||||||
for (let i = 0; i < document.styleSheets.length; i++) {
|
for (let i = 0; i < document.styleSheets.length; i++) {
|
||||||
const sheet = document.styleSheets[i];
|
const sheet = document.styleSheets[i];
|
||||||
if (sheet.href === link.href) {
|
if (sheet.href === link.href) {
|
||||||
@ -21,19 +21,19 @@ const webkitLoadCheck = function(link, callback) {
|
|||||||
}, 10);
|
}, 10);
|
||||||
};
|
};
|
||||||
|
|
||||||
const noop = function() {};
|
const noop = () => {};
|
||||||
|
|
||||||
const loadCSS = function(url) {
|
const loadCSS = url => {
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise((resolve, reject) => {
|
||||||
const link = document.createElement('link');
|
const link = document.createElement('link');
|
||||||
const timeout = setTimeout(function() {
|
const timeout = setTimeout(() => {
|
||||||
reject('Unable to load CSS');
|
reject('Unable to load CSS');
|
||||||
}, waitSeconds * 1000);
|
}, waitSeconds * 1000);
|
||||||
|
|
||||||
const _callback = function(error) {
|
const _callback = error => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
link.onload = link.onerror = noop;
|
link.onload = link.onerror = noop;
|
||||||
setTimeout(function() {
|
setTimeout(() => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
} else {
|
} else {
|
||||||
@ -47,14 +47,14 @@ const loadCSS = function(url) {
|
|||||||
link.href = url;
|
link.href = url;
|
||||||
|
|
||||||
if (!isWebkit) {
|
if (!isWebkit) {
|
||||||
link.onload = function() {
|
link.onload = () => {
|
||||||
_callback(undefined);
|
_callback(undefined);
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
webkitLoadCheck(link, _callback);
|
webkitLoadCheck(link, _callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
link.onerror = function(evt: any) {
|
link.onerror = (evt: any) => {
|
||||||
_callback(evt.error || new Error('Error loading CSS file.'));
|
_callback(evt.error || new Error('Error loading CSS file.'));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -450,6 +450,7 @@ kbn.valueFormats.currencySEK = kbn.formatBuilders.currency('kr');
|
|||||||
kbn.valueFormats.currencyCZK = kbn.formatBuilders.currency('czk');
|
kbn.valueFormats.currencyCZK = kbn.formatBuilders.currency('czk');
|
||||||
kbn.valueFormats.currencyCHF = kbn.formatBuilders.currency('CHF');
|
kbn.valueFormats.currencyCHF = kbn.formatBuilders.currency('CHF');
|
||||||
kbn.valueFormats.currencyPLN = kbn.formatBuilders.currency('zł');
|
kbn.valueFormats.currencyPLN = kbn.formatBuilders.currency('zł');
|
||||||
|
kbn.valueFormats.currencyBTC = kbn.formatBuilders.currency('฿');
|
||||||
|
|
||||||
// Data (Binary)
|
// Data (Binary)
|
||||||
kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b');
|
kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b');
|
||||||
@ -882,6 +883,7 @@ kbn.getUnitFormats = function() {
|
|||||||
{ text: 'Czech koruna (czk)', value: 'currencyCZK' },
|
{ text: 'Czech koruna (czk)', value: 'currencyCZK' },
|
||||||
{ text: 'Swiss franc (CHF)', value: 'currencyCHF' },
|
{ text: 'Swiss franc (CHF)', value: 'currencyCHF' },
|
||||||
{ text: 'Polish Złoty (PLN)', value: 'currencyPLN' },
|
{ text: 'Polish Złoty (PLN)', value: 'currencyPLN' },
|
||||||
|
{ text: 'Bitcoin (฿)', value: 'currencyBTC' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -128,7 +128,7 @@ export class ChangeTracker {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ignore template variable values
|
// ignore template variable values
|
||||||
_.each(dash.templating.list, function(value) {
|
_.each(dash.templating.list, value => {
|
||||||
value.current = null;
|
value.current = null;
|
||||||
value.options = null;
|
value.options = null;
|
||||||
value.filters = null;
|
value.filters = null;
|
||||||
|
@ -59,7 +59,7 @@ export class DashboardLoaderSrv {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
promise.then(function(result) {
|
promise.then(result => {
|
||||||
if (result.meta.dashboardNotFound !== true) {
|
if (result.meta.dashboardNotFound !== true) {
|
||||||
impressionSrv.addDashboardImpression(result.dashboard.id);
|
impressionSrv.addDashboardImpression(result.dashboard.id);
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ function dashRepeatOptionDirective(variableSrv) {
|
|||||||
scope: {
|
scope: {
|
||||||
panel: '=',
|
panel: '=',
|
||||||
},
|
},
|
||||||
link: function(scope, element) {
|
link: (scope, element) => {
|
||||||
element.css({ display: 'block', width: '100%' });
|
element.css({ display: 'block', width: '100%' });
|
||||||
|
|
||||||
scope.variables = variableSrv.variables.map(item => {
|
scope.variables = variableSrv.variables.map(item => {
|
||||||
@ -36,7 +36,7 @@ function dashRepeatOptionDirective(variableSrv) {
|
|||||||
scope.panel.repeatDirection = 'h';
|
scope.panel.repeatDirection = 'h';
|
||||||
}
|
}
|
||||||
|
|
||||||
scope.optionChanged = function() {
|
scope.optionChanged = () => {
|
||||||
if (scope.panel.repeat) {
|
if (scope.panel.repeat) {
|
||||||
scope.panel.repeatDirection = 'h';
|
scope.panel.repeatDirection = 'h';
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ export function ShareModalCtrl($scope, $rootScope, $location, $timeout, timeSrv,
|
|||||||
};
|
};
|
||||||
$scope.editor = { index: $scope.tabIndex || 0 };
|
$scope.editor = { index: $scope.tabIndex || 0 };
|
||||||
|
|
||||||
$scope.init = function() {
|
$scope.init = () => {
|
||||||
$scope.modeSharePanel = $scope.panel ? true : false;
|
$scope.modeSharePanel = $scope.panel ? true : false;
|
||||||
|
|
||||||
$scope.tabs = [{ title: 'Link', src: 'shareLink.html' }];
|
$scope.tabs = [{ title: 'Link', src: 'shareLink.html' }];
|
||||||
@ -34,7 +34,7 @@ export function ShareModalCtrl($scope, $rootScope, $location, $timeout, timeSrv,
|
|||||||
$scope.buildUrl();
|
$scope.buildUrl();
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.buildUrl = function() {
|
$scope.buildUrl = () => {
|
||||||
let baseUrl = $location.absUrl();
|
let baseUrl = $location.absUrl();
|
||||||
const queryStart = baseUrl.indexOf('?');
|
const queryStart = baseUrl.indexOf('?');
|
||||||
|
|
||||||
@ -90,7 +90,7 @@ export function ShareModalCtrl($scope, $rootScope, $location, $timeout, timeSrv,
|
|||||||
|
|
||||||
// This function will try to return the proper full name of the local timezone
|
// This function will try to return the proper full name of the local timezone
|
||||||
// Chrome does not handle the timezone offset (but phantomjs does)
|
// Chrome does not handle the timezone offset (but phantomjs does)
|
||||||
$scope.getLocalTimeZone = function() {
|
$scope.getLocalTimeZone = () => {
|
||||||
const utcOffset = '&tz=UTC' + encodeURIComponent(moment().format('Z'));
|
const utcOffset = '&tz=UTC' + encodeURIComponent(moment().format('Z'));
|
||||||
|
|
||||||
// Older browser does not the internationalization API
|
// Older browser does not the internationalization API
|
||||||
@ -111,7 +111,7 @@ export function ShareModalCtrl($scope, $rootScope, $location, $timeout, timeSrv,
|
|||||||
return '&tz=' + encodeURIComponent(options.timeZone);
|
return '&tz=' + encodeURIComponent(options.timeZone);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.getShareUrl = function() {
|
$scope.getShareUrl = () => {
|
||||||
return $scope.shareUrl;
|
return $scope.shareUrl;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -25,8 +25,8 @@ export class ShareSnapshotCtrl {
|
|||||||
{ text: 'Public on the web', value: 3 },
|
{ text: 'Public on the web', value: 3 },
|
||||||
];
|
];
|
||||||
|
|
||||||
$scope.init = function() {
|
$scope.init = () => {
|
||||||
backendSrv.get('/api/snapshot/shared-options').then(function(options) {
|
backendSrv.get('/api/snapshot/shared-options').then(options => {
|
||||||
$scope.externalUrl = options['externalSnapshotURL'];
|
$scope.externalUrl = options['externalSnapshotURL'];
|
||||||
$scope.sharingButtonText = options['externalSnapshotName'];
|
$scope.sharingButtonText = options['externalSnapshotName'];
|
||||||
$scope.externalEnabled = options['externalEnabled'];
|
$scope.externalEnabled = options['externalEnabled'];
|
||||||
@ -35,7 +35,7 @@ export class ShareSnapshotCtrl {
|
|||||||
|
|
||||||
$scope.apiUrl = '/api/snapshots';
|
$scope.apiUrl = '/api/snapshots';
|
||||||
|
|
||||||
$scope.createSnapshot = function(external) {
|
$scope.createSnapshot = external => {
|
||||||
$scope.dashboard.snapshot = {
|
$scope.dashboard.snapshot = {
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
};
|
};
|
||||||
@ -49,12 +49,12 @@ export class ShareSnapshotCtrl {
|
|||||||
|
|
||||||
$rootScope.$broadcast('refresh');
|
$rootScope.$broadcast('refresh');
|
||||||
|
|
||||||
$timeout(function() {
|
$timeout(() => {
|
||||||
$scope.saveSnapshot(external);
|
$scope.saveSnapshot(external);
|
||||||
}, $scope.snapshot.timeoutSeconds * 1000);
|
}, $scope.snapshot.timeoutSeconds * 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.saveSnapshot = function(external) {
|
$scope.saveSnapshot = external => {
|
||||||
const dash = $scope.dashboard.getSaveModelClone();
|
const dash = $scope.dashboard.getSaveModelClone();
|
||||||
$scope.scrubDashboard(dash);
|
$scope.scrubDashboard(dash);
|
||||||
|
|
||||||
@ -67,7 +67,7 @@ export class ShareSnapshotCtrl {
|
|||||||
const postUrl = external ? $scope.externalUrl + $scope.apiUrl : $scope.apiUrl;
|
const postUrl = external ? $scope.externalUrl + $scope.apiUrl : $scope.apiUrl;
|
||||||
|
|
||||||
backendSrv.post(postUrl, cmdData).then(
|
backendSrv.post(postUrl, cmdData).then(
|
||||||
function(results) {
|
results => {
|
||||||
$scope.loading = false;
|
$scope.loading = false;
|
||||||
|
|
||||||
if (external) {
|
if (external) {
|
||||||
@ -88,17 +88,17 @@ export class ShareSnapshotCtrl {
|
|||||||
|
|
||||||
$scope.step = 2;
|
$scope.step = 2;
|
||||||
},
|
},
|
||||||
function() {
|
() => {
|
||||||
$scope.loading = false;
|
$scope.loading = false;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.getSnapshotUrl = function() {
|
$scope.getSnapshotUrl = () => {
|
||||||
return $scope.snapshotUrl;
|
return $scope.snapshotUrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.scrubDashboard = function(dash) {
|
$scope.scrubDashboard = dash => {
|
||||||
// change title
|
// change title
|
||||||
dash.title = $scope.snapshot.name;
|
dash.title = $scope.snapshot.name;
|
||||||
|
|
||||||
@ -106,7 +106,7 @@ export class ShareSnapshotCtrl {
|
|||||||
dash.time = timeSrv.timeRange();
|
dash.time = timeSrv.timeRange();
|
||||||
|
|
||||||
// remove panel queries & links
|
// remove panel queries & links
|
||||||
_.each(dash.panels, function(panel) {
|
_.each(dash.panels, panel => {
|
||||||
panel.targets = [];
|
panel.targets = [];
|
||||||
panel.links = [];
|
panel.links = [];
|
||||||
panel.datasource = null;
|
panel.datasource = null;
|
||||||
@ -114,10 +114,10 @@ export class ShareSnapshotCtrl {
|
|||||||
|
|
||||||
// remove annotation queries
|
// remove annotation queries
|
||||||
dash.annotations.list = _.chain(dash.annotations.list)
|
dash.annotations.list = _.chain(dash.annotations.list)
|
||||||
.filter(function(annotation) {
|
.filter(annotation => {
|
||||||
return annotation.enable;
|
return annotation.enable;
|
||||||
})
|
})
|
||||||
.map(function(annotation) {
|
.map(annotation => {
|
||||||
return {
|
return {
|
||||||
name: annotation.name,
|
name: annotation.name,
|
||||||
enable: annotation.enable,
|
enable: annotation.enable,
|
||||||
@ -131,7 +131,7 @@ export class ShareSnapshotCtrl {
|
|||||||
.value();
|
.value();
|
||||||
|
|
||||||
// remove template queries
|
// remove template queries
|
||||||
_.each(dash.templating.list, function(variable) {
|
_.each(dash.templating.list, variable => {
|
||||||
variable.query = '';
|
variable.query = '';
|
||||||
variable.options = variable.current;
|
variable.options = variable.current;
|
||||||
variable.refresh = false;
|
variable.refresh = false;
|
||||||
@ -149,21 +149,21 @@ export class ShareSnapshotCtrl {
|
|||||||
|
|
||||||
// cleanup snapshotData
|
// cleanup snapshotData
|
||||||
delete $scope.dashboard.snapshot;
|
delete $scope.dashboard.snapshot;
|
||||||
$scope.dashboard.forEachPanel(function(panel) {
|
$scope.dashboard.forEachPanel(panel => {
|
||||||
delete panel.snapshotData;
|
delete panel.snapshotData;
|
||||||
});
|
});
|
||||||
_.each($scope.dashboard.annotations.list, function(annotation) {
|
_.each($scope.dashboard.annotations.list, annotation => {
|
||||||
delete annotation.snapshotData;
|
delete annotation.snapshotData;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.deleteSnapshot = function() {
|
$scope.deleteSnapshot = () => {
|
||||||
backendSrv.get($scope.deleteUrl).then(function() {
|
backendSrv.get($scope.deleteUrl).then(() => {
|
||||||
$scope.step = 3;
|
$scope.step = 3;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.saveExternalSnapshotRef = function(cmdData, results) {
|
$scope.saveExternalSnapshotRef = (cmdData, results) => {
|
||||||
// save external in local instance as well
|
// save external in local instance as well
|
||||||
cmdData.external = true;
|
cmdData.external = true;
|
||||||
cmdData.key = results.key;
|
cmdData.key = results.key;
|
||||||
|
@ -5,10 +5,10 @@ export function inputDateDirective() {
|
|||||||
return {
|
return {
|
||||||
restrict: 'A',
|
restrict: 'A',
|
||||||
require: 'ngModel',
|
require: 'ngModel',
|
||||||
link: function($scope, $elem, attrs, ngModel) {
|
link: ($scope, $elem, attrs, ngModel) => {
|
||||||
const format = 'YYYY-MM-DD HH:mm:ss';
|
const format = 'YYYY-MM-DD HH:mm:ss';
|
||||||
|
|
||||||
const fromUser = function(text) {
|
const fromUser = text => {
|
||||||
if (text.indexOf('now') !== -1) {
|
if (text.indexOf('now') !== -1) {
|
||||||
if (!dateMath.isValid(text)) {
|
if (!dateMath.isValid(text)) {
|
||||||
ngModel.$setValidity('error', false);
|
ngModel.$setValidity('error', false);
|
||||||
@ -34,7 +34,7 @@ export function inputDateDirective() {
|
|||||||
return parsed;
|
return parsed;
|
||||||
};
|
};
|
||||||
|
|
||||||
const toUser = function(currentValue) {
|
const toUser = currentValue => {
|
||||||
if (moment.isMoment(currentValue)) {
|
if (moment.isMoment(currentValue)) {
|
||||||
return currentValue.format(format);
|
return currentValue.format(format);
|
||||||
} else {
|
} else {
|
||||||
|
@ -16,11 +16,11 @@ function uploadDashboardDirective(timer, alertSrv, $location) {
|
|||||||
scope: {
|
scope: {
|
||||||
onUpload: '&',
|
onUpload: '&',
|
||||||
},
|
},
|
||||||
link: function(scope) {
|
link: scope => {
|
||||||
function file_selected(evt) {
|
function file_selected(evt) {
|
||||||
const files = evt.target.files; // FileList object
|
const files = evt.target.files; // FileList object
|
||||||
const readerOnload = function() {
|
const readerOnload = () => {
|
||||||
return function(e) {
|
return e => {
|
||||||
let dash;
|
let dash;
|
||||||
try {
|
try {
|
||||||
dash = JSON.parse(e.target.result);
|
dash = JSON.parse(e.target.result);
|
||||||
@ -30,7 +30,7 @@ function uploadDashboardDirective(timer, alertSrv, $location) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
scope.$apply(function() {
|
scope.$apply(() => {
|
||||||
scope.onUpload({ dash: dash });
|
scope.onUpload({ dash: dash });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -22,18 +22,18 @@ export class DashboardViewState {
|
|||||||
self.$scope = $scope;
|
self.$scope = $scope;
|
||||||
self.dashboard = $scope.dashboard;
|
self.dashboard = $scope.dashboard;
|
||||||
|
|
||||||
$scope.onAppEvent('$routeUpdate', function() {
|
$scope.onAppEvent('$routeUpdate', () => {
|
||||||
const urlState = self.getQueryStringState();
|
const urlState = self.getQueryStringState();
|
||||||
if (self.needsSync(urlState)) {
|
if (self.needsSync(urlState)) {
|
||||||
self.update(urlState, true);
|
self.update(urlState, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.onAppEvent('panel-change-view', function(evt, payload) {
|
$scope.onAppEvent('panel-change-view', (evt, payload) => {
|
||||||
self.update(payload);
|
self.update(payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.onAppEvent('panel-initialized', function(evt, payload) {
|
$scope.onAppEvent('panel-initialized', (evt, payload) => {
|
||||||
self.registerPanel(payload.scope);
|
self.registerPanel(payload.scope);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -156,7 +156,7 @@ export class DashboardViewState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getPanelScope(id) {
|
getPanelScope(id) {
|
||||||
return _.find(this.panelScopes, function(panelScope) {
|
return _.find(this.panelScopes, panelScope => {
|
||||||
return panelScope.ctrl.panel.id === id;
|
return panelScope.ctrl.panel.id === id;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -176,7 +176,7 @@ export class DashboardViewState {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$timeout(function() {
|
this.$timeout(() => {
|
||||||
if (self.oldTimeRange !== ctrl.range) {
|
if (self.oldTimeRange !== ctrl.range) {
|
||||||
self.$rootScope.$broadcast('refresh');
|
self.$rootScope.$broadcast('refresh');
|
||||||
} else {
|
} else {
|
||||||
@ -216,7 +216,7 @@ export class DashboardViewState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const unbind = panelScope.$on('$destroy', function() {
|
const unbind = panelScope.$on('$destroy', () => {
|
||||||
self.panelScopes = _.without(self.panelScopes, panelScope);
|
self.panelScopes = _.without(self.panelScopes, panelScope);
|
||||||
unbind();
|
unbind();
|
||||||
});
|
});
|
||||||
@ -226,7 +226,7 @@ export class DashboardViewState {
|
|||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
export function dashboardViewStateSrv($location, $timeout, $rootScope) {
|
export function dashboardViewStateSrv($location, $timeout, $rootScope) {
|
||||||
return {
|
return {
|
||||||
create: function($scope) {
|
create: $scope => {
|
||||||
return new DashboardViewState($scope, $location, $timeout, $rootScope);
|
return new DashboardViewState($scope, $location, $timeout, $rootScope);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -10,7 +10,7 @@ function dashLinksContainer() {
|
|||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
controller: 'DashLinksContainerCtrl',
|
controller: 'DashLinksContainerCtrl',
|
||||||
template: '<dash-link ng-repeat="link in generatedLinks" link="link"></dash-link>',
|
template: '<dash-link ng-repeat="link in generatedLinks" link="link"></dash-link>',
|
||||||
link: function() {},
|
link: () => {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ function dashLinksContainer() {
|
|||||||
function dashLink($compile, $sanitize, linkSrv) {
|
function dashLink($compile, $sanitize, linkSrv) {
|
||||||
return {
|
return {
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
link: function(scope, elem) {
|
link: (scope, elem) => {
|
||||||
const link = scope.link;
|
const link = scope.link;
|
||||||
let template =
|
let template =
|
||||||
'<div class="gf-form">' +
|
'<div class="gf-form">' +
|
||||||
@ -130,16 +130,16 @@ export class DashLinksContainerCtrl {
|
|||||||
function updateDashLinks() {
|
function updateDashLinks() {
|
||||||
const promises = _.map($scope.links, buildLinks);
|
const promises = _.map($scope.links, buildLinks);
|
||||||
|
|
||||||
$q.all(promises).then(function(results) {
|
$q.all(promises).then(results => {
|
||||||
$scope.generatedLinks = _.flatten(results);
|
$scope.generatedLinks = _.flatten(results);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.searchDashboards = function(link, limit) {
|
$scope.searchDashboards = (link, limit) => {
|
||||||
return backendSrv.search({ tag: link.tags, limit: limit }).then(function(results) {
|
return backendSrv.search({ tag: link.tags, limit: limit }).then(results => {
|
||||||
return _.reduce(
|
return _.reduce(
|
||||||
results,
|
results,
|
||||||
function(memo, dash) {
|
(memo, dash) => {
|
||||||
// do not add current dashboard
|
// do not add current dashboard
|
||||||
if (dash.id !== currentDashId) {
|
if (dash.id !== currentDashId) {
|
||||||
memo.push({
|
memo.push({
|
||||||
@ -158,9 +158,9 @@ export class DashLinksContainerCtrl {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.fillDropdown = function(link) {
|
$scope.fillDropdown = link => {
|
||||||
$scope.searchDashboards(link, 100).then(function(results) {
|
$scope.searchDashboards(link, 100).then(results => {
|
||||||
_.each(results, function(hit) {
|
_.each(results, hit => {
|
||||||
hit.url = linkSrv.getLinkUrl(hit);
|
hit.url = linkSrv.getLinkUrl(hit);
|
||||||
});
|
});
|
||||||
link.searchHits = results;
|
link.searchHits = results;
|
||||||
|
@ -9,7 +9,7 @@ export class ChangePasswordCtrl {
|
|||||||
$scope.ldapEnabled = config.ldapEnabled;
|
$scope.ldapEnabled = config.ldapEnabled;
|
||||||
$scope.navModel = navModelSrv.getNav('profile', 'change-password', 0);
|
$scope.navModel = navModelSrv.getNav('profile', 'change-password', 0);
|
||||||
|
|
||||||
$scope.changePassword = function() {
|
$scope.changePassword = () => {
|
||||||
if (!$scope.userForm.$valid) {
|
if (!$scope.userForm.$valid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -19,7 +19,7 @@ export class ChangePasswordCtrl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
backendSrv.put('/api/user/password', $scope.command).then(function() {
|
backendSrv.put('/api/user/password', $scope.command).then(() => {
|
||||||
$location.path('profile');
|
$location.path('profile');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -7,9 +7,9 @@ export class NewOrgCtrl {
|
|||||||
$scope.navModel = navModelSrv.getNav('cfg', 'admin', 'global-orgs', 1);
|
$scope.navModel = navModelSrv.getNav('cfg', 'admin', 'global-orgs', 1);
|
||||||
$scope.newOrg = { name: '' };
|
$scope.newOrg = { name: '' };
|
||||||
|
|
||||||
$scope.createOrg = function() {
|
$scope.createOrg = () => {
|
||||||
backendSrv.post('/api/orgs/', $scope.newOrg).then(function(result) {
|
backendSrv.post('/api/orgs/', $scope.newOrg).then(result => {
|
||||||
backendSrv.post('/api/user/using/' + result.orgId).then(function() {
|
backendSrv.post('/api/user/using/' + result.orgId).then(() => {
|
||||||
window.location.href = config.appSubUrl + '/org';
|
window.location.href = config.appSubUrl + '/org';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -8,22 +8,22 @@ export class OrgApiKeysCtrl {
|
|||||||
$scope.roleTypes = ['Viewer', 'Editor', 'Admin'];
|
$scope.roleTypes = ['Viewer', 'Editor', 'Admin'];
|
||||||
$scope.token = { role: 'Viewer' };
|
$scope.token = { role: 'Viewer' };
|
||||||
|
|
||||||
$scope.init = function() {
|
$scope.init = () => {
|
||||||
$scope.getTokens();
|
$scope.getTokens();
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.getTokens = function() {
|
$scope.getTokens = () => {
|
||||||
backendSrv.get('/api/auth/keys').then(function(tokens) {
|
backendSrv.get('/api/auth/keys').then(tokens => {
|
||||||
$scope.tokens = tokens;
|
$scope.tokens = tokens;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.removeToken = function(id) {
|
$scope.removeToken = id => {
|
||||||
backendSrv.delete('/api/auth/keys/' + id).then($scope.getTokens);
|
backendSrv.delete('/api/auth/keys/' + id).then($scope.getTokens);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.addToken = function() {
|
$scope.addToken = () => {
|
||||||
backendSrv.post('/api/auth/keys', $scope.token).then(function(result) {
|
backendSrv.post('/api/auth/keys', $scope.token).then(result => {
|
||||||
const modalScope = $scope.$new(true);
|
const modalScope = $scope.$new(true);
|
||||||
modalScope.key = result.key;
|
modalScope.key = result.key;
|
||||||
modalScope.rootPath = window.location.origin + $scope.$root.appSubUrl;
|
modalScope.rootPath = window.location.origin + $scope.$root.appSubUrl;
|
||||||
|
@ -14,18 +14,18 @@ export class SelectOrgCtrl {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.init = function() {
|
$scope.init = () => {
|
||||||
$scope.getUserOrgs();
|
$scope.getUserOrgs();
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.getUserOrgs = function() {
|
$scope.getUserOrgs = () => {
|
||||||
backendSrv.get('/api/user/orgs').then(function(orgs) {
|
backendSrv.get('/api/user/orgs').then(orgs => {
|
||||||
$scope.orgs = orgs;
|
$scope.orgs = orgs;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.setUsingOrg = function(org) {
|
$scope.setUsingOrg = org => {
|
||||||
backendSrv.post('/api/user/using/' + org.orgId).then(function() {
|
backendSrv.post('/api/user/using/' + org.orgId).then(() => {
|
||||||
window.location.href = config.appSubUrl + '/';
|
window.location.href = config.appSubUrl + '/';
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -54,13 +54,13 @@ const panelTemplate = `
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
module.directive('grafanaPanel', function($rootScope, $document, $timeout) {
|
module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
|
||||||
return {
|
return {
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
template: panelTemplate,
|
template: panelTemplate,
|
||||||
transclude: true,
|
transclude: true,
|
||||||
scope: { ctrl: '=' },
|
scope: { ctrl: '=' },
|
||||||
link: function(scope, elem) {
|
link: (scope, elem) => {
|
||||||
const panelContainer = elem.find('.panel-container');
|
const panelContainer = elem.find('.panel-container');
|
||||||
const panelContent = elem.find('.panel-content');
|
const panelContent = elem.find('.panel-content');
|
||||||
const cornerInfoElem = elem.find('.panel-info-corner');
|
const cornerInfoElem = elem.find('.panel-info-corner');
|
||||||
@ -184,7 +184,7 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) {
|
|||||||
|
|
||||||
infoDrop = new Drop({
|
infoDrop = new Drop({
|
||||||
target: cornerInfoElem[0],
|
target: cornerInfoElem[0],
|
||||||
content: function() {
|
content: () => {
|
||||||
return ctrl.getInfoContent({ mode: 'tooltip' });
|
return ctrl.getInfoContent({ mode: 'tooltip' });
|
||||||
},
|
},
|
||||||
classes: ctrl.error ? 'drop-error' : 'drop-help',
|
classes: ctrl.error ? 'drop-error' : 'drop-help',
|
||||||
@ -208,7 +208,7 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) {
|
|||||||
scope.$watchGroup(['ctrl.error', 'ctrl.panel.description'], updatePanelCornerInfo);
|
scope.$watchGroup(['ctrl.error', 'ctrl.panel.description'], updatePanelCornerInfo);
|
||||||
scope.$watchCollection('ctrl.panel.links', updatePanelCornerInfo);
|
scope.$watchCollection('ctrl.panel.links', updatePanelCornerInfo);
|
||||||
|
|
||||||
cornerInfoElem.on('click', function() {
|
cornerInfoElem.on('click', () => {
|
||||||
infoDrop.close();
|
infoDrop.close();
|
||||||
scope.$apply(ctrl.openInspector.bind(ctrl));
|
scope.$apply(ctrl.openInspector.bind(ctrl));
|
||||||
});
|
});
|
||||||
@ -216,7 +216,7 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) {
|
|||||||
elem.on('mouseenter', mouseEnter);
|
elem.on('mouseenter', mouseEnter);
|
||||||
elem.on('mouseleave', mouseLeave);
|
elem.on('mouseleave', mouseLeave);
|
||||||
|
|
||||||
scope.$on('$destroy', function() {
|
scope.$on('$destroy', () => {
|
||||||
elem.off();
|
elem.off();
|
||||||
cornerInfoElem.off();
|
cornerInfoElem.off();
|
||||||
|
|
||||||
@ -232,7 +232,7 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
module.directive('panelHelpCorner', function($rootScope) {
|
module.directive('panelHelpCorner', $rootScope => {
|
||||||
return {
|
return {
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
template: `
|
template: `
|
||||||
@ -242,6 +242,6 @@ module.directive('panelHelpCorner', function($rootScope) {
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
`,
|
`,
|
||||||
link: function(scope, elem) {},
|
link: (scope, elem) => {},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -85,12 +85,12 @@ function panelHeader($compile) {
|
|||||||
return {
|
return {
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
template: template,
|
template: template,
|
||||||
link: function(scope, elem, attrs) {
|
link: (scope, elem, attrs) => {
|
||||||
const menuElem = elem.find('.panel-menu');
|
const menuElem = elem.find('.panel-menu');
|
||||||
let menuScope;
|
let menuScope;
|
||||||
let isDragged;
|
let isDragged;
|
||||||
|
|
||||||
elem.click(function(evt) {
|
elem.click(evt => {
|
||||||
const targetClass = evt.target.className;
|
const targetClass = evt.target.className;
|
||||||
|
|
||||||
// remove existing scope
|
// remove existing scope
|
||||||
|
@ -170,8 +170,8 @@ export function queryTroubleshooter() {
|
|||||||
panelCtrl: '=',
|
panelCtrl: '=',
|
||||||
isOpen: '=',
|
isOpen: '=',
|
||||||
},
|
},
|
||||||
link: function(scope, elem, attrs, ctrl) {
|
link: (scope, elem, attrs, ctrl) => {
|
||||||
ctrl.renderJsonExplorer = function(data) {
|
ctrl.renderJsonExplorer = data => {
|
||||||
const jsonElem = elem.find('.query-troubleshooter-json');
|
const jsonElem = elem.find('.query-troubleshooter-json');
|
||||||
|
|
||||||
ctrl.jsonExplorer = new JsonExplorer(data, 3, {
|
ctrl.jsonExplorer = new JsonExplorer(data, 3, {
|
||||||
|
@ -7,7 +7,7 @@ export class SoloPanelCtrl {
|
|||||||
constructor($scope, $routeParams, $location, dashboardLoaderSrv, contextSrv, backendSrv) {
|
constructor($scope, $routeParams, $location, dashboardLoaderSrv, contextSrv, backendSrv) {
|
||||||
let panelId;
|
let panelId;
|
||||||
|
|
||||||
$scope.init = function() {
|
$scope.init = () => {
|
||||||
contextSrv.sidemenu = false;
|
contextSrv.sidemenu = false;
|
||||||
appEvents.emit('toggle-sidemenu-hidden');
|
appEvents.emit('toggle-sidemenu-hidden');
|
||||||
|
|
||||||
@ -27,13 +27,13 @@ export class SoloPanelCtrl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug, $routeParams.uid).then(function(result) {
|
dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug, $routeParams.uid).then(result => {
|
||||||
result.meta.soloMode = true;
|
result.meta.soloMode = true;
|
||||||
$scope.initDashboard(result, $scope);
|
$scope.initDashboard(result, $scope);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.initPanelScope = function() {
|
$scope.initPanelScope = () => {
|
||||||
const panelInfo = $scope.dashboard.getPanelInfoById(panelId);
|
const panelInfo = $scope.dashboard.getPanelInfoById(panelId);
|
||||||
|
|
||||||
// fake row ctrl scope
|
// fake row ctrl scope
|
||||||
|
@ -10,7 +10,7 @@ function panelLinksEditor() {
|
|||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
controller: 'PanelLinksEditorCtrl',
|
controller: 'PanelLinksEditorCtrl',
|
||||||
templateUrl: 'public/app/features/panellinks/module.html',
|
templateUrl: 'public/app/features/panellinks/module.html',
|
||||||
link: function() {},
|
link: () => {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,15 +19,15 @@ export class PanelLinksEditorCtrl {
|
|||||||
constructor($scope, backendSrv) {
|
constructor($scope, backendSrv) {
|
||||||
$scope.panel.links = $scope.panel.links || [];
|
$scope.panel.links = $scope.panel.links || [];
|
||||||
|
|
||||||
$scope.addLink = function() {
|
$scope.addLink = () => {
|
||||||
$scope.panel.links.push({
|
$scope.panel.links.push({
|
||||||
type: 'dashboard',
|
type: 'dashboard',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.searchDashboards = function(queryStr, callback) {
|
$scope.searchDashboards = (queryStr, callback) => {
|
||||||
backendSrv.search({ query: queryStr }).then(function(hits) {
|
backendSrv.search({ query: queryStr }).then(hits => {
|
||||||
const dashboards = _.map(hits, function(dash) {
|
const dashboards = _.map(hits, dash => {
|
||||||
return dash.title;
|
return dash.title;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -35,8 +35,8 @@ export class PanelLinksEditorCtrl {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.dashboardChanged = function(link) {
|
$scope.dashboardChanged = link => {
|
||||||
backendSrv.search({ query: link.dashboard }).then(function(hits) {
|
backendSrv.search({ query: link.dashboard }).then(hits => {
|
||||||
const dashboard = _.find(hits, { title: link.dashboard });
|
const dashboard = _.find(hits, { title: link.dashboard });
|
||||||
if (dashboard) {
|
if (dashboard) {
|
||||||
if (dashboard.url) {
|
if (dashboard.url) {
|
||||||
@ -50,7 +50,7 @@ export class PanelLinksEditorCtrl {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.deleteLink = function(link) {
|
$scope.deleteLink = link => {
|
||||||
$scope.panel.links = _.without($scope.panel.links, link);
|
$scope.panel.links = _.without($scope.panel.links, link);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ function grafanaRoutes($routeProvider) {
|
|||||||
.when('/playlists/play/:id', {
|
.when('/playlists/play/:id', {
|
||||||
template: '',
|
template: '',
|
||||||
resolve: {
|
resolve: {
|
||||||
init: function(playlistSrv, $route) {
|
init: (playlistSrv, $route) => {
|
||||||
const playlistId = $route.current.params.id;
|
const playlistId = $route.current.params.id;
|
||||||
playlistSrv.start(playlistId);
|
playlistSrv.start(playlistId);
|
||||||
},
|
},
|
||||||
|
@ -200,7 +200,7 @@ export class DataSourceEditCtrl {
|
|||||||
|
|
||||||
coreModule.controller('DataSourceEditCtrl', DataSourceEditCtrl);
|
coreModule.controller('DataSourceEditCtrl', DataSourceEditCtrl);
|
||||||
|
|
||||||
coreModule.directive('datasourceHttpSettings', function() {
|
coreModule.directive('datasourceHttpSettings', () => {
|
||||||
return {
|
return {
|
||||||
scope: {
|
scope: {
|
||||||
current: '=',
|
current: '=',
|
||||||
@ -209,15 +209,15 @@ coreModule.directive('datasourceHttpSettings', function() {
|
|||||||
},
|
},
|
||||||
templateUrl: 'public/app/features/plugins/partials/ds_http_settings.html',
|
templateUrl: 'public/app/features/plugins/partials/ds_http_settings.html',
|
||||||
link: {
|
link: {
|
||||||
pre: function($scope, elem, attrs) {
|
pre: ($scope, elem, attrs) => {
|
||||||
// do not show access option if direct access is disabled
|
// do not show access option if direct access is disabled
|
||||||
$scope.showAccessOption = $scope.noDirectAccess !== 'true';
|
$scope.showAccessOption = $scope.noDirectAccess !== 'true';
|
||||||
$scope.showAccessHelp = false;
|
$scope.showAccessHelp = false;
|
||||||
$scope.toggleAccessHelp = function() {
|
$scope.toggleAccessHelp = () => {
|
||||||
$scope.showAccessHelp = !$scope.showAccessHelp;
|
$scope.showAccessHelp = !$scope.showAccessHelp;
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.getSuggestUrls = function() {
|
$scope.getSuggestUrls = () => {
|
||||||
return [$scope.suggestUrl];
|
return [$scope.suggestUrl];
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -36,7 +36,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
|
|||||||
// handle relative template urls for plugin templates
|
// handle relative template urls for plugin templates
|
||||||
options.Component.templateUrl = relativeTemplateUrlToAbs(options.Component.templateUrl, options.baseUrl);
|
options.Component.templateUrl = relativeTemplateUrlToAbs(options.Component.templateUrl, options.baseUrl);
|
||||||
|
|
||||||
return function() {
|
return () => {
|
||||||
return {
|
return {
|
||||||
templateUrl: options.Component.templateUrl,
|
templateUrl: options.Component.templateUrl,
|
||||||
template: options.Component.template,
|
template: options.Component.template,
|
||||||
@ -71,12 +71,12 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
|
|||||||
const panelInfo = config.panels[scope.panel.type];
|
const panelInfo = config.panels[scope.panel.type];
|
||||||
let panelCtrlPromise = Promise.resolve(UnknownPanelCtrl);
|
let panelCtrlPromise = Promise.resolve(UnknownPanelCtrl);
|
||||||
if (panelInfo) {
|
if (panelInfo) {
|
||||||
panelCtrlPromise = importPluginModule(panelInfo.module).then(function(panelModule) {
|
panelCtrlPromise = importPluginModule(panelInfo.module).then(panelModule => {
|
||||||
return panelModule.PanelCtrl;
|
return panelModule.PanelCtrl;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return panelCtrlPromise.then(function(PanelCtrl: any) {
|
return panelCtrlPromise.then((PanelCtrl: any) => {
|
||||||
componentInfo.Component = PanelCtrl;
|
componentInfo.Component = PanelCtrl;
|
||||||
|
|
||||||
if (!PanelCtrl || PanelCtrl.registered) {
|
if (!PanelCtrl || PanelCtrl.registered) {
|
||||||
@ -128,7 +128,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
|
|||||||
}
|
}
|
||||||
// Annotations
|
// Annotations
|
||||||
case 'annotations-query-ctrl': {
|
case 'annotations-query-ctrl': {
|
||||||
return importPluginModule(scope.ctrl.currentDatasource.meta.module).then(function(dsModule) {
|
return importPluginModule(scope.ctrl.currentDatasource.meta.module).then(dsModule => {
|
||||||
return {
|
return {
|
||||||
baseUrl: scope.ctrl.currentDatasource.meta.baseUrl,
|
baseUrl: scope.ctrl.currentDatasource.meta.baseUrl,
|
||||||
name: 'annotations-query-ctrl-' + scope.ctrl.currentDatasource.meta.id,
|
name: 'annotations-query-ctrl-' + scope.ctrl.currentDatasource.meta.id,
|
||||||
@ -144,7 +144,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
|
|||||||
// Datasource ConfigCtrl
|
// Datasource ConfigCtrl
|
||||||
case 'datasource-config-ctrl': {
|
case 'datasource-config-ctrl': {
|
||||||
const dsMeta = scope.ctrl.datasourceMeta;
|
const dsMeta = scope.ctrl.datasourceMeta;
|
||||||
return importPluginModule(dsMeta.module).then(function(dsModule): any {
|
return importPluginModule(dsMeta.module).then((dsModule): any => {
|
||||||
if (!dsModule.ConfigCtrl) {
|
if (!dsModule.ConfigCtrl) {
|
||||||
return { notFound: true };
|
return { notFound: true };
|
||||||
}
|
}
|
||||||
@ -161,7 +161,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
|
|||||||
// AppConfigCtrl
|
// AppConfigCtrl
|
||||||
case 'app-config-ctrl': {
|
case 'app-config-ctrl': {
|
||||||
const model = scope.ctrl.model;
|
const model = scope.ctrl.model;
|
||||||
return importPluginModule(model.module).then(function(appModule) {
|
return importPluginModule(model.module).then(appModule => {
|
||||||
return {
|
return {
|
||||||
baseUrl: model.baseUrl,
|
baseUrl: model.baseUrl,
|
||||||
name: 'app-config-' + model.id,
|
name: 'app-config-' + model.id,
|
||||||
@ -174,7 +174,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
|
|||||||
// App Page
|
// App Page
|
||||||
case 'app-page': {
|
case 'app-page': {
|
||||||
const appModel = scope.ctrl.appModel;
|
const appModel = scope.ctrl.appModel;
|
||||||
return importPluginModule(appModel.module).then(function(appModule) {
|
return importPluginModule(appModel.module).then(appModule => {
|
||||||
return {
|
return {
|
||||||
baseUrl: appModel.baseUrl,
|
baseUrl: appModel.baseUrl,
|
||||||
name: 'app-page-' + appModel.id + '-' + scope.ctrl.page.slug,
|
name: 'app-page-' + appModel.id + '-' + scope.ctrl.page.slug,
|
||||||
@ -206,9 +206,9 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
|
|||||||
elem.empty();
|
elem.empty();
|
||||||
|
|
||||||
// let a binding digest cycle complete before adding to dom
|
// let a binding digest cycle complete before adding to dom
|
||||||
setTimeout(function() {
|
setTimeout(() => {
|
||||||
elem.append(child);
|
elem.append(child);
|
||||||
scope.$applyAsync(function() {
|
scope.$applyAsync(() => {
|
||||||
scope.$broadcast('component-did-mount');
|
scope.$broadcast('component-did-mount');
|
||||||
scope.$broadcast('refresh');
|
scope.$broadcast('refresh');
|
||||||
});
|
});
|
||||||
@ -239,9 +239,9 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
link: function(scope, elem, attrs) {
|
link: (scope, elem, attrs) => {
|
||||||
getModule(scope, attrs)
|
getModule(scope, attrs)
|
||||||
.then(function(componentInfo) {
|
.then(componentInfo => {
|
||||||
registerPluginComponent(scope, elem, attrs, componentInfo);
|
registerPluginComponent(scope, elem, attrs, componentInfo);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
@ -10,7 +10,7 @@ import { CustomVariable } from './custom_variable';
|
|||||||
import { ConstantVariable } from './constant_variable';
|
import { ConstantVariable } from './constant_variable';
|
||||||
import { AdhocVariable } from './adhoc_variable';
|
import { AdhocVariable } from './adhoc_variable';
|
||||||
|
|
||||||
coreModule.factory('templateSrv', function() {
|
coreModule.factory('templateSrv', () => {
|
||||||
return templateSrv;
|
return templateSrv;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ export class CustomVariable implements Variable {
|
|||||||
|
|
||||||
updateOptions() {
|
updateOptions() {
|
||||||
// extract options in comma separated string
|
// extract options in comma separated string
|
||||||
this.options = _.map(this.query.split(/[,]+/), function(text) {
|
this.options = _.map(this.query.split(/[,]+/), text => {
|
||||||
return { text: text.trim(), value: text.trim() };
|
return { text: text.trim(), value: text.trim() };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -30,31 +30,31 @@ export class VariableEditorCtrl {
|
|||||||
|
|
||||||
$scope.hideOptions = [{ value: 0, text: '' }, { value: 1, text: 'Label' }, { value: 2, text: 'Variable' }];
|
$scope.hideOptions = [{ value: 0, text: '' }, { value: 1, text: 'Label' }, { value: 2, text: 'Variable' }];
|
||||||
|
|
||||||
$scope.init = function() {
|
$scope.init = () => {
|
||||||
$scope.mode = 'list';
|
$scope.mode = 'list';
|
||||||
|
|
||||||
$scope.variables = variableSrv.variables;
|
$scope.variables = variableSrv.variables;
|
||||||
$scope.reset();
|
$scope.reset();
|
||||||
|
|
||||||
$scope.$watch('mode', function(val) {
|
$scope.$watch('mode', val => {
|
||||||
if (val === 'new') {
|
if (val === 'new') {
|
||||||
$scope.reset();
|
$scope.reset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.setMode = function(mode) {
|
$scope.setMode = mode => {
|
||||||
$scope.mode = mode;
|
$scope.mode = mode;
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.add = function() {
|
$scope.add = () => {
|
||||||
if ($scope.isValid()) {
|
if ($scope.isValid()) {
|
||||||
variableSrv.addVariable($scope.current);
|
variableSrv.addVariable($scope.current);
|
||||||
$scope.update();
|
$scope.update();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.isValid = function() {
|
$scope.isValid = () => {
|
||||||
if (!$scope.ctrl.form.$valid) {
|
if (!$scope.ctrl.form.$valid) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -84,7 +84,7 @@ export class VariableEditorCtrl {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.validate = function() {
|
$scope.validate = () => {
|
||||||
$scope.infoText = '';
|
$scope.infoText = '';
|
||||||
if ($scope.current.type === 'adhoc' && $scope.current.datasource !== null) {
|
if ($scope.current.type === 'adhoc' && $scope.current.datasource !== null) {
|
||||||
$scope.infoText = 'Adhoc filters are applied automatically to all queries that target this datasource';
|
$scope.infoText = 'Adhoc filters are applied automatically to all queries that target this datasource';
|
||||||
@ -96,7 +96,7 @@ export class VariableEditorCtrl {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.runQuery = function() {
|
$scope.runQuery = () => {
|
||||||
$scope.optionsLimit = 20;
|
$scope.optionsLimit = 20;
|
||||||
return variableSrv.updateOptions($scope.current).catch(err => {
|
return variableSrv.updateOptions($scope.current).catch(err => {
|
||||||
if (err.data && err.data.message) {
|
if (err.data && err.data.message) {
|
||||||
@ -106,23 +106,23 @@ export class VariableEditorCtrl {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.edit = function(variable) {
|
$scope.edit = variable => {
|
||||||
$scope.current = variable;
|
$scope.current = variable;
|
||||||
$scope.currentIsNew = false;
|
$scope.currentIsNew = false;
|
||||||
$scope.mode = 'edit';
|
$scope.mode = 'edit';
|
||||||
$scope.validate();
|
$scope.validate();
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.duplicate = function(variable) {
|
$scope.duplicate = variable => {
|
||||||
const clone = _.cloneDeep(variable.getSaveModel());
|
const clone = _.cloneDeep(variable.getSaveModel());
|
||||||
$scope.current = variableSrv.createVariableFromModel(clone);
|
$scope.current = variableSrv.createVariableFromModel(clone);
|
||||||
$scope.current.name = 'copy_of_' + variable.name;
|
$scope.current.name = 'copy_of_' + variable.name;
|
||||||
variableSrv.addVariable($scope.current);
|
variableSrv.addVariable($scope.current);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.update = function() {
|
$scope.update = () => {
|
||||||
if ($scope.isValid()) {
|
if ($scope.isValid()) {
|
||||||
$scope.runQuery().then(function() {
|
$scope.runQuery().then(() => {
|
||||||
$scope.reset();
|
$scope.reset();
|
||||||
$scope.mode = 'list';
|
$scope.mode = 'list';
|
||||||
templateSrv.updateTemplateData();
|
templateSrv.updateTemplateData();
|
||||||
@ -130,18 +130,18 @@ export class VariableEditorCtrl {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.reset = function() {
|
$scope.reset = () => {
|
||||||
$scope.currentIsNew = true;
|
$scope.currentIsNew = true;
|
||||||
$scope.current = variableSrv.createVariableFromModel({ type: 'query' });
|
$scope.current = variableSrv.createVariableFromModel({ type: 'query' });
|
||||||
|
|
||||||
// this is done here in case a new data source type variable was added
|
// this is done here in case a new data source type variable was added
|
||||||
$scope.datasources = _.filter(datasourceSrv.getMetricSources(), function(ds) {
|
$scope.datasources = _.filter(datasourceSrv.getMetricSources(), ds => {
|
||||||
return !ds.meta.mixed && ds.value !== null;
|
return !ds.meta.mixed && ds.value !== null;
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.datasourceTypes = _($scope.datasources)
|
$scope.datasourceTypes = _($scope.datasources)
|
||||||
.uniqBy('meta.id')
|
.uniqBy('meta.id')
|
||||||
.map(function(ds) {
|
.map(ds => {
|
||||||
return { text: ds.meta.name, value: ds.meta.id };
|
return { text: ds.meta.name, value: ds.meta.id };
|
||||||
})
|
})
|
||||||
.value();
|
.value();
|
||||||
@ -164,11 +164,11 @@ export class VariableEditorCtrl {
|
|||||||
$scope.validate();
|
$scope.validate();
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.removeVariable = function(variable) {
|
$scope.removeVariable = variable => {
|
||||||
variableSrv.removeVariable(variable);
|
variableSrv.removeVariable(variable);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.showMoreOptions = function() {
|
$scope.showMoreOptions = () => {
|
||||||
$scope.optionsLimit += 20;
|
$scope.optionsLimit += 20;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@ export class IntervalVariable implements Variable {
|
|||||||
|
|
||||||
updateOptions() {
|
updateOptions() {
|
||||||
// extract options between quotes and/or comma
|
// extract options between quotes and/or comma
|
||||||
this.options = _.map(this.query.match(/(["'])(.*?)\1|\w+/g), function(text) {
|
this.options = _.map(this.query.match(/(["'])(.*?)\1|\w+/g), text => {
|
||||||
text = text.replace(/["']+/g, '');
|
text = text.replace(/["']+/g, '');
|
||||||
return { text: text.trim(), value: text.trim() };
|
return { text: text.trim(), value: text.trim() };
|
||||||
});
|
});
|
||||||
|
@ -106,8 +106,8 @@ export class QueryVariable implements Variable {
|
|||||||
getValuesForTag(tagKey) {
|
getValuesForTag(tagKey) {
|
||||||
return this.datasourceSrv.get(this.datasource).then(datasource => {
|
return this.datasourceSrv.get(this.datasource).then(datasource => {
|
||||||
const query = this.tagValuesQuery.replace('$tag', tagKey);
|
const query = this.tagValuesQuery.replace('$tag', tagKey);
|
||||||
return this.metricFindQuery(datasource, query).then(function(results) {
|
return this.metricFindQuery(datasource, query).then(results => {
|
||||||
return _.map(results, function(value) {
|
return _.map(results, value => {
|
||||||
return value.text;
|
return value.text;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -77,7 +77,7 @@ export class TemplateSrv {
|
|||||||
if (value instanceof Array && value.length === 0) {
|
if (value instanceof Array && value.length === 0) {
|
||||||
return '__empty__';
|
return '__empty__';
|
||||||
}
|
}
|
||||||
const quotedValues = _.map(value, function(val) {
|
const quotedValues = _.map(value, val => {
|
||||||
return '"' + luceneEscape(val) + '"';
|
return '"' + luceneEscape(val) + '"';
|
||||||
});
|
});
|
||||||
return '(' + quotedValues.join(' OR ') + ')';
|
return '(' + quotedValues.join(' OR ') + ')';
|
||||||
@ -248,7 +248,7 @@ export class TemplateSrv {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fillVariableValuesForUrl(params, scopedVars) {
|
fillVariableValuesForUrl(params, scopedVars) {
|
||||||
_.each(this.variables, function(variable) {
|
_.each(this.variables, variable => {
|
||||||
if (scopedVars && scopedVars[variable.name] !== void 0) {
|
if (scopedVars && scopedVars[variable.name] !== void 0) {
|
||||||
if (scopedVars[variable.name].skipUrlSync) {
|
if (scopedVars[variable.name].skipUrlSync) {
|
||||||
return;
|
return;
|
||||||
@ -264,7 +264,7 @@ export class TemplateSrv {
|
|||||||
}
|
}
|
||||||
|
|
||||||
distributeVariable(value, variable) {
|
distributeVariable(value, variable) {
|
||||||
value = _.map(value, function(val, index) {
|
value = _.map(value, (val, index) => {
|
||||||
if (index !== 0) {
|
if (index !== 0) {
|
||||||
return variable + '=' + val;
|
return variable + '=' + val;
|
||||||
} else {
|
} else {
|
||||||
|
@ -122,12 +122,13 @@ export class VariableSrv {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const g = this.createGraph();
|
const g = this.createGraph();
|
||||||
const promises = g
|
const node = g.getNode(variable.name);
|
||||||
.getNode(variable.name)
|
let promises = [];
|
||||||
.getOptimizedInputEdges()
|
if (node) {
|
||||||
.map(e => {
|
promises = node.getOptimizedInputEdges().map(e => {
|
||||||
return this.updateOptions(this.variables.find(v => v.name === e.inputNode.name));
|
return this.updateOptions(this.variables.find(v => v.name === e.inputNode.name));
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return this.$q.all(promises).then(() => {
|
return this.$q.all(promises).then(() => {
|
||||||
if (emitChangeEvents) {
|
if (emitChangeEvents) {
|
||||||
@ -174,10 +175,10 @@ export class VariableSrv {
|
|||||||
selected = variable.options[0];
|
selected = variable.options[0];
|
||||||
} else {
|
} else {
|
||||||
selected = {
|
selected = {
|
||||||
value: _.map(selected, function(val) {
|
value: _.map(selected, val => {
|
||||||
return val.value;
|
return val.value;
|
||||||
}),
|
}),
|
||||||
text: _.map(selected, function(val) {
|
text: _.map(selected, val => {
|
||||||
return val.text;
|
return val.text;
|
||||||
}).join(' + '),
|
}).join(' + '),
|
||||||
};
|
};
|
||||||
@ -249,7 +250,7 @@ export class VariableSrv {
|
|||||||
const params = this.$location.search();
|
const params = this.$location.search();
|
||||||
|
|
||||||
// remove variable params
|
// remove variable params
|
||||||
_.each(params, function(value, key) {
|
_.each(params, (value, key) => {
|
||||||
if (key.indexOf('var-') === 0) {
|
if (key.indexOf('var-') === 0) {
|
||||||
delete params[key];
|
delete params[key];
|
||||||
}
|
}
|
||||||
|
63
public/app/plugins/datasource/postgres/config_ctrl.ts
Normal file
63
public/app/plugins/datasource/postgres/config_ctrl.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
export class PostgresConfigCtrl {
|
||||||
|
static templateUrl = 'partials/config.html';
|
||||||
|
|
||||||
|
current: any;
|
||||||
|
datasourceSrv: any;
|
||||||
|
showTimescaleDBHelp: boolean;
|
||||||
|
|
||||||
|
/** @ngInject */
|
||||||
|
constructor($scope, datasourceSrv) {
|
||||||
|
this.datasourceSrv = datasourceSrv;
|
||||||
|
this.current.jsonData.sslmode = this.current.jsonData.sslmode || 'verify-full';
|
||||||
|
this.current.jsonData.postgresVersion = this.current.jsonData.postgresVersion || 903;
|
||||||
|
this.showTimescaleDBHelp = false;
|
||||||
|
this.autoDetectFeatures();
|
||||||
|
}
|
||||||
|
|
||||||
|
autoDetectFeatures() {
|
||||||
|
if (!this.current.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.datasourceSrv.loadDatasource(this.current.name).then(ds => {
|
||||||
|
return ds.getVersion().then(version => {
|
||||||
|
version = Number(version[0].text);
|
||||||
|
|
||||||
|
// timescaledb is only available for 9.6+
|
||||||
|
if (version >= 906) {
|
||||||
|
ds.getTimescaleDBVersion().then(version => {
|
||||||
|
if (version.length === 1) {
|
||||||
|
this.current.jsonData.timescaledb = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const major = Math.trunc(version / 100);
|
||||||
|
const minor = version % 100;
|
||||||
|
let name = String(major);
|
||||||
|
if (version < 1000) {
|
||||||
|
name = String(major) + '.' + String(minor);
|
||||||
|
}
|
||||||
|
if (!_.find(this.postgresVersions, (p: any) => p.value === version)) {
|
||||||
|
this.postgresVersions.push({ name: name, value: version });
|
||||||
|
}
|
||||||
|
this.current.jsonData.postgresVersion = version;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleTimescaleDBHelp() {
|
||||||
|
this.showTimescaleDBHelp = !this.showTimescaleDBHelp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the value portion is derived from postgres server_version_num/100
|
||||||
|
postgresVersions = [
|
||||||
|
{ name: '9.3', value: 903 },
|
||||||
|
{ name: '9.4', value: 904 },
|
||||||
|
{ name: '9.5', value: 905 },
|
||||||
|
{ name: '9.6', value: 906 },
|
||||||
|
{ name: '10', value: 1000 },
|
||||||
|
];
|
||||||
|
}
|
@ -1,22 +1,27 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import ResponseParser from './response_parser';
|
import ResponseParser from './response_parser';
|
||||||
|
import PostgresQuery from 'app/plugins/datasource/postgres/postgres_query';
|
||||||
|
|
||||||
export class PostgresDatasource {
|
export class PostgresDatasource {
|
||||||
id: any;
|
id: any;
|
||||||
name: any;
|
name: any;
|
||||||
|
jsonData: any;
|
||||||
responseParser: ResponseParser;
|
responseParser: ResponseParser;
|
||||||
|
queryModel: PostgresQuery;
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(instanceSettings, private backendSrv, private $q, private templateSrv) {
|
constructor(instanceSettings, private backendSrv, private $q, private templateSrv, private timeSrv) {
|
||||||
this.name = instanceSettings.name;
|
this.name = instanceSettings.name;
|
||||||
this.id = instanceSettings.id;
|
this.id = instanceSettings.id;
|
||||||
|
this.jsonData = instanceSettings.jsonData;
|
||||||
this.responseParser = new ResponseParser(this.$q);
|
this.responseParser = new ResponseParser(this.$q);
|
||||||
|
this.queryModel = new PostgresQuery({});
|
||||||
}
|
}
|
||||||
|
|
||||||
interpolateVariable(value, variable) {
|
interpolateVariable(value, variable) {
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
if (variable.multi || variable.includeAll) {
|
if (variable.multi || variable.includeAll) {
|
||||||
return "'" + value.replace(/'/g, `''`) + "'";
|
return this.queryModel.quoteLiteral(value);
|
||||||
} else {
|
} else {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
@ -26,23 +31,25 @@ export class PostgresDatasource {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const quotedValues = _.map(value, function(val) {
|
const quotedValues = _.map(value, v => {
|
||||||
return "'" + val.replace(/'/g, `''`) + "'";
|
return this.queryModel.quoteLiteral(v);
|
||||||
});
|
});
|
||||||
return quotedValues.join(',');
|
return quotedValues.join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
query(options) {
|
query(options) {
|
||||||
const queries = _.filter(options.targets, item => {
|
const queries = _.filter(options.targets, target => {
|
||||||
return item.hide !== true;
|
return target.hide !== true;
|
||||||
}).map(item => {
|
}).map(target => {
|
||||||
|
const queryModel = new PostgresQuery(target, this.templateSrv, options.scopedVars);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
refId: item.refId,
|
refId: target.refId,
|
||||||
intervalMs: options.intervalMs,
|
intervalMs: options.intervalMs,
|
||||||
maxDataPoints: options.maxDataPoints,
|
maxDataPoints: options.maxDataPoints,
|
||||||
datasourceId: this.id,
|
datasourceId: this.id,
|
||||||
rawSql: this.templateSrv.replace(item.rawSql, options.scopedVars, this.interpolateVariable),
|
rawSql: queryModel.render(this.interpolateVariable),
|
||||||
format: item.format,
|
format: target.format,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -103,17 +110,13 @@ export class PostgresDatasource {
|
|||||||
format: 'table',
|
format: 'table',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const range = this.timeSrv.timeRange();
|
||||||
const data = {
|
const data = {
|
||||||
queries: [interpolatedQuery],
|
queries: [interpolatedQuery],
|
||||||
|
from: range.from.valueOf().toString(),
|
||||||
|
to: range.to.valueOf().toString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (optionalOptions && optionalOptions.range && optionalOptions.range.from) {
|
|
||||||
data['from'] = optionalOptions.range.from.valueOf().toString();
|
|
||||||
}
|
|
||||||
if (optionalOptions && optionalOptions.range && optionalOptions.range.to) {
|
|
||||||
data['to'] = optionalOptions.range.to.valueOf().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.backendSrv
|
return this.backendSrv
|
||||||
.datasourceRequest({
|
.datasourceRequest({
|
||||||
url: '/api/tsdb/query',
|
url: '/api/tsdb/query',
|
||||||
@ -123,6 +126,14 @@ export class PostgresDatasource {
|
|||||||
.then(data => this.responseParser.parseMetricFindQueryResult(refId, data));
|
.then(data => this.responseParser.parseMetricFindQueryResult(refId, data));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getVersion() {
|
||||||
|
return this.metricFindQuery("SELECT current_setting('server_version_num')::int/100", {});
|
||||||
|
}
|
||||||
|
|
||||||
|
getTimescaleDBVersion() {
|
||||||
|
return this.metricFindQuery("SELECT extversion FROM pg_extension WHERE extname = 'timescaledb'", {});
|
||||||
|
}
|
||||||
|
|
||||||
testDatasource() {
|
testDatasource() {
|
||||||
return this.metricFindQuery('SELECT 1', {})
|
return this.metricFindQuery('SELECT 1', {})
|
||||||
.then(res => {
|
.then(res => {
|
||||||
|
176
public/app/plugins/datasource/postgres/meta_query.ts
Normal file
176
public/app/plugins/datasource/postgres/meta_query.ts
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
export class PostgresMetaQuery {
|
||||||
|
constructor(private target, private queryModel) {}
|
||||||
|
|
||||||
|
getOperators(datatype: string) {
|
||||||
|
switch (datatype) {
|
||||||
|
case 'float4':
|
||||||
|
case 'float8': {
|
||||||
|
return ['=', '!=', '<', '<=', '>', '>='];
|
||||||
|
}
|
||||||
|
case 'text':
|
||||||
|
case 'varchar':
|
||||||
|
case 'char': {
|
||||||
|
return ['=', '!=', '<', '<=', '>', '>=', 'IN', 'NOT IN', 'LIKE', 'NOT LIKE', '~', '~*', '!~', '!~*'];
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return ['=', '!=', '<', '<=', '>', '>=', 'IN', 'NOT IN'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// quote identifier as literal to use in metadata queries
|
||||||
|
quoteIdentAsLiteral(value) {
|
||||||
|
return this.queryModel.quoteLiteral(this.queryModel.unquoteIdentifier(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
findMetricTable() {
|
||||||
|
// query that returns first table found that has a timestamp(tz) column and a float column
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
quote_ident(table_name) as table_name,
|
||||||
|
( SELECT
|
||||||
|
quote_ident(column_name) as column_name
|
||||||
|
FROM information_schema.columns c
|
||||||
|
WHERE
|
||||||
|
c.table_schema = t.table_schema AND
|
||||||
|
c.table_name = t.table_name AND
|
||||||
|
udt_name IN ('timestamptz','timestamp')
|
||||||
|
ORDER BY ordinal_position LIMIT 1
|
||||||
|
) AS time_column,
|
||||||
|
( SELECT
|
||||||
|
quote_ident(column_name) AS column_name
|
||||||
|
FROM information_schema.columns c
|
||||||
|
WHERE
|
||||||
|
c.table_schema = t.table_schema AND
|
||||||
|
c.table_name = t.table_name AND
|
||||||
|
udt_name='float8'
|
||||||
|
ORDER BY ordinal_position LIMIT 1
|
||||||
|
) AS value_column
|
||||||
|
FROM information_schema.tables t
|
||||||
|
WHERE
|
||||||
|
table_schema IN (
|
||||||
|
SELECT CASE WHEN trim(unnest) = '"$user"' THEN user ELSE trim(unnest) END
|
||||||
|
FROM unnest(string_to_array(current_setting('search_path'),','))
|
||||||
|
) AND
|
||||||
|
EXISTS
|
||||||
|
( SELECT 1
|
||||||
|
FROM information_schema.columns c
|
||||||
|
WHERE
|
||||||
|
c.table_schema = t.table_schema AND
|
||||||
|
c.table_name = t.table_name AND
|
||||||
|
udt_name IN ('timestamptz','timestamp')
|
||||||
|
) AND
|
||||||
|
EXISTS
|
||||||
|
( SELECT 1
|
||||||
|
FROM information_schema.columns c
|
||||||
|
WHERE
|
||||||
|
c.table_schema = t.table_schema AND
|
||||||
|
c.table_name = t.table_name AND
|
||||||
|
udt_name='float8'
|
||||||
|
)
|
||||||
|
LIMIT 1
|
||||||
|
;`;
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildSchemaConstraint() {
|
||||||
|
const query = `
|
||||||
|
table_schema IN (
|
||||||
|
SELECT CASE WHEN trim(unnest) = \'"$user"\' THEN user ELSE trim(unnest) END
|
||||||
|
FROM unnest(string_to_array(current_setting(\'search_path\'),\',\'))
|
||||||
|
)`;
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTableConstraint(table: string) {
|
||||||
|
let query = '';
|
||||||
|
|
||||||
|
// check for schema qualified table
|
||||||
|
if (table.includes('.')) {
|
||||||
|
const parts = table.split('.');
|
||||||
|
query = 'table_schema = ' + this.quoteIdentAsLiteral(parts[0]);
|
||||||
|
query += ' AND table_name = ' + this.quoteIdentAsLiteral(parts[1]);
|
||||||
|
return query;
|
||||||
|
} else {
|
||||||
|
query = `
|
||||||
|
table_schema IN (
|
||||||
|
SELECT CASE WHEN trim(unnest) = \'"$user"\' THEN user ELSE trim(unnest) END
|
||||||
|
FROM unnest(string_to_array(current_setting(\'search_path\'),\',\'))
|
||||||
|
)`;
|
||||||
|
query += ' AND table_name = ' + this.quoteIdentAsLiteral(table);
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTableQuery() {
|
||||||
|
let query = 'SELECT quote_ident(table_name) FROM information_schema.tables WHERE ';
|
||||||
|
query += this.buildSchemaConstraint();
|
||||||
|
query += ' ORDER BY table_name';
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildColumnQuery(type?: string) {
|
||||||
|
let query = 'SELECT quote_ident(column_name) FROM information_schema.columns WHERE ';
|
||||||
|
query += this.buildTableConstraint(this.target.table);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'time': {
|
||||||
|
query +=
|
||||||
|
" AND data_type IN ('timestamp without time zone','timestamp with time zone','bigint','integer','double precision','real')";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'metric': {
|
||||||
|
query += " AND data_type IN ('text','character','character varying')";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'value': {
|
||||||
|
query += " AND data_type IN ('bigint','integer','double precision','real')";
|
||||||
|
query += ' AND column_name <> ' + this.quoteIdentAsLiteral(this.target.timeColumn);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'group': {
|
||||||
|
query += " AND data_type IN ('text','character','character varying')";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query += ' ORDER BY column_name';
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildValueQuery(column: string) {
|
||||||
|
let query = 'SELECT DISTINCT quote_literal(' + column + ')';
|
||||||
|
query += ' FROM ' + this.target.table;
|
||||||
|
query += ' WHERE $__timeFilter(' + this.target.timeColumn + ')';
|
||||||
|
query += ' ORDER BY 1 LIMIT 100';
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildDatatypeQuery(column: string) {
|
||||||
|
let query = `
|
||||||
|
SELECT udt_name
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE
|
||||||
|
table_schema IN (
|
||||||
|
SELECT schema FROM (
|
||||||
|
SELECT CASE WHEN trim(unnest) = \'"$user"\' THEN user ELSE trim(unnest) END as schema
|
||||||
|
FROM unnest(string_to_array(current_setting(\'search_path\'),\',\'))
|
||||||
|
) s
|
||||||
|
WHERE EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = s.schema)
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
query += ' AND table_name = ' + this.quoteIdentAsLiteral(this.target.table);
|
||||||
|
query += ' AND column_name = ' + this.quoteIdentAsLiteral(column);
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildAggregateQuery() {
|
||||||
|
let query = 'SELECT DISTINCT proname FROM pg_aggregate ';
|
||||||
|
query += 'INNER JOIN pg_proc ON pg_aggregate.aggfnoid = pg_proc.oid ';
|
||||||
|
query += 'INNER JOIN pg_type ON pg_type.oid=pg_proc.prorettype ';
|
||||||
|
query += "WHERE pronargs=1 AND typname IN ('float8') AND aggkind='n' ORDER BY 1";
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user