mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into websocket
This commit is contained in:
commit
c711fb67c8
@ -16,6 +16,7 @@
|
||||
* **KairosDB** The data source is no longer included in default builds, but can easily be installed via improved plugin system, closes [#3524](https://github.com/grafana/grafana/issues/3524)
|
||||
|
||||
### Enhancements
|
||||
* **LDAP**: Support for nested LDAP Groups, closes [#4401](https://github.com/grafana/grafana/issues/4401), [#3808](https://github.com/grafana/grafana/issues/3808)
|
||||
* **Sessions**: Support for memcached as session storage, closes [#3458](https://github.com/grafana/grafana/issues/3458)
|
||||
* **mysql**: Grafana now supports ssl for mysql, closes [#3584](https://github.com/grafana/grafana/issues/3584)
|
||||
* **snapshot**: Annotations are now included in snapshots, closes [#3635](https://github.com/grafana/grafana/issues/3635)
|
||||
|
46
docs/sources/plugins/app.md
Normal file
46
docs/sources/plugins/app.md
Normal file
@ -0,0 +1,46 @@
|
||||
---
|
||||
page_title: App plugin
|
||||
page_description: App plugin for Grafana
|
||||
page_keywords: grafana, plugins, documentation
|
||||
---
|
||||
|
||||
> Our goal is not to have a very extensive documentation but rather have actual code that people can look at. An example implementation of an app can be found in this [example app repo](https://github.com/grafana/example-app)
|
||||
|
||||
# Apps
|
||||
|
||||
App plugins is a new kind of grafana plugin that can bundle datasource and panel plugins within one package. It also enable the plugin author to create custom pages within grafana. The custom pages enables the plugin author to include things like documentation, sign up forms or controlling other services using HTTP requests.
|
||||
|
||||
Datasource and panel plugins will show up like normal plugins. The app pages will be available in the main menu.
|
||||
|
||||
<img class="no-shadow" src="/img/v3/app-in-main-menu.png">
|
||||
|
||||
## Enabling app plugins
|
||||
After installing an app it have to be enabled before it show up as an datasource or panel. You can do that on the app page in the config tab.
|
||||
|
||||
## README.md
|
||||
|
||||
The readme file in the mounted folder will show up in the overview tab on the app page.
|
||||
|
||||
## Module exports
|
||||
```javascript
|
||||
export {
|
||||
ExampleAppConfigCtrl as ConfigCtrl,
|
||||
StreamPageCtrl,
|
||||
LogsPageCtrl
|
||||
};
|
||||
```
|
||||
The only required export is the ConfigCtrl. Both StreamPageCtrl and LogsPageCtrl are custom pages defined in plugin.json
|
||||
|
||||
## Custom pages
|
||||
Custom pages are defined in the plugin.json like this.
|
||||
```json
|
||||
"pages": [
|
||||
{ "name": "Live stream", "component": "StreamPageCtrl", "role": "Editor"},
|
||||
{ "name": "Log view", "component": "LogsPageCtrl", "role": "Viewer"}
|
||||
]
|
||||
```
|
||||
The component field have to match one of the components exported in the module.js in the root of the plugin.
|
||||
|
||||
## Bundled plugins
|
||||
|
||||
When Grafana starts it will scan all directories within an app plugin and load folders containing a plugin.json as an plugin.
|
@ -4,7 +4,7 @@ page_description: Datasource plugins for Grafana
|
||||
page_keywords: grafana, plugins, documentation
|
||||
---
|
||||
|
||||
> Our goal is not to have a very extensive documentation but rather have actual code that people can look at. An example implementation of a datasource can be found in the grafana repo under /examples/datasource-plugin-genericdatasource
|
||||
> Our goal is not to have a very extensive documentation but rather have actual code that people can look at. An example implementation of a datasource can be found in this [example datasource repo](https://github.com/grafana/simple-json-datasource)
|
||||
|
||||
# Datasources
|
||||
|
||||
|
@ -22,9 +22,7 @@ The easiset way to try your plugin with grafana is to [setup grafana for develop
|
||||
## Examples / boilerplate
|
||||
We currently have three different examples that you can fork to get started developing your grafana plugin.
|
||||
|
||||
- [generic-datasource](https://github.com/grafana/grafana/tree/master/examples/datasource-plugin-genericdatasource) (small datasource plugin for quering json data from backends)
|
||||
- [simple-json-datasource](https://github.com/grafana/simple-json-datasource) (small datasource plugin for quering json data from backends)
|
||||
- [panel-boilderplate-es5](https://github.com/grafana/grafana/tree/master/examples/panel-boilerplate-es5)
|
||||
- [nginx-app](https://github.com/grafana/grafana/tree/master/examples/nginx-app)
|
||||
- [example-app](https://github.com/grafana/example-app)
|
||||
|
||||
## Publish your plugin
|
||||
We are currently working on this.
|
||||
|
@ -17,30 +17,30 @@ On Linux systems the grafana-cli will assume that the grafana plugin directory i
|
||||
|
||||
List available plugins
|
||||
```
|
||||
grafana-cli list-remote
|
||||
grafana-cli plugins list-remote
|
||||
```
|
||||
|
||||
Install a plugin type
|
||||
```
|
||||
grafana-cli install <plugin-id>
|
||||
grafana-cli plugins install <plugin-id>
|
||||
```
|
||||
|
||||
List installed plugins
|
||||
```
|
||||
grafana-cli ls
|
||||
grafana-cli plugins ls
|
||||
```
|
||||
|
||||
Upgrade all installed plugins
|
||||
```
|
||||
grafana-cli upgrade-all
|
||||
grafana-cli plugins upgrade-all
|
||||
```
|
||||
|
||||
Upgrade one plugin
|
||||
```
|
||||
grafana-cli upgrade <plugin-id>
|
||||
grafana-cli plugins upgrade <plugin-id>
|
||||
```
|
||||
|
||||
Remove one plugin
|
||||
```
|
||||
grafana-cli remove <plugin-id>
|
||||
grafana-cli plugins remove <plugin-id>
|
||||
```
|
||||
|
@ -30,7 +30,8 @@ From the Grafana Server Admin page, you can access the System Info page which su
|
||||
|
||||
## Why would I have multiple Organizations?
|
||||
|
||||
In many cases, a Grafana installation will only have one Organization. There's no need to create multiple Organizations
|
||||
if you want all your users to have access to the same set of dashboards and data. In a multitenant deployment,
|
||||
Organizations in Grafana are best suited for a **multi-tenant deployment**. In a multi-tenant deployment,
|
||||
Organizations can be used to provide a full Grafana experience to different sets of users from a single Grafana instance,
|
||||
at the convenience of the Grafana Administrator.
|
||||
at the convenience of the Grafana Administrator.
|
||||
|
||||
In most cases, a Grafana installation will only have **one** Organization. Since dashboards, data sources and other configuration items are not shared between organizations, there's no need to create multiple Organizations if you want all your users to have access to the same set of dashboards and data.
|
||||
|
@ -80,7 +80,7 @@ func Register(r *macaron.Macaron) {
|
||||
r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot)
|
||||
r.Get("/api/snapshot/shared-options/", GetSharingOptions)
|
||||
r.Get("/api/snapshots/:key", GetDashboardSnapshot)
|
||||
r.Get("/api/snapshots-delete/:key", DeleteDashboardSnapshot)
|
||||
r.Get("/api/snapshots-delete/:key", reqEditorRole, DeleteDashboardSnapshot)
|
||||
|
||||
// api renew session based on remember cookie
|
||||
r.Get("/api/login/ping", quota("session"), LoginApiPing)
|
||||
|
@ -3,18 +3,18 @@ package dtos
|
||||
import "github.com/grafana/grafana/pkg/plugins"
|
||||
|
||||
type PluginSetting struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Id string `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Pinned bool `json:"pinned"`
|
||||
Module string `json:"module"`
|
||||
BaseUrl string `json:"baseUrl"`
|
||||
Info *plugins.PluginInfo `json:"info"`
|
||||
Pages []*plugins.AppPluginPage `json:"pages"`
|
||||
Includes []*plugins.PluginInclude `json:"includes"`
|
||||
Dependencies *plugins.PluginDependencies `json:"dependencies"`
|
||||
JsonData map[string]interface{} `json:"jsonData"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Id string `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Pinned bool `json:"pinned"`
|
||||
Module string `json:"module"`
|
||||
BaseUrl string `json:"baseUrl"`
|
||||
Info *plugins.PluginInfo `json:"info"`
|
||||
Includes []*plugins.PluginInclude `json:"includes"`
|
||||
Dependencies *plugins.PluginDependencies `json:"dependencies"`
|
||||
JsonData map[string]interface{} `json:"jsonData"`
|
||||
DefaultNavUrl string `json:"defaultNavUrl"`
|
||||
}
|
||||
|
||||
type PluginListItem struct {
|
||||
|
@ -56,8 +56,8 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
|
||||
if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
|
||||
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Divider: true})
|
||||
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "New", Url: setting.AppSubUrl + "/dashboard/new"})
|
||||
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "Import", Url: setting.AppSubUrl + "/import/dashboard"})
|
||||
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "New", Icon: "fa fa-plus", Url: setting.AppSubUrl + "/dashboard/new"})
|
||||
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "Import", Icon: "fa fa-download", Url: setting.AppSubUrl + "/import/dashboard"})
|
||||
}
|
||||
|
||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
||||
@ -88,22 +88,35 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
|
||||
for _, plugin := range enabledPlugins.Apps {
|
||||
if plugin.Pinned {
|
||||
pageLink := &dtos.NavLink{
|
||||
appLink := &dtos.NavLink{
|
||||
Text: plugin.Name,
|
||||
Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit",
|
||||
Url: plugin.DefaultNavUrl,
|
||||
Img: plugin.Info.Logos.Small,
|
||||
}
|
||||
|
||||
for _, page := range plugin.Pages {
|
||||
if !page.SuppressNav {
|
||||
pageLink.Children = append(pageLink.Children, &dtos.NavLink{
|
||||
Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/page/" + page.Slug,
|
||||
Text: page.Name,
|
||||
})
|
||||
for _, include := range plugin.Includes {
|
||||
if include.Type == "page" && include.AddToNav {
|
||||
link := &dtos.NavLink{
|
||||
Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/page/" + include.Slug,
|
||||
Text: include.Name,
|
||||
}
|
||||
appLink.Children = append(appLink.Children, link)
|
||||
}
|
||||
if include.Type == "dashboard" && include.AddToNav {
|
||||
link := &dtos.NavLink{
|
||||
Url: setting.AppSubUrl + "/dashboard/db/" + include.Slug,
|
||||
Text: include.Name,
|
||||
}
|
||||
appLink.Children = append(appLink.Children, link)
|
||||
}
|
||||
}
|
||||
|
||||
data.MainNavLinks = append(data.MainNavLinks, pageLink)
|
||||
if c.OrgRole == m.ROLE_ADMIN {
|
||||
appLink.Children = append(appLink.Children, &dtos.NavLink{Divider: true})
|
||||
appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "fa fa-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"})
|
||||
}
|
||||
|
||||
data.MainNavLinks = append(data.MainNavLinks, appLink)
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,10 +126,10 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
||||
Icon: "fa fa-fw fa-cogs",
|
||||
Url: setting.AppSubUrl + "/admin",
|
||||
Children: []*dtos.NavLink{
|
||||
{Text: "Global Users", Icon: "fa fa-fw fa-cogs", Url: setting.AppSubUrl + "/admin/users"},
|
||||
{Text: "Global Orgs", Icon: "fa fa-fw fa-cogs", Url: setting.AppSubUrl + "/admin/orgs"},
|
||||
{Text: "Server Settings", Icon: "fa fa-fw fa-cogs", Url: setting.AppSubUrl + "/admin/settings"},
|
||||
{Text: "Server Stats", Icon: "fa-fw fa-cogs", Url: setting.AppSubUrl + "/admin/stats"},
|
||||
{Text: "Global Users", Url: setting.AppSubUrl + "/admin/users"},
|
||||
{Text: "Global Orgs", Url: setting.AppSubUrl + "/admin/orgs"},
|
||||
{Text: "Server Settings", Url: setting.AppSubUrl + "/admin/settings"},
|
||||
{Text: "Server Stats", Url: setting.AppSubUrl + "/admin/stats"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -72,18 +72,15 @@ func GetPluginSettingById(c *middleware.Context) Response {
|
||||
} else {
|
||||
|
||||
dto := &dtos.PluginSetting{
|
||||
Type: def.Type,
|
||||
Id: def.Id,
|
||||
Name: def.Name,
|
||||
Info: &def.Info,
|
||||
Dependencies: &def.Dependencies,
|
||||
Includes: def.Includes,
|
||||
BaseUrl: def.BaseUrl,
|
||||
Module: def.Module,
|
||||
}
|
||||
|
||||
if app, exists := plugins.Apps[pluginId]; exists {
|
||||
dto.Pages = app.Pages
|
||||
Type: def.Type,
|
||||
Id: def.Id,
|
||||
Name: def.Name,
|
||||
Info: &def.Info,
|
||||
Dependencies: &def.Dependencies,
|
||||
Includes: def.Includes,
|
||||
BaseUrl: def.BaseUrl,
|
||||
Module: def.Module,
|
||||
DefaultNavUrl: def.DefaultNavUrl,
|
||||
}
|
||||
|
||||
query := m.GetPluginSettingByIdQuery{PluginId: pluginId, OrgId: c.OrgId}
|
||||
|
@ -22,7 +22,7 @@ func runCommand(command func(commandLine CommandLine) error) func(context *cli.C
|
||||
}
|
||||
}
|
||||
|
||||
var Commands = []cli.Command{
|
||||
var pluginCommands = []cli.Command{
|
||||
{
|
||||
Name: "install",
|
||||
Usage: "install <plugin name>",
|
||||
@ -49,3 +49,11 @@ var Commands = []cli.Command{
|
||||
Action: runCommand(removeCommand),
|
||||
},
|
||||
}
|
||||
|
||||
var Commands = []cli.Command{
|
||||
{
|
||||
Name: "plugins",
|
||||
Usage: "Manage plugins for grafana",
|
||||
Subcommands: pluginCommands,
|
||||
},
|
||||
}
|
||||
|
@ -12,10 +12,6 @@ import (
|
||||
var version = "master"
|
||||
|
||||
func getGrafanaPluginPath() string {
|
||||
if os.Getenv("GF_PLUGIN_DIR") != "" {
|
||||
return os.Getenv("GF_PLUGIN_DIR")
|
||||
}
|
||||
|
||||
os := runtime.GOOS
|
||||
if os == "windows" {
|
||||
return "C:\\opt\\grafana\\plugins"
|
||||
@ -34,14 +30,16 @@ func main() {
|
||||
app.Version = version
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "path",
|
||||
Usage: "path to the grafana installation",
|
||||
Value: getGrafanaPluginPath(),
|
||||
Name: "path",
|
||||
Usage: "path to the grafana installation",
|
||||
Value: getGrafanaPluginPath(),
|
||||
EnvVar: "GF_PLUGIN_DIR",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "repo",
|
||||
Usage: "url to the plugin repository",
|
||||
Value: "https://grafana-net.raintank.io/api/plugins",
|
||||
Name: "repo",
|
||||
Usage: "url to the plugin repository",
|
||||
Value: "https://grafana-net.raintank.io/api/plugins",
|
||||
EnvVar: "GF_PLUGIN_REPO",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "debug, d",
|
||||
|
@ -6,16 +6,9 @@ import (
|
||||
|
||||
"github.com/gosimple/slug"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type AppPluginPage struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Component string `json:"component"`
|
||||
Role models.RoleType `json:"role"`
|
||||
SuppressNav bool `json:"suppressNav"`
|
||||
}
|
||||
|
||||
type AppPluginCss struct {
|
||||
Light string `json:"light"`
|
||||
Dark string `json:"dark"`
|
||||
@ -23,7 +16,6 @@ type AppPluginCss struct {
|
||||
|
||||
type AppPlugin struct {
|
||||
FrontendPluginBase
|
||||
Pages []*AppPluginPage `json:"pages"`
|
||||
Routes []*AppPluginRoute `json:"routes"`
|
||||
|
||||
FoundChildPlugins []*PluginInclude `json:"-"`
|
||||
@ -84,10 +76,18 @@ func (app *AppPlugin) initApp() {
|
||||
}
|
||||
}
|
||||
|
||||
app.DefaultNavUrl = setting.AppSubUrl + "/plugins/" + app.Id + "/edit"
|
||||
|
||||
// slugify pages
|
||||
for _, page := range app.Pages {
|
||||
if page.Slug == "" {
|
||||
page.Slug = slug.Make(page.Name)
|
||||
for _, include := range app.Includes {
|
||||
if include.Slug == "" {
|
||||
include.Slug = slug.Make(include.Name)
|
||||
}
|
||||
if include.Type == "page" && include.DefaultNav {
|
||||
app.DefaultNavUrl = setting.AppSubUrl + "/plugins/" + app.Id + "/page/" + include.Slug
|
||||
}
|
||||
if include.Type == "dashboard" && include.DefaultNav {
|
||||
app.DefaultNavUrl = setting.AppSubUrl + "/dashboard/db/" + include.Slug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@ -41,6 +42,7 @@ type PluginBase struct {
|
||||
|
||||
IncludedInAppId string `json:"-"`
|
||||
PluginDir string `json:"-"`
|
||||
DefaultNavUrl string `json:"-"`
|
||||
|
||||
// cache for readme file contents
|
||||
Readme []byte `json:"-"`
|
||||
@ -74,10 +76,16 @@ type PluginDependencies struct {
|
||||
}
|
||||
|
||||
type PluginInclude struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Type string `json:"type"`
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Type string `json:"type"`
|
||||
Component string `json:"component"`
|
||||
Role models.RoleType `json:"role"`
|
||||
AddToNav bool `json:"addToNav"`
|
||||
DefaultNav bool `json:"defaultNav"`
|
||||
Slug string `json:"slug"`
|
||||
|
||||
Id string `json:"-"`
|
||||
}
|
||||
|
||||
type PluginDependencyItem struct {
|
||||
|
@ -8,9 +8,9 @@
|
||||
<i class="fa fa-chevron-left"></i>
|
||||
</a>
|
||||
|
||||
<a href="{{::ctrl.titleUrl}}" class="navbar-page-btn" ng-show="ctrl.title">
|
||||
<i class="{{::ctrl.icon}}"></i>
|
||||
{{::ctrl.title}}
|
||||
<a href="{{ctrl.titleUrl}}" class="navbar-page-btn" ng-show="ctrl.title">
|
||||
<i class="{{ctrl.icon}}"></i>
|
||||
{{ctrl.title}}
|
||||
</a>
|
||||
|
||||
<div ng-transclude></div>
|
||||
|
@ -40,7 +40,10 @@
|
||||
</a>
|
||||
<ul class="dropdown-menu" role="menu" ng-if="::item.children">
|
||||
<li ng-repeat="child in ::item.children" ng-class="{divider: child.divider}">
|
||||
<a href="{{::child.url}}">{{::child.text}}</a>
|
||||
<a href="{{::child.url}}">
|
||||
<i class="{{::child.icon}}" ng-show="::child.icon"></i>
|
||||
{{::child.text}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
@ -86,7 +86,7 @@ export class SideMenuCtrl {
|
||||
|
||||
switchOrg(orgId) {
|
||||
this.backendSrv.post('/api/user/using/' + orgId).then(() => {
|
||||
window.location.href = window.location.href;
|
||||
window.location.href = `${config.appSubUrl}/`;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -105,6 +105,13 @@ function (angular, _, coreModule, config) {
|
||||
});
|
||||
}
|
||||
|
||||
//populate error obj on Internal Error
|
||||
if (_.isString(err.data) && err.status === 500) {
|
||||
err.data = {
|
||||
error: err.statusText
|
||||
};
|
||||
}
|
||||
|
||||
// for Prometheus
|
||||
if (!err.data.message && _.isString(err.data.error)) {
|
||||
err.data.message = err.data.error;
|
||||
|
@ -1,21 +1,21 @@
|
||||
<navbar title="Organization" icon="icon-gf icon-gf-users">
|
||||
</navbar>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
<div class="page-container" ng-form="playlistEditForm">
|
||||
<div class="page-header">
|
||||
<h1>New Organization</h1>
|
||||
</div>
|
||||
|
||||
<h2 style="margin-top: 30px;">Add Organization</h2>
|
||||
<p class="playlist-description">Each organization contains their own dashboards, data sources and configuration, and cannot be shared between orgs. While users may belong to more than one, mutiple organization are most frequently used in multi-tenant deployments. </p>
|
||||
|
||||
<form name="form" class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Org. name</span>
|
||||
<input type="text" ng-model="newOrg.name" required class="gf-form-input" placeholder="organization name">
|
||||
</div>
|
||||
<br>
|
||||
<div class="gf-form-buttons-row">
|
||||
<button class="btn btn-success pull-right" ng-click="createOrg()">Create</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Org. name</span>
|
||||
<input type="text" ng-model="newOrg.name" required class="gf-form-input max-width-21" placeholder="organization name">
|
||||
</div>
|
||||
<br>
|
||||
<div class="gf-form-buttons-row">
|
||||
<button class="btn btn-success" ng-click="createOrg()">Create</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h1>Preferences</h1>
|
||||
<h1>Org Preferences</h1>
|
||||
</div>
|
||||
|
||||
<h3 class="page-heading">General</h3>
|
||||
|
@ -56,6 +56,7 @@ function (angular, $, _, Tether) {
|
||||
|
||||
template += '<a class="panel-menu-link" ';
|
||||
if (item.click) { template += ' ng-click="' + item.click + '"'; }
|
||||
if (item.href) { template += ' href="' + item.href + '"'; }
|
||||
template += '>';
|
||||
template += item.text + '</a>';
|
||||
});
|
||||
|
@ -4,8 +4,8 @@
|
||||
<div class="page-container">
|
||||
|
||||
<div class="page-header">
|
||||
<h1 ng-show="isNew">Add data source</h1>
|
||||
<h1 ng-show="!isNew">Edit data source</h1>
|
||||
<h1 ng-show="ctrl.isNew">Add data source</h1>
|
||||
<h1 ng-hide="ctrl.isNew">Edit data source</h1>
|
||||
|
||||
<div class="page-header-tabs" ng-show="ctrl.hasDashboards">
|
||||
<ul class="gf-tabs">
|
||||
|
@ -62,10 +62,6 @@
|
||||
<i class="{{plug.icon}}"></i>
|
||||
{{plug.name}}
|
||||
</li>
|
||||
<li ng-repeat="page in ctrl.model.pages" class="plugin-info-list-item">
|
||||
<i class="icon-gf icon-gf-share"></i>
|
||||
<a href="plugins/{{ctrl.model.id}}/page/{{page.slug}}">{{page.name}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section class="page-sidebar-section">
|
||||
|
@ -1,7 +1,7 @@
|
||||
<navbar icon="icon-gf icon-gf-apps" title="{{ctrl.appModel.name}}" title-url="plugins/{{ctrl.pluginId}}/edit">
|
||||
<navbar icon="icon-gf icon-gf-apps" title="{{ctrl.appModel.name}}" title-url="{{ctrl.appModel.defaultNavUrl}}">
|
||||
</navbar>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page-container" >
|
||||
<div ng-if="ctrl.page">
|
||||
<plugin-component type="app-page">
|
||||
</plugin-component>
|
||||
|
@ -11,11 +11,13 @@ export class AppPageCtrl {
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv, private $routeParams: any, private $rootScope) {
|
||||
this.pluginId = $routeParams.pluginId;
|
||||
|
||||
this.backendSrv.get(`/api/plugins/${this.pluginId}/settings`).then(app => {
|
||||
this.appModel = app;
|
||||
this.page = _.findWhere(app.pages, {slug: this.$routeParams.slug});
|
||||
this.page = _.findWhere(app.includes, {slug: this.$routeParams.slug});
|
||||
|
||||
if (!this.page) {
|
||||
$rootScope.appEvent('alert-error', ['App Page Not Found', '']);
|
||||
this.$rootScope.appEvent('alert-error', ['App Page Not Found', '']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ function (angular, _) {
|
||||
};
|
||||
|
||||
function regexEscape(value) {
|
||||
return value.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
return value.replace(/[\\^$*+?.()|[\]{}\/]/g, '\\$&');
|
||||
}
|
||||
|
||||
function luceneEscape(value) {
|
||||
|
@ -101,6 +101,12 @@ function (angular, _, $) {
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (!ctrl.panel.legend.show) {
|
||||
elem.empty();
|
||||
firstRender = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstRender) {
|
||||
elem.append($container);
|
||||
$container.on('click', '.graph-legend-icon', openColorSelector);
|
||||
|
@ -1,22 +0,0 @@
|
||||
<div class="graph-wrapper" ng-class="{'graph-legend-rightside': ctrl.panel.legend.rightSide}">
|
||||
<div class="graph-canvas-wrapper">
|
||||
|
||||
<div ng-if="datapointsWarning" class="datapoints-warning">
|
||||
<span class="small" ng-show="!datapointsCount">
|
||||
No datapoints <tip>No datapoints returned from metric query</tip>
|
||||
</span>
|
||||
<span class="small" ng-show="datapointsOutside">Datapoints outside time range <tip>Can be caused by timezone mismatch between browser and graphite server</tip></span>
|
||||
</div>
|
||||
|
||||
<div grafana-graph class="histogram-chart" ng-dblclick="ctrl.zoomOut()">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="graph-legend-wrapper" ng-if="ctrl.panel.legend.show" graph-legend></div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
|
||||
|
@ -4,6 +4,7 @@ import './graph';
|
||||
import './legend';
|
||||
import './series_overrides_ctrl';
|
||||
|
||||
import template from './template';
|
||||
import moment from 'moment';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import _ from 'lodash';
|
||||
@ -83,7 +84,7 @@ var panelDefaults = {
|
||||
};
|
||||
|
||||
class GraphCtrl extends MetricsPanelCtrl {
|
||||
static templateUrl = 'module.html';
|
||||
static template = template;
|
||||
|
||||
hiddenSeries: any = {};
|
||||
seriesList: any = [];
|
||||
|
@ -1,102 +1,123 @@
|
||||
<div class="editor-row gf-form-group">
|
||||
<div class="section">
|
||||
<h5>Chart Options</h5>
|
||||
<editor-opt-bool text="Bars" model="ctrl.panel.bars" change="ctrl.render()"></editor-opt-bool>
|
||||
<editor-opt-bool text="Lines" model="ctrl.panel.lines" change="ctrl.render()"></editor-opt-bool>
|
||||
<editor-opt-bool text="Points" model="ctrl.panel.points" change="ctrl.render()"></editor-opt-bool>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h5>Line options</h5>
|
||||
<div class="editor-option" ng-show="ctrl.panel.lines">
|
||||
<label class="small">Line Fill</label>
|
||||
<select class="input-mini" ng-model="ctrl.panel.fill" ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10]" ng-change="ctrl.render()"></select>
|
||||
<h5 class="section-heading">Draw Modes</h5>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-5">Bars</label>
|
||||
<editor-checkbox text="Enable" model="ctrl.panel.bars" change="ctrl.render()"></editor-checkbox>
|
||||
</div>
|
||||
<div class="editor-option" ng-show="ctrl.panel.lines">
|
||||
<label class="small">Line Width</label>
|
||||
<select class="input-mini" ng-model="ctrl.panel.linewidth" ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10]" ng-change="ctrl.render()"></select>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-5">Lines</label>
|
||||
<editor-checkbox text="Enable" model="ctrl.panel.lines" change="ctrl.render()"></editor-checkbox>
|
||||
</div>
|
||||
<div class="editor-option" ng-show="ctrl.panel.points">
|
||||
<label class="small">Point Radius</label>
|
||||
<select class="input-mini" ng-model="ctrl.panel.pointradius" ng-options="f for f in [1,2,3,4,5,6,7,8,9,10]" ng-change="ctrl.render()"></select>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-5">Points</label>
|
||||
<editor-checkbox text="Enable" model="ctrl.panel.points" change="ctrl.render()"></editor-checkbox>
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Null point mode<tip>Define how null values should be drawn</tip></label>
|
||||
<select class="input-medium" ng-model="ctrl.panel.nullPointMode" ng-options="f for f in ['connected', 'null', 'null as zero']" ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
|
||||
<editor-opt-bool text="Staircase line" model="ctrl.panel.steppedLine" change="ctrl.render()"></editor-opt-bool>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h5>Multiple Series</h5>
|
||||
|
||||
<editor-opt-bool text="Stack" model="ctrl.panel.stack" change="ctrl.render()"></editor-opt-bool>
|
||||
<editor-opt-bool text="Percent" model="ctrl.panel.percentage" change="ctrl.render()" tip="Stack as a percentage of total"></editor-opt-bool>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h5>Rendering</h5>
|
||||
<div class="editor-option">
|
||||
<label class="small">Flot <tip>client side</tip></label>
|
||||
<input type="radio" class="input-small" ng-model="ctrl.panel.renderer" value="flot" ng-change="ctrl.refresh()" />
|
||||
<h5 class="section-heading">Mode Options</h5>
|
||||
<div class="gf-form" ng-show="ctrl.panel.lines">
|
||||
<label class="gf-form-label width-8">Fill</label>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.fill" ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10]" ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Graphite PNG <tip>server side</tip></label>
|
||||
<input type="radio" class="input-small" ng-model="ctrl.panel.renderer" value="png" ng-change="ctr.refresh()" />
|
||||
<div class="gf-form" ng-show="ctrl.panel.lines">
|
||||
<label class="gf-form-label width-8">Line Width</label>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.linewidth" ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10]" ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.panel.points">
|
||||
<label class="gf-form-label width-8">Point Radius</label>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.pointradius" ng-options="f for f in [1,2,3,4,5,6,7,8,9,10]" ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.panel.lines">
|
||||
<label class="gf-form-label width-8">Staircase</label>
|
||||
<editor-checkbox text="" model="ctrl.panel.steppedLine" change="ctrl.render()"></editor-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h5 class="section-heading">Misc options</h5>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-7">Null value</label>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input max-width-8" ng-model="ctrl.panel.nullPointMode" ng-options="f for f in ['connected', 'null', 'null as zero']" ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-7">Renderer</label>
|
||||
<div class="gf-form-select-wrapper max-width-8">
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.renderer" ng-options="f for f in ['flot', 'png']" ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-7">Tooltip mode</label>
|
||||
<div class="gf-form-select-wrapper max-width-8">
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.tooltip.shared" ng-options="f.value as f.text for f in [{text: 'All series', value: true}, {text: 'Single', value: false}]" ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h5>Tooltip</h5>
|
||||
<editor-opt-bool
|
||||
text="All series" model="ctrl.panel.tooltip.shared" change="ctrl.render()"
|
||||
tip="Show all series on same tooltip and a x croshair to help follow all series">
|
||||
</editor-opt-bool>
|
||||
<div class="editor-option" ng-show="ctrl.panel.stack">
|
||||
<label class="small">Stacked Values <tip>How should the values in stacked charts to be calculated?</tip></label>
|
||||
<select class="input-small" ng-model="ctrl.panel.tooltip.value_type" ng-options="f for f in ['cumulative','individual']" ng-change="ctrl.render()"></select>
|
||||
<h5 class="section-heading">Multiple Series</h5>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-7">Stack</label>
|
||||
<editor-checkbox text="Enable" model="ctrl.panel.stack" change="ctrl.render()"></editor-checkbox>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.panel.stack">
|
||||
<label class="gf-form-label width-7">Percent</label>
|
||||
<editor-checkbox text="Enable" model="ctrl.panel.percent" change="ctrl.render()"></editor-checkbox>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.panel.stack">
|
||||
<label class="gf-form-label width-7">Tooltip value</label>
|
||||
<div class="gf-form-select-wrapper max-width-8">
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.tooltip.value_type" ng-options="f for f in ['cumulative','individual']" ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="editor-row">
|
||||
<div class="section">
|
||||
<h5>Series specific overrides <tip>Regex match example: /server[0-3]/i </tip></h5>
|
||||
<div class="tight-form-container">
|
||||
<div class="tight-form" ng-repeat="override in ctrl.panel.seriesOverrides" ng-controller="SeriesOverridesCtrl">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item">
|
||||
<i class="fa fa-remove pointer" ng-click="ctrl.removeSeriesOverride(override)"></i>
|
||||
</li>
|
||||
<h5>Series specific overrides <tip>Regex match example: /server[0-3]/i </tip></h5>
|
||||
<div class="tight-form-container">
|
||||
<div class="tight-form" ng-repeat="override in ctrl.panel.seriesOverrides" ng-controller="SeriesOverridesCtrl">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item">
|
||||
<i class="fa fa-remove pointer" ng-click="ctrl.removeSeriesOverride(override)"></i>
|
||||
</li>
|
||||
|
||||
<li class="tight-form-item">
|
||||
alias or regex
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
alias or regex
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<input type="text" ng-model="override.alias" bs-typeahead="getSeriesNames" ng-blur="ctrl.render()" data-min-length=0 data-items=100 class="input-medium tight-form-input" >
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" ng-model="override.alias" bs-typeahead="getSeriesNames" ng-blur="ctrl.render()" data-min-length=0 data-items=100 class="input-medium tight-form-input" >
|
||||
</li>
|
||||
|
||||
<li class="tight-form-item" ng-repeat="option in currentOverrides">
|
||||
<i class="pointer fa fa-remove" ng-click="removeOverride(option)"></i>
|
||||
<span ng-show="option.propertyName === 'color'">
|
||||
Color: <i class="fa fa-circle" ng-style="{color:option.value}"></i>
|
||||
</span>
|
||||
<span ng-show="option.propertyName !== 'color'">
|
||||
{{option.name}}: {{option.value}}
|
||||
</span>
|
||||
</li>
|
||||
<li class="tight-form-item" ng-repeat="option in currentOverrides">
|
||||
<i class="pointer fa fa-remove" ng-click="removeOverride(option)"></i>
|
||||
<span ng-show="option.propertyName === 'color'">
|
||||
Color: <i class="fa fa-circle" ng-style="{color:option.value}"></i>
|
||||
</span>
|
||||
<span ng-show="option.propertyName !== 'color'">
|
||||
{{option.name}}: {{option.value}}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<li class="dropdown" dropdown-typeahead="overrideMenu" dropdown-typeahead-on-select="setOverride($item, $subItem)">
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
<li class="dropdown" dropdown-typeahead="overrideMenu" dropdown-typeahead-on-select="setOverride($item, $subItem)">
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-inverse" style="margin-top: 20px" ng-click="ctrl.addSeriesOverride()">
|
||||
Add series specific option
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-inverse" style="margin-top: 20px" ng-click="ctrl.addSeriesOverride()">
|
||||
Add series specific option
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
28
public/app/plugins/panel/graph/template.ts
Normal file
28
public/app/plugins/panel/graph/template.ts
Normal file
@ -0,0 +1,28 @@
|
||||
var template = `
|
||||
<div class="graph-wrapper" ng-class="{'graph-legend-rightside': ctrl.panel.legend.rightSide}">
|
||||
<div class="graph-canvas-wrapper">
|
||||
|
||||
<div ng-if="datapointsWarning" class="datapoints-warning">
|
||||
<span class="small" ng-show="!datapointsCount">
|
||||
No datapoints <tip>No datapoints returned from metric query</tip>
|
||||
</span>
|
||||
<span class="small" ng-show="datapointsOutside">
|
||||
Datapoints outside time range
|
||||
<tip>Can be caused by timezone mismatch between browser and graphite server</tip>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div grafana-graph class="histogram-chart" ng-dblclick="ctrl.zoomOut()">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="graph-legend-wrapper" graph-legend></div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
`;
|
||||
|
||||
export default template;
|
||||
|
||||
|
@ -12,7 +12,7 @@ var panelDefaults = {
|
||||
export class TextPanelCtrl extends PanelCtrl {
|
||||
static templateUrl = `public/app/plugins/panel/text/module.html`;
|
||||
|
||||
converter: any;
|
||||
remarkable: any;
|
||||
content: string;
|
||||
|
||||
/** @ngInject */
|
||||
@ -54,21 +54,16 @@ export class TextPanelCtrl extends PanelCtrl {
|
||||
}
|
||||
|
||||
renderMarkdown(content) {
|
||||
var text = content
|
||||
.replace(/&/g, '&')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/</g, '<');
|
||||
|
||||
if (this.converter) {
|
||||
this.updateContent(this.converter.makeHtml(text));
|
||||
} else {
|
||||
if (!this.remarkable) {
|
||||
return System.import('remarkable').then(Remarkable => {
|
||||
var md = new Remarkable();
|
||||
this.remarkable = new Remarkable();
|
||||
this.$scope.$apply(() => {
|
||||
this.updateContent(md.render(text));
|
||||
this.updateContent(this.remarkable.render(content));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.updateContent(this.remarkable.render(content));
|
||||
}
|
||||
|
||||
updateContent(html) {
|
||||
|
@ -126,22 +126,22 @@ $btn-primary-bg-hl: lighten($brand-primary, 8%);
|
||||
$btn-secondary-bg: $blue-dark;
|
||||
$btn-secondary-bg-hl: lighten($blue-dark, 3%);
|
||||
|
||||
$btn-success-bg-hl: darken($green, 3%);
|
||||
$btn-success-bg: lighten($green, 3%);
|
||||
$btn-success-bg-hl: darken($green, 3%);
|
||||
|
||||
$btn-warning-bg: $brand-warning;
|
||||
$btn-warning-bg-hl: lighten($brand-warning, 8%);
|
||||
|
||||
$btn-danger-bg: lighten($red, 3%);
|
||||
$btn-danger-bg-hl: darken($red, 3%);
|
||||
$btn-danger-bg: $red;
|
||||
$btn-danger-bg-hl: lighten($red, 5%);
|
||||
|
||||
$btn-inverse-bg: $dark-3;
|
||||
$btn-inverse-bg-hl: lighten($dark-3, 1%);
|
||||
$btn-inverse-bg-hl: lighten($dark-3, 4%);
|
||||
$btn-inverse-text-color: $link-color;
|
||||
|
||||
$btn-link-color: $gray-3;
|
||||
|
||||
$iconContainerBackground: $black;
|
||||
$iconContainerBackground: $black;
|
||||
|
||||
// Forms
|
||||
// -------------------------
|
||||
|
@ -161,7 +161,7 @@ $table-sm-cell-padding: .3rem !default;
|
||||
// Forms
|
||||
$input-padding-x: .75rem !default;
|
||||
$input-padding-y: .6rem !default;
|
||||
$input-line-height: 1.35rem !default;
|
||||
$input-line-height: 1.42rem !default;
|
||||
|
||||
$input-btn-border-width: 1px;
|
||||
$input-border-radius: 0 $border-radius $border-radius 0 !default;
|
||||
@ -216,7 +216,7 @@ $btn-padding-y-sm: .25rem !default;
|
||||
$btn-padding-x-lg: 1.5rem !default;
|
||||
$btn-padding-y-lg: .75rem !default;
|
||||
|
||||
$btn-border-radius: 3px;
|
||||
$btn-border-radius: 2px;
|
||||
|
||||
// sidemenu
|
||||
$side-menu-width: 14rem;
|
||||
|
@ -95,8 +95,9 @@
|
||||
// Inverse appears as dark gray
|
||||
.btn-inverse {
|
||||
@include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl, $btn-inverse-text-color);
|
||||
box-shadow: none;
|
||||
border: 1px solid $tight-form-func-highlight-bg;
|
||||
//background: $card-background;
|
||||
box-shadow: $card-shadow;
|
||||
//border: 1px solid $tight-form-func-highlight-bg;
|
||||
}
|
||||
|
||||
.btn-outline-primary {
|
||||
|
@ -58,13 +58,21 @@
|
||||
&:hover {
|
||||
background: $card-background-hover;
|
||||
}
|
||||
|
||||
.label-tag {
|
||||
margin-left: 6px;
|
||||
font-size: 11px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-item-body {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-item-details {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-item-header {
|
||||
@ -77,10 +85,16 @@
|
||||
|
||||
.card-item-name {
|
||||
color: $headings-color;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-item-sub-name {
|
||||
color: $text-color-weak;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-list-layout-grid {
|
||||
|
@ -139,6 +139,10 @@ $gf-form-label-margin: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.gf-form-select-wrapper + .gf-form-select-wrapper {
|
||||
margin-left: $gf-form-label-margin;
|
||||
}
|
||||
|
||||
.gf-form-btn {
|
||||
padding: $input-padding-y $input-padding-x;
|
||||
line-height: $input-line-height;
|
||||
|
@ -282,8 +282,7 @@
|
||||
color: $text-color;
|
||||
text-shadow: $textShadow;
|
||||
@include gradient-vertical($primaryColor, $secondaryColor);
|
||||
border-color: $secondaryColor $secondaryColor darken($secondaryColor, 15%);
|
||||
border-color: rgba(0,0,0,.1) rgba(0,0,0,.1) fadein(rgba(0,0,0,.1), 15%);
|
||||
border-color: $primaryColor;
|
||||
}
|
||||
|
||||
// Gradients
|
||||
|
@ -135,6 +135,11 @@ define([
|
||||
expect(result).to.be('test|test2');
|
||||
});
|
||||
|
||||
it('slash should be properly escaped in regex format', function() {
|
||||
var result = _templateSrv.formatValue('Gi3/14', 'regex');
|
||||
expect(result).to.be('Gi3\\/14');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('can check if variable exists', function() {
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"rules": {
|
||||
"class-name": true,
|
||||
"comment-format": [true, "check-space"],
|
||||
"comment-format": [false, "check-space"],
|
||||
"curly": true,
|
||||
"eofline": true,
|
||||
"forin": false,
|
||||
|
Loading…
Reference in New Issue
Block a user