mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge remote-tracking branch 'upstream/master' into postgres-query-builder
This commit is contained in:
commit
107c0d81d2
@ -13,6 +13,8 @@
|
|||||||
* **Prometheus**: Show template variable candidate in query editor [#9210](https://github.com/grafana/grafana/issues/9210), thx [@mtanda](https://github.com/mtanda)
|
* **Prometheus**: Show template variable candidate in query editor [#9210](https://github.com/grafana/grafana/issues/9210), thx [@mtanda](https://github.com/mtanda)
|
||||||
* **Prometheus**: Support POST for query and query_range [#9859](https://github.com/grafana/grafana/pull/9859), thx [@mtanda](https://github.com/mtanda)
|
* **Prometheus**: Support POST for query and query_range [#9859](https://github.com/grafana/grafana/pull/9859), thx [@mtanda](https://github.com/mtanda)
|
||||||
* **Alerting**: Add support for retries on alert queries [#5855](https://github.com/grafana/grafana/issues/5855), thx [@Thib17](https://github.com/Thib17)
|
* **Alerting**: Add support for retries on alert queries [#5855](https://github.com/grafana/grafana/issues/5855), thx [@Thib17](https://github.com/Thib17)
|
||||||
|
* **Table**: Table plugin value mappings [#7119](https://github.com/grafana/grafana/issues/7119), thx [infernix](https://github.com/infernix)
|
||||||
|
* **IE11**: IE 11 compatibility [#11165](https://github.com/grafana/grafana/issues/11165)
|
||||||
|
|
||||||
### Minor
|
### Minor
|
||||||
* **OpsGenie**: Add triggered alerts as description [#11046](https://github.com/grafana/grafana/pull/11046), thx [@llamashoes](https://github.com/llamashoes)
|
* **OpsGenie**: Add triggered alerts as description [#11046](https://github.com/grafana/grafana/pull/11046), thx [@llamashoes](https://github.com/llamashoes)
|
||||||
@ -25,6 +27,8 @@
|
|||||||
* **Shortcuts**: Add shortcut for duplicate panel [#11102](https://github.com/grafana/grafana/issues/11102)
|
* **Shortcuts**: Add shortcut for duplicate panel [#11102](https://github.com/grafana/grafana/issues/11102)
|
||||||
* **AuthProxy**: Support IPv6 in Auth proxy white list [#11330](https://github.com/grafana/grafana/pull/11330), thx [@corny](https://github.com/corny)
|
* **AuthProxy**: Support IPv6 in Auth proxy white list [#11330](https://github.com/grafana/grafana/pull/11330), thx [@corny](https://github.com/corny)
|
||||||
* **SMTP**: Don't connect to STMP server using TLS unless configured. [#7189](https://github.com/grafana/grafana/issues/7189)
|
* **SMTP**: Don't connect to STMP server using TLS unless configured. [#7189](https://github.com/grafana/grafana/issues/7189)
|
||||||
|
* **Prometheus**: Escape backslash in labels correctly. [#10555](https://github.com/grafana/grafana/issues/10555), thx [@roidelapluie](https://github.com/roidelapluie)
|
||||||
|
* **Variables** Case-insensitive sorting for template values [#11128](https://github.com/grafana/grafana/issues/11128) thx [@cross](https://github.com/cross)
|
||||||
|
|
||||||
# 5.0.4 (2018-03-28)
|
# 5.0.4 (2018-03-28)
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ upgrading Grafana please check here before creating an issue.
|
|||||||
- [Datasource plugin written in typescript](https://github.com/grafana/typescript-template-datasource)
|
- [Datasource plugin written in typescript](https://github.com/grafana/typescript-template-datasource)
|
||||||
- [Simple json dataource plugin](https://github.com/grafana/simple-json-datasource)
|
- [Simple json dataource plugin](https://github.com/grafana/simple-json-datasource)
|
||||||
- [Plugin development guide](http://docs.grafana.org/plugins/developing/development/)
|
- [Plugin development guide](http://docs.grafana.org/plugins/developing/development/)
|
||||||
|
- [Webpack Grafana plugin template project](https://github.com/CorpGlory/grafana-plugin-template-webpack)
|
||||||
|
|
||||||
## Changes in v4.6
|
## Changes in v4.6
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ EXPOSE 389
|
|||||||
VOLUME ["/etc/ldap", "/var/lib/ldap"]
|
VOLUME ["/etc/ldap", "/var/lib/ldap"]
|
||||||
|
|
||||||
COPY modules/ /etc/ldap.dist/modules
|
COPY modules/ /etc/ldap.dist/modules
|
||||||
|
COPY prepopulate/ /etc/ldap.dist/prepopulate
|
||||||
|
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@ EOF
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n "$SLAPD_ADDITIONAL_SCHEMAS" ]]; then
|
if [[ -n "$SLAPD_ADDITIONAL_SCHEMAS" ]]; then
|
||||||
IFS=","; declare -a schemas=($SLAPD_ADDITIONAL_SCHEMAS)
|
IFS=","; declare -a schemas=($SLAPD_ADDITIONAL_SCHEMAS); unset IFS
|
||||||
|
|
||||||
for schema in "${schemas[@]}"; do
|
for schema in "${schemas[@]}"; do
|
||||||
slapadd -n0 -F /etc/ldap/slapd.d -l "/etc/ldap/schema/${schema}.ldif" >/dev/null 2>&1
|
slapadd -n0 -F /etc/ldap/slapd.d -l "/etc/ldap/schema/${schema}.ldif" >/dev/null 2>&1
|
||||||
@ -73,14 +73,18 @@ EOF
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n "$SLAPD_ADDITIONAL_MODULES" ]]; then
|
if [[ -n "$SLAPD_ADDITIONAL_MODULES" ]]; then
|
||||||
IFS=","; declare -a modules=($SLAPD_ADDITIONAL_MODULES)
|
IFS=","; declare -a modules=($SLAPD_ADDITIONAL_MODULES); unset IFS
|
||||||
|
|
||||||
for module in "${modules[@]}"; do
|
for module in "${modules[@]}"; do
|
||||||
slapadd -n0 -F /etc/ldap/slapd.d -l "/etc/ldap/modules/${module}.ldif" >/dev/null 2>&1
|
slapadd -n0 -F /etc/ldap/slapd.d -l "/etc/ldap/modules/${module}.ldif" >/dev/null 2>&1
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
chown -R openldap:openldap /etc/ldap/slapd.d/
|
for file in `ls /etc/ldap/prepopulate/*.ldif`; do
|
||||||
|
slapadd -F /etc/ldap/slapd.d -l "$file"
|
||||||
|
done
|
||||||
|
|
||||||
|
chown -R openldap:openldap /etc/ldap/slapd.d/ /var/lib/ldap/ /var/run/slapd/
|
||||||
else
|
else
|
||||||
slapd_configs_in_env=`env | grep 'SLAPD_'`
|
slapd_configs_in_env=`env | grep 'SLAPD_'`
|
||||||
|
|
||||||
|
13
docker/blocks/openldap/notes.md
Normal file
13
docker/blocks/openldap/notes.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Notes on OpenLdap Docker Block
|
||||||
|
|
||||||
|
Any ldif files added to the prepopulate subdirectory will be automatically imported into the OpenLdap database.
|
||||||
|
|
||||||
|
The ldif files add three users, `ldapviewer`, `ldapeditor` and `ldapadmin`. Two groups, `admins` and `users`, are added that correspond with the group mappings in the default conf/ldap.toml. `ldapadmin` is a member of `admins` and `ldapeditor` is a member of `users`.
|
||||||
|
|
||||||
|
Note that users that are added here need to specify a `memberOf` attribute manually as well as the `member` attribute for the group. The `memberOf` module usually does this automatically (if you add a group in Apache Directory Studio for example) but this does not work in the entrypoint script as it uses the `slapadd` command to add entries before the server has started and before the `memberOf` module is loaded.
|
||||||
|
|
||||||
|
After adding ldif files to `prepopulate`:
|
||||||
|
|
||||||
|
1. Remove your current docker image: `docker rm docker_openldap_1`
|
||||||
|
2. Build: `docker-compose build`
|
||||||
|
3. `docker-compose up`
|
10
docker/blocks/openldap/prepopulate/admin.ldif
Normal file
10
docker/blocks/openldap/prepopulate/admin.ldif
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
dn: cn=ldapadmin,dc=grafana,dc=org
|
||||||
|
mail: ldapadmin@grafana.com
|
||||||
|
userPassword: grafana
|
||||||
|
objectClass: person
|
||||||
|
objectClass: top
|
||||||
|
objectClass: inetOrgPerson
|
||||||
|
objectClass: organizationalPerson
|
||||||
|
sn: ldapadmin
|
||||||
|
cn: ldapadmin
|
||||||
|
memberOf: cn=admins,dc=grafana,dc=org
|
5
docker/blocks/openldap/prepopulate/adminsgroup.ldif
Normal file
5
docker/blocks/openldap/prepopulate/adminsgroup.ldif
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
dn: cn=admins,dc=grafana,dc=org
|
||||||
|
cn: admins
|
||||||
|
member: cn=ldapadmin,dc=grafana,dc=org
|
||||||
|
objectClass: groupOfNames
|
||||||
|
objectClass: top
|
10
docker/blocks/openldap/prepopulate/editor.ldif
Normal file
10
docker/blocks/openldap/prepopulate/editor.ldif
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
dn: cn=ldapeditor,dc=grafana,dc=org
|
||||||
|
mail: ldapeditor@grafana.com
|
||||||
|
userPassword: grafana
|
||||||
|
objectClass: person
|
||||||
|
objectClass: top
|
||||||
|
objectClass: inetOrgPerson
|
||||||
|
objectClass: organizationalPerson
|
||||||
|
sn: ldapeditor
|
||||||
|
cn: ldapeditor
|
||||||
|
memberOf: cn=users,dc=grafana,dc=org
|
5
docker/blocks/openldap/prepopulate/usersgroup.ldif
Normal file
5
docker/blocks/openldap/prepopulate/usersgroup.ldif
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
dn: cn=users,dc=grafana,dc=org
|
||||||
|
cn: users
|
||||||
|
member: cn=ldapeditor,dc=grafana,dc=org
|
||||||
|
objectClass: groupOfNames
|
||||||
|
objectClass: top
|
9
docker/blocks/openldap/prepopulate/viewer.ldif
Normal file
9
docker/blocks/openldap/prepopulate/viewer.ldif
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
dn: cn=ldapviewer,dc=grafana,dc=org
|
||||||
|
mail: ldapviewer@grafana.com
|
||||||
|
userPassword: grafana
|
||||||
|
objectClass: person
|
||||||
|
objectClass: top
|
||||||
|
objectClass: inetOrgPerson
|
||||||
|
objectClass: organizationalPerson
|
||||||
|
sn: ldapviewer
|
||||||
|
cn: ldapviewer
|
@ -71,13 +71,13 @@ Each field in the dashboard JSON is explained below with its usage:
|
|||||||
| **timepicker** | timepicker metadata, see [timepicker section](#timepicker) for details |
|
| **timepicker** | timepicker metadata, see [timepicker section](#timepicker) for details |
|
||||||
| **templating** | templating metadata, see [templating section](#templating) for details |
|
| **templating** | templating metadata, see [templating section](#templating) for details |
|
||||||
| **annotations** | annotations metadata, see [annotations section](#annotations) for details |
|
| **annotations** | annotations metadata, see [annotations section](#annotations) for details |
|
||||||
| **schemaVersion** | version of the JSON schema (integer), incremented each time a Grafana update brings changes to the said schema |
|
| **schemaVersion** | version of the JSON schema (integer), incremented each time a Grafana update brings changes to said schema |
|
||||||
| **version** | version of the dashboard (integer), incremented each time the dashboard is updated |
|
| **version** | version of the dashboard (integer), incremented each time the dashboard is updated |
|
||||||
| **panels** | panels array, see below for detail. |
|
| **panels** | panels array, see below for detail. |
|
||||||
|
|
||||||
## Panels
|
## Panels
|
||||||
|
|
||||||
Panels are the building blocks a dashboard. It consists of datasource queries, type of graphs, aliases, etc. Panel JSON consists of an array of JSON objects, each representing a different panel. Most of the fields are common for all panels but some fields depends on the panel type. Following is an example of panel JSON of a text panel.
|
Panels are the building blocks of a dashboard. It consists of datasource queries, type of graphs, aliases, etc. Panel JSON consists of an array of JSON objects, each representing a different panel. Most of the fields are common for all panels but some fields depend on the panel type. Following is an example of panel JSON of a text panel.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"panels": [
|
"panels": [
|
||||||
@ -105,7 +105,7 @@ The gridPos property describes the panel size and position in grid coordinates.
|
|||||||
- `x` The x position, in same unit as `w`.
|
- `x` The x position, in same unit as `w`.
|
||||||
- `y` The y position, in same unit as `h`.
|
- `y` The y position, in same unit as `h`.
|
||||||
|
|
||||||
The grid has a negative gravity that moves panels up if there i empty space above a panel.
|
The grid has a negative gravity that moves panels up if there is empty space above a panel.
|
||||||
|
|
||||||
### timepicker
|
### timepicker
|
||||||
|
|
||||||
@ -161,7 +161,7 @@ Usage of the fields is explained below:
|
|||||||
|
|
||||||
### templating
|
### templating
|
||||||
|
|
||||||
`templating` fields contains array of template variables with their saved values along with some other metadata, for example:
|
The `templating` field contains an array of template variables with their saved values along with some other metadata, for example:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"templating": {
|
"templating": {
|
||||||
@ -236,7 +236,7 @@ Usage of the above mentioned fields in the templating section is explained below
|
|||||||
| Name | Usage |
|
| Name | Usage |
|
||||||
| ---- | ----- |
|
| ---- | ----- |
|
||||||
| **enable** | whether templating is enabled or not |
|
| **enable** | whether templating is enabled or not |
|
||||||
| **list** | an array of objects representing, each representing one template variable |
|
| **list** | an array of objects each representing one template variable |
|
||||||
| **allFormat** | format to use while fetching all values from datasource, eg: `wildcard`, `glob`, `regex`, `pipe`, etc. |
|
| **allFormat** | format to use while fetching all values from datasource, eg: `wildcard`, `glob`, `regex`, `pipe`, etc. |
|
||||||
| **current** | shows current selected variable text/value on the dashboard |
|
| **current** | shows current selected variable text/value on the dashboard |
|
||||||
| **datasource** | shows datasource for the variables |
|
| **datasource** | shows datasource for the variables |
|
||||||
|
@ -104,10 +104,10 @@
|
|||||||
"test": "grunt test",
|
"test": "grunt test",
|
||||||
"test:coverage": "grunt test --coverage=true",
|
"test:coverage": "grunt test --coverage=true",
|
||||||
"lint": "tslint -c tslint.json --project tsconfig.json --type-check",
|
"lint": "tslint -c tslint.json --project tsconfig.json --type-check",
|
||||||
"karma": "node ./node_modules/grunt-cli/bin/grunt karma:dev",
|
"karma": "grunt karma:dev",
|
||||||
"jest": "node ./node_modules/jest-cli/bin/jest.js --notify --watch",
|
"jest": "jest --notify --watch",
|
||||||
"api-tests": "node ./node_modules/jest-cli/bin/jest.js --notify --watch --config=tests/api/jest.js",
|
"api-tests": "jest --notify --watch --config=tests/api/jest.js",
|
||||||
"precommit": "lint-staged && node ./node_modules/grunt-cli/bin/grunt precommit"
|
"precommit": "lint-staged && grunt precommit"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{ts,tsx}": [
|
"*.{ts,tsx}": [
|
||||||
|
@ -111,7 +111,7 @@ func (g *GrafanaServerImpl) initLogging() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.log.Error(err.Error())
|
fmt.Fprintf(os.Stderr, "Failed to start grafana. error: %s\n", err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,7 +223,7 @@ func shouldRedactURLKey(s string) bool {
|
|||||||
return strings.Contains(uppercased, "DATABASE_URL")
|
return strings.Contains(uppercased, "DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyEnvVariableOverrides() {
|
func applyEnvVariableOverrides() error {
|
||||||
appliedEnvOverrides = make([]string, 0)
|
appliedEnvOverrides = make([]string, 0)
|
||||||
for _, section := range Cfg.Sections() {
|
for _, section := range Cfg.Sections() {
|
||||||
for _, key := range section.Keys() {
|
for _, key := range section.Keys() {
|
||||||
@ -238,7 +238,10 @@ func applyEnvVariableOverrides() {
|
|||||||
envValue = "*********"
|
envValue = "*********"
|
||||||
}
|
}
|
||||||
if shouldRedactURLKey(envKey) {
|
if shouldRedactURLKey(envKey) {
|
||||||
u, _ := url.Parse(envValue)
|
u, err := url.Parse(envValue)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not parse environment variable. key: %s, value: %s. error: %v", envKey, envValue, err)
|
||||||
|
}
|
||||||
ui := u.User
|
ui := u.User
|
||||||
if ui != nil {
|
if ui != nil {
|
||||||
_, exists := ui.Password()
|
_, exists := ui.Password()
|
||||||
@ -252,6 +255,8 @@ func applyEnvVariableOverrides() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyCommandLineDefaultProperties(props map[string]string) {
|
func applyCommandLineDefaultProperties(props map[string]string) {
|
||||||
@ -377,7 +382,7 @@ func loadSpecifedConfigFile(configFile string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfiguration(args *CommandLineArgs) {
|
func loadConfiguration(args *CommandLineArgs) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// load config defaults
|
// load config defaults
|
||||||
@ -395,7 +400,7 @@ func loadConfiguration(args *CommandLineArgs) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(fmt.Sprintf("Failed to parse defaults.ini, %v", err))
|
fmt.Println(fmt.Sprintf("Failed to parse defaults.ini, %v", err))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
Cfg.BlockMode = false
|
Cfg.BlockMode = false
|
||||||
@ -413,7 +418,10 @@ func loadConfiguration(args *CommandLineArgs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// apply environment overrides
|
// apply environment overrides
|
||||||
applyEnvVariableOverrides()
|
err = applyEnvVariableOverrides()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// apply command line overrides
|
// apply command line overrides
|
||||||
applyCommandLineProperties(commandLineProps)
|
applyCommandLineProperties(commandLineProps)
|
||||||
@ -424,6 +432,8 @@ func loadConfiguration(args *CommandLineArgs) {
|
|||||||
// update data path and logging config
|
// update data path and logging config
|
||||||
DataPath = makeAbsolute(Cfg.Section("paths").Key("data").String(), HomePath)
|
DataPath = makeAbsolute(Cfg.Section("paths").Key("data").String(), HomePath)
|
||||||
initLogging()
|
initLogging()
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func pathExists(path string) bool {
|
func pathExists(path string) bool {
|
||||||
@ -471,7 +481,10 @@ func validateStaticRootPath() error {
|
|||||||
|
|
||||||
func NewConfigContext(args *CommandLineArgs) error {
|
func NewConfigContext(args *CommandLineArgs) error {
|
||||||
setHomePath(args)
|
setHomePath(args)
|
||||||
loadConfiguration(args)
|
err := loadConfiguration(args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
Env = Cfg.Section("").Key("app_mode").MustString("development")
|
Env = Cfg.Section("").Key("app_mode").MustString("development")
|
||||||
InstanceName = Cfg.Section("").Key("instance_name").MustString("unknown_instance_name")
|
InstanceName = Cfg.Section("").Key("instance_name").MustString("unknown_instance_name")
|
||||||
|
@ -37,6 +37,13 @@ func TestLoadingSettings(t *testing.T) {
|
|||||||
So(appliedEnvOverrides, ShouldContain, "GF_SECURITY_ADMIN_PASSWORD=*********")
|
So(appliedEnvOverrides, ShouldContain, "GF_SECURITY_ADMIN_PASSWORD=*********")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("Should return an error when url is invalid", func() {
|
||||||
|
os.Setenv("GF_DATABASE_URL", "postgres.%31://grafana:secret@postgres:5432/grafana")
|
||||||
|
err := NewConfigContext(&CommandLineArgs{HomePath: "../../"})
|
||||||
|
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
Convey("Should replace password in URL when url environment is defined", func() {
|
Convey("Should replace password in URL when url environment is defined", func() {
|
||||||
os.Setenv("GF_DATABASE_URL", "mysql://user:secret@localhost:3306/database")
|
os.Setenv("GF_DATABASE_URL", "mysql://user:secret@localhost:3306/database")
|
||||||
NewConfigContext(&CommandLineArgs{HomePath: "../../"})
|
NewConfigContext(&CommandLineArgs{HomePath: "../../"})
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
<div class="search-section__header" ng-show="section.hideHeader"></div>
|
<div class="search-section__header" ng-show="section.hideHeader"></div>
|
||||||
|
|
||||||
<div ng-if="section.expanded">
|
<div ng-if="section.expanded">
|
||||||
<a ng-repeat="item in section.items" class="search-item" ng-class="{'selected': item.selected}" ng-href="{{::item.url}}" >
|
<a ng-repeat="item in section.items" class="search-item search-item--indent" ng-class="{'selected': item.selected}" ng-href="{{::item.url}}" >
|
||||||
<div ng-click="ctrl.toggleSelection(item, $event)">
|
<div ng-click="ctrl.toggleSelection(item, $event)">
|
||||||
<gf-form-switch
|
<gf-form-switch
|
||||||
ng-show="ctrl.editable"
|
ng-show="ctrl.editable"
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
define([
|
|
||||||
'./alert_srv',
|
|
||||||
'./util_srv',
|
|
||||||
'./context_srv',
|
|
||||||
'./timer',
|
|
||||||
'./analytics',
|
|
||||||
'./popover_srv',
|
|
||||||
'./segment_srv',
|
|
||||||
'./backend_srv',
|
|
||||||
'./dynamic_directive_srv',
|
|
||||||
'./bridge_srv'
|
|
||||||
],
|
|
||||||
function () {});
|
|
10
public/app/core/services/all.ts
Normal file
10
public/app/core/services/all.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import './alert_srv';
|
||||||
|
import './util_srv';
|
||||||
|
import './context_srv';
|
||||||
|
import './timer';
|
||||||
|
import './analytics';
|
||||||
|
import './popover_srv';
|
||||||
|
import './segment_srv';
|
||||||
|
import './backend_srv';
|
||||||
|
import './dynamic_directive_srv';
|
||||||
|
import './bridge_srv';
|
@ -10,6 +10,7 @@ import 'mousetrap-global-bind';
|
|||||||
export class KeybindingSrv {
|
export class KeybindingSrv {
|
||||||
helpModal: boolean;
|
helpModal: boolean;
|
||||||
modalOpen = false;
|
modalOpen = false;
|
||||||
|
timepickerOpen = false;
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(private $rootScope, private $location) {
|
constructor(private $rootScope, private $location) {
|
||||||
@ -22,6 +23,8 @@ export class KeybindingSrv {
|
|||||||
|
|
||||||
this.setupGlobal();
|
this.setupGlobal();
|
||||||
appEvents.on('show-modal', () => (this.modalOpen = true));
|
appEvents.on('show-modal', () => (this.modalOpen = true));
|
||||||
|
$rootScope.onAppEvent('timepickerOpen', () => (this.timepickerOpen = true));
|
||||||
|
$rootScope.onAppEvent('timepickerClosed', () => (this.timepickerOpen = false));
|
||||||
}
|
}
|
||||||
|
|
||||||
setupGlobal() {
|
setupGlobal() {
|
||||||
@ -73,7 +76,12 @@ export class KeybindingSrv {
|
|||||||
appEvents.emit('hide-modal');
|
appEvents.emit('hide-modal');
|
||||||
|
|
||||||
if (!this.modalOpen) {
|
if (!this.modalOpen) {
|
||||||
this.$rootScope.appEvent('panel-change-view', { fullscreen: false, edit: false });
|
if (this.timepickerOpen) {
|
||||||
|
this.$rootScope.appEvent('closeTimepicker');
|
||||||
|
this.timepickerOpen = false;
|
||||||
|
} else {
|
||||||
|
this.$rootScope.appEvent('panel-change-view', { fullscreen: false, edit: false });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.modalOpen = false;
|
this.modalOpen = false;
|
||||||
}
|
}
|
||||||
|
@ -1,111 +0,0 @@
|
|||||||
define([
|
|
||||||
'angular',
|
|
||||||
'lodash',
|
|
||||||
'../core_module',
|
|
||||||
],
|
|
||||||
function (angular, _, coreModule) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
coreModule.default.service('uiSegmentSrv', function($sce, templateSrv) {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
function MetricSegment(options) {
|
|
||||||
if (options === '*' || options.value === '*') {
|
|
||||||
this.value = '*';
|
|
||||||
this.html = $sce.trustAsHtml('<i class="fa fa-asterisk"><i>');
|
|
||||||
this.type = options.type;
|
|
||||||
this.expandable = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_.isString(options)) {
|
|
||||||
this.value = options;
|
|
||||||
this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// temp hack to work around legacy inconsistency in segment model
|
|
||||||
this.text = options.value;
|
|
||||||
|
|
||||||
this.cssClass = options.cssClass;
|
|
||||||
this.custom = options.custom;
|
|
||||||
this.type = options.type;
|
|
||||||
this.fake = options.fake;
|
|
||||||
this.value = options.value;
|
|
||||||
this.selectMode = options.selectMode;
|
|
||||||
this.type = options.type;
|
|
||||||
this.expandable = options.expandable;
|
|
||||||
this.html = options.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getSegmentForValue = function(value, fallbackText) {
|
|
||||||
if (value) {
|
|
||||||
return this.newSegment(value);
|
|
||||||
} else {
|
|
||||||
return this.newSegment({value: fallbackText, fake: true});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.newSelectMeasurement = function() {
|
|
||||||
return new MetricSegment({value: 'select measurement', fake: true});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.newFake = function(text, type, cssClass) {
|
|
||||||
return new MetricSegment({value: text, fake: true, type: type, cssClass: cssClass});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.newSegment = function(options) {
|
|
||||||
return new MetricSegment(options);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.newKey = function(key) {
|
|
||||||
return new MetricSegment({value: key, type: 'key', cssClass: 'query-segment-key' });
|
|
||||||
};
|
|
||||||
|
|
||||||
this.newKeyValue = function(value) {
|
|
||||||
return new MetricSegment({value: value, type: 'value', cssClass: 'query-segment-value' });
|
|
||||||
};
|
|
||||||
|
|
||||||
this.newCondition = function(condition) {
|
|
||||||
return new MetricSegment({value: condition, type: 'condition', cssClass: 'query-keyword' });
|
|
||||||
};
|
|
||||||
|
|
||||||
this.newOperator = function(op) {
|
|
||||||
return new MetricSegment({value: op, type: 'operator', cssClass: 'query-segment-operator' });
|
|
||||||
};
|
|
||||||
|
|
||||||
this.newOperators = function(ops) {
|
|
||||||
return _.map(ops, function(op) {
|
|
||||||
return new MetricSegment({value: op, type: 'operator', cssClass: 'query-segment-operator' });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.transformToSegments = function(addTemplateVars, variableTypeFilter) {
|
|
||||||
return function(results) {
|
|
||||||
var segments = _.map(results, function(segment) {
|
|
||||||
return self.newSegment({value: segment.text, expandable: segment.expandable});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (addTemplateVars) {
|
|
||||||
_.each(templateSrv.variables, function(variable) {
|
|
||||||
if (variableTypeFilter === void 0 || variableTypeFilter === variable.type) {
|
|
||||||
segments.unshift(self.newSegment({ type: 'value', value: '$' + variable.name, expandable: true }));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return segments;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
this.newSelectMetric = function() {
|
|
||||||
return new MetricSegment({value: 'select metric', fake: true});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.newPlusButton = function() {
|
|
||||||
return new MetricSegment({fake: true, html: '<i class="fa fa-plus "></i>', type: 'plus-button', cssClass: 'query-part' });
|
|
||||||
};
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
111
public/app/core/services/segment_srv.ts
Normal file
111
public/app/core/services/segment_srv.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import coreModule from '../core_module';
|
||||||
|
|
||||||
|
/** @ngInject */
|
||||||
|
export function uiSegmentSrv($sce, templateSrv) {
|
||||||
|
let self = this;
|
||||||
|
|
||||||
|
function MetricSegment(options) {
|
||||||
|
if (options === '*' || options.value === '*') {
|
||||||
|
this.value = '*';
|
||||||
|
this.html = $sce.trustAsHtml('<i class="fa fa-asterisk"><i>');
|
||||||
|
this.type = options.type;
|
||||||
|
this.expandable = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_.isString(options)) {
|
||||||
|
this.value = options;
|
||||||
|
this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// temp hack to work around legacy inconsistency in segment model
|
||||||
|
this.text = options.value;
|
||||||
|
|
||||||
|
this.cssClass = options.cssClass;
|
||||||
|
this.custom = options.custom;
|
||||||
|
this.type = options.type;
|
||||||
|
this.fake = options.fake;
|
||||||
|
this.value = options.value;
|
||||||
|
this.selectMode = options.selectMode;
|
||||||
|
this.type = options.type;
|
||||||
|
this.expandable = options.expandable;
|
||||||
|
this.html = options.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getSegmentForValue = function(value, fallbackText) {
|
||||||
|
if (value) {
|
||||||
|
return this.newSegment(value);
|
||||||
|
} else {
|
||||||
|
return this.newSegment({ value: fallbackText, fake: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.newSelectMeasurement = function() {
|
||||||
|
return new MetricSegment({ value: 'select measurement', fake: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
this.newFake = function(text, type, cssClass) {
|
||||||
|
return new MetricSegment({ value: text, fake: true, type: type, cssClass: cssClass });
|
||||||
|
};
|
||||||
|
|
||||||
|
this.newSegment = function(options) {
|
||||||
|
return new MetricSegment(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.newKey = function(key) {
|
||||||
|
return new MetricSegment({ value: key, type: 'key', cssClass: 'query-segment-key' });
|
||||||
|
};
|
||||||
|
|
||||||
|
this.newKeyValue = function(value) {
|
||||||
|
return new MetricSegment({ value: value, type: 'value', cssClass: 'query-segment-value' });
|
||||||
|
};
|
||||||
|
|
||||||
|
this.newCondition = function(condition) {
|
||||||
|
return new MetricSegment({ value: condition, type: 'condition', cssClass: 'query-keyword' });
|
||||||
|
};
|
||||||
|
|
||||||
|
this.newOperator = function(op) {
|
||||||
|
return new MetricSegment({ value: op, type: 'operator', cssClass: 'query-segment-operator' });
|
||||||
|
};
|
||||||
|
|
||||||
|
this.newOperators = function(ops) {
|
||||||
|
return _.map(ops, function(op) {
|
||||||
|
return new MetricSegment({ value: op, type: 'operator', cssClass: 'query-segment-operator' });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.transformToSegments = function(addTemplateVars, variableTypeFilter) {
|
||||||
|
return function(results) {
|
||||||
|
let segments = _.map(results, function(segment) {
|
||||||
|
return self.newSegment({ value: segment.text, expandable: segment.expandable });
|
||||||
|
});
|
||||||
|
|
||||||
|
if (addTemplateVars) {
|
||||||
|
_.each(templateSrv.variables, function(variable) {
|
||||||
|
if (variableTypeFilter === void 0 || variableTypeFilter === variable.type) {
|
||||||
|
segments.unshift(self.newSegment({ type: 'value', value: '$' + variable.name, expandable: true }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
this.newSelectMetric = function() {
|
||||||
|
return new MetricSegment({ value: 'select metric', fake: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
this.newPlusButton = function() {
|
||||||
|
return new MetricSegment({
|
||||||
|
fake: true,
|
||||||
|
html: '<i class="fa fa-plus "></i>',
|
||||||
|
type: 'plus-button',
|
||||||
|
cssClass: 'query-part',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
coreModule.service('uiSegmentSrv', uiSegmentSrv);
|
@ -1,9 +1,5 @@
|
|||||||
export class ThresholdMapper {
|
export class ThresholdMapper {
|
||||||
static alertToGraphThresholds(panel) {
|
static alertToGraphThresholds(panel) {
|
||||||
if (panel.type !== 'graph') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < panel.alert.conditions.length; i++) {
|
for (var i = 0; i < panel.alert.conditions.length; i++) {
|
||||||
let condition = panel.alert.conditions[i];
|
let condition = panel.alert.conditions[i];
|
||||||
if (condition.type !== 'query') {
|
if (condition.type !== 'query') {
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
define([
|
|
||||||
'./panellinks/module',
|
|
||||||
'./dashlinks/module',
|
|
||||||
'./annotations/all',
|
|
||||||
'./templating/all',
|
|
||||||
'./plugins/all',
|
|
||||||
'./dashboard/all',
|
|
||||||
'./playlist/all',
|
|
||||||
'./snapshot/all',
|
|
||||||
'./panel/all',
|
|
||||||
'./org/all',
|
|
||||||
'./admin/admin',
|
|
||||||
'./alerting/all',
|
|
||||||
'./styleguide/styleguide',
|
|
||||||
], function () {});
|
|
13
public/app/features/all.ts
Normal file
13
public/app/features/all.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import './panellinks/module';
|
||||||
|
import './dashlinks/module';
|
||||||
|
import './annotations/all';
|
||||||
|
import './templating/all';
|
||||||
|
import './plugins/all';
|
||||||
|
import './dashboard/all';
|
||||||
|
import './playlist/all';
|
||||||
|
import './snapshot/all';
|
||||||
|
import './panel/all';
|
||||||
|
import './org/all';
|
||||||
|
import './admin/admin';
|
||||||
|
import './alerting/all';
|
||||||
|
import './styleguide/styleguide';
|
@ -22,7 +22,6 @@ export class TimePickerCtrl {
|
|||||||
refresh: any;
|
refresh: any;
|
||||||
isUtc: boolean;
|
isUtc: boolean;
|
||||||
firstDayOfWeek: number;
|
firstDayOfWeek: number;
|
||||||
closeDropdown: any;
|
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
@ -32,6 +31,7 @@ export class TimePickerCtrl {
|
|||||||
$rootScope.onAppEvent('shift-time-forward', () => this.move(1), $scope);
|
$rootScope.onAppEvent('shift-time-forward', () => this.move(1), $scope);
|
||||||
$rootScope.onAppEvent('shift-time-backward', () => this.move(-1), $scope);
|
$rootScope.onAppEvent('shift-time-backward', () => this.move(-1), $scope);
|
||||||
$rootScope.onAppEvent('refresh', this.onRefresh.bind(this), $scope);
|
$rootScope.onAppEvent('refresh', this.onRefresh.bind(this), $scope);
|
||||||
|
$rootScope.onAppEvent('closeTimepicker', this.openDropdown.bind(this), $scope);
|
||||||
|
|
||||||
// init options
|
// init options
|
||||||
this.panel = this.dashboard.timepicker;
|
this.panel = this.dashboard.timepicker;
|
||||||
@ -96,7 +96,7 @@ export class TimePickerCtrl {
|
|||||||
|
|
||||||
openDropdown() {
|
openDropdown() {
|
||||||
if (this.isOpen) {
|
if (this.isOpen) {
|
||||||
this.isOpen = false;
|
this.closeDropdown();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,6 +112,12 @@ export class TimePickerCtrl {
|
|||||||
|
|
||||||
this.refresh.options.unshift({ text: 'off' });
|
this.refresh.options.unshift({ text: 'off' });
|
||||||
this.isOpen = true;
|
this.isOpen = true;
|
||||||
|
this.$rootScope.appEvent('timepickerOpen');
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDropdown() {
|
||||||
|
this.isOpen = false;
|
||||||
|
this.$rootScope.appEvent('timepickerClosed');
|
||||||
}
|
}
|
||||||
|
|
||||||
applyCustom() {
|
applyCustom() {
|
||||||
@ -120,7 +126,7 @@ export class TimePickerCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.timeSrv.setTime(this.editTimeRaw);
|
this.timeSrv.setTime(this.editTimeRaw);
|
||||||
this.isOpen = false;
|
this.closeDropdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
absoluteFromChanged() {
|
absoluteFromChanged() {
|
||||||
@ -143,7 +149,7 @@ export class TimePickerCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.timeSrv.setTime(range);
|
this.timeSrv.setTime(range);
|
||||||
this.isOpen = false;
|
this.closeDropdown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,12 +35,12 @@ export class Tracker {
|
|||||||
|
|
||||||
$window.onbeforeunload = () => {
|
$window.onbeforeunload = () => {
|
||||||
if (this.ignoreChanges()) {
|
if (this.ignoreChanges()) {
|
||||||
return null;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (this.hasChanges()) {
|
if (this.hasChanges()) {
|
||||||
return 'There are unsaved changes to this dashboard';
|
return 'There are unsaved changes to this dashboard';
|
||||||
}
|
}
|
||||||
return null;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
scope.$on('$locationChangeStart', (event, next) => {
|
scope.$on('$locationChangeStart', (event, next) => {
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
define([
|
|
||||||
'./panel_header',
|
|
||||||
'./panel_directive',
|
|
||||||
'./solo_panel_ctrl',
|
|
||||||
'./query_ctrl',
|
|
||||||
'./panel_editor_tab',
|
|
||||||
'./query_editor_row',
|
|
||||||
'./query_troubleshooter',
|
|
||||||
], function () {});
|
|
7
public/app/features/panel/all.ts
Normal file
7
public/app/features/panel/all.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import './panel_header';
|
||||||
|
import './panel_directive';
|
||||||
|
import './solo_panel_ctrl';
|
||||||
|
import './query_ctrl';
|
||||||
|
import './panel_editor_tab';
|
||||||
|
import './query_editor_row';
|
||||||
|
import './query_troubleshooter';
|
@ -1,7 +0,0 @@
|
|||||||
define([
|
|
||||||
'./playlists_ctrl',
|
|
||||||
'./playlist_search',
|
|
||||||
'./playlist_srv',
|
|
||||||
'./playlist_edit_ctrl',
|
|
||||||
'./playlist_routes'
|
|
||||||
], function () {});
|
|
5
public/app/features/playlist/all.ts
Normal file
5
public/app/features/playlist/all.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import './playlists_ctrl';
|
||||||
|
import './playlist_search';
|
||||||
|
import './playlist_srv';
|
||||||
|
import './playlist_edit_ctrl';
|
||||||
|
import './playlist_routes';
|
@ -1,39 +0,0 @@
|
|||||||
define([
|
|
||||||
'angular',
|
|
||||||
'lodash'
|
|
||||||
],
|
|
||||||
function (angular) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var module = angular.module('grafana.routes');
|
|
||||||
|
|
||||||
module.config(function($routeProvider) {
|
|
||||||
$routeProvider
|
|
||||||
.when('/playlists', {
|
|
||||||
templateUrl: 'public/app/features/playlist/partials/playlists.html',
|
|
||||||
controllerAs: 'ctrl',
|
|
||||||
controller : 'PlaylistsCtrl'
|
|
||||||
})
|
|
||||||
.when('/playlists/create', {
|
|
||||||
templateUrl: 'public/app/features/playlist/partials/playlist.html',
|
|
||||||
controllerAs: 'ctrl',
|
|
||||||
controller : 'PlaylistEditCtrl'
|
|
||||||
})
|
|
||||||
.when('/playlists/edit/:id', {
|
|
||||||
templateUrl: 'public/app/features/playlist/partials/playlist.html',
|
|
||||||
controllerAs: 'ctrl',
|
|
||||||
controller : 'PlaylistEditCtrl'
|
|
||||||
})
|
|
||||||
.when('/playlists/play/:id', {
|
|
||||||
templateUrl: 'public/app/features/playlist/partials/playlists.html',
|
|
||||||
controllerAs: 'ctrl',
|
|
||||||
controller : 'PlaylistsCtrl',
|
|
||||||
resolve: {
|
|
||||||
init: function(playlistSrv, $route) {
|
|
||||||
var playlistId = $route.current.params.id;
|
|
||||||
playlistSrv.start(playlistId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
33
public/app/features/playlist/playlist_routes.ts
Normal file
33
public/app/features/playlist/playlist_routes.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import angular from 'angular';
|
||||||
|
|
||||||
|
function grafanaRoutes($routeProvider) {
|
||||||
|
$routeProvider
|
||||||
|
.when('/playlists', {
|
||||||
|
templateUrl: 'public/app/features/playlist/partials/playlists.html',
|
||||||
|
controllerAs: 'ctrl',
|
||||||
|
controller: 'PlaylistsCtrl',
|
||||||
|
})
|
||||||
|
.when('/playlists/create', {
|
||||||
|
templateUrl: 'public/app/features/playlist/partials/playlist.html',
|
||||||
|
controllerAs: 'ctrl',
|
||||||
|
controller: 'PlaylistEditCtrl',
|
||||||
|
})
|
||||||
|
.when('/playlists/edit/:id', {
|
||||||
|
templateUrl: 'public/app/features/playlist/partials/playlist.html',
|
||||||
|
controllerAs: 'ctrl',
|
||||||
|
controller: 'PlaylistEditCtrl',
|
||||||
|
})
|
||||||
|
.when('/playlists/play/:id', {
|
||||||
|
templateUrl: 'public/app/features/playlist/partials/playlists.html',
|
||||||
|
controllerAs: 'ctrl',
|
||||||
|
controller: 'PlaylistsCtrl',
|
||||||
|
resolve: {
|
||||||
|
init: function(playlistSrv, $route) {
|
||||||
|
let playlistId = $route.current.params.id;
|
||||||
|
playlistSrv.start(playlistId);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module('grafana.routes').config(grafanaRoutes);
|
@ -23,6 +23,8 @@ export class VariableEditorCtrl {
|
|||||||
{ value: 2, text: 'Alphabetical (desc)' },
|
{ value: 2, text: 'Alphabetical (desc)' },
|
||||||
{ value: 3, text: 'Numerical (asc)' },
|
{ value: 3, text: 'Numerical (asc)' },
|
||||||
{ value: 4, text: 'Numerical (desc)' },
|
{ value: 4, text: 'Numerical (desc)' },
|
||||||
|
{ value: 5, text: 'Alphabetical (case-insensitive, asc)' },
|
||||||
|
{ value: 6, text: 'Alphabetical (case-insensitive, desc)' },
|
||||||
];
|
];
|
||||||
|
|
||||||
$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' }];
|
||||||
|
@ -197,6 +197,8 @@ export class QueryVariable implements Variable {
|
|||||||
return parseInt(matches[1], 10);
|
return parseInt(matches[1], 10);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else if (sortType === 3) {
|
||||||
|
options = _.sortBy(options, opt => { return _.toLower(opt.text); });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reverseSort) {
|
if (reverseSort) {
|
||||||
|
@ -40,11 +40,11 @@ describe('QueryVariable', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('can convert and sort metric names', () => {
|
describe('can convert and sort metric names', () => {
|
||||||
var variable = new QueryVariable({}, null, null, null, null);
|
const variable = new QueryVariable({}, null, null, null, null);
|
||||||
variable.sort = 3; // Numerical (asc)
|
let input;
|
||||||
|
|
||||||
describe('can sort a mixed array of metric variables', () => {
|
beforeEach(() => {
|
||||||
var input = [
|
input = [
|
||||||
{ text: '0', value: '0' },
|
{ text: '0', value: '0' },
|
||||||
{ text: '1', value: '1' },
|
{ text: '1', value: '1' },
|
||||||
{ text: null, value: 3 },
|
{ text: null, value: 3 },
|
||||||
@ -58,11 +58,18 @@ describe('QueryVariable', () => {
|
|||||||
{ text: '', value: undefined },
|
{ text: '', value: undefined },
|
||||||
{ text: undefined, value: '' },
|
{ text: undefined, value: '' },
|
||||||
];
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('can sort a mixed array of metric variables in numeric order', () => {
|
||||||
|
let result;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
variable.sort = 3; // Numerical (asc)
|
||||||
|
result = variable.metricNamesToVariableValues(input);
|
||||||
|
});
|
||||||
|
|
||||||
var result = variable.metricNamesToVariableValues(input);
|
|
||||||
it('should return in same order', () => {
|
it('should return in same order', () => {
|
||||||
var i = 0;
|
var i = 0;
|
||||||
|
|
||||||
expect(result.length).toBe(11);
|
expect(result.length).toBe(11);
|
||||||
expect(result[i++].text).toBe('');
|
expect(result[i++].text).toBe('');
|
||||||
expect(result[i++].text).toBe('0');
|
expect(result[i++].text).toBe('0');
|
||||||
@ -73,5 +80,27 @@ describe('QueryVariable', () => {
|
|||||||
expect(result[i++].text).toBe('6');
|
expect(result[i++].text).toBe('6');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('can sort a mixed array of metric variables in alphabetical order', () => {
|
||||||
|
let result;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
variable.sort = 5; // Alphabetical CI (asc)
|
||||||
|
result = variable.metricNamesToVariableValues(input);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return in same order', () => {
|
||||||
|
var i = 0;
|
||||||
|
console.log(result);
|
||||||
|
expect(result.length).toBe(11);
|
||||||
|
expect(result[i++].text).toBe('');
|
||||||
|
expect(result[i++].text).toBe('0');
|
||||||
|
expect(result[i++].text).toBe('1');
|
||||||
|
expect(result[i++].text).toBe('10');
|
||||||
|
expect(result[i++].text).toBe('3');
|
||||||
|
expect(result[i++].text).toBe('4');
|
||||||
|
expect(result[i++].text).toBe('5');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -4,6 +4,7 @@ import $ from 'jquery';
|
|||||||
import rst2html from 'rst2html';
|
import rst2html from 'rst2html';
|
||||||
import Drop from 'tether-drop';
|
import Drop from 'tether-drop';
|
||||||
|
|
||||||
|
/** @ngInject */
|
||||||
export function graphiteAddFunc($compile) {
|
export function graphiteAddFunc($compile) {
|
||||||
const inputTemplate =
|
const inputTemplate =
|
||||||
'<input type="text"' + ' class="gf-form-input"' + ' spellcheck="false" style="display:none"></input>';
|
'<input type="text"' + ' class="gf-form-input"' + ' spellcheck="false" style="display:none"></input>';
|
||||||
|
@ -3,6 +3,7 @@ import _ from 'lodash';
|
|||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import rst2html from 'rst2html';
|
import rst2html from 'rst2html';
|
||||||
|
|
||||||
|
/** @ngInject */
|
||||||
export function graphiteFuncEditor($compile, templateSrv, popoverSrv) {
|
export function graphiteFuncEditor($compile, templateSrv, popoverSrv) {
|
||||||
const funcSpanTemplate = '<a ng-click="">{{func.def.name}}</a><span>(</span>';
|
const funcSpanTemplate = '<a ng-click="">{{func.def.name}}</a><span>(</span>';
|
||||||
const paramTemplate =
|
const paramTemplate =
|
||||||
|
@ -28,7 +28,7 @@ An annotation is an event that is overlayed on top of graphs. The query can have
|
|||||||
Macros:
|
Macros:
|
||||||
- $__time(column) -> column AS time
|
- $__time(column) -> column AS time
|
||||||
- $__timeEpoch(column) -> DATEDIFF(second, '1970-01-01', column) AS time
|
- $__timeEpoch(column) -> DATEDIFF(second, '1970-01-01', column) AS time
|
||||||
- $__timeFilter(column) -> column >= DATEADD(s, 18446744066914186738, '1970-01-01') AND column &t;= DATEADD(s, 18446744066914187038, '1970-01-01')
|
- $__timeFilter(column) -> column >= DATEADD(s, 18446744066914186738, '1970-01-01') AND column <= DATEADD(s, 18446744066914187038, '1970-01-01')
|
||||||
- $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877
|
- $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877
|
||||||
|
|
||||||
Or build your own conditionals using these macros which just return the values:
|
Or build your own conditionals using these macros which just return the values:
|
||||||
|
@ -49,7 +49,7 @@ Table:
|
|||||||
Macros:
|
Macros:
|
||||||
- $__time(column) -> column AS time
|
- $__time(column) -> column AS time
|
||||||
- $__timeEpoch(column) -> DATEDIFF(second, '1970-01-01', column) AS time
|
- $__timeEpoch(column) -> DATEDIFF(second, '1970-01-01', column) AS time
|
||||||
- $__timeFilter(column) -> column >= DATEADD(s, 18446744066914186738, '1970-01-01') AND column &t;= DATEADD(s, 18446744066914187038, '1970-01-01')
|
- $__timeFilter(column) -> column >= DATEADD(s, 18446744066914186738, '1970-01-01') AND column <= DATEADD(s, 18446744066914187038, '1970-01-01')
|
||||||
- $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877
|
- $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877
|
||||||
- $__timeGroup(column, '5m'[, fillvalue]) -> CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300. Providing a <i>fillValue</i> of <i>NULL</i> or floating value will automatically fill empty series in timerange with that value.
|
- $__timeGroup(column, '5m'[, fillvalue]) -> CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300. Providing a <i>fillValue</i> of <i>NULL</i> or floating value will automatically fill empty series in timerange with that value.
|
||||||
|
|
||||||
|
@ -6,8 +6,12 @@ import * as dateMath from 'app/core/utils/datemath';
|
|||||||
import PrometheusMetricFindQuery from './metric_find_query';
|
import PrometheusMetricFindQuery from './metric_find_query';
|
||||||
import { ResultTransformer } from './result_transformer';
|
import { ResultTransformer } from './result_transformer';
|
||||||
|
|
||||||
function prometheusSpecialRegexEscape(value) {
|
export function prometheusRegularEscape(value) {
|
||||||
return value.replace(/[\\^$*+?.()|[\]{}]/g, '\\\\$&');
|
return value.replace(/'/g, "\\\\'");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prometheusSpecialRegexEscape(value) {
|
||||||
|
return prometheusRegularEscape(value.replace(/\\/g, '\\\\\\\\').replace(/[$^*{}\[\]+?.()]/g, '\\\\$&'));
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PrometheusDatasource {
|
export class PrometheusDatasource {
|
||||||
@ -80,7 +84,7 @@ export class PrometheusDatasource {
|
|||||||
interpolateQueryExpr(value, variable, defaultFormatFn) {
|
interpolateQueryExpr(value, variable, defaultFormatFn) {
|
||||||
// if no multi or include all do not regexEscape
|
// if no multi or include all do not regexEscape
|
||||||
if (!variable.multi && !variable.includeAll) {
|
if (!variable.multi && !variable.includeAll) {
|
||||||
return value;
|
return prometheusRegularEscape(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import q from 'q';
|
import q from 'q';
|
||||||
import { PrometheusDatasource } from '../datasource';
|
import { PrometheusDatasource, prometheusSpecialRegexEscape, prometheusRegularEscape } from '../datasource';
|
||||||
|
|
||||||
describe('PrometheusDatasource', () => {
|
describe('PrometheusDatasource', () => {
|
||||||
let ctx: any = {};
|
let ctx: any = {};
|
||||||
@ -101,4 +101,41 @@ describe('PrometheusDatasource', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Prometheus regular escaping', function() {
|
||||||
|
it('should not escape simple string', function() {
|
||||||
|
expect(prometheusRegularEscape('cryptodepression')).toEqual('cryptodepression');
|
||||||
|
});
|
||||||
|
it("should escape '", function() {
|
||||||
|
expect(prometheusRegularEscape("looking'glass")).toEqual("looking\\\\'glass");
|
||||||
|
});
|
||||||
|
it('should escape multiple characters', function() {
|
||||||
|
expect(prometheusRegularEscape("'looking'glass'")).toEqual("\\\\'looking\\\\'glass\\\\'");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Prometheus regexes escaping', function() {
|
||||||
|
it('should not escape simple string', function() {
|
||||||
|
expect(prometheusSpecialRegexEscape('cryptodepression')).toEqual('cryptodepression');
|
||||||
|
});
|
||||||
|
it('should escape $^*+?.()\\', function() {
|
||||||
|
expect(prometheusSpecialRegexEscape("looking'glass")).toEqual("looking\\\\'glass");
|
||||||
|
expect(prometheusSpecialRegexEscape('looking{glass')).toEqual('looking\\\\{glass');
|
||||||
|
expect(prometheusSpecialRegexEscape('looking}glass')).toEqual('looking\\\\}glass');
|
||||||
|
expect(prometheusSpecialRegexEscape('looking[glass')).toEqual('looking\\\\[glass');
|
||||||
|
expect(prometheusSpecialRegexEscape('looking]glass')).toEqual('looking\\\\]glass');
|
||||||
|
expect(prometheusSpecialRegexEscape('looking$glass')).toEqual('looking\\\\$glass');
|
||||||
|
expect(prometheusSpecialRegexEscape('looking^glass')).toEqual('looking\\\\^glass');
|
||||||
|
expect(prometheusSpecialRegexEscape('looking*glass')).toEqual('looking\\\\*glass');
|
||||||
|
expect(prometheusSpecialRegexEscape('looking+glass')).toEqual('looking\\\\+glass');
|
||||||
|
expect(prometheusSpecialRegexEscape('looking?glass')).toEqual('looking\\\\?glass');
|
||||||
|
expect(prometheusSpecialRegexEscape('looking.glass')).toEqual('looking\\\\.glass');
|
||||||
|
expect(prometheusSpecialRegexEscape('looking(glass')).toEqual('looking\\\\(glass');
|
||||||
|
expect(prometheusSpecialRegexEscape('looking)glass')).toEqual('looking\\\\)glass');
|
||||||
|
expect(prometheusSpecialRegexEscape('looking\\glass')).toEqual('looking\\\\\\\\glass');
|
||||||
|
});
|
||||||
|
it('should escape multiple special characters', function() {
|
||||||
|
expect(prometheusSpecialRegexEscape('+looking$glass?')).toEqual('\\\\+looking\\\\$glass\\\\?');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,292 +0,0 @@
|
|||||||
define([
|
|
||||||
'jquery',
|
|
||||||
'app/core/core',
|
|
||||||
],
|
|
||||||
function ($, core) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var appEvents = core.appEvents;
|
|
||||||
|
|
||||||
function GraphTooltip(elem, dashboard, scope, getSeriesFn) {
|
|
||||||
var self = this;
|
|
||||||
var ctrl = scope.ctrl;
|
|
||||||
var panel = ctrl.panel;
|
|
||||||
|
|
||||||
var $tooltip = $('<div class="graph-tooltip">');
|
|
||||||
|
|
||||||
this.destroy = function() {
|
|
||||||
$tooltip.remove();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.findHoverIndexFromDataPoints = function(posX, series, last) {
|
|
||||||
var ps = series.datapoints.pointsize;
|
|
||||||
var initial = last*ps;
|
|
||||||
var len = series.datapoints.points.length;
|
|
||||||
for (var j = initial; j < len; j += ps) {
|
|
||||||
// Special case of a non stepped line, highlight the very last point just before a null point
|
|
||||||
if ((!series.lines.steps && series.datapoints.points[initial] != null && series.datapoints.points[j] == null)
|
|
||||||
//normal case
|
|
||||||
|| series.datapoints.points[j] > posX) {
|
|
||||||
return Math.max(j - ps, 0)/ps;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return j/ps - 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.findHoverIndexFromData = function(posX, series) {
|
|
||||||
var lower = 0;
|
|
||||||
var upper = series.data.length - 1;
|
|
||||||
var middle;
|
|
||||||
while (true) {
|
|
||||||
if (lower > upper) {
|
|
||||||
return Math.max(upper, 0);
|
|
||||||
}
|
|
||||||
middle = Math.floor((lower + upper) / 2);
|
|
||||||
if (series.data[middle][0] === posX) {
|
|
||||||
return middle;
|
|
||||||
} else if (series.data[middle][0] < posX) {
|
|
||||||
lower = middle + 1;
|
|
||||||
} else {
|
|
||||||
upper = middle - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.renderAndShow = function(absoluteTime, innerHtml, pos, xMode) {
|
|
||||||
if (xMode === 'time') {
|
|
||||||
innerHtml = '<div class="graph-tooltip-time">'+ absoluteTime + '</div>' + innerHtml;
|
|
||||||
}
|
|
||||||
$tooltip.html(innerHtml).place_tt(pos.pageX + 20, pos.pageY);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.getMultiSeriesPlotHoverInfo = function(seriesList, pos) {
|
|
||||||
var value, i, series, hoverIndex, hoverDistance, pointTime, yaxis;
|
|
||||||
// 3 sub-arrays, 1st for hidden series, 2nd for left yaxis, 3rd for right yaxis.
|
|
||||||
var results = [[],[],[]];
|
|
||||||
|
|
||||||
//now we know the current X (j) position for X and Y values
|
|
||||||
var last_value = 0; //needed for stacked values
|
|
||||||
|
|
||||||
var minDistance, minTime;
|
|
||||||
|
|
||||||
for (i = 0; i < seriesList.length; i++) {
|
|
||||||
series = seriesList[i];
|
|
||||||
|
|
||||||
if (!series.data.length || (panel.legend.hideEmpty && series.allIsNull)) {
|
|
||||||
// Init value so that it does not brake series sorting
|
|
||||||
results[0].push({ hidden: true, value: 0 });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!series.data.length || (panel.legend.hideZero && series.allIsZero)) {
|
|
||||||
// Init value so that it does not brake series sorting
|
|
||||||
results[0].push({ hidden: true, value: 0 });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
hoverIndex = this.findHoverIndexFromData(pos.x, series);
|
|
||||||
hoverDistance = pos.x - series.data[hoverIndex][0];
|
|
||||||
pointTime = series.data[hoverIndex][0];
|
|
||||||
|
|
||||||
// Take the closest point before the cursor, or if it does not exist, the closest after
|
|
||||||
if (! minDistance
|
|
||||||
|| (hoverDistance >=0 && (hoverDistance < minDistance || minDistance < 0))
|
|
||||||
|| (hoverDistance < 0 && hoverDistance > minDistance)) {
|
|
||||||
minDistance = hoverDistance;
|
|
||||||
minTime = pointTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (series.stack) {
|
|
||||||
if (panel.tooltip.value_type === 'individual') {
|
|
||||||
value = series.data[hoverIndex][1];
|
|
||||||
} else if (!series.stack) {
|
|
||||||
value = series.data[hoverIndex][1];
|
|
||||||
} else {
|
|
||||||
last_value += series.data[hoverIndex][1];
|
|
||||||
value = last_value;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
value = series.data[hoverIndex][1];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Highlighting multiple Points depending on the plot type
|
|
||||||
if (series.lines.steps || series.stack) {
|
|
||||||
// stacked and steppedLine plots can have series with different length.
|
|
||||||
// Stacked series can increase its length on each new stacked serie if null points found,
|
|
||||||
// to speed the index search we begin always on the last found hoverIndex.
|
|
||||||
hoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Be sure we have a yaxis so that it does not brake series sorting
|
|
||||||
yaxis = 0;
|
|
||||||
if (series.yaxis) {
|
|
||||||
yaxis = series.yaxis.n;
|
|
||||||
}
|
|
||||||
|
|
||||||
results[yaxis].push({
|
|
||||||
value: value,
|
|
||||||
hoverIndex: hoverIndex,
|
|
||||||
color: series.color,
|
|
||||||
label: series.aliasEscaped,
|
|
||||||
time: pointTime,
|
|
||||||
distance: hoverDistance,
|
|
||||||
index: i
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contat the 3 sub-arrays
|
|
||||||
results = results[0].concat(results[1],results[2]);
|
|
||||||
|
|
||||||
// Time of the point closer to pointer
|
|
||||||
results.time = minTime;
|
|
||||||
|
|
||||||
return results;
|
|
||||||
};
|
|
||||||
|
|
||||||
elem.mouseleave(function () {
|
|
||||||
if (panel.tooltip.shared) {
|
|
||||||
var plot = elem.data().plot;
|
|
||||||
if (plot) {
|
|
||||||
$tooltip.detach();
|
|
||||||
plot.unhighlight();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
appEvents.emit('graph-hover-clear');
|
|
||||||
});
|
|
||||||
|
|
||||||
elem.bind("plothover", function (event, pos, item) {
|
|
||||||
self.show(pos, item);
|
|
||||||
|
|
||||||
// broadcast to other graph panels that we are hovering!
|
|
||||||
pos.panelRelY = (pos.pageY - elem.offset().top) / elem.height();
|
|
||||||
appEvents.emit('graph-hover', {pos: pos, panel: panel});
|
|
||||||
});
|
|
||||||
|
|
||||||
elem.bind("plotclick", function (event, pos, item) {
|
|
||||||
appEvents.emit('graph-click', {pos: pos, panel: panel, item: item});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.clear = function(plot) {
|
|
||||||
$tooltip.detach();
|
|
||||||
plot.clearCrosshair();
|
|
||||||
plot.unhighlight();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.show = function(pos, item) {
|
|
||||||
var plot = elem.data().plot;
|
|
||||||
var plotData = plot.getData();
|
|
||||||
var xAxes = plot.getXAxes();
|
|
||||||
var xMode = xAxes[0].options.mode;
|
|
||||||
var seriesList = getSeriesFn();
|
|
||||||
var allSeriesMode = panel.tooltip.shared;
|
|
||||||
var group, value, absoluteTime, hoverInfo, i, series, seriesHtml, tooltipFormat;
|
|
||||||
|
|
||||||
// if panelRelY is defined another panel wants us to show a tooltip
|
|
||||||
// get pageX from position on x axis and pageY from relative position in original panel
|
|
||||||
if (pos.panelRelY) {
|
|
||||||
var pointOffset = plot.pointOffset({x: pos.x});
|
|
||||||
if (Number.isNaN(pointOffset.left) || pointOffset.left < 0 || pointOffset.left > elem.width()) {
|
|
||||||
self.clear(plot);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
pos.pageX = elem.offset().left + pointOffset.left;
|
|
||||||
pos.pageY = elem.offset().top + elem.height() * pos.panelRelY;
|
|
||||||
var isVisible = pos.pageY >= $(window).scrollTop() && pos.pageY <= $(window).innerHeight() + $(window).scrollTop();
|
|
||||||
if (!isVisible) {
|
|
||||||
self.clear(plot);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
plot.setCrosshair(pos);
|
|
||||||
allSeriesMode = true;
|
|
||||||
|
|
||||||
if (dashboard.sharedCrosshairModeOnly()) {
|
|
||||||
// if only crosshair mode we are done
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (seriesList.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (seriesList[0].hasMsResolution) {
|
|
||||||
tooltipFormat = 'YYYY-MM-DD HH:mm:ss.SSS';
|
|
||||||
} else {
|
|
||||||
tooltipFormat = 'YYYY-MM-DD HH:mm:ss';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allSeriesMode) {
|
|
||||||
plot.unhighlight();
|
|
||||||
|
|
||||||
var seriesHoverInfo = self.getMultiSeriesPlotHoverInfo(plotData, pos);
|
|
||||||
|
|
||||||
seriesHtml = '';
|
|
||||||
|
|
||||||
absoluteTime = dashboard.formatDate(seriesHoverInfo.time, tooltipFormat);
|
|
||||||
|
|
||||||
// Dynamically reorder the hovercard for the current time point if the
|
|
||||||
// option is enabled.
|
|
||||||
if (panel.tooltip.sort === 2) {
|
|
||||||
seriesHoverInfo.sort(function(a, b) {
|
|
||||||
return b.value - a.value;
|
|
||||||
});
|
|
||||||
} else if (panel.tooltip.sort === 1) {
|
|
||||||
seriesHoverInfo.sort(function(a, b) {
|
|
||||||
return a.value - b.value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i = 0; i < seriesHoverInfo.length; i++) {
|
|
||||||
hoverInfo = seriesHoverInfo[i];
|
|
||||||
|
|
||||||
if (hoverInfo.hidden) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var highlightClass = '';
|
|
||||||
if (item && hoverInfo.index === item.seriesIndex) {
|
|
||||||
highlightClass = 'graph-tooltip-list-item--highlight';
|
|
||||||
}
|
|
||||||
|
|
||||||
series = seriesList[hoverInfo.index];
|
|
||||||
|
|
||||||
value = series.formatValue(hoverInfo.value);
|
|
||||||
|
|
||||||
seriesHtml += '<div class="graph-tooltip-list-item ' + highlightClass + '"><div class="graph-tooltip-series-name">';
|
|
||||||
seriesHtml += '<i class="fa fa-minus" style="color:' + hoverInfo.color +';"></i> ' + hoverInfo.label + ':</div>';
|
|
||||||
seriesHtml += '<div class="graph-tooltip-value">' + value + '</div></div>';
|
|
||||||
plot.highlight(hoverInfo.index, hoverInfo.hoverIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.renderAndShow(absoluteTime, seriesHtml, pos, xMode);
|
|
||||||
}
|
|
||||||
// single series tooltip
|
|
||||||
else if (item) {
|
|
||||||
series = seriesList[item.seriesIndex];
|
|
||||||
group = '<div class="graph-tooltip-list-item"><div class="graph-tooltip-series-name">';
|
|
||||||
group += '<i class="fa fa-minus" style="color:' + item.series.color +';"></i> ' + series.aliasEscaped + ':</div>';
|
|
||||||
|
|
||||||
if (panel.stack && panel.tooltip.value_type === 'individual') {
|
|
||||||
value = item.datapoint[1] - item.datapoint[2];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
value = item.datapoint[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
value = series.formatValue(value);
|
|
||||||
|
|
||||||
absoluteTime = dashboard.formatDate(item.datapoint[0], tooltipFormat);
|
|
||||||
|
|
||||||
group += '<div class="graph-tooltip-value">' + value + '</div>';
|
|
||||||
|
|
||||||
self.renderAndShow(absoluteTime, group, pos, xMode);
|
|
||||||
}
|
|
||||||
// no hit
|
|
||||||
else {
|
|
||||||
$tooltip.detach();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return GraphTooltip;
|
|
||||||
});
|
|
289
public/app/plugins/panel/graph/graph_tooltip.ts
Normal file
289
public/app/plugins/panel/graph/graph_tooltip.ts
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
import $ from 'jquery';
|
||||||
|
import { appEvents } from 'app/core/core';
|
||||||
|
|
||||||
|
export default function GraphTooltip(elem, dashboard, scope, getSeriesFn) {
|
||||||
|
let self = this;
|
||||||
|
let ctrl = scope.ctrl;
|
||||||
|
let panel = ctrl.panel;
|
||||||
|
|
||||||
|
let $tooltip = $('<div class="graph-tooltip">');
|
||||||
|
|
||||||
|
this.destroy = function() {
|
||||||
|
$tooltip.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.findHoverIndexFromDataPoints = function(posX, series, last) {
|
||||||
|
let ps = series.datapoints.pointsize;
|
||||||
|
let initial = last * ps;
|
||||||
|
let len = series.datapoints.points.length;
|
||||||
|
let j;
|
||||||
|
for (j = initial; j < len; j += ps) {
|
||||||
|
// Special case of a non stepped line, highlight the very last point just before a null point
|
||||||
|
if (
|
||||||
|
(!series.lines.steps && series.datapoints.points[initial] != null && series.datapoints.points[j] == null) ||
|
||||||
|
//normal case
|
||||||
|
series.datapoints.points[j] > posX
|
||||||
|
) {
|
||||||
|
return Math.max(j - ps, 0) / ps;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return j / ps - 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.findHoverIndexFromData = function(posX, series) {
|
||||||
|
let lower = 0;
|
||||||
|
let upper = series.data.length - 1;
|
||||||
|
let middle;
|
||||||
|
while (true) {
|
||||||
|
if (lower > upper) {
|
||||||
|
return Math.max(upper, 0);
|
||||||
|
}
|
||||||
|
middle = Math.floor((lower + upper) / 2);
|
||||||
|
if (series.data[middle][0] === posX) {
|
||||||
|
return middle;
|
||||||
|
} else if (series.data[middle][0] < posX) {
|
||||||
|
lower = middle + 1;
|
||||||
|
} else {
|
||||||
|
upper = middle - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.renderAndShow = function(absoluteTime, innerHtml, pos, xMode) {
|
||||||
|
if (xMode === 'time') {
|
||||||
|
innerHtml = '<div class="graph-tooltip-time">' + absoluteTime + '</div>' + innerHtml;
|
||||||
|
}
|
||||||
|
$tooltip.html(innerHtml).place_tt(pos.pageX + 20, pos.pageY);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.getMultiSeriesPlotHoverInfo = function(seriesList, pos) {
|
||||||
|
let value, i, series, hoverIndex, hoverDistance, pointTime, yaxis;
|
||||||
|
// 3 sub-arrays, 1st for hidden series, 2nd for left yaxis, 3rd for right yaxis.
|
||||||
|
let results: any = [[], [], []];
|
||||||
|
|
||||||
|
//now we know the current X (j) position for X and Y values
|
||||||
|
let last_value = 0; //needed for stacked values
|
||||||
|
|
||||||
|
let minDistance, minTime;
|
||||||
|
|
||||||
|
for (i = 0; i < seriesList.length; i++) {
|
||||||
|
series = seriesList[i];
|
||||||
|
|
||||||
|
if (!series.data.length || (panel.legend.hideEmpty && series.allIsNull)) {
|
||||||
|
// Init value so that it does not brake series sorting
|
||||||
|
results[0].push({ hidden: true, value: 0 });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!series.data.length || (panel.legend.hideZero && series.allIsZero)) {
|
||||||
|
// Init value so that it does not brake series sorting
|
||||||
|
results[0].push({ hidden: true, value: 0 });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
hoverIndex = this.findHoverIndexFromData(pos.x, series);
|
||||||
|
hoverDistance = pos.x - series.data[hoverIndex][0];
|
||||||
|
pointTime = series.data[hoverIndex][0];
|
||||||
|
|
||||||
|
// Take the closest point before the cursor, or if it does not exist, the closest after
|
||||||
|
if (
|
||||||
|
!minDistance ||
|
||||||
|
(hoverDistance >= 0 && (hoverDistance < minDistance || minDistance < 0)) ||
|
||||||
|
(hoverDistance < 0 && hoverDistance > minDistance)
|
||||||
|
) {
|
||||||
|
minDistance = hoverDistance;
|
||||||
|
minTime = pointTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (series.stack) {
|
||||||
|
if (panel.tooltip.value_type === 'individual') {
|
||||||
|
value = series.data[hoverIndex][1];
|
||||||
|
} else if (!series.stack) {
|
||||||
|
value = series.data[hoverIndex][1];
|
||||||
|
} else {
|
||||||
|
last_value += series.data[hoverIndex][1];
|
||||||
|
value = last_value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value = series.data[hoverIndex][1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlighting multiple Points depending on the plot type
|
||||||
|
if (series.lines.steps || series.stack) {
|
||||||
|
// stacked and steppedLine plots can have series with different length.
|
||||||
|
// Stacked series can increase its length on each new stacked serie if null points found,
|
||||||
|
// to speed the index search we begin always on the last found hoverIndex.
|
||||||
|
hoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Be sure we have a yaxis so that it does not brake series sorting
|
||||||
|
yaxis = 0;
|
||||||
|
if (series.yaxis) {
|
||||||
|
yaxis = series.yaxis.n;
|
||||||
|
}
|
||||||
|
|
||||||
|
results[yaxis].push({
|
||||||
|
value: value,
|
||||||
|
hoverIndex: hoverIndex,
|
||||||
|
color: series.color,
|
||||||
|
label: series.aliasEscaped,
|
||||||
|
time: pointTime,
|
||||||
|
distance: hoverDistance,
|
||||||
|
index: i,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contat the 3 sub-arrays
|
||||||
|
results = results[0].concat(results[1], results[2]);
|
||||||
|
|
||||||
|
// Time of the point closer to pointer
|
||||||
|
results.time = minTime;
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
elem.mouseleave(function() {
|
||||||
|
if (panel.tooltip.shared) {
|
||||||
|
let plot = elem.data().plot;
|
||||||
|
if (plot) {
|
||||||
|
$tooltip.detach();
|
||||||
|
plot.unhighlight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appEvents.emit('graph-hover-clear');
|
||||||
|
});
|
||||||
|
|
||||||
|
elem.bind('plothover', function(event, pos, item) {
|
||||||
|
self.show(pos, item);
|
||||||
|
|
||||||
|
// broadcast to other graph panels that we are hovering!
|
||||||
|
pos.panelRelY = (pos.pageY - elem.offset().top) / elem.height();
|
||||||
|
appEvents.emit('graph-hover', { pos: pos, panel: panel });
|
||||||
|
});
|
||||||
|
|
||||||
|
elem.bind('plotclick', function(event, pos, item) {
|
||||||
|
appEvents.emit('graph-click', { pos: pos, panel: panel, item: item });
|
||||||
|
});
|
||||||
|
|
||||||
|
this.clear = function(plot) {
|
||||||
|
$tooltip.detach();
|
||||||
|
plot.clearCrosshair();
|
||||||
|
plot.unhighlight();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.show = function(pos, item) {
|
||||||
|
let plot = elem.data().plot;
|
||||||
|
let plotData = plot.getData();
|
||||||
|
let xAxes = plot.getXAxes();
|
||||||
|
let xMode = xAxes[0].options.mode;
|
||||||
|
let seriesList = getSeriesFn();
|
||||||
|
let allSeriesMode = panel.tooltip.shared;
|
||||||
|
let group, value, absoluteTime, hoverInfo, i, series, seriesHtml, tooltipFormat;
|
||||||
|
|
||||||
|
// if panelRelY is defined another panel wants us to show a tooltip
|
||||||
|
// get pageX from position on x axis and pageY from relative position in original panel
|
||||||
|
if (pos.panelRelY) {
|
||||||
|
let pointOffset = plot.pointOffset({ x: pos.x });
|
||||||
|
if (Number.isNaN(pointOffset.left) || pointOffset.left < 0 || pointOffset.left > elem.width()) {
|
||||||
|
self.clear(plot);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pos.pageX = elem.offset().left + pointOffset.left;
|
||||||
|
pos.pageY = elem.offset().top + elem.height() * pos.panelRelY;
|
||||||
|
let isVisible =
|
||||||
|
pos.pageY >= $(window).scrollTop() && pos.pageY <= $(window).innerHeight() + $(window).scrollTop();
|
||||||
|
if (!isVisible) {
|
||||||
|
self.clear(plot);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plot.setCrosshair(pos);
|
||||||
|
allSeriesMode = true;
|
||||||
|
|
||||||
|
if (dashboard.sharedCrosshairModeOnly()) {
|
||||||
|
// if only crosshair mode we are done
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seriesList.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seriesList[0].hasMsResolution) {
|
||||||
|
tooltipFormat = 'YYYY-MM-DD HH:mm:ss.SSS';
|
||||||
|
} else {
|
||||||
|
tooltipFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allSeriesMode) {
|
||||||
|
plot.unhighlight();
|
||||||
|
|
||||||
|
let seriesHoverInfo = self.getMultiSeriesPlotHoverInfo(plotData, pos);
|
||||||
|
|
||||||
|
seriesHtml = '';
|
||||||
|
|
||||||
|
absoluteTime = dashboard.formatDate(seriesHoverInfo.time, tooltipFormat);
|
||||||
|
|
||||||
|
// Dynamically reorder the hovercard for the current time point if the
|
||||||
|
// option is enabled.
|
||||||
|
if (panel.tooltip.sort === 2) {
|
||||||
|
seriesHoverInfo.sort(function(a, b) {
|
||||||
|
return b.value - a.value;
|
||||||
|
});
|
||||||
|
} else if (panel.tooltip.sort === 1) {
|
||||||
|
seriesHoverInfo.sort(function(a, b) {
|
||||||
|
return a.value - b.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0; i < seriesHoverInfo.length; i++) {
|
||||||
|
hoverInfo = seriesHoverInfo[i];
|
||||||
|
|
||||||
|
if (hoverInfo.hidden) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let highlightClass = '';
|
||||||
|
if (item && hoverInfo.index === item.seriesIndex) {
|
||||||
|
highlightClass = 'graph-tooltip-list-item--highlight';
|
||||||
|
}
|
||||||
|
|
||||||
|
series = seriesList[hoverInfo.index];
|
||||||
|
|
||||||
|
value = series.formatValue(hoverInfo.value);
|
||||||
|
|
||||||
|
seriesHtml +=
|
||||||
|
'<div class="graph-tooltip-list-item ' + highlightClass + '"><div class="graph-tooltip-series-name">';
|
||||||
|
seriesHtml +=
|
||||||
|
'<i class="fa fa-minus" style="color:' + hoverInfo.color + ';"></i> ' + hoverInfo.label + ':</div>';
|
||||||
|
seriesHtml += '<div class="graph-tooltip-value">' + value + '</div></div>';
|
||||||
|
plot.highlight(hoverInfo.index, hoverInfo.hoverIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.renderAndShow(absoluteTime, seriesHtml, pos, xMode);
|
||||||
|
} else if (item) {
|
||||||
|
// single series tooltip
|
||||||
|
series = seriesList[item.seriesIndex];
|
||||||
|
group = '<div class="graph-tooltip-list-item"><div class="graph-tooltip-series-name">';
|
||||||
|
group +=
|
||||||
|
'<i class="fa fa-minus" style="color:' + item.series.color + ';"></i> ' + series.aliasEscaped + ':</div>';
|
||||||
|
|
||||||
|
if (panel.stack && panel.tooltip.value_type === 'individual') {
|
||||||
|
value = item.datapoint[1] - item.datapoint[2];
|
||||||
|
} else {
|
||||||
|
value = item.datapoint[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
value = series.formatValue(value);
|
||||||
|
|
||||||
|
absoluteTime = dashboard.formatDate(item.datapoint[0], tooltipFormat);
|
||||||
|
|
||||||
|
group += '<div class="graph-tooltip-value">' + value + '</div>';
|
||||||
|
|
||||||
|
self.renderAndShow(absoluteTime, group, pos, xMode);
|
||||||
|
} else {
|
||||||
|
// no hit
|
||||||
|
$tooltip.detach();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -131,8 +131,11 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
|
|||||||
elem.empty();
|
elem.empty();
|
||||||
|
|
||||||
// Set min-width if side style and there is a value, otherwise remove the CSS propery
|
// Set min-width if side style and there is a value, otherwise remove the CSS propery
|
||||||
var width = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth + 'px' : '';
|
// Set width so it works with IE11
|
||||||
|
var width: any = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth + 'px' : '';
|
||||||
|
var ieWidth: any = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth - 1 + 'px' : '';
|
||||||
elem.css('min-width', width);
|
elem.css('min-width', width);
|
||||||
|
elem.css('width', ieWidth);
|
||||||
|
|
||||||
elem.toggleClass('graph-legend-table', panel.legend.alignAsTable === true);
|
elem.toggleClass('graph-legend-table', panel.legend.alignAsTable === true);
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ var scope = {
|
|||||||
|
|
||||||
var elem = $('<div></div>');
|
var elem = $('<div></div>');
|
||||||
var dashboard = {};
|
var dashboard = {};
|
||||||
|
var getSeriesFn;
|
||||||
|
|
||||||
function describeSharedTooltip(desc, fn) {
|
function describeSharedTooltip(desc, fn) {
|
||||||
var ctx: any = {};
|
var ctx: any = {};
|
||||||
@ -30,7 +31,7 @@ function describeSharedTooltip(desc, fn) {
|
|||||||
describe(desc, function() {
|
describe(desc, function() {
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
ctx.setupFn();
|
ctx.setupFn();
|
||||||
var tooltip = new GraphTooltip(elem, dashboard, scope);
|
var tooltip = new GraphTooltip(elem, dashboard, scope, getSeriesFn);
|
||||||
ctx.results = tooltip.getMultiSeriesPlotHoverInfo(ctx.data, ctx.pos);
|
ctx.results = tooltip.getMultiSeriesPlotHoverInfo(ctx.data, ctx.pos);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ function describeSharedTooltip(desc, fn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('findHoverIndexFromData', function() {
|
describe('findHoverIndexFromData', function() {
|
||||||
var tooltip = new GraphTooltip(elem, dashboard, scope);
|
var tooltip = new GraphTooltip(elem, dashboard, scope, getSeriesFn);
|
||||||
var series = {
|
var series = {
|
||||||
data: [[100, 0], [101, 0], [102, 0], [103, 0], [104, 0], [105, 0], [106, 0], [107, 0]],
|
data: [[100, 0], [101, 0], [102, 0], [103, 0], [104, 0], [105, 0], [106, 0], [107, 0]],
|
||||||
};
|
};
|
||||||
|
@ -163,10 +163,10 @@
|
|||||||
<span>
|
<span>
|
||||||
Use special variables to specify cell values:
|
Use special variables to specify cell values:
|
||||||
<br>
|
<br>
|
||||||
<em>$__cell</em> refers to current cell value
|
<em>${__cell}</em> refers to current cell value
|
||||||
<br>
|
<br>
|
||||||
<em>$__cell_n</em> refers to Nth column value in current row. Column indexes are started from 0. For instance,
|
<em>${__cell_n}</em> refers to Nth column value in current row. Column indexes are started from 0. For instance,
|
||||||
<em>$__cell_1</em> refers to second column's value.
|
<em>${__cell_1}</em> refers to second column's value.
|
||||||
</span>
|
</span>
|
||||||
</info-popover>
|
</info-popover>
|
||||||
</div>
|
</div>
|
||||||
|
@ -59,9 +59,8 @@ $critical: #ec2128;
|
|||||||
$body-bg: $gray-7;
|
$body-bg: $gray-7;
|
||||||
$page-bg: $gray-7;
|
$page-bg: $gray-7;
|
||||||
$body-color: $gray-1;
|
$body-color: $gray-1;
|
||||||
//$text-color: $dark-4;
|
|
||||||
$text-color: $gray-1;
|
$text-color: $gray-1;
|
||||||
$text-color-strong: $white;
|
$text-color-strong: $dark-2;
|
||||||
$text-color-weak: $gray-2;
|
$text-color-weak: $gray-2;
|
||||||
$text-color-faint: $gray-4;
|
$text-color-faint: $gray-4;
|
||||||
$text-color-emphasis: $dark-5;
|
$text-color-emphasis: $dark-5;
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
.gicon {
|
.gicon {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 1.1057142857em;
|
//width: 1.1057142857em;
|
||||||
height: 1.1057142857em;
|
//height: 1.1057142857em;
|
||||||
|
height: 22px;
|
||||||
|
width: 22px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
|
@ -31,7 +31,6 @@
|
|||||||
//padding: 0.5rem 1.5rem 0.5rem 0;
|
//padding: 0.5rem 1.5rem 0.5rem 0;
|
||||||
padding: 1rem 1rem 0.75rem 1rem;
|
padding: 1rem 1rem 0.75rem 1rem;
|
||||||
height: 51px;
|
height: 51px;
|
||||||
line-height: 51px;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
outline: none;
|
outline: none;
|
||||||
background: $side-menu-bg;
|
background: $side-menu-bg;
|
||||||
@ -61,6 +60,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
|
.search-item--indent {
|
||||||
|
margin-left: 14px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-dropdown__col_2 {
|
.search-dropdown__col_2 {
|
||||||
|
@ -178,6 +178,7 @@ li.sidemenu-org-switcher {
|
|||||||
padding: 0.4rem 1rem 0.4rem 0.65rem;
|
padding: 0.4rem 1rem 0.4rem 0.65rem;
|
||||||
min-height: $navbarHeight;
|
min-height: $navbarHeight;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
height: $navbarHeight - 1px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: $navbarButtonBackgroundHighlight;
|
background: $navbarButtonBackgroundHighlight;
|
||||||
|
@ -43,7 +43,7 @@
|
|||||||
font-size: 120%;
|
font-size: 120%;
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $white;
|
color: $text-color-strong;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,7 +108,8 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
padding: 0 28px 0 16px;
|
//margin-right: 8px;
|
||||||
|
padding: 0 4px 0 2px;
|
||||||
.icon-gf,
|
.icon-gf,
|
||||||
.fa {
|
.fa {
|
||||||
font-size: 200%;
|
font-size: 200%;
|
||||||
|
@ -33,7 +33,7 @@ div.flot-text {
|
|||||||
border: $panel-border;
|
border: $panel-border;
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
height: 100%;
|
//height: 100%;
|
||||||
|
|
||||||
&.panel-transparent {
|
&.panel-transparent {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
@ -3,6 +3,7 @@ $login-border: #8daac5;
|
|||||||
.login {
|
.login {
|
||||||
background-position: center;
|
background-position: center;
|
||||||
min-height: 85vh;
|
min-height: 85vh;
|
||||||
|
height: 80vh;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
@ -290,9 +291,14 @@ select:-webkit-autofill:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
@include media-breakpoint-up(md) {
|
||||||
|
.login-content {
|
||||||
|
flex: 1 0 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.login-branding {
|
.login-branding {
|
||||||
width: 45%;
|
width: 45%;
|
||||||
padding: 2rem 4rem;
|
padding: 2rem 4rem;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
.logo-icon {
|
.logo-icon {
|
||||||
width: 130px;
|
width: 130px;
|
||||||
@ -371,7 +377,7 @@ select:-webkit-autofill:focus {
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
content: "";
|
content: '';
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user