Merge branch 'master' into 14388/alert-tab-ux-update
This commit is contained in:
@@ -19,7 +19,7 @@ version: 2
|
||||
jobs:
|
||||
mysql-integration-test:
|
||||
docker:
|
||||
- image: circleci/golang:1.11
|
||||
- image: circleci/golang:1.11.4
|
||||
- image: circleci/mysql:5.6-ram
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: rootpass
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
postgres-integration-test:
|
||||
docker:
|
||||
- image: circleci/golang:1.11
|
||||
- image: circleci/golang:1.11.4
|
||||
- image: circleci/postgres:9.3-ram
|
||||
environment:
|
||||
POSTGRES_USER: grafanatest
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
|
||||
gometalinter:
|
||||
docker:
|
||||
- image: circleci/golang:1.11
|
||||
- image: circleci/golang:1.11.4
|
||||
environment:
|
||||
# we need CGO because of go-sqlite3
|
||||
CGO_ENABLED: 1
|
||||
@@ -117,7 +117,7 @@ jobs:
|
||||
|
||||
test-backend:
|
||||
docker:
|
||||
- image: circleci/golang:1.11
|
||||
- image: circleci/golang:1.11.4
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
@@ -175,7 +175,7 @@ jobs:
|
||||
|
||||
build:
|
||||
docker:
|
||||
- image: grafana/build-container:1.2.1
|
||||
- image: grafana/build-container:1.2.2
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
* **Admin**: When multiple user invitations, all links are the same as the first user who was invited [#14483](https://github.com/grafana/grafana/issues/14483)
|
||||
* **LDAP**: Upgrade go-ldap to v3 [#14548](https://github.com/grafana/grafana/issues/14548)
|
||||
* **Proxy whitelist**: Add CIDR capability to auth_proxy whitelist [#14546](https://github.com/grafana/grafana/issues/14546), thx [@jacobrichard](https://github.com/jacobrichard)
|
||||
* **OAuth**: Support OAuth providers that are not RFC6749 compliant [#14562](https://github.com/grafana/grafana/issues/14562), thx [@tdabasinskas](https://github.com/tdabasinskas)
|
||||
|
||||
### Bug fixes
|
||||
* **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486)
|
||||
|
||||
# 5.4.2 (2018-12-13)
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# Golang build container
|
||||
FROM golang:1.11
|
||||
FROM golang:1.11.4
|
||||
|
||||
WORKDIR $GOPATH/src/github.com/grafana/grafana
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ Choose this option to build on platforms other than linux/amd64 and/or not have
|
||||
|
||||
The resulting image will be tagged as `grafana/grafana:dev`
|
||||
|
||||
Notice: If you are using Docker for MacOS, be sure to let limit of Memory bigger than 2 GiB (at docker -> Perferences -> Advanced), otherwize you may faild at `grunt build`
|
||||
Notice: If you are using Docker for MacOS, be sure to let limit of Memory bigger than 2 GiB (at docker -> Preferences -> Advanced), otherwize you may faild at `grunt build`
|
||||
|
||||
### Dev config
|
||||
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ clone_folder: c:\gopath\src\github.com\grafana\grafana
|
||||
environment:
|
||||
nodejs_version: "8"
|
||||
GOPATH: C:\gopath
|
||||
GOVERSION: 1.11
|
||||
GOVERSION: 1.11.4
|
||||
|
||||
install:
|
||||
- rmdir c:\go /s /q
|
||||
|
||||
@@ -335,6 +335,7 @@ tls_skip_verify_insecure = false
|
||||
tls_client_cert =
|
||||
tls_client_key =
|
||||
tls_client_ca =
|
||||
send_client_credentials_via_post = false
|
||||
|
||||
#################################### Basic Auth ##########################
|
||||
[auth.basic]
|
||||
|
||||
@@ -284,6 +284,10 @@ log_queries =
|
||||
;tls_client_key =
|
||||
;tls_client_ca =
|
||||
|
||||
; Set to true to enable sending client_id and client_secret via POST body instead of Basic authentication HTTP header
|
||||
; This might be required if the OAuth provider is not RFC6749 compliant, only supporting credentials passed via POST payload
|
||||
;send_client_credentials_via_post = false
|
||||
|
||||
#################################### Grafana.com Auth ####################
|
||||
[auth.grafana_com]
|
||||
;enabled = false
|
||||
|
||||
@@ -17,7 +17,7 @@ can find examples using Okta, BitBucket, OneLogin and Azure.
|
||||
|
||||
This callback URL must match the full HTTP address that you use in your browser to access Grafana, but with the prefix path of `/login/generic_oauth`.
|
||||
|
||||
You may have to set the `root_url` option of `[server]` for the callback URL to be
|
||||
You may have to set the `root_url` option of `[server]` for the callback URL to be
|
||||
correct. For example in case you are serving Grafana behind a proxy.
|
||||
|
||||
Example config:
|
||||
@@ -209,6 +209,17 @@ allowed_organizations =
|
||||
token_url = https://<your domain>.my.centrify.com/OAuth2/Token/<Application ID>
|
||||
```
|
||||
|
||||
## Set up OAuth2 with non-compliant providers
|
||||
|
||||
Some OAuth2 providers might not support `client_id` and `client_secret` passed via Basic Authentication HTTP header, which
|
||||
results in `invalid_client` error. To allow Grafana to authenticate via these type of providers, the client identifiers must be
|
||||
send via POST body, which can be enabled via the following settings:
|
||||
|
||||
```bash
|
||||
[auth.generic_oauth]
|
||||
send_client_credentials_via_post = true
|
||||
```
|
||||
|
||||
<hr>
|
||||
|
||||
|
||||
|
||||
@@ -164,6 +164,14 @@ func GetPluginMarkdown(c *m.ReqContext) Response {
|
||||
return Error(500, "Could not get markdown file", err)
|
||||
}
|
||||
|
||||
// fallback try readme
|
||||
if len(content) == 0 {
|
||||
content, err = plugins.GetPluginMarkdown(pluginID, "readme")
|
||||
if err != nil {
|
||||
return Error(501, "Could not get markdown file", err)
|
||||
}
|
||||
}
|
||||
|
||||
resp := Respond(200, content)
|
||||
resp.Header("Content-Type", "text/plain; charset=utf-8")
|
||||
return resp
|
||||
|
||||
@@ -3,10 +3,8 @@ package plugins
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-model/go/datasource"
|
||||
@@ -29,7 +27,6 @@ type DataSourcePlugin struct {
|
||||
QueryOptions map[string]bool `json:"queryOptions,omitempty"`
|
||||
BuiltIn bool `json:"builtIn,omitempty"`
|
||||
Mixed bool `json:"mixed,omitempty"`
|
||||
HasQueryHelp bool `json:"hasQueryHelp,omitempty"`
|
||||
Routes []*AppPluginRoute `json:"routes"`
|
||||
|
||||
Backend bool `json:"backend,omitempty"`
|
||||
@@ -48,15 +45,6 @@ func (p *DataSourcePlugin) Load(decoder *json.Decoder, pluginDir string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// look for help markdown
|
||||
helpPath := filepath.Join(p.PluginDir, "QUERY_HELP.md")
|
||||
if _, err := os.Stat(helpPath); os.IsNotExist(err) {
|
||||
helpPath = filepath.Join(p.PluginDir, "query_help.md")
|
||||
}
|
||||
if _, err := os.Stat(helpPath); err == nil {
|
||||
p.HasQueryHelp = true
|
||||
}
|
||||
|
||||
DataSources[p.Id] = p
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []
|
||||
|
||||
var result notifierStateSlice
|
||||
for _, notification := range query.Result {
|
||||
not, err := n.createNotifierFor(notification)
|
||||
not, err := InitNotifier(notification)
|
||||
if err != nil {
|
||||
n.log.Error("Could not create notifier", "notifier", notification.Id, "error", err)
|
||||
continue
|
||||
@@ -195,7 +195,8 @@ func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (n *notificationService) createNotifierFor(model *m.AlertNotification) (Notifier, error) {
|
||||
// InitNotifier instantiate a new notifier based on the model
|
||||
func InitNotifier(model *m.AlertNotification) (Notifier, error) {
|
||||
notifierPlugin, found := notifierFactories[model.Type]
|
||||
if !found {
|
||||
return nil, errors.New("Unsupported notification type")
|
||||
@@ -208,6 +209,7 @@ type NotifierFactory func(notification *m.AlertNotification) (Notifier, error)
|
||||
|
||||
var notifierFactories = make(map[string]*NotifierPlugin)
|
||||
|
||||
// RegisterNotifier register an notifier
|
||||
func RegisterNotifier(plugin *NotifierPlugin) {
|
||||
notifierFactories[plugin.Type] = plugin
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
|
||||
Settings: cmd.Settings,
|
||||
}
|
||||
|
||||
notifiers, err := notifier.createNotifierFor(model)
|
||||
notifiers, err := InitNotifier(model)
|
||||
|
||||
if err != nil {
|
||||
log.Error2("Failed to create notifier", "error", err.Error())
|
||||
|
||||
@@ -53,14 +53,14 @@ func GetDataSourceByName(query *m.GetDataSourceByNameQuery) error {
|
||||
}
|
||||
|
||||
func GetDataSources(query *m.GetDataSourcesQuery) error {
|
||||
sess := x.Limit(1000, 0).Where("org_id=?", query.OrgId).Asc("name")
|
||||
sess := x.Limit(5000, 0).Where("org_id=?", query.OrgId).Asc("name")
|
||||
|
||||
query.Result = make([]*m.DataSource, 0)
|
||||
return sess.Find(&query.Result)
|
||||
}
|
||||
|
||||
func GetAllDataSources(query *m.GetAllDataSourcesQuery) error {
|
||||
sess := x.Limit(1000, 0).Asc("name")
|
||||
sess := x.Limit(5000, 0).Asc("name")
|
||||
|
||||
query.Result = make([]*m.DataSource, 0)
|
||||
return sess.Find(&query.Result)
|
||||
|
||||
@@ -345,8 +345,12 @@ func GetUserOrgList(query *m.GetUserOrgListQuery) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func newSignedInUserCacheKey(orgID, userID int64) string {
|
||||
return fmt.Sprintf("signed-in-user-%d-%d", userID, orgID)
|
||||
}
|
||||
|
||||
func (ss *SqlStore) GetSignedInUserWithCache(query *m.GetSignedInUserQuery) error {
|
||||
cacheKey := fmt.Sprintf("signed-in-user-%d-%d", query.UserId, query.OrgId)
|
||||
cacheKey := newSignedInUserCacheKey(query.OrgId, query.UserId)
|
||||
if cached, found := ss.CacheService.Get(cacheKey); found {
|
||||
query.Result = cached.(*m.SignedInUser)
|
||||
return nil
|
||||
@@ -357,6 +361,7 @@ func (ss *SqlStore) GetSignedInUserWithCache(query *m.GetSignedInUserQuery) erro
|
||||
return err
|
||||
}
|
||||
|
||||
cacheKey = newSignedInUserCacheKey(query.Result.OrgId, query.UserId)
|
||||
ss.CacheService.Set(cacheKey, query.Result, time.Second*5)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
func TestUserDataAccess(t *testing.T) {
|
||||
|
||||
Convey("Testing DB", t, func() {
|
||||
InitTestDB(t)
|
||||
ss := InitTestDB(t)
|
||||
|
||||
Convey("Creating a user", func() {
|
||||
cmd := &m.CreateUserCommand{
|
||||
@@ -153,6 +153,27 @@ func TestUserDataAccess(t *testing.T) {
|
||||
So(prefsQuery.Result.UserId, ShouldEqual, 0)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("when retreiving signed in user for orgId=0 result should return active org id", func() {
|
||||
ss.CacheService.Flush()
|
||||
|
||||
query := &m.GetSignedInUserQuery{OrgId: users[1].OrgId, UserId: users[1].Id}
|
||||
err := ss.GetSignedInUserWithCache(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result, ShouldNotBeNil)
|
||||
So(query.OrgId, ShouldEqual, users[1].OrgId)
|
||||
err = SetUsingOrg(&m.SetUsingOrgCommand{UserId: users[1].Id, OrgId: users[0].OrgId})
|
||||
So(err, ShouldBeNil)
|
||||
query = &m.GetSignedInUserQuery{OrgId: 0, UserId: users[1].Id}
|
||||
err = ss.GetSignedInUserWithCache(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result, ShouldNotBeNil)
|
||||
So(query.Result.OrgId, ShouldEqual, users[0].OrgId)
|
||||
|
||||
cacheKey := newSignedInUserCacheKey(query.Result.OrgId, query.UserId)
|
||||
_, found := ss.CacheService.Get(cacheKey)
|
||||
So(found, ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
package setting
|
||||
|
||||
type OAuthInfo struct {
|
||||
ClientId, ClientSecret string
|
||||
Scopes []string
|
||||
AuthUrl, TokenUrl string
|
||||
Enabled bool
|
||||
EmailAttributeName string
|
||||
AllowedDomains []string
|
||||
HostedDomain string
|
||||
ApiUrl string
|
||||
AllowSignup bool
|
||||
Name string
|
||||
TlsClientCert string
|
||||
TlsClientKey string
|
||||
TlsClientCa string
|
||||
TlsSkipVerify bool
|
||||
ClientId, ClientSecret string
|
||||
Scopes []string
|
||||
AuthUrl, TokenUrl string
|
||||
Enabled bool
|
||||
EmailAttributeName string
|
||||
AllowedDomains []string
|
||||
HostedDomain string
|
||||
ApiUrl string
|
||||
AllowSignup bool
|
||||
Name string
|
||||
TlsClientCert string
|
||||
TlsClientKey string
|
||||
TlsClientCa string
|
||||
TlsSkipVerify bool
|
||||
SendClientCredentialsViaPost bool
|
||||
}
|
||||
|
||||
type OAuther struct {
|
||||
|
||||
+22
-16
@@ -63,28 +63,34 @@ func NewOAuthService() {
|
||||
for _, name := range allOauthes {
|
||||
sec := setting.Raw.Section("auth." + name)
|
||||
info := &setting.OAuthInfo{
|
||||
ClientId: sec.Key("client_id").String(),
|
||||
ClientSecret: sec.Key("client_secret").String(),
|
||||
Scopes: util.SplitString(sec.Key("scopes").String()),
|
||||
AuthUrl: sec.Key("auth_url").String(),
|
||||
TokenUrl: sec.Key("token_url").String(),
|
||||
ApiUrl: sec.Key("api_url").String(),
|
||||
Enabled: sec.Key("enabled").MustBool(),
|
||||
EmailAttributeName: sec.Key("email_attribute_name").String(),
|
||||
AllowedDomains: util.SplitString(sec.Key("allowed_domains").String()),
|
||||
HostedDomain: sec.Key("hosted_domain").String(),
|
||||
AllowSignup: sec.Key("allow_sign_up").MustBool(),
|
||||
Name: sec.Key("name").MustString(name),
|
||||
TlsClientCert: sec.Key("tls_client_cert").String(),
|
||||
TlsClientKey: sec.Key("tls_client_key").String(),
|
||||
TlsClientCa: sec.Key("tls_client_ca").String(),
|
||||
TlsSkipVerify: sec.Key("tls_skip_verify_insecure").MustBool(),
|
||||
ClientId: sec.Key("client_id").String(),
|
||||
ClientSecret: sec.Key("client_secret").String(),
|
||||
Scopes: util.SplitString(sec.Key("scopes").String()),
|
||||
AuthUrl: sec.Key("auth_url").String(),
|
||||
TokenUrl: sec.Key("token_url").String(),
|
||||
ApiUrl: sec.Key("api_url").String(),
|
||||
Enabled: sec.Key("enabled").MustBool(),
|
||||
EmailAttributeName: sec.Key("email_attribute_name").String(),
|
||||
AllowedDomains: util.SplitString(sec.Key("allowed_domains").String()),
|
||||
HostedDomain: sec.Key("hosted_domain").String(),
|
||||
AllowSignup: sec.Key("allow_sign_up").MustBool(),
|
||||
Name: sec.Key("name").MustString(name),
|
||||
TlsClientCert: sec.Key("tls_client_cert").String(),
|
||||
TlsClientKey: sec.Key("tls_client_key").String(),
|
||||
TlsClientCa: sec.Key("tls_client_ca").String(),
|
||||
TlsSkipVerify: sec.Key("tls_skip_verify_insecure").MustBool(),
|
||||
SendClientCredentialsViaPost: sec.Key("send_client_credentials_via_post").MustBool(),
|
||||
}
|
||||
|
||||
if !info.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
// handle the clients that do not properly support Basic auth headers and require passing client_id/client_secret via POST payload
|
||||
if info.SendClientCredentialsViaPost {
|
||||
oauth2.RegisterBrokenAuthHeaderProvider(info.TokenUrl)
|
||||
}
|
||||
|
||||
if name == "grafananet" {
|
||||
name = grafanaCom
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import Remarkable from 'remarkable';
|
||||
import { getBackendSrv } from '../../services/backend_srv';
|
||||
|
||||
interface Props {
|
||||
plugin: {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isError: boolean;
|
||||
isLoading: boolean;
|
||||
help: string;
|
||||
}
|
||||
|
||||
export class PluginHelp extends PureComponent<Props, State> {
|
||||
state = {
|
||||
isError: false,
|
||||
isLoading: false,
|
||||
help: '',
|
||||
};
|
||||
|
||||
componentDidMount(): void {
|
||||
this.loadHelp();
|
||||
}
|
||||
|
||||
constructPlaceholderInfo() {
|
||||
return 'No plugin help or readme markdown file was found';
|
||||
}
|
||||
|
||||
loadHelp = () => {
|
||||
const { plugin, type } = this.props;
|
||||
this.setState({ isLoading: true });
|
||||
|
||||
getBackendSrv()
|
||||
.get(`/api/plugins/${plugin.id}/markdown/${type}`)
|
||||
.then(response => {
|
||||
const markdown = new Remarkable();
|
||||
const helpHtml = markdown.render(response);
|
||||
|
||||
if (response === '' && type === 'help') {
|
||||
this.setState({
|
||||
isError: false,
|
||||
isLoading: false,
|
||||
help: this.constructPlaceholderInfo(),
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
isError: false,
|
||||
isLoading: false,
|
||||
help: helpHtml,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
isError: true,
|
||||
isLoading: false,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { type } = this.props;
|
||||
const { isError, isLoading, help } = this.state;
|
||||
|
||||
if (isLoading) {
|
||||
return <h2>Loading help...</h2>;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <h3>'Error occurred when loading help'</h3>;
|
||||
}
|
||||
|
||||
if (type === 'panel_help' && help === '') {
|
||||
}
|
||||
|
||||
return <div className="markdown-html" dangerouslySetInnerHTML={{ __html: help }} />;
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ const DEFAULT_THEME_LIGHT = 'ace/theme/textmate';
|
||||
const DEFAULT_MODE = 'text';
|
||||
const DEFAULT_MAX_LINES = 10;
|
||||
const DEFAULT_TAB_SIZE = 2;
|
||||
const DEFAULT_BEHAVIOURS = true;
|
||||
const DEFAULT_BEHAVIORS = true;
|
||||
const DEFAULT_SNIPPETS = true;
|
||||
|
||||
const editorTemplate = `<div></div>`;
|
||||
@@ -61,7 +61,7 @@ function link(scope, elem, attrs) {
|
||||
const maxLines = attrs.maxLines || DEFAULT_MAX_LINES;
|
||||
const showGutter = attrs.showGutter !== undefined;
|
||||
const tabSize = attrs.tabSize || DEFAULT_TAB_SIZE;
|
||||
const behavioursEnabled = attrs.behavioursEnabled ? attrs.behavioursEnabled === 'true' : DEFAULT_BEHAVIOURS;
|
||||
const behavioursEnabled = attrs.behavioursEnabled ? attrs.behavioursEnabled === 'true' : DEFAULT_BEHAVIORS;
|
||||
const snippetsEnabled = attrs.snippetsEnabled ? attrs.snippetsEnabled === 'true' : DEFAULT_SNIPPETS;
|
||||
|
||||
// Initialize editor
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Based on work https://github.com/mohsen1/json-formatter-js
|
||||
// Licence MIT, Copyright (c) 2015 Mohsen Azimi
|
||||
// License MIT, Copyright (c) 2015 Mohsen Azimi
|
||||
|
||||
/*
|
||||
* Escapes `"` characters from string
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Based on work https://github.com/mohsen1/json-formatter-js
|
||||
// Licence MIT, Copyright (c) 2015 Mohsen Azimi
|
||||
// License MIT, Copyright (c) 2015 Mohsen Azimi
|
||||
|
||||
import { isObject, getObjectName, getType, getValuePreview, cssClass, createElement } from './helpers';
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ describe('Render', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render organisation switcher', () => {
|
||||
it('should render organization switcher', () => {
|
||||
const wrapper = setup({
|
||||
link: {
|
||||
showOrgSwitcher: true,
|
||||
|
||||
@@ -73,7 +73,7 @@ exports[`Render should render component 1`] = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Render should render organisation switcher 1`] = `
|
||||
exports[`Render should render organization switcher 1`] = `
|
||||
<div
|
||||
className="sidemenu-item dropdown dropup"
|
||||
>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { DashboardModel } from 'app/features/dashboard/dashboard_model';
|
||||
|
||||
export class BackendSrv {
|
||||
private inFlightRequests = {};
|
||||
private HTTP_REQUEST_CANCELLED = -1;
|
||||
private HTTP_REQUEST_CANCELED = -1;
|
||||
private noBackendCache: boolean;
|
||||
|
||||
/** @ngInject */
|
||||
@@ -178,7 +178,7 @@ export class BackendSrv {
|
||||
return response;
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.status === this.HTTP_REQUEST_CANCELLED) {
|
||||
if (err.status === this.HTTP_REQUEST_CANCELED) {
|
||||
throw { err, cancelled: true };
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export class SearchSrv {
|
||||
}
|
||||
|
||||
private queryForRecentDashboards() {
|
||||
const dashIds = _.take(impressionSrv.getDashboardOpened(), 5);
|
||||
const dashIds = _.take(impressionSrv.getDashboardOpened(), 30);
|
||||
if (dashIds.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
@@ -70,7 +70,7 @@ export class SearchSrv {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this.backendSrv.search({ starred: true, limit: 5 }).then(result => {
|
||||
return this.backendSrv.search({ starred: true, limit: 30 }).then(result => {
|
||||
if (result.length > 0) {
|
||||
sections['starred'] = {
|
||||
title: 'Starred',
|
||||
|
||||
@@ -2,7 +2,7 @@ import _ from 'lodash';
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
// This service really just tracks a list of $timeout promises to give us a
|
||||
// method for cancelling them all when we need to
|
||||
// method for canceling them all when we need to
|
||||
export class Timer {
|
||||
timers = [];
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ describe('file_export', () => {
|
||||
],
|
||||
rows: [
|
||||
[123, 'some_string', 1.234, true],
|
||||
[1000, 'some_string', 1.234567891, true],
|
||||
[0o765, 'some string with " in the middle', 1e-2, false],
|
||||
[0o765, 'some string with "" in the middle', 1e-2, false],
|
||||
[0o765, 'some string with """ in the middle', 1e-2, false],
|
||||
@@ -89,6 +90,7 @@ describe('file_export', () => {
|
||||
const expectedText =
|
||||
'"integer_value";"string_value";"float_value";"boolean_value"\r\n' +
|
||||
'123;"some_string";1.234;true\r\n' +
|
||||
'1000;"some_string";1.234567891;true\r\n' +
|
||||
'501;"some string with "" in the middle";0.01;false\r\n' +
|
||||
'501;"some string with """" in the middle";0.01;false\r\n' +
|
||||
'501;"some string with """""" in the middle";0.01;false\r\n' +
|
||||
|
||||
@@ -40,7 +40,7 @@ export default class TableModel {
|
||||
this.rows.sort((a, b) => {
|
||||
a = a[options.col];
|
||||
b = b[options.col];
|
||||
// Sort null or undefined seperately from comparable values
|
||||
// Sort null or undefined separately from comparable values
|
||||
return +(a == null) - +(b == null) || +(a > b) || -(a < b);
|
||||
});
|
||||
|
||||
|
||||
@@ -41,10 +41,8 @@ function formatSpecialHeader(useExcelHeader) {
|
||||
function formatRow(row, addEndRowDelimiter = true) {
|
||||
let text = '';
|
||||
for (let i = 0; i < row.length; i += 1) {
|
||||
if (isBoolean(row[i]) || isNullOrUndefined(row[i])) {
|
||||
if (isBoolean(row[i]) || isNumber(row[i]) || isNullOrUndefined(row[i])) {
|
||||
text += row[i];
|
||||
} else if (isNumber(row[i])) {
|
||||
text += row[i].toLocaleString();
|
||||
} else {
|
||||
text += `${QUOTE}${csvEscaped(htmlUnescaped(htmlDecoded(row[i])))}${QUOTE}`;
|
||||
}
|
||||
|
||||
@@ -484,6 +484,14 @@ kbn.valueFormats.Mbits = kbn.formatBuilders.decimalSIPrefix('bps', 2);
|
||||
kbn.valueFormats.GBs = kbn.formatBuilders.decimalSIPrefix('Bs', 3);
|
||||
kbn.valueFormats.Gbits = kbn.formatBuilders.decimalSIPrefix('bps', 3);
|
||||
|
||||
// Floating Point Operations per Second
|
||||
kbn.valueFormats.flops = kbn.formatBuilders.decimalSIPrefix('FLOP/s');
|
||||
kbn.valueFormats.mflops = kbn.formatBuilders.decimalSIPrefix('FLOP/s', 2);
|
||||
kbn.valueFormats.gflops = kbn.formatBuilders.decimalSIPrefix('FLOP/s', 3);
|
||||
kbn.valueFormats.tflops = kbn.formatBuilders.decimalSIPrefix('FLOP/s', 4);
|
||||
kbn.valueFormats.pflops = kbn.formatBuilders.decimalSIPrefix('FLOP/s', 5);
|
||||
kbn.valueFormats.eflops = kbn.formatBuilders.decimalSIPrefix('FLOP/s', 6);
|
||||
|
||||
// Hash Rate
|
||||
kbn.valueFormats.Hs = kbn.formatBuilders.decimalSIPrefix('H/s');
|
||||
kbn.valueFormats.KHs = kbn.formatBuilders.decimalSIPrefix('H/s', 1);
|
||||
@@ -1019,6 +1027,17 @@ kbn.getUnitFormats = () => {
|
||||
{ text: 'exahashes/sec', value: 'EHs' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'computation throughput',
|
||||
submenu: [
|
||||
{ text: 'FLOP/s', value: 'flops' },
|
||||
{ text: 'MFLOP/s', value: 'mflops' },
|
||||
{ text: 'GFLOP/s', value: 'gflops' },
|
||||
{ text: 'TFLOP/s', value: 'tflops' },
|
||||
{ text: 'PFLOP/s', value: 'pflops' },
|
||||
{ text: 'EFLOP/s', value: 'eflops' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'throughput',
|
||||
submenu: [
|
||||
@@ -1085,7 +1104,7 @@ kbn.getUnitFormats = () => {
|
||||
{ text: 'Watt (W)', value: 'watt' },
|
||||
{ text: 'Kilowatt (kW)', value: 'kwatt' },
|
||||
{ text: 'Milliwatt (mW)', value: 'mwatt' },
|
||||
{ text: 'Watt per square metre (W/m²)', value: 'Wm2' },
|
||||
{ text: 'Watt per square meter (W/m²)', value: 'Wm2' },
|
||||
{ text: 'Volt-ampere (VA)', value: 'voltamp' },
|
||||
{ text: 'Kilovolt-ampere (kVA)', value: 'kvoltamp' },
|
||||
{ text: 'Volt-ampere reactive (var)', value: 'voltampreact' },
|
||||
@@ -1182,14 +1201,14 @@ kbn.getUnitFormats = () => {
|
||||
submenu: [
|
||||
{ text: 'parts-per-million (ppm)', value: 'ppm' },
|
||||
{ text: 'parts-per-billion (ppb)', value: 'conppb' },
|
||||
{ text: 'nanogram per cubic metre (ng/m³)', value: 'conngm3' },
|
||||
{ text: 'nanogram per normal cubic metre (ng/Nm³)', value: 'conngNm3' },
|
||||
{ text: 'microgram per cubic metre (μg/m³)', value: 'conμgm3' },
|
||||
{ text: 'microgram per normal cubic metre (μg/Nm³)', value: 'conμgNm3' },
|
||||
{ text: 'milligram per cubic metre (mg/m³)', value: 'conmgm3' },
|
||||
{ text: 'milligram per normal cubic metre (mg/Nm³)', value: 'conmgNm3' },
|
||||
{ text: 'gram per cubic metre (g/m³)', value: 'congm3' },
|
||||
{ text: 'gram per normal cubic metre (g/Nm³)', value: 'congNm3' },
|
||||
{ text: 'nanogram per cubic meter (ng/m³)', value: 'conngm3' },
|
||||
{ text: 'nanogram per normal cubic meter (ng/Nm³)', value: 'conngNm3' },
|
||||
{ text: 'microgram per cubic meter (μg/m³)', value: 'conμgm3' },
|
||||
{ text: 'microgram per normal cubic meter (μg/Nm³)', value: 'conμgNm3' },
|
||||
{ text: 'milligram per cubic meter (mg/m³)', value: 'conmgm3' },
|
||||
{ text: 'milligram per normal cubic meter (mg/Nm³)', value: 'conmgNm3' },
|
||||
{ text: 'gram per cubic meter (g/m³)', value: 'congm3' },
|
||||
{ text: 'gram per normal cubic meter (g/Nm³)', value: 'congNm3' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -14,6 +14,7 @@ import { PanelEditor } from './PanelEditor';
|
||||
import { PanelModel } from '../panel_model';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import { PanelPlugin } from 'app/types';
|
||||
import { PanelResizer } from './PanelResizer';
|
||||
|
||||
export interface Props {
|
||||
panel: PanelModel;
|
||||
@@ -158,10 +159,21 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
|
||||
return (
|
||||
<div className={containerClass}>
|
||||
<div className={panelWrapperClass} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
|
||||
{plugin.exports.Panel && this.renderReactPanel()}
|
||||
{plugin.exports.PanelCtrl && this.renderAngularPanel()}
|
||||
</div>
|
||||
<PanelResizer
|
||||
isEditing={!!isEditing}
|
||||
panel={panel}
|
||||
render={(panelHeight: number | 'inherit') => (
|
||||
<div
|
||||
className={panelWrapperClass}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
style={{ height: panelHeight }}
|
||||
>
|
||||
{plugin.exports.Panel && this.renderReactPanel()}
|
||||
{plugin.exports.PanelCtrl && this.renderAngularPanel()}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{panel.isEditing && (
|
||||
<PanelEditor
|
||||
panel={panel}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { throttle } from 'lodash';
|
||||
import Draggable from 'react-draggable';
|
||||
|
||||
import { PanelModel } from '../panel_model';
|
||||
|
||||
interface Props {
|
||||
isEditing: boolean;
|
||||
render: (height: number | 'inherit') => JSX.Element;
|
||||
panel: PanelModel;
|
||||
}
|
||||
|
||||
interface State {
|
||||
editorHeight: number;
|
||||
}
|
||||
|
||||
export class PanelResizer extends PureComponent<Props, State> {
|
||||
initialHeight: number = Math.floor(document.documentElement.scrollHeight * 0.4);
|
||||
prevEditorHeight: number;
|
||||
throttledChangeHeight: (height: number) => void;
|
||||
throttledResizeDone: () => void;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { panel } = this.props;
|
||||
|
||||
this.state = {
|
||||
editorHeight: this.initialHeight,
|
||||
};
|
||||
|
||||
this.throttledChangeHeight = throttle(this.changeHeight, 20, { trailing: true });
|
||||
this.throttledResizeDone = throttle(() => {
|
||||
panel.resizeDone();
|
||||
}, 50);
|
||||
}
|
||||
|
||||
get largestHeight() {
|
||||
return document.documentElement.scrollHeight * 0.9;
|
||||
}
|
||||
get smallestHeight() {
|
||||
return 100;
|
||||
}
|
||||
|
||||
changeHeight = height => {
|
||||
const sh = this.smallestHeight;
|
||||
const lh = this.largestHeight;
|
||||
height = height < sh ? sh : height;
|
||||
height = height > lh ? lh : height;
|
||||
|
||||
this.prevEditorHeight = this.state.editorHeight;
|
||||
this.setState({
|
||||
editorHeight: height,
|
||||
});
|
||||
};
|
||||
|
||||
onDrag = (evt, data) => {
|
||||
const newHeight = this.state.editorHeight + data.y;
|
||||
this.throttledChangeHeight(newHeight);
|
||||
this.throttledResizeDone();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { render, isEditing } = this.props;
|
||||
const { editorHeight } = this.state;
|
||||
|
||||
return (
|
||||
<>
|
||||
{render(isEditing ? editorHeight : 'inherit')}
|
||||
{isEditing && (
|
||||
<div className="panel-editor-container__resizer">
|
||||
<Draggable axis="y" grid={[100, 1]} onDrag={this.onDrag} position={{ x: 0, y: 0 }}>
|
||||
<div className="panel-editor-resizer">
|
||||
<div className="panel-editor-resizer__handle" />
|
||||
</div>
|
||||
</Draggable>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
// Libraries
|
||||
import React, { SFC, PureComponent } from 'react';
|
||||
import Remarkable from 'remarkable';
|
||||
import _ from 'lodash';
|
||||
|
||||
// Components
|
||||
@@ -22,6 +21,7 @@ import config from 'app/core/config';
|
||||
import { PanelModel } from '../panel_model';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import { DataSourceSelectItem, DataQuery } from 'app/types';
|
||||
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
|
||||
|
||||
interface Props {
|
||||
panel: PanelModel;
|
||||
@@ -128,43 +128,13 @@ export class QueriesTab extends PureComponent<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
loadHelp = () => {
|
||||
const { currentDS } = this.state;
|
||||
const hasHelp = currentDS.meta.hasQueryHelp;
|
||||
|
||||
if (hasHelp) {
|
||||
this.setState({
|
||||
helpContent: <h3>Loading help...</h3>,
|
||||
isLoadingHelp: true,
|
||||
});
|
||||
|
||||
this.backendSrv
|
||||
.get(`/api/plugins/${currentDS.meta.id}/markdown/query_help`)
|
||||
.then(res => {
|
||||
const md = new Remarkable();
|
||||
const helpHtml = md.render(res);
|
||||
this.setState({
|
||||
helpContent: <div className="markdown-html" dangerouslySetInnerHTML={{ __html: helpHtml }} />,
|
||||
isLoadingHelp: false,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
helpContent: <h3>'Error occured when loading help'</h3>,
|
||||
isLoadingHelp: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
renderQueryInspector = () => {
|
||||
const { panel } = this.props;
|
||||
return <QueryInspector panel={panel} LoadingPlaceholder={LoadingPlaceholder} />;
|
||||
};
|
||||
|
||||
renderHelp = () => {
|
||||
const { helpContent, isLoadingHelp } = this.state;
|
||||
return isLoadingHelp ? <LoadingPlaceholder text="Loading help..." /> : helpContent;
|
||||
return <PluginHelp plugin={this.state.currentDS.meta} type="query_help" />;
|
||||
};
|
||||
|
||||
onAddQuery = (query?: Partial<DataQuery>) => {
|
||||
@@ -233,7 +203,6 @@ export class QueriesTab extends PureComponent<Props, State> {
|
||||
render() {
|
||||
const { panel } = this.props;
|
||||
const { currentDS, isAddingMixed } = this.state;
|
||||
const { hasQueryHelp } = currentDS.meta;
|
||||
|
||||
const queryInspector = {
|
||||
title: 'Query Inspector',
|
||||
@@ -243,8 +212,6 @@ export class QueriesTab extends PureComponent<Props, State> {
|
||||
const dsHelp = {
|
||||
heading: 'Help',
|
||||
icon: 'fa fa-question',
|
||||
disabled: !hasQueryHelp,
|
||||
onClick: this.loadHelp,
|
||||
render: this.renderHelp,
|
||||
};
|
||||
|
||||
|
||||
@@ -38,7 +38,33 @@ interface Props {
|
||||
datasource: DataSourceSelectItem;
|
||||
}
|
||||
|
||||
export class QueryOptions extends PureComponent<Props> {
|
||||
interface State {
|
||||
relativeTime: string;
|
||||
timeShift: string;
|
||||
}
|
||||
|
||||
export class QueryOptions extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
relativeTime: props.panel.timeFrom || '',
|
||||
timeShift: props.panel.timeShift || '',
|
||||
};
|
||||
}
|
||||
|
||||
onRelativeTimeChange = event => {
|
||||
this.setState({
|
||||
relativeTime: event.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onTimeShiftChange = event => {
|
||||
this.setState({
|
||||
timeShift: event.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onOverrideTime = (evt, status: InputStatus) => {
|
||||
const { value } = evt.target;
|
||||
const { panel } = this.props;
|
||||
@@ -128,8 +154,10 @@ export class QueryOptions extends PureComponent<Props> {
|
||||
});
|
||||
}
|
||||
|
||||
render = () => {
|
||||
render() {
|
||||
const hideTimeOverride = this.props.panel.hideTimeOverride;
|
||||
const { relativeTime, timeShift } = this.state;
|
||||
|
||||
return (
|
||||
<div className="gf-form-inline">
|
||||
{this.renderOptions()}
|
||||
@@ -140,9 +168,11 @@ export class QueryOptions extends PureComponent<Props> {
|
||||
type="text"
|
||||
className="width-6"
|
||||
placeholder="1h"
|
||||
onChange={this.onRelativeTimeChange}
|
||||
onBlur={this.onOverrideTime}
|
||||
validationEvents={timeRangeValidationEvents}
|
||||
hideErrorMessage={true}
|
||||
value={relativeTime}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -152,9 +182,11 @@ export class QueryOptions extends PureComponent<Props> {
|
||||
type="text"
|
||||
className="width-6"
|
||||
placeholder="1h"
|
||||
onChange={this.onTimeShiftChange}
|
||||
onBlur={this.onTimeShift}
|
||||
validationEvents={timeRangeValidationEvents}
|
||||
hideErrorMessage={true}
|
||||
value={timeShift}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -163,5 +195,5 @@ export class QueryOptions extends PureComponent<Props> {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoa
|
||||
// Components
|
||||
import { EditorTabBody } from './EditorTabBody';
|
||||
import { VizTypePicker } from './VizTypePicker';
|
||||
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
|
||||
import { FadeIn } from 'app/core/components/Animations/FadeIn';
|
||||
import { PanelOptionSection } from './PanelOptionSection';
|
||||
|
||||
@@ -105,6 +106,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
const panelCtrl = scope.$$childHead.ctrl;
|
||||
panelCtrl.initEditMode();
|
||||
|
||||
let template = '';
|
||||
for (let i = 0; i < panelCtrl.editorTabs.length; i++) {
|
||||
@@ -198,12 +200,20 @@ export class VisualizationTab extends PureComponent<Props, State> {
|
||||
}
|
||||
};
|
||||
|
||||
renderHelp = () => <PluginHelp plugin={this.props.plugin} type="help" />;
|
||||
|
||||
render() {
|
||||
const { plugin } = this.props;
|
||||
const { isVizPickerOpen, searchQuery } = this.state;
|
||||
|
||||
const pluginHelp = {
|
||||
heading: 'Help',
|
||||
icon: 'fa fa-question',
|
||||
render: this.renderHelp,
|
||||
};
|
||||
|
||||
return (
|
||||
<EditorTabBody heading="Visualization" renderToolbar={this.renderToolbar}>
|
||||
<EditorTabBody heading="Visualization" renderToolbar={this.renderToolbar} toolbarItems={[pluginHelp]}>
|
||||
<>
|
||||
<FadeIn in={isVizPickerOpen} duration={200} unmountOnExit={true}>
|
||||
<VizTypePicker
|
||||
|
||||
@@ -67,7 +67,7 @@ export function updateDashboardPermission(
|
||||
|
||||
const updated = toUpdateItem(item);
|
||||
|
||||
// if this is the item we want to update, update it's permisssion
|
||||
// if this is the item we want to update, update it's permission
|
||||
if (itemToUpdate === item) {
|
||||
updated.permission = level;
|
||||
}
|
||||
|
||||
@@ -24,60 +24,67 @@
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.isOpen" class="gf-timepicker-dropdown">
|
||||
<form name="timeForm" class="gf-timepicker-absolute-section">
|
||||
<h3 class="section-heading">Custom range</h3>
|
||||
|
||||
<label class="small">From:</label>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-28">
|
||||
<input type="text" class="gf-form-input input-large" ng-model="ctrl.editTimeRaw.from" input-datetime>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button class="btn gf-form-btn btn-primary" type="button" ng-click="openFromPicker=!openFromPicker">
|
||||
<i class="fa fa-calendar"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="popover-box">
|
||||
<div class="popover-box__header">
|
||||
<span class="popover-box__title">Quick ranges</span>
|
||||
</div>
|
||||
|
||||
<div ng-if="openFromPicker">
|
||||
<datepicker ng-model="ctrl.absolute.fromJs" class="gf-timepicker-component" show-weeks="false" starting-day="ctrl.firstDayOfWeek" ng-change="ctrl.absoluteFromChanged()"></datepicker>
|
||||
<div class="popover-box__body gf-timepicker-relative-section">
|
||||
<ul ng-repeat="group in ctrl.timeOptions">
|
||||
<li bindonce ng-repeat='option in group' ng-class="{active: option.active}">
|
||||
<a ng-click="ctrl.setRelativeFilter(option)" bo-text="option.display"></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<label class="small">To:</label>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-28">
|
||||
<input type="text" class="gf-form-input input-large" ng-model="ctrl.editTimeRaw.to" input-datetime>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button class="btn gf-form-btn btn-primary" type="button" ng-click="openToPicker=!openToPicker">
|
||||
<i class="fa fa-calendar"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="popover-box">
|
||||
<div class="popover-box__header">
|
||||
<span class="popover-box__title">Custom range</span>
|
||||
</div>
|
||||
|
||||
<div ng-if="openToPicker">
|
||||
<datepicker ng-model="ctrl.absolute.toJs" class="gf-timepicker-component" show-weeks="false" starting-day="ctrl.firstDayOfWeek" ng-change="ctrl.absoluteToChanged()"></datepicker>
|
||||
</div>
|
||||
|
||||
<label class="small">Refreshing every:</label>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-28">
|
||||
<select ng-model="ctrl.refresh.value" class="gf-form-input input-medium" ng-options="f.value as f.text for f in ctrl.refresh.options"></select>
|
||||
<form name="timeForm" class="popover-box__body gf-timepicker-absolute-section">
|
||||
<label class="small">From:</label>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-28">
|
||||
<input type="text" class="gf-form-input input-large" ng-model="ctrl.editTimeRaw.from" input-datetime>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button class="btn gf-form-btn btn-primary" type="button" ng-click="openFromPicker=!openFromPicker">
|
||||
<i class="fa fa-calendar"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button type="submit" class="btn gf-form-btn btn-secondary" ng-click="ctrl.applyCustom();" ng-disabled="!timeForm.$valid">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="gf-timepicker-relative-section">
|
||||
<h3 class="section-heading">Quick ranges</h3>
|
||||
<ul ng-repeat="group in ctrl.timeOptions">
|
||||
<li bindonce ng-repeat='option in group' ng-class="{active: option.active}">
|
||||
<a ng-click="ctrl.setRelativeFilter(option)" bo-text="option.display"></a>
|
||||
</li>
|
||||
</ul>
|
||||
<div ng-if="openFromPicker">
|
||||
<datepicker ng-model="ctrl.absolute.fromJs" class="gf-timepicker-component" show-weeks="false" starting-day="ctrl.firstDayOfWeek" ng-change="ctrl.absoluteFromChanged()"></datepicker>
|
||||
</div>
|
||||
|
||||
|
||||
<label class="small">To:</label>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-28">
|
||||
<input type="text" class="gf-form-input input-large" ng-model="ctrl.editTimeRaw.to" input-datetime>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button class="btn gf-form-btn btn-primary" type="button" ng-click="openToPicker=!openToPicker">
|
||||
<i class="fa fa-calendar"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="openToPicker">
|
||||
<datepicker ng-model="ctrl.absolute.toJs" class="gf-timepicker-component" show-weeks="false" starting-day="ctrl.firstDayOfWeek" ng-change="ctrl.absoluteToChanged()"></datepicker>
|
||||
</div>
|
||||
|
||||
<label class="small">Refreshing every:</label>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-28">
|
||||
<select ng-model="ctrl.refresh.value" class="gf-form-input input-medium" ng-options="f.value as f.text for f in ctrl.refresh.options"></select>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button type="submit" class="btn gf-form-btn btn-secondary" ng-click="ctrl.applyCustom();" ng-disabled="!timeForm.$valid">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+16
-4
@@ -61,7 +61,10 @@ exports[`Render should render alpha info text 1`] = `
|
||||
},
|
||||
"description": "pretty decent plugin",
|
||||
"links": Array [
|
||||
"one link",
|
||||
Object {
|
||||
"name": "project",
|
||||
"url": "one link",
|
||||
},
|
||||
],
|
||||
"logos": Object {
|
||||
"large": "large/logo",
|
||||
@@ -160,7 +163,10 @@ exports[`Render should render beta info text 1`] = `
|
||||
},
|
||||
"description": "pretty decent plugin",
|
||||
"links": Array [
|
||||
"one link",
|
||||
Object {
|
||||
"name": "project",
|
||||
"url": "one link",
|
||||
},
|
||||
],
|
||||
"logos": Object {
|
||||
"large": "large/logo",
|
||||
@@ -254,7 +260,10 @@ exports[`Render should render component 1`] = `
|
||||
},
|
||||
"description": "pretty decent plugin",
|
||||
"links": Array [
|
||||
"one link",
|
||||
Object {
|
||||
"name": "project",
|
||||
"url": "one link",
|
||||
},
|
||||
],
|
||||
"logos": Object {
|
||||
"large": "large/logo",
|
||||
@@ -353,7 +362,10 @@ exports[`Render should render is ready only message 1`] = `
|
||||
},
|
||||
"description": "pretty decent plugin",
|
||||
"links": Array [
|
||||
"one link",
|
||||
Object {
|
||||
"name": "project",
|
||||
"url": "one link",
|
||||
},
|
||||
],
|
||||
"logos": Object {
|
||||
"large": "large/logo",
|
||||
|
||||
@@ -73,7 +73,7 @@ export function getDataSourceLoadingNav(pageName: string): NavModel {
|
||||
url: '',
|
||||
},
|
||||
description: '',
|
||||
links: [''],
|
||||
links: [{ name: '', url: '' }],
|
||||
logos: {
|
||||
large: '',
|
||||
small: '',
|
||||
|
||||
@@ -482,7 +482,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
} else {
|
||||
// Modify query only at index
|
||||
nextQueries = initialQueries.map((query, i) => {
|
||||
// Synchronise all queries with local query cache to ensure consistency
|
||||
// Synchronize all queries with local query cache to ensure consistency
|
||||
// TODO still needed?
|
||||
return i === index
|
||||
? {
|
||||
|
||||
@@ -88,7 +88,7 @@ export default class PlaceholdersBuffer {
|
||||
orders.push({ index: parts.length - 1, order });
|
||||
textOffset += part.length + match.length;
|
||||
}
|
||||
// Ensures string serialisation still works if no placeholders were parsed
|
||||
// Ensures string serialization still works if no placeholders were parsed
|
||||
// and also accounts for the remainder of text with placeholders
|
||||
parts.push(text.slice(textOffset));
|
||||
return {
|
||||
|
||||
@@ -232,60 +232,60 @@ export default class TimePicker extends PureComponent<TimePickerProps, TimePicke
|
||||
const timeOptions = this.getTimeOptions();
|
||||
return (
|
||||
<div ref={this.dropdownRef} className="gf-timepicker-dropdown">
|
||||
<div className="gf-timepicker-absolute-section">
|
||||
<h3 className="section-heading">Custom range</h3>
|
||||
|
||||
<label className="small">From:</label>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form max-width-28">
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input input-large timepicker-from"
|
||||
value={fromRaw}
|
||||
onChange={this.handleChangeFrom}
|
||||
/>
|
||||
</div>
|
||||
<div className="popover-box">
|
||||
<div className="popover-box__header">
|
||||
<span className="popover-box__title">Quick ranges</span>
|
||||
</div>
|
||||
|
||||
<label className="small">To:</label>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form max-width-28">
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input input-large timepicker-to"
|
||||
value={toRaw}
|
||||
onChange={this.handleChangeTo}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <label className="small">Refreshing every:</label>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form max-width-28">
|
||||
<select className="gf-form-input input-medium" ng-options="f.value as f.text for f in ctrl.refresh.options"></select>
|
||||
</div>
|
||||
</div> */}
|
||||
<div className="gf-form">
|
||||
<button className="btn gf-form-btn btn-secondary" onClick={this.handleClickApply}>
|
||||
Apply
|
||||
</button>
|
||||
<div className="popover-box__body gf-timepicker-relative-section">
|
||||
{Object.keys(timeOptions).map(section => {
|
||||
const group = timeOptions[section];
|
||||
return (
|
||||
<ul key={section}>
|
||||
{group.map(option => (
|
||||
<li className={option.active ? 'active' : ''} key={option.display}>
|
||||
<a onClick={() => this.handleClickRelativeOption(option)}>{option.display}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="gf-timepicker-relative-section">
|
||||
<h3 className="section-heading">Quick ranges</h3>
|
||||
{Object.keys(timeOptions).map(section => {
|
||||
const group = timeOptions[section];
|
||||
return (
|
||||
<ul key={section}>
|
||||
{group.map(option => (
|
||||
<li className={option.active ? 'active' : ''} key={option.display}>
|
||||
<a onClick={() => this.handleClickRelativeOption(option)}>{option.display}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
})}
|
||||
<div className="popover-box">
|
||||
<div className="popover-box__header">
|
||||
<span className="popover-box__title">Custom range</span>
|
||||
</div>
|
||||
<div className="popover-box__body gf-timepicker-absolute-section">
|
||||
<label className="small">From:</label>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form max-width-28">
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input input-large timepicker-from"
|
||||
value={fromRaw}
|
||||
onChange={this.handleChangeFrom}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="small">To:</label>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form max-width-28">
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input input-large timepicker-to"
|
||||
value={toRaw}
|
||||
onChange={this.handleChangeTo}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="gf-form">
|
||||
<button className="btn gf-form-btn btn-secondary" onClick={this.handleClickApply}>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -112,7 +112,7 @@ export function updateFolderPermission(itemToUpdate: DashboardAcl, level: Permis
|
||||
|
||||
const updated = toUpdateItem(item);
|
||||
|
||||
// if this is the item we want to update, update it's permisssion
|
||||
// if this is the item we want to update, update it's permission
|
||||
if (itemToUpdate === item) {
|
||||
updated.permission = level;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, any>;
|
||||
|
||||
export enum ActionTypes {
|
||||
LoadOrganization = 'LOAD_ORGANISATION',
|
||||
LoadOrganization = 'LOAD_ORGANIZATION',
|
||||
SetOrganizationName = 'SET_ORGANIZATION_NAME',
|
||||
}
|
||||
|
||||
@@ -19,9 +19,9 @@ interface SetOrganizationNameAction {
|
||||
payload: string;
|
||||
}
|
||||
|
||||
const organisationLoaded = (organisation: Organization) => ({
|
||||
const organizationLoaded = (organization: Organization) => ({
|
||||
type: ActionTypes.LoadOrganization,
|
||||
payload: organisation,
|
||||
payload: organization,
|
||||
});
|
||||
|
||||
export const setOrganizationName = (orgName: string) => ({
|
||||
@@ -33,10 +33,10 @@ export type Action = LoadOrganizationAction | SetOrganizationNameAction;
|
||||
|
||||
export function loadOrganization(): ThunkResult<void> {
|
||||
return async dispatch => {
|
||||
const organisationResponse = await getBackendSrv().get('/api/org');
|
||||
dispatch(organisationLoaded(organisationResponse));
|
||||
const organizationResponse = await getBackendSrv().get('/api/org');
|
||||
dispatch(organizationLoaded(organizationResponse));
|
||||
|
||||
return organisationResponse;
|
||||
return organizationResponse;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,6 @@ class MetricsPanelCtrl extends PanelCtrl {
|
||||
constructor($scope, $injector) {
|
||||
super($scope, $injector);
|
||||
|
||||
// make metrics tab the default
|
||||
this.editorTabIndex = 1;
|
||||
this.$q = $injector.get('$q');
|
||||
this.contextSrv = $injector.get('contextSrv');
|
||||
this.datasourceSrv = $injector.get('datasourceSrv');
|
||||
@@ -90,7 +88,7 @@ class MetricsPanelCtrl extends PanelCtrl {
|
||||
.then(this.issueQueries.bind(this))
|
||||
.then(this.handleQueryResult.bind(this))
|
||||
.catch(err => {
|
||||
// if cancelled keep loading set to true
|
||||
// if canceled keep loading set to true
|
||||
if (err.cancelled) {
|
||||
console.log('Panel request cancelled', err);
|
||||
return;
|
||||
|
||||
@@ -18,7 +18,6 @@ export class PanelCtrl {
|
||||
panel: any;
|
||||
error: any;
|
||||
dashboard: any;
|
||||
editorTabIndex: number;
|
||||
pluginName: string;
|
||||
pluginId: string;
|
||||
editorTabs: any;
|
||||
@@ -39,7 +38,7 @@ export class PanelCtrl {
|
||||
this.$location = $injector.get('$location');
|
||||
this.$scope = $scope;
|
||||
this.$timeout = $injector.get('$timeout');
|
||||
this.editorTabIndex = 0;
|
||||
this.editorTabs = [];
|
||||
this.events = this.panel.events;
|
||||
this.timing = {};
|
||||
|
||||
@@ -90,10 +89,10 @@ export class PanelCtrl {
|
||||
}
|
||||
|
||||
initEditMode() {
|
||||
this.editorTabs = [];
|
||||
|
||||
this.editModeInitiated = true;
|
||||
this.events.emit('init-edit-mode', null);
|
||||
if (!this.editModeInitiated) {
|
||||
this.editModeInitiated = true;
|
||||
this.events.emit('init-edit-mode', null);
|
||||
}
|
||||
}
|
||||
|
||||
addEditorTab(title, directiveFn, index?, icon?) {
|
||||
@@ -212,11 +211,6 @@ export class PanelCtrl {
|
||||
this.containerHeight = $(window).height();
|
||||
}
|
||||
|
||||
// hacky solution
|
||||
if (this.panel.isEditing && !this.editModeInitiated) {
|
||||
this.initEditMode();
|
||||
}
|
||||
|
||||
this.height = this.containerHeight - (PANEL_BORDER + PANEL_HEADER_HEIGHT);
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ export const getMockPlugin = () => {
|
||||
url: 'url/to/GrafanaLabs',
|
||||
},
|
||||
description: 'pretty decent plugin',
|
||||
links: ['one link'],
|
||||
links: [{ name: 'project', url: 'one link' }],
|
||||
logos: { small: 'small/logo', large: 'large/logo' },
|
||||
screenshots: [{ path: `screenshot` }],
|
||||
updated: '2018-09-26',
|
||||
|
||||
@@ -77,8 +77,8 @@ describe('VariableSrv init', function(this: any) {
|
||||
{
|
||||
name: 'apps',
|
||||
type: type,
|
||||
current: { text: 'test', value: 'test' },
|
||||
options: [{ text: 'test', value: 'test' }],
|
||||
current: { text: 'Test', value: 'test' },
|
||||
options: [{ text: 'Test', value: 'test' }],
|
||||
},
|
||||
];
|
||||
scenario.urlParams['var-apps'] = 'new';
|
||||
@@ -161,11 +161,11 @@ describe('VariableSrv init', function(this: any) {
|
||||
name: 'apps',
|
||||
type: 'query',
|
||||
multi: true,
|
||||
current: { text: 'val1', value: 'val1' },
|
||||
current: { text: 'Val1', value: 'val1' },
|
||||
options: [
|
||||
{ text: 'val1', value: 'val1' },
|
||||
{ text: 'val2', value: 'val2' },
|
||||
{ text: 'val3', value: 'val3', selected: true },
|
||||
{ text: 'Val1', value: 'val1' },
|
||||
{ text: 'Val2', value: 'val2' },
|
||||
{ text: 'Val3', value: 'val3', selected: true },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -177,7 +177,7 @@ describe('VariableSrv init', function(this: any) {
|
||||
expect(variable.current.value.length).toBe(2);
|
||||
expect(variable.current.value[0]).toBe('val2');
|
||||
expect(variable.current.value[1]).toBe('val1');
|
||||
expect(variable.current.text).toBe('val2 + val1');
|
||||
expect(variable.current.text).toBe('Val2 + Val1');
|
||||
expect(variable.options[0].selected).toBe(true);
|
||||
expect(variable.options[1].selected).toBe(true);
|
||||
});
|
||||
@@ -188,6 +188,30 @@ describe('VariableSrv init', function(this: any) {
|
||||
});
|
||||
});
|
||||
|
||||
describeInitScenario(
|
||||
'when template variable is present in url multiple times and variables have no text',
|
||||
scenario => {
|
||||
scenario.setup(() => {
|
||||
scenario.variables = [
|
||||
{
|
||||
name: 'apps',
|
||||
type: 'query',
|
||||
multi: true,
|
||||
},
|
||||
];
|
||||
scenario.urlParams['var-apps'] = ['val1', 'val2'];
|
||||
});
|
||||
|
||||
it('should display concatenated values in text', () => {
|
||||
const variable = ctx.variableSrv.variables[0];
|
||||
expect(variable.current.value.length).toBe(2);
|
||||
expect(variable.current.value[0]).toBe('val1');
|
||||
expect(variable.current.value[1]).toBe('val2');
|
||||
expect(variable.current.text).toBe('val1 + val2');
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
describeInitScenario('when template variable is present in url multiple times using key/values', scenario => {
|
||||
scenario.setup(() => {
|
||||
scenario.variables = [
|
||||
|
||||
@@ -237,8 +237,10 @@ export class VariableSrv {
|
||||
setOptionAsCurrent(variable, option) {
|
||||
variable.current = _.cloneDeep(option);
|
||||
|
||||
if (_.isArray(variable.current.text)) {
|
||||
if (_.isArray(variable.current.text) && variable.current.text.length > 0) {
|
||||
variable.current.text = variable.current.text.join(' + ');
|
||||
} else if (_.isArray(variable.current.value) && variable.current.value[0] !== '$__all') {
|
||||
variable.current.text = variable.current.value.join(' + ');
|
||||
}
|
||||
|
||||
this.selectOptionsForCurrentValue(variable);
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('getLoglevel()', () => {
|
||||
});
|
||||
|
||||
describe('parseLabels()', () => {
|
||||
it('returns no labels on emtpy labels string', () => {
|
||||
it('returns no labels on empty labels string', () => {
|
||||
expect(parseLabels('')).toEqual({});
|
||||
expect(parseLabels('{}')).toEqual({});
|
||||
});
|
||||
@@ -46,7 +46,7 @@ describe('parseLabels()', () => {
|
||||
});
|
||||
|
||||
describe('formatLabels()', () => {
|
||||
it('returns no labels on emtpy label set', () => {
|
||||
it('returns no labels on empty label set', () => {
|
||||
expect(formatLabels({})).toEqual('');
|
||||
expect(formatLabels({}, 'foo')).toEqual('foo');
|
||||
});
|
||||
|
||||
@@ -60,7 +60,6 @@ class DashListCtrl extends PanelCtrl {
|
||||
}
|
||||
|
||||
onInitEditMode() {
|
||||
this.editorTabIndex = 1;
|
||||
this.modes = ['starred', 'search', 'recently viewed'];
|
||||
this.addEditorTab('Options', 'public/app/plugins/panel/dashlist/editor.html');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Graph Panel - Native Plugin
|
||||
# Graph Panel
|
||||
|
||||
The Graph is the main graph panel and is **included** with Grafana. It provides a very rich set of graphing options.
|
||||
This is the main Graph panel and is **included** with Grafana. It provides a very rich set of graphing options.
|
||||
|
||||
Read more about it here:
|
||||
For full reference documentation:
|
||||
|
||||
[http://docs.grafana.org/reference/graph/](http://docs.grafana.org/reference/graph/)
|
||||
[http://docs.grafana.org/reference/graph/](http://docs.grafana.org/reference/graph/)
|
||||
|
||||
@@ -737,7 +737,7 @@ class GraphElement {
|
||||
if (min && max && ticks) {
|
||||
const range = max - min;
|
||||
const secPerTick = range / ticks / 1000;
|
||||
// Need have 10 milisecond margin on the day range
|
||||
// Need have 10 millisecond margin on the day range
|
||||
// As sometimes last 24 hour dashboard evaluates to more than 86400000
|
||||
const oneDay = 86400010;
|
||||
const oneYear = 31536000000;
|
||||
|
||||
@@ -54,7 +54,7 @@ export function createEditPopover(element, event, plot) {
|
||||
const eventManager = plot.getOptions().events.manager;
|
||||
if (eventManager.editorOpen) {
|
||||
// update marker element to attach to (needed in case of legend on the right
|
||||
// when there is a double render pass and the inital marker element is removed)
|
||||
// when there is a double render pass and the initial marker element is removed)
|
||||
markerElementToAttachTo = element;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ class PluginListCtrl extends PanelCtrl {
|
||||
}
|
||||
|
||||
onInitEditMode() {
|
||||
this.editorTabIndex = 1;
|
||||
this.addEditorTab('Options', 'public/app/plugins/panel/pluginlist/editor.html');
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ export class TextPanelCtrl extends PanelCtrl {
|
||||
|
||||
onInitEditMode() {
|
||||
this.addEditorTab('Options', 'public/app/plugins/panel/text/editor.html');
|
||||
this.editorTabIndex = 1;
|
||||
|
||||
if (this.panel.mode === 'text') {
|
||||
this.panel.mode = 'markdown';
|
||||
|
||||
@@ -57,13 +57,18 @@ export interface PluginInclude {
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface PluginMetaInfoLink {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface PluginMetaInfo {
|
||||
author: {
|
||||
name: string;
|
||||
url?: string;
|
||||
};
|
||||
description: string;
|
||||
links: string[];
|
||||
links: PluginMetaInfoLink[];
|
||||
logos: {
|
||||
large: string;
|
||||
small: string;
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"folderId": 0,
|
||||
"headings": true,
|
||||
"id": 3,
|
||||
"limit": 4,
|
||||
"limit": 30,
|
||||
"links": [],
|
||||
"query": "",
|
||||
"recent": true,
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
@import 'components/thresholds';
|
||||
@import 'components/toggle_button_group';
|
||||
@import 'components/value-mappings';
|
||||
@import 'components/popover-box';
|
||||
|
||||
// PAGES
|
||||
@import 'pages/login';
|
||||
|
||||
@@ -400,3 +400,7 @@ $logs-color-unkown: $gray-2;
|
||||
$button-toggle-group-btn-active-bg: linear-gradient(90deg, $orange, $red);
|
||||
$button-toggle-group-btn-active-shadow: inset 0 0 4px $black;
|
||||
$button-toggle-group-btn-seperator-border: 1px solid $page-bg;
|
||||
|
||||
$vertical-resize-handle-bg: $dark-5;
|
||||
$vertical-resize-handle-dots: $gray-1;
|
||||
$vertical-resize-handle-dots-hover: $gray-2;
|
||||
|
||||
@@ -409,3 +409,7 @@ $logs-color-unkown: $gray-5;
|
||||
$button-toggle-group-btn-active-bg: $brand-primary;
|
||||
$button-toggle-group-btn-active-shadow: inset 0 0 4px $white;
|
||||
$button-toggle-group-btn-seperator-border: 1px solid $gray-6;
|
||||
|
||||
$vertical-resize-handle-bg: $gray-4;
|
||||
$vertical-resize-handle-dots: $gray-3;
|
||||
$vertical-resize-handle-dots-hover: $gray-2;
|
||||
|
||||
@@ -84,46 +84,34 @@
|
||||
}
|
||||
}
|
||||
|
||||
.panel-editor-resizer {
|
||||
position: absolute;
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
top: -23px;
|
||||
text-align: center;
|
||||
border-bottom: 2px dashed transparent;
|
||||
|
||||
&:hover {
|
||||
transition: border-color 0.2s ease-in 0.4s;
|
||||
transition-delay: 0.2s;
|
||||
border-color: $text-color-faint;
|
||||
}
|
||||
.panel-editor-container__resizer {
|
||||
position: relative;
|
||||
margin-top: -3px;
|
||||
}
|
||||
|
||||
.panel-editor-resizer__handle {
|
||||
display: inline-block;
|
||||
width: 180px;
|
||||
position: relative;
|
||||
border-radius: 2px;
|
||||
height: 7px;
|
||||
cursor: grabbing;
|
||||
background: $input-label-bg;
|
||||
top: -9px;
|
||||
display: block;
|
||||
background: $vertical-resize-handle-bg;
|
||||
width: 150px;
|
||||
margin-left: -75px;
|
||||
height: 6px;
|
||||
cursor: ns-resize;
|
||||
border-radius: 3px;
|
||||
margin: 0 auto;
|
||||
|
||||
&:hover {
|
||||
transition: background 0.2s ease-in 0.4s;
|
||||
transition-delay: 0.2s;
|
||||
background: linear-gradient(90deg, $orange, $red);
|
||||
.panel-editor-resizer__handle-dots {
|
||||
transition: opacity 0.2s ease-in;
|
||||
opacity: 0;
|
||||
}
|
||||
&::before {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
top: 2px;
|
||||
border-top: 2px dotted $vertical-resize-handle-dots;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-editor-resizer__handle-dots {
|
||||
border-top: 2px dashed $text-color-faint;
|
||||
position: relative;
|
||||
top: 4px;
|
||||
&:hover::before {
|
||||
border-color: $vertical-resize-handle-dots-hover;
|
||||
}
|
||||
}
|
||||
|
||||
.viz-picker {
|
||||
@@ -149,7 +137,6 @@
|
||||
display: flex;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
//border: 1px solid transparent;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: 6px;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Colours
|
||||
// Colors
|
||||
$progress-color-dark: $panel-bg !default;
|
||||
$progress-color: $panel-bg !default;
|
||||
$progress-color-light: $panel-bg !default;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
.popover-box {
|
||||
background-color: $popover-bg;
|
||||
color: $popover-color;
|
||||
border: 1px solid $popover-border-color;
|
||||
border-radius: $border-radius;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.popover-box__header {
|
||||
background-color: $popover-border-color;
|
||||
padding: 6px 10px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.popover-box__title {
|
||||
font-weight: $font-weight-semi-bold;
|
||||
padding-right: $spacer;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.popover-box__body {
|
||||
padding: 20px 10px 10px 10px;
|
||||
}
|
||||
|
||||
.popover-box__close {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -66,9 +66,6 @@
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
.threshold-row-base {
|
||||
}
|
||||
|
||||
.threshold-row-remove {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -13,23 +13,28 @@
|
||||
}
|
||||
|
||||
.gf-timepicker-dropdown {
|
||||
position: absolute;
|
||||
top: $navbarHeight;
|
||||
right: 0;
|
||||
padding: 10px 20px;
|
||||
background-color: $page-bg;
|
||||
border-radius: 0 0 0 4px;
|
||||
box-shadow: $search-shadow;
|
||||
z-index: $zindex-dropdown;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: $navbarHeight;
|
||||
width: 550px;
|
||||
|
||||
.gf-timepicker-absolute-section {
|
||||
width: 290px;
|
||||
float: left;
|
||||
padding: 0 10px;
|
||||
select {
|
||||
width: 183px;
|
||||
margin-bottom: 0;
|
||||
.popover-box {
|
||||
max-width: 100%;
|
||||
|
||||
&:first-child {
|
||||
border-radius: $border-radius $border-radius 0 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 $border-radius $border-radius;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,9 +53,9 @@
|
||||
}
|
||||
|
||||
.gf-timepicker-relative-section {
|
||||
padding: 0 20px 0 30px;
|
||||
min-height: 237px;
|
||||
float: left;
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
float: left;
|
||||
|
||||
Reference in New Issue
Block a user