mirror of
https://github.com/grafana/grafana.git
synced 2025-12-21 03:54:29 +08:00
Compare commits
26 Commits
sriram/pos
...
v6.2.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7824f66cd3 | ||
|
|
71c1e8a731 | ||
|
|
74bc94bcec | ||
|
|
f566da0ff6 | ||
|
|
216aff96fd | ||
|
|
9de11c25a6 | ||
|
|
599e1030d8 | ||
|
|
d62da61d8a | ||
|
|
a2ca973925 | ||
|
|
98da29fd7b | ||
|
|
888ff61d30 | ||
|
|
c1be3adf3b | ||
|
|
ede9d9964d | ||
|
|
5cd69e8d39 | ||
|
|
ceb3672482 | ||
|
|
e519a9d2c4 | ||
|
|
2a3d6604c0 | ||
|
|
9cf0ea5395 | ||
|
|
db37e138bf | ||
|
|
a5a6d43f47 | ||
|
|
d9950aa4f1 | ||
|
|
6e9a395063 | ||
|
|
7374aafb90 | ||
|
|
b54e9880b4 | ||
|
|
09672a287f | ||
|
|
9d877d670e |
@@ -66,8 +66,8 @@ RUN mkdir -p "$GF_PATHS_HOME/.aws" && \
|
||||
"$GF_PATHS_DATA" && \
|
||||
cp "$GF_PATHS_HOME/conf/sample.ini" "$GF_PATHS_CONFIG" && \
|
||||
cp "$GF_PATHS_HOME/conf/ldap.toml" /etc/grafana/ldap.toml && \
|
||||
chown -R grafana:grafana "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" && \
|
||||
chmod 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS"
|
||||
chown -R grafana:grafana "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING" && \
|
||||
chmod 777 -R "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING"
|
||||
|
||||
COPY --from=0 /go/src/github.com/grafana/grafana/bin/linux-amd64/grafana-server /go/src/github.com/grafana/grafana/bin/linux-amd64/grafana-cli ./bin/
|
||||
COPY --from=1 /usr/src/app/public ./public
|
||||
|
||||
1337
devenv/dev-dashboards/datasource-testdata/new_features_in_v62.json
Normal file
1337
devenv/dev-dashboards/datasource-testdata/new_features_in_v62.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -51,6 +51,36 @@ then the Grafana proxy will transform it into "https://management.azure.com/foo/
|
||||
|
||||
The `method` parameter is optional. It can be set to any HTTP verb to provide more fine-grained control.
|
||||
|
||||
### Dynamic Routes
|
||||
|
||||
When using routes, you can also reference a variable stored in JsonData or SecureJsonData which will be interpolated when connecting to the datasource.
|
||||
|
||||
With JsonData:
|
||||
```json
|
||||
"routes": [
|
||||
{
|
||||
"path": "custom/api/v5/*",
|
||||
"method": "*",
|
||||
"url": "{{.JsonData.dynamicUrl}}",
|
||||
...
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
With SecureJsonData:
|
||||
```json
|
||||
"routes": [{
|
||||
"path": "custom/api/v5/*",
|
||||
"method": "*",
|
||||
"url": "{{.SecureJsonData.dynamicUrl}}",
|
||||
...
|
||||
}]
|
||||
```
|
||||
|
||||
In the above example, the app is able to set the value for `dynamicUrl` in JsonData or SecureJsonData and it will be replaced on-demand.
|
||||
|
||||
An app using this feature can be found [here](https://github.com/grafana/kentik-app).
|
||||
|
||||
## Encrypting Sensitive Data
|
||||
|
||||
When a user saves a password or secret with your datasource plugin's Config page, then you can save data to a column in the datasource table called `secureJsonData` that is an encrypted blob. Any data saved in the blob is encrypted by Grafana and can only be decrypted by the Grafana server on the backend. This means once a password is saved, no sensitive data is sent to the browser. If the password is saved in the `jsonData` blob or the `password` field then it is unencrypted and anyone with Admin access (with the help of Chrome Developer Tools) can read it.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"company": "Grafana Labs"
|
||||
},
|
||||
"name": "grafana",
|
||||
"version": "6.2.0-pre",
|
||||
"version": "6.2.0-beta2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/grafana/grafana.git"
|
||||
|
||||
@@ -42,9 +42,10 @@ export class CustomScrollbar extends Component<Props> {
|
||||
|
||||
updateScroll() {
|
||||
const ref = this.ref.current;
|
||||
const { scrollTop } = this.props;
|
||||
|
||||
if (ref && !isNil(this.props.scrollTop)) {
|
||||
ref.scrollTop(this.props.scrollTop);
|
||||
if (ref && !isNil(scrollTop)) {
|
||||
ref.scrollTop(scrollTop);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +71,44 @@ export class CustomScrollbar extends Component<Props> {
|
||||
this.updateScroll();
|
||||
}
|
||||
|
||||
renderTrack = (track: 'track-vertical' | 'track-horizontal', hideTrack: boolean | undefined, passedProps: any) => {
|
||||
return (
|
||||
<div
|
||||
{...passedProps}
|
||||
className={cx(
|
||||
css`
|
||||
visibility: ${hideTrack ? 'none' : 'visible'};
|
||||
`,
|
||||
track
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderThumb = (thumb: 'thumb-horizontal' | 'thumb-vertical', passedProps: any) => {
|
||||
return <div {...passedProps} className={thumb} />;
|
||||
};
|
||||
|
||||
renderTrackHorizontal = (passedProps: any) => {
|
||||
return this.renderTrack('track-horizontal', this.props.hideHorizontalTrack, passedProps);
|
||||
};
|
||||
|
||||
renderTrackVertical = (passedProps: any) => {
|
||||
return this.renderTrack('track-vertical', this.props.hideVerticalTrack, passedProps);
|
||||
};
|
||||
|
||||
renderThumbHorizontal = (passedProps: any) => {
|
||||
return this.renderThumb('thumb-horizontal', passedProps);
|
||||
};
|
||||
|
||||
renderThumbVertical = (passedProps: any) => {
|
||||
return this.renderThumb('thumb-vertical', passedProps);
|
||||
};
|
||||
|
||||
renderView = (passedProps: any) => {
|
||||
return <div {...passedProps} className="view" />;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
@@ -80,8 +119,6 @@ export class CustomScrollbar extends Component<Props> {
|
||||
autoHide,
|
||||
autoHideTimeout,
|
||||
hideTracksWhenNotNeeded,
|
||||
hideHorizontalTrack,
|
||||
hideVerticalTrack,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -97,31 +134,11 @@ export class CustomScrollbar extends Component<Props> {
|
||||
// Before these where set to inhert but that caused problems with cut of legends in firefox
|
||||
autoHeightMax={autoHeightMax}
|
||||
autoHeightMin={autoHeightMin}
|
||||
renderTrackHorizontal={props => (
|
||||
<div
|
||||
{...props}
|
||||
className={cx(
|
||||
css`
|
||||
visibility: ${hideHorizontalTrack ? 'none' : 'visible'};
|
||||
`,
|
||||
'track-horizontal'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
renderTrackVertical={props => (
|
||||
<div
|
||||
{...props}
|
||||
className={cx(
|
||||
css`
|
||||
visibility: ${hideVerticalTrack ? 'none' : 'visible'};
|
||||
`,
|
||||
'track-vertical'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
renderThumbHorizontal={props => <div {...props} className="thumb-horizontal" />}
|
||||
renderThumbVertical={props => <div {...props} className="thumb-vertical" />}
|
||||
renderView={props => <div {...props} className="view" />}
|
||||
renderTrackHorizontal={this.renderTrackHorizontal}
|
||||
renderTrackVertical={this.renderTrackVertical}
|
||||
renderThumbHorizontal={this.renderThumbHorizontal}
|
||||
renderThumbVertical={this.renderThumbVertical}
|
||||
renderView={this.renderView}
|
||||
>
|
||||
{children}
|
||||
</Scrollbars>
|
||||
|
||||
@@ -37,7 +37,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="css-17l4171 track-horizontal"
|
||||
className="css-52gpmd track-horizontal"
|
||||
style={
|
||||
Object {
|
||||
"display": "none",
|
||||
@@ -58,7 +58,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="css-17l4171 track-vertical"
|
||||
className="css-52gpmd track-vertical"
|
||||
style={
|
||||
Object {
|
||||
"display": "none",
|
||||
|
||||
@@ -69,14 +69,14 @@ export class Gauge extends PureComponent<Props> {
|
||||
|
||||
const backgroundColor = selectThemeVariant(
|
||||
{
|
||||
dark: theme.colors.dark3,
|
||||
light: '#e6e6e6',
|
||||
dark: theme.colors.dark8,
|
||||
light: theme.colors.gray6,
|
||||
},
|
||||
theme.type
|
||||
);
|
||||
|
||||
const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;
|
||||
const gaugeWidth = Math.min(dimension / 6, 40) / gaugeWidthReduceRatio;
|
||||
const gaugeWidth = Math.min(dimension / 5.5, 40) / gaugeWidthReduceRatio;
|
||||
const thresholdMarkersWidth = gaugeWidth / 5;
|
||||
const fontSize = Math.min(dimension / 5.5, 100) * (value.text !== null ? this.getFontScale(value.text.length) : 1);
|
||||
const thresholdLabelFontSize = fontSize / 2.5;
|
||||
@@ -181,7 +181,7 @@ function calculateGaugeAutoProps(width: number, height: number, title: string |
|
||||
const titleFontSize = Math.min((width * 0.15) / 1.5, 20); // 20% of height * line-height, max 40px
|
||||
const titleHeight = titleFontSize * 1.5;
|
||||
const availableHeight = showLabel ? height - titleHeight : height;
|
||||
const gaugeHeight = Math.min(availableHeight * 0.7, width);
|
||||
const gaugeHeight = Math.min(availableHeight, width);
|
||||
|
||||
return {
|
||||
showLabel,
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface PanelData {
|
||||
}
|
||||
|
||||
export interface PanelProps<T = any> {
|
||||
id: number; // ID within the current dashboard
|
||||
data: PanelData;
|
||||
// TODO: annotation?: PanelData;
|
||||
|
||||
|
||||
@@ -47,8 +47,8 @@ RUN mkdir -p "$GF_PATHS_HOME/.aws" && \
|
||||
"$GF_PATHS_DATA" && \
|
||||
cp "$GF_PATHS_HOME/conf/sample.ini" "$GF_PATHS_CONFIG" && \
|
||||
cp "$GF_PATHS_HOME/conf/ldap.toml" /etc/grafana/ldap.toml && \
|
||||
chown -R grafana:grafana "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" && \
|
||||
chmod 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS"
|
||||
chown -R grafana:grafana "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING" && \
|
||||
chmod -R 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING"
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
|
||||
@@ -233,7 +233,7 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er
|
||||
|
||||
if len(appLink.Children) > 0 && 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: "gicon gicon-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"})
|
||||
appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "gicon gicon-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/"})
|
||||
}
|
||||
|
||||
if len(appLink.Children) > 0 {
|
||||
|
||||
@@ -67,14 +67,14 @@ func (provider *accessTokenProvider) getAccessToken(data templateData) (string,
|
||||
}
|
||||
}
|
||||
|
||||
urlInterpolated, err := interpolateString(provider.route.TokenAuth.Url, data)
|
||||
urlInterpolated, err := InterpolateString(provider.route.TokenAuth.Url, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
params := make(url.Values)
|
||||
for key, value := range provider.route.TokenAuth.Params {
|
||||
interpolatedParam, err := interpolateString(value, data)
|
||||
interpolatedParam, err := InterpolateString(value, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -119,7 +119,7 @@ func (provider *accessTokenProvider) getJwtAccessToken(ctx context.Context, data
|
||||
conf := &jwt.Config{}
|
||||
|
||||
if val, ok := provider.route.JwtTokenAuth.Params["client_email"]; ok {
|
||||
interpolatedVal, err := interpolateString(val, data)
|
||||
interpolatedVal, err := InterpolateString(val, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -127,7 +127,7 @@ func (provider *accessTokenProvider) getJwtAccessToken(ctx context.Context, data
|
||||
}
|
||||
|
||||
if val, ok := provider.route.JwtTokenAuth.Params["private_key"]; ok {
|
||||
interpolatedVal, err := interpolateString(val, data)
|
||||
interpolatedVal, err := InterpolateString(val, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -135,7 +135,7 @@ func (provider *accessTokenProvider) getJwtAccessToken(ctx context.Context, data
|
||||
}
|
||||
|
||||
if val, ok := provider.route.JwtTokenAuth.Params["token_uri"]; ok {
|
||||
interpolatedVal, err := interpolateString(val, data)
|
||||
interpolatedVal, err := InterpolateString(val, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package pluginproxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
@@ -24,7 +22,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
|
||||
SecureJsonData: ds.SecureJsonData.Decrypt(),
|
||||
}
|
||||
|
||||
interpolatedURL, err := interpolateString(route.Url, data)
|
||||
interpolatedURL, err := InterpolateString(route.Url, data)
|
||||
if err != nil {
|
||||
logger.Error("Error interpolating proxy url", "error", err)
|
||||
return
|
||||
@@ -81,24 +79,9 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
|
||||
logger.Info("Requesting", "url", req.URL.String())
|
||||
}
|
||||
|
||||
func interpolateString(text string, data templateData) (string, error) {
|
||||
t, err := template.New("content").Parse(text)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not parse template %s", text)
|
||||
}
|
||||
|
||||
var contentBuf bytes.Buffer
|
||||
err = t.Execute(&contentBuf, data)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to execute template %s", text)
|
||||
}
|
||||
|
||||
return contentBuf.String(), nil
|
||||
}
|
||||
|
||||
func addHeaders(reqHeaders *http.Header, route *plugins.AppPluginRoute, data templateData) error {
|
||||
for _, header := range route.Headers {
|
||||
interpolated, err := interpolateString(header.Content, data)
|
||||
interpolated, err := InterpolateString(header.Content, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ func TestDsAuthProvider(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
interpolated, err := interpolateString("{{.SecureJsonData.Test}}", data)
|
||||
interpolated, err := InterpolateString("{{.SecureJsonData.Test}}", data)
|
||||
So(err, ShouldBeNil)
|
||||
So(interpolated, ShouldEqual, "0asd+asd")
|
||||
})
|
||||
|
||||
@@ -2,12 +2,13 @@ package pluginproxy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
@@ -38,6 +39,24 @@ func getHeaders(route *plugins.AppPluginRoute, orgId int64, appID string) (http.
|
||||
return result, err
|
||||
}
|
||||
|
||||
func updateURL(route *plugins.AppPluginRoute, orgId int64, appID string) (string, error) {
|
||||
query := m.GetPluginSettingByIdQuery{OrgId: orgId, PluginId: appID}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
data := templateData{
|
||||
JsonData: query.Result.JsonData,
|
||||
SecureJsonData: query.Result.SecureJsonData.Decrypt(),
|
||||
}
|
||||
interpolated, err := InterpolateString(route.Url, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return interpolated, err
|
||||
}
|
||||
|
||||
// NewApiPluginProxy create a plugin proxy
|
||||
func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPluginRoute, appID string, cfg *setting.Cfg) *httputil.ReverseProxy {
|
||||
targetURL, _ := url.Parse(route.Url)
|
||||
|
||||
@@ -48,7 +67,6 @@ func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPl
|
||||
req.Host = targetURL.Host
|
||||
|
||||
req.URL.Path = util.JoinURLFragments(targetURL.Path, proxyPath)
|
||||
|
||||
// clear cookie headers
|
||||
req.Header.Del("Cookie")
|
||||
req.Header.Del("Set-Cookie")
|
||||
@@ -72,13 +90,13 @@ func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPl
|
||||
}
|
||||
|
||||
// Create a HTTP header with the context in it.
|
||||
ctxJson, err := json.Marshal(ctx.SignedInUser)
|
||||
ctxJSON, err := json.Marshal(ctx.SignedInUser)
|
||||
if err != nil {
|
||||
ctx.JsonApiErr(500, "failed to marshal context to json.", err)
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Add("X-Grafana-Context", string(ctxJson))
|
||||
req.Header.Add("X-Grafana-Context", string(ctxJSON))
|
||||
|
||||
if cfg.SendUserHeader && !ctx.SignedInUser.IsAnonymous {
|
||||
req.Header.Add("X-Grafana-User", ctx.SignedInUser.Login)
|
||||
@@ -97,6 +115,27 @@ func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPl
|
||||
}
|
||||
}
|
||||
|
||||
if len(route.Url) > 0 {
|
||||
interpolatedURL, err := updateURL(route, ctx.OrgId, appID)
|
||||
if err != nil {
|
||||
ctx.JsonApiErr(500, "Could not interpolate plugin route url", err)
|
||||
}
|
||||
targetURL, err := url.Parse(interpolatedURL)
|
||||
if err != nil {
|
||||
ctx.JsonApiErr(500, "Could not parse custom url: %v", err)
|
||||
return
|
||||
}
|
||||
req.URL.Scheme = targetURL.Scheme
|
||||
req.URL.Host = targetURL.Host
|
||||
req.Host = targetURL.Host
|
||||
req.URL.Path = util.JoinURLFragments(targetURL.Path, proxyPath)
|
||||
|
||||
if err != nil {
|
||||
ctx.JsonApiErr(500, "Could not interpolate plugin route url", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// reqBytes, _ := httputil.DumpRequestOut(req, true);
|
||||
// log.Trace("Proxying plugin request: %s", string(reqBytes))
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ func TestPluginProxy(t *testing.T) {
|
||||
},
|
||||
},
|
||||
&setting.Cfg{SendUserHeader: true},
|
||||
nil,
|
||||
)
|
||||
|
||||
Convey("Should add header with username", func() {
|
||||
@@ -69,6 +70,7 @@ func TestPluginProxy(t *testing.T) {
|
||||
},
|
||||
},
|
||||
&setting.Cfg{SendUserHeader: false},
|
||||
nil,
|
||||
)
|
||||
Convey("Should not add header with username", func() {
|
||||
// Get will return empty string even if header is not set
|
||||
@@ -82,6 +84,7 @@ func TestPluginProxy(t *testing.T) {
|
||||
SignedInUser: &m.SignedInUser{IsAnonymous: true},
|
||||
},
|
||||
&setting.Cfg{SendUserHeader: true},
|
||||
nil,
|
||||
)
|
||||
|
||||
Convey("Should not add header with username", func() {
|
||||
@@ -89,14 +92,59 @@ func TestPluginProxy(t *testing.T) {
|
||||
So(req.Header.Get("X-Grafana-User"), ShouldEqual, "")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When getting templated url", t, func() {
|
||||
route := &plugins.AppPluginRoute{
|
||||
Url: "{{.JsonData.dynamicUrl}}",
|
||||
Method: "GET",
|
||||
}
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetPluginSettingByIdQuery) error {
|
||||
query.Result = &m.PluginSetting{
|
||||
JsonData: map[string]interface{}{
|
||||
"dynamicUrl": "https://dynamic.grafana.com",
|
||||
},
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
req := getPluginProxiedRequest(
|
||||
&m.ReqContext{
|
||||
SignedInUser: &m.SignedInUser{
|
||||
Login: "test_user",
|
||||
},
|
||||
},
|
||||
&setting.Cfg{SendUserHeader: true},
|
||||
route,
|
||||
)
|
||||
Convey("Headers should be updated", func() {
|
||||
header, err := getHeaders(route, 1, "my-app")
|
||||
So(err, ShouldBeNil)
|
||||
So(header.Get("X-Grafana-User"), ShouldEqual, "")
|
||||
})
|
||||
Convey("Should set req.URL to be interpolated value from jsonData", func() {
|
||||
So(req.URL.String(), ShouldEqual, "https://dynamic.grafana.com")
|
||||
})
|
||||
Convey("Route url should not be modified", func() {
|
||||
So(route.Url, ShouldEqual, "{{.JsonData.dynamicUrl}}")
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// getPluginProxiedRequest is a helper for easier setup of tests based on global config and ReqContext.
|
||||
func getPluginProxiedRequest(ctx *m.ReqContext, cfg *setting.Cfg) *http.Request {
|
||||
route := &plugins.AppPluginRoute{}
|
||||
func getPluginProxiedRequest(ctx *m.ReqContext, cfg *setting.Cfg, route *plugins.AppPluginRoute) *http.Request {
|
||||
// insert dummy route if none is specified
|
||||
if route == nil {
|
||||
route = &plugins.AppPluginRoute{
|
||||
Path: "api/v4/",
|
||||
Url: "https://www.google.com",
|
||||
ReqRole: m.ROLE_EDITOR,
|
||||
}
|
||||
}
|
||||
proxy := NewApiPluginProxy(ctx, "", route, "", cfg)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
|
||||
req, err := http.NewRequest(http.MethodGet, route.Url, nil)
|
||||
So(err, ShouldBeNil)
|
||||
proxy.Director(req)
|
||||
return req
|
||||
|
||||
49
pkg/api/pluginproxy/utils.go
Normal file
49
pkg/api/pluginproxy/utils.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package pluginproxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"text/template"
|
||||
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
)
|
||||
|
||||
// InterpolateString accepts template data and return a string with substitutions
|
||||
func InterpolateString(text string, data templateData) (string, error) {
|
||||
t, err := template.New("content").Parse(text)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not parse template %s", text)
|
||||
}
|
||||
|
||||
var contentBuf bytes.Buffer
|
||||
err = t.Execute(&contentBuf, data)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to execute template %s", text)
|
||||
}
|
||||
|
||||
return contentBuf.String(), nil
|
||||
}
|
||||
|
||||
// InterpolateURL accepts template data and return a string with substitutions
|
||||
func InterpolateURL(anURL *url.URL, route *plugins.AppPluginRoute, orgID int64, appID string) (*url.URL, error) {
|
||||
query := m.GetPluginSettingByIdQuery{OrgId: orgID, PluginId: appID}
|
||||
result, err := url.Parse(anURL.String())
|
||||
if query.Result != nil {
|
||||
if len(query.Result.JsonData) > 0 {
|
||||
data := templateData{
|
||||
JsonData: query.Result.JsonData,
|
||||
}
|
||||
interpolatedResult, err := InterpolateString(anURL.String(), data)
|
||||
if err == nil {
|
||||
result, err = url.Parse(interpolatedResult)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error parsing plugin route url %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
21
pkg/api/pluginproxy/utils_test.go
Normal file
21
pkg/api/pluginproxy/utils_test.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package pluginproxy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestInterpolateString(t *testing.T) {
|
||||
Convey("When interpolating string", t, func() {
|
||||
data := templateData{
|
||||
SecureJsonData: map[string]string{
|
||||
"Test": "0asd+asd",
|
||||
},
|
||||
}
|
||||
|
||||
interpolated, err := InterpolateString("{{.SecureJsonData.Test}}", data)
|
||||
So(err, ShouldBeNil)
|
||||
So(interpolated, ShouldEqual, "0asd+asd")
|
||||
})
|
||||
}
|
||||
@@ -39,10 +39,14 @@ func (dc *databaseCache) Run(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (dc *databaseCache) internalRunGC() {
|
||||
now := getTime().Unix()
|
||||
sql := `DELETE FROM cache_data WHERE (? - created_at) >= expires AND expires <> 0`
|
||||
err := dc.SQLStore.WithDbSession(context.Background(), func(session *sqlstore.DBSession) error {
|
||||
now := getTime().Unix()
|
||||
sql := `DELETE FROM cache_data WHERE (? - created_at) >= expires AND expires <> 0`
|
||||
|
||||
_, err := session.Exec(sql, now)
|
||||
return err
|
||||
})
|
||||
|
||||
_, err := dc.SQLStore.NewSession().Exec(sql, now)
|
||||
if err != nil {
|
||||
dc.log.Error("failed to run garbage collect", "error", err)
|
||||
}
|
||||
@@ -80,44 +84,48 @@ func (dc *databaseCache) Get(key string) (interface{}, error) {
|
||||
}
|
||||
|
||||
func (dc *databaseCache) Set(key string, value interface{}, expire time.Duration) error {
|
||||
item := &cachedItem{Val: value}
|
||||
data, err := encodeGob(item)
|
||||
if err != nil {
|
||||
return dc.SQLStore.WithTransactionalDbSession(context.Background(), func(session *sqlstore.DBSession) error {
|
||||
item := &cachedItem{Val: value}
|
||||
data, err := encodeGob(item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var cacheHit CacheData
|
||||
has, err := session.Where("cache_key = ?", key).Get(&cacheHit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var expiresInSeconds int64
|
||||
if expire != 0 {
|
||||
expiresInSeconds = int64(expire) / int64(time.Second)
|
||||
}
|
||||
|
||||
// insert or update depending on if item already exist
|
||||
if has {
|
||||
sql := `UPDATE cache_data SET data=?, created_at=?, expires=? WHERE cache_key=?`
|
||||
_, err = session.Exec(sql, data, getTime().Unix(), expiresInSeconds, key)
|
||||
} else {
|
||||
sql := `INSERT INTO cache_data (cache_key,data,created_at,expires) VALUES(?,?,?,?)`
|
||||
_, err = session.Exec(sql, key, data, getTime().Unix(), expiresInSeconds)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
session := dc.SQLStore.NewSession()
|
||||
|
||||
var cacheHit CacheData
|
||||
has, err := session.Where("cache_key = ?", key).Get(&cacheHit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var expiresInSeconds int64
|
||||
if expire != 0 {
|
||||
expiresInSeconds = int64(expire) / int64(time.Second)
|
||||
}
|
||||
|
||||
// insert or update depending on if item already exist
|
||||
if has {
|
||||
sql := `UPDATE cache_data SET data=?, created_at=?, expires=? WHERE cache_key=?`
|
||||
_, err = session.Exec(sql, data, getTime().Unix(), expiresInSeconds, key)
|
||||
} else {
|
||||
sql := `INSERT INTO cache_data (cache_key,data,created_at,expires) VALUES(?,?,?,?)`
|
||||
_, err = session.Exec(sql, key, data, getTime().Unix(), expiresInSeconds)
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (dc *databaseCache) Delete(key string) error {
|
||||
sql := "DELETE FROM cache_data WHERE cache_key=?"
|
||||
_, err := dc.SQLStore.NewSession().Exec(sql, key)
|
||||
return dc.SQLStore.WithDbSession(context.Background(), func(session *sqlstore.DBSession) error {
|
||||
sql := "DELETE FROM cache_data WHERE cache_key=?"
|
||||
_, err := session.Exec(sql, key)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// CacheData is the struct representing the table in the database
|
||||
type CacheData struct {
|
||||
CacheKey string
|
||||
Data []byte
|
||||
|
||||
@@ -356,7 +356,12 @@ export function seriesDataToLogsModel(seriesData: SeriesData[], intervalMs: numb
|
||||
return logsModel;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return {
|
||||
hasUniqueLabels: false,
|
||||
rows: [],
|
||||
meta: [],
|
||||
series: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function logSeriesToLogsModel(logSeries: SeriesData[]): LogsModel {
|
||||
|
||||
@@ -333,22 +333,29 @@ describe('LogsParsers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const emptyLogsModel = {
|
||||
hasUniqueLabels: false,
|
||||
rows: [],
|
||||
meta: [],
|
||||
series: [],
|
||||
};
|
||||
|
||||
describe('seriesDataToLogsModel', () => {
|
||||
it('given empty series should return undefined', () => {
|
||||
expect(seriesDataToLogsModel([] as SeriesData[], 0)).toBeUndefined();
|
||||
it('given empty series should return empty logs model', () => {
|
||||
expect(seriesDataToLogsModel([] as SeriesData[], 0)).toMatchObject(emptyLogsModel);
|
||||
});
|
||||
|
||||
it('given series without correct series name should not be processed', () => {
|
||||
it('given series without correct series name should return empty logs model', () => {
|
||||
const series: SeriesData[] = [
|
||||
{
|
||||
fields: [],
|
||||
rows: [],
|
||||
},
|
||||
];
|
||||
expect(seriesDataToLogsModel(series, 0)).toBeUndefined();
|
||||
expect(seriesDataToLogsModel(series, 0)).toMatchObject(emptyLogsModel);
|
||||
});
|
||||
|
||||
it('given series without a time field should not be processed', () => {
|
||||
it('given series without a time field should return empty logs model', () => {
|
||||
const series: SeriesData[] = [
|
||||
{
|
||||
fields: [
|
||||
@@ -360,10 +367,10 @@ describe('seriesDataToLogsModel', () => {
|
||||
rows: [],
|
||||
},
|
||||
];
|
||||
expect(seriesDataToLogsModel(series, 0)).toBeUndefined();
|
||||
expect(seriesDataToLogsModel(series, 0)).toMatchObject(emptyLogsModel);
|
||||
});
|
||||
|
||||
it('given series without a string field should not be processed', () => {
|
||||
it('given series without a string field should return empty logs model', () => {
|
||||
const series: SeriesData[] = [
|
||||
{
|
||||
fields: [
|
||||
@@ -375,7 +382,7 @@ describe('seriesDataToLogsModel', () => {
|
||||
rows: [],
|
||||
},
|
||||
];
|
||||
expect(seriesDataToLogsModel(series, 0)).toBeUndefined();
|
||||
expect(seriesDataToLogsModel(series, 0)).toMatchObject(emptyLogsModel);
|
||||
});
|
||||
|
||||
it('given one series should return expected logs model', () => {
|
||||
|
||||
@@ -212,7 +212,7 @@ exports[`ServerStats Should render table with stats 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="css-17l4171 track-horizontal"
|
||||
className="css-52gpmd track-horizontal"
|
||||
style={
|
||||
Object {
|
||||
"display": "none",
|
||||
@@ -233,7 +233,7 @@ exports[`ServerStats Should render table with stats 1`] = `
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="css-17l4171 track-vertical"
|
||||
className="css-52gpmd track-vertical"
|
||||
style={
|
||||
Object {
|
||||
"display": "none",
|
||||
|
||||
@@ -61,6 +61,7 @@ export interface State {
|
||||
isFullscreen: boolean;
|
||||
fullscreenPanel: PanelModel | null;
|
||||
scrollTop: number;
|
||||
updateScrollTop: number;
|
||||
rememberScrollTop: number;
|
||||
showLoadingState: boolean;
|
||||
}
|
||||
@@ -73,6 +74,7 @@ export class DashboardPage extends PureComponent<Props, State> {
|
||||
showLoadingState: false,
|
||||
fullscreenPanel: null,
|
||||
scrollTop: 0,
|
||||
updateScrollTop: null,
|
||||
rememberScrollTop: 0,
|
||||
};
|
||||
|
||||
@@ -168,7 +170,7 @@ export class DashboardPage extends PureComponent<Props, State> {
|
||||
isEditing: false,
|
||||
isFullscreen: false,
|
||||
fullscreenPanel: null,
|
||||
scrollTop: this.state.rememberScrollTop,
|
||||
updateScrollTop: this.state.rememberScrollTop,
|
||||
},
|
||||
this.triggerPanelsRendering.bind(this)
|
||||
);
|
||||
@@ -204,7 +206,7 @@ export class DashboardPage extends PureComponent<Props, State> {
|
||||
|
||||
setScrollTop = (e: MouseEvent<HTMLElement>): void => {
|
||||
const target = e.target as HTMLElement;
|
||||
this.setState({ scrollTop: target.scrollTop });
|
||||
this.setState({ scrollTop: target.scrollTop, updateScrollTop: null });
|
||||
};
|
||||
|
||||
onAddPanel = () => {
|
||||
@@ -251,7 +253,7 @@ export class DashboardPage extends PureComponent<Props, State> {
|
||||
|
||||
render() {
|
||||
const { dashboard, editview, $injector, isInitSlow, initError } = this.props;
|
||||
const { isSettingsOpening, isEditing, isFullscreen, scrollTop } = this.state;
|
||||
const { isSettingsOpening, isEditing, isFullscreen, scrollTop, updateScrollTop } = this.state;
|
||||
|
||||
if (!dashboard) {
|
||||
if (isInitSlow) {
|
||||
@@ -285,9 +287,9 @@ export class DashboardPage extends PureComponent<Props, State> {
|
||||
/>
|
||||
<div className="scroll-canvas scroll-canvas--dashboard">
|
||||
<CustomScrollbar
|
||||
autoHeightMin={'100%'}
|
||||
autoHeightMin="100%"
|
||||
setScrollTop={this.setScrollTop}
|
||||
scrollTop={scrollTop}
|
||||
scrollTop={updateScrollTop}
|
||||
updateAfterMountMs={500}
|
||||
className="custom-scrollbar--page"
|
||||
>
|
||||
|
||||
@@ -111,7 +111,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
|
||||
autoHideTimeout={200}
|
||||
className="custom-scrollbar--page"
|
||||
hideTracksWhenNotNeeded={false}
|
||||
scrollTop={0}
|
||||
scrollTop={null}
|
||||
setScrollTop={[Function]}
|
||||
updateAfterMountMs={500}
|
||||
>
|
||||
@@ -349,7 +349,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
||||
autoHideTimeout={200}
|
||||
className="custom-scrollbar--page"
|
||||
hideTracksWhenNotNeeded={false}
|
||||
scrollTop={0}
|
||||
scrollTop={null}
|
||||
setScrollTop={[Function]}
|
||||
updateAfterMountMs={500}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import { DashboardGrid, Props } from './DashboardGrid';
|
||||
import { DashboardModel } from '../state';
|
||||
|
||||
interface ScenarioContext {
|
||||
props: Props;
|
||||
wrapper?: ShallowWrapper<Props, any, DashboardGrid>;
|
||||
setup?: (fn: () => void) => void;
|
||||
setProps: (props: Partial<Props>) => void;
|
||||
}
|
||||
|
||||
function getTestDashboard(overrides?: any, metaOverrides?: any): DashboardModel {
|
||||
const data = Object.assign(
|
||||
{
|
||||
title: 'My dashboard',
|
||||
panels: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'graph',
|
||||
title: 'My graph',
|
||||
gridPos: { x: 0, y: 0, w: 24, h: 10 },
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'graph2',
|
||||
title: 'My graph2',
|
||||
gridPos: { x: 0, y: 10, w: 25, h: 10 },
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'graph3',
|
||||
title: 'My graph3',
|
||||
gridPos: { x: 0, y: 20, w: 25, h: 100 },
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
type: 'graph4',
|
||||
title: 'My graph4',
|
||||
gridPos: { x: 0, y: 120, w: 25, h: 10 },
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides
|
||||
);
|
||||
|
||||
const meta = Object.assign({ canSave: true, canEdit: true }, metaOverrides);
|
||||
return new DashboardModel(data, meta);
|
||||
}
|
||||
|
||||
function dashboardGridScenario(description, scenarioFn: (ctx: ScenarioContext) => void) {
|
||||
describe(description, () => {
|
||||
let setupFn: () => void;
|
||||
|
||||
const ctx: ScenarioContext = {
|
||||
setup: fn => {
|
||||
setupFn = fn;
|
||||
},
|
||||
props: {
|
||||
isEditing: false,
|
||||
isFullscreen: false,
|
||||
scrollTop: null,
|
||||
dashboard: getTestDashboard(),
|
||||
},
|
||||
setProps: (props: Partial<Props>) => {
|
||||
Object.assign(ctx.props, props);
|
||||
if (ctx.wrapper) {
|
||||
ctx.wrapper.setProps(ctx.props);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setupFn();
|
||||
ctx.wrapper = shallow(<DashboardGrid {...ctx.props} />);
|
||||
});
|
||||
|
||||
scenarioFn(ctx);
|
||||
});
|
||||
}
|
||||
|
||||
describe('DashboardGrid', () => {
|
||||
dashboardGridScenario('Can render dashboard grid', ctx => {
|
||||
ctx.setup(() => {});
|
||||
|
||||
it('Should render', () => {
|
||||
expect(ctx.wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -205,7 +205,7 @@ export class DashboardGrid extends PureComponent<Props> {
|
||||
return false;
|
||||
}
|
||||
|
||||
const top = parseInt(elem.style.top.replace('px', ''), 10);
|
||||
const top = elem.offsetTop;
|
||||
const height = panel.gridPos.h * GRID_CELL_HEIGHT + 40;
|
||||
const bottom = top + height;
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
||||
import DashboardGrid from './DashboardGrid';
|
||||
|
||||
react2AngularDirective('dashboardGrid', DashboardGrid, [['dashboard', { watchDepth: 'reference' }]]);
|
||||
@@ -250,6 +250,7 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
{loading === LoadingState.Loading && this.renderLoadingState()}
|
||||
<div className="panel-content">
|
||||
<PanelComponent
|
||||
id={panel.id}
|
||||
data={data}
|
||||
timeRange={data.request ? data.request.range : this.timeSrv.timeRange()}
|
||||
options={panel.getOptions(plugin.defaults)}
|
||||
|
||||
@@ -81,16 +81,21 @@ export class PanelHeader extends Component<Props, State> {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PanelHeaderCorner
|
||||
panel={panel}
|
||||
title={panel.title}
|
||||
description={panel.description}
|
||||
scopedVars={panel.scopedVars}
|
||||
links={panel.links}
|
||||
error={error}
|
||||
/>
|
||||
<div className={panelHeaderClass}>
|
||||
<div className="panel-title-container" onClick={this.onMenuToggle} onMouseDown={this.onMouseDown}>
|
||||
<PanelHeaderCorner
|
||||
panel={panel}
|
||||
title={panel.title}
|
||||
description={panel.description}
|
||||
scopedVars={panel.scopedVars}
|
||||
links={panel.links}
|
||||
error={error}
|
||||
/>
|
||||
<div
|
||||
className="panel-title-container"
|
||||
onClick={this.onMenuToggle}
|
||||
onMouseDown={this.onMouseDown}
|
||||
aria-label="Panel Title"
|
||||
>
|
||||
<div className="panel-title">
|
||||
<span className="icon-gf panel-alert-icon" />
|
||||
<span className="panel-title-text">
|
||||
|
||||
@@ -0,0 +1,996 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DashboardGrid Can render dashboard grid Should render 1`] = `
|
||||
<SizeMe(GridWrapper)
|
||||
className="layout"
|
||||
isDraggable={true}
|
||||
isFullscreen={false}
|
||||
isResizable={true}
|
||||
layout={
|
||||
Array [
|
||||
Object {
|
||||
"h": 10,
|
||||
"i": "1",
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
Object {
|
||||
"h": 10,
|
||||
"i": "2",
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 10,
|
||||
},
|
||||
Object {
|
||||
"h": 100,
|
||||
"i": "3",
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 20,
|
||||
},
|
||||
Object {
|
||||
"h": 10,
|
||||
"i": "4",
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 120,
|
||||
},
|
||||
]
|
||||
}
|
||||
onDragStop={[Function]}
|
||||
onLayoutChange={[Function]}
|
||||
onResize={[Function]}
|
||||
onResizeStop={[Function]}
|
||||
onWidthChange={[Function]}
|
||||
>
|
||||
<div
|
||||
className=""
|
||||
id="panel-1"
|
||||
key="1"
|
||||
>
|
||||
<DashboardPanel
|
||||
dashboard={
|
||||
DashboardModel {
|
||||
"annotations": Object {
|
||||
"list": Array [
|
||||
Object {
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard",
|
||||
},
|
||||
],
|
||||
},
|
||||
"autoUpdate": undefined,
|
||||
"description": undefined,
|
||||
"editable": true,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {
|
||||
"panel-added": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"panel-removed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"repeats-processed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"row-collapsed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"row-expanded": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"view-mode-changed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
},
|
||||
"_eventsCount": 6,
|
||||
},
|
||||
},
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": Array [],
|
||||
"meta": Object {
|
||||
"canEdit": true,
|
||||
"canMakeEditable": false,
|
||||
"canSave": true,
|
||||
"canShare": true,
|
||||
"canStar": true,
|
||||
"fullscreen": false,
|
||||
"isEditing": false,
|
||||
"showSettings": true,
|
||||
},
|
||||
"originalTemplating": Array [],
|
||||
"originalTime": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"panels": Array [
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"id": 1,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph",
|
||||
"transparent": false,
|
||||
"type": "graph",
|
||||
},
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 10,
|
||||
},
|
||||
"id": 2,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph2",
|
||||
"transparent": false,
|
||||
"type": "graph2",
|
||||
},
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 100,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 20,
|
||||
},
|
||||
"id": 3,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph3",
|
||||
"transparent": false,
|
||||
"type": "graph3",
|
||||
},
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 120,
|
||||
},
|
||||
"id": 4,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph4",
|
||||
"transparent": false,
|
||||
"type": "graph4",
|
||||
},
|
||||
],
|
||||
"refresh": undefined,
|
||||
"revision": undefined,
|
||||
"schemaVersion": 18,
|
||||
"snapshot": undefined,
|
||||
"style": "dark",
|
||||
"tags": Array [],
|
||||
"templating": Object {
|
||||
"list": Array [],
|
||||
},
|
||||
"time": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"timepicker": Object {},
|
||||
"timezone": "",
|
||||
"title": "My dashboard",
|
||||
"uid": null,
|
||||
"version": 0,
|
||||
}
|
||||
}
|
||||
isInView={false}
|
||||
panel={
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"id": 1,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph",
|
||||
"transparent": false,
|
||||
"type": "graph",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className=""
|
||||
id="panel-2"
|
||||
key="2"
|
||||
>
|
||||
<DashboardPanel
|
||||
dashboard={
|
||||
DashboardModel {
|
||||
"annotations": Object {
|
||||
"list": Array [
|
||||
Object {
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard",
|
||||
},
|
||||
],
|
||||
},
|
||||
"autoUpdate": undefined,
|
||||
"description": undefined,
|
||||
"editable": true,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {
|
||||
"panel-added": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"panel-removed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"repeats-processed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"row-collapsed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"row-expanded": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"view-mode-changed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
},
|
||||
"_eventsCount": 6,
|
||||
},
|
||||
},
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": Array [],
|
||||
"meta": Object {
|
||||
"canEdit": true,
|
||||
"canMakeEditable": false,
|
||||
"canSave": true,
|
||||
"canShare": true,
|
||||
"canStar": true,
|
||||
"fullscreen": false,
|
||||
"isEditing": false,
|
||||
"showSettings": true,
|
||||
},
|
||||
"originalTemplating": Array [],
|
||||
"originalTime": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"panels": Array [
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"id": 1,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph",
|
||||
"transparent": false,
|
||||
"type": "graph",
|
||||
},
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 10,
|
||||
},
|
||||
"id": 2,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph2",
|
||||
"transparent": false,
|
||||
"type": "graph2",
|
||||
},
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 100,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 20,
|
||||
},
|
||||
"id": 3,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph3",
|
||||
"transparent": false,
|
||||
"type": "graph3",
|
||||
},
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 120,
|
||||
},
|
||||
"id": 4,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph4",
|
||||
"transparent": false,
|
||||
"type": "graph4",
|
||||
},
|
||||
],
|
||||
"refresh": undefined,
|
||||
"revision": undefined,
|
||||
"schemaVersion": 18,
|
||||
"snapshot": undefined,
|
||||
"style": "dark",
|
||||
"tags": Array [],
|
||||
"templating": Object {
|
||||
"list": Array [],
|
||||
},
|
||||
"time": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"timepicker": Object {},
|
||||
"timezone": "",
|
||||
"title": "My dashboard",
|
||||
"uid": null,
|
||||
"version": 0,
|
||||
}
|
||||
}
|
||||
isInView={false}
|
||||
panel={
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 10,
|
||||
},
|
||||
"id": 2,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph2",
|
||||
"transparent": false,
|
||||
"type": "graph2",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className=""
|
||||
id="panel-3"
|
||||
key="3"
|
||||
>
|
||||
<DashboardPanel
|
||||
dashboard={
|
||||
DashboardModel {
|
||||
"annotations": Object {
|
||||
"list": Array [
|
||||
Object {
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard",
|
||||
},
|
||||
],
|
||||
},
|
||||
"autoUpdate": undefined,
|
||||
"description": undefined,
|
||||
"editable": true,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {
|
||||
"panel-added": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"panel-removed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"repeats-processed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"row-collapsed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"row-expanded": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"view-mode-changed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
},
|
||||
"_eventsCount": 6,
|
||||
},
|
||||
},
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": Array [],
|
||||
"meta": Object {
|
||||
"canEdit": true,
|
||||
"canMakeEditable": false,
|
||||
"canSave": true,
|
||||
"canShare": true,
|
||||
"canStar": true,
|
||||
"fullscreen": false,
|
||||
"isEditing": false,
|
||||
"showSettings": true,
|
||||
},
|
||||
"originalTemplating": Array [],
|
||||
"originalTime": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"panels": Array [
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"id": 1,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph",
|
||||
"transparent": false,
|
||||
"type": "graph",
|
||||
},
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 10,
|
||||
},
|
||||
"id": 2,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph2",
|
||||
"transparent": false,
|
||||
"type": "graph2",
|
||||
},
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 100,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 20,
|
||||
},
|
||||
"id": 3,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph3",
|
||||
"transparent": false,
|
||||
"type": "graph3",
|
||||
},
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 120,
|
||||
},
|
||||
"id": 4,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph4",
|
||||
"transparent": false,
|
||||
"type": "graph4",
|
||||
},
|
||||
],
|
||||
"refresh": undefined,
|
||||
"revision": undefined,
|
||||
"schemaVersion": 18,
|
||||
"snapshot": undefined,
|
||||
"style": "dark",
|
||||
"tags": Array [],
|
||||
"templating": Object {
|
||||
"list": Array [],
|
||||
},
|
||||
"time": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"timepicker": Object {},
|
||||
"timezone": "",
|
||||
"title": "My dashboard",
|
||||
"uid": null,
|
||||
"version": 0,
|
||||
}
|
||||
}
|
||||
isInView={false}
|
||||
panel={
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 100,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 20,
|
||||
},
|
||||
"id": 3,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph3",
|
||||
"transparent": false,
|
||||
"type": "graph3",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className=""
|
||||
id="panel-4"
|
||||
key="4"
|
||||
>
|
||||
<DashboardPanel
|
||||
dashboard={
|
||||
DashboardModel {
|
||||
"annotations": Object {
|
||||
"list": Array [
|
||||
Object {
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard",
|
||||
},
|
||||
],
|
||||
},
|
||||
"autoUpdate": undefined,
|
||||
"description": undefined,
|
||||
"editable": true,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {
|
||||
"panel-added": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"panel-removed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"repeats-processed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"row-collapsed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"row-expanded": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
"view-mode-changed": EE {
|
||||
"context": [Circular],
|
||||
"fn": [Function],
|
||||
"once": false,
|
||||
},
|
||||
},
|
||||
"_eventsCount": 6,
|
||||
},
|
||||
},
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": Array [],
|
||||
"meta": Object {
|
||||
"canEdit": true,
|
||||
"canMakeEditable": false,
|
||||
"canSave": true,
|
||||
"canShare": true,
|
||||
"canStar": true,
|
||||
"fullscreen": false,
|
||||
"isEditing": false,
|
||||
"showSettings": true,
|
||||
},
|
||||
"originalTemplating": Array [],
|
||||
"originalTime": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"panels": Array [
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"id": 1,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph",
|
||||
"transparent": false,
|
||||
"type": "graph",
|
||||
},
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 10,
|
||||
},
|
||||
"id": 2,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph2",
|
||||
"transparent": false,
|
||||
"type": "graph2",
|
||||
},
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 100,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 20,
|
||||
},
|
||||
"id": 3,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph3",
|
||||
"transparent": false,
|
||||
"type": "graph3",
|
||||
},
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 120,
|
||||
},
|
||||
"id": 4,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph4",
|
||||
"transparent": false,
|
||||
"type": "graph4",
|
||||
},
|
||||
],
|
||||
"refresh": undefined,
|
||||
"revision": undefined,
|
||||
"schemaVersion": 18,
|
||||
"snapshot": undefined,
|
||||
"style": "dark",
|
||||
"tags": Array [],
|
||||
"templating": Object {
|
||||
"list": Array [],
|
||||
},
|
||||
"time": Object {
|
||||
"from": "now-6h",
|
||||
"to": "now",
|
||||
},
|
||||
"timepicker": Object {},
|
||||
"timezone": "",
|
||||
"title": "My dashboard",
|
||||
"uid": null,
|
||||
"version": 0,
|
||||
}
|
||||
}
|
||||
isInView={false}
|
||||
panel={
|
||||
PanelModel {
|
||||
"cachedPluginOptions": Object {},
|
||||
"datasource": null,
|
||||
"events": Emitter {
|
||||
"emitter": EventEmitter {
|
||||
"_events": Object {},
|
||||
"_eventsCount": 0,
|
||||
},
|
||||
},
|
||||
"gridPos": Object {
|
||||
"h": 10,
|
||||
"w": 25,
|
||||
"x": 0,
|
||||
"y": 120,
|
||||
},
|
||||
"id": 4,
|
||||
"isInView": false,
|
||||
"targets": Array [
|
||||
Object {
|
||||
"refId": "A",
|
||||
},
|
||||
],
|
||||
"title": "My graph4",
|
||||
"transparent": false,
|
||||
"type": "graph4",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</SizeMe(GridWrapper)>
|
||||
`;
|
||||
@@ -1,5 +1,3 @@
|
||||
import './dashgrid/DashboardGridDirective';
|
||||
|
||||
// Services
|
||||
import './services/UnsavedChangesSrv';
|
||||
import './services/DashboardLoaderSrv';
|
||||
|
||||
@@ -324,7 +324,7 @@ export class PanelModel {
|
||||
}
|
||||
|
||||
hasTitle() {
|
||||
return !!this.title.length;
|
||||
return this.title && this.title.length > 0;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
||||
@@ -275,7 +275,6 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
||||
<LogsContainer
|
||||
width={width}
|
||||
exploreId={exploreId}
|
||||
onChangeTime={this.onChangeTime}
|
||||
onClickLabel={this.onClickLabel}
|
||||
onStartScanning={this.onStartScanning}
|
||||
onStopScanning={this.onStopScanning}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { connect } from 'react-redux';
|
||||
import moment from 'moment';
|
||||
import { RawTimeRange, TimeRange, LogLevel, TimeZone, AbsoluteTimeRange } from '@grafana/ui';
|
||||
|
||||
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||
import { LogsModel, LogsDedupStrategy } from 'app/core/logs_model';
|
||||
import { StoreState } from 'app/types';
|
||||
|
||||
import { toggleLogs, changeDedupStrategy } from './state/actions';
|
||||
import { toggleLogs, changeDedupStrategy, changeTime } from './state/actions';
|
||||
import Logs from './Logs';
|
||||
import Panel from './Panel';
|
||||
import { toggleLogLevelAction } from 'app/features/explore/state/actionTypes';
|
||||
@@ -20,7 +21,6 @@ interface LogsContainerProps {
|
||||
logsHighlighterExpressions?: string[];
|
||||
logsResult?: LogsModel;
|
||||
dedupedResult?: LogsModel;
|
||||
onChangeTime: (range: AbsoluteTimeRange) => void;
|
||||
onClickLabel: (key: string, value: string) => void;
|
||||
onStartScanning: () => void;
|
||||
onStopScanning: () => void;
|
||||
@@ -35,9 +35,19 @@ interface LogsContainerProps {
|
||||
dedupStrategy: LogsDedupStrategy;
|
||||
hiddenLogLevels: Set<LogLevel>;
|
||||
width: number;
|
||||
changeTime: typeof changeTime;
|
||||
}
|
||||
|
||||
export class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||
onChangeTime = (absRange: AbsoluteTimeRange) => {
|
||||
const { exploreId, timeZone, changeTime } = this.props;
|
||||
const range = {
|
||||
from: timeZone.isUtc ? moment.utc(absRange.from) : moment(absRange.from),
|
||||
to: timeZone.isUtc ? moment.utc(absRange.to) : moment(absRange.to),
|
||||
};
|
||||
|
||||
changeTime(exploreId, range);
|
||||
};
|
||||
onClickLogsButton = () => {
|
||||
this.props.toggleLogs(this.props.exploreId, this.props.showingLogs);
|
||||
};
|
||||
@@ -61,7 +71,6 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||
logsHighlighterExpressions,
|
||||
logsResult,
|
||||
dedupedResult,
|
||||
onChangeTime,
|
||||
onClickLabel,
|
||||
onStartScanning,
|
||||
onStopScanning,
|
||||
@@ -83,7 +92,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||
exploreId={exploreId}
|
||||
highlighterExpressions={logsHighlighterExpressions}
|
||||
loading={loading}
|
||||
onChangeTime={onChangeTime}
|
||||
onChangeTime={this.onChangeTime}
|
||||
onClickLabel={onClickLabel}
|
||||
onStartScanning={onStartScanning}
|
||||
onStopScanning={onStopScanning}
|
||||
@@ -130,6 +139,7 @@ const mapDispatchToProps = {
|
||||
toggleLogs,
|
||||
changeDedupStrategy,
|
||||
toggleLogLevelAction,
|
||||
changeTime,
|
||||
};
|
||||
|
||||
export default hot(module)(
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
import React, { FC, useContext } from 'react';
|
||||
import { css } from 'emotion';
|
||||
import { PluginState, Tooltip, ThemeContext } from '@grafana/ui';
|
||||
import { PopperContent } from '@grafana/ui/src/components/Tooltip/PopperController';
|
||||
|
||||
interface Props {
|
||||
state?: PluginState;
|
||||
}
|
||||
|
||||
function getPluginStateInfoText(state?: PluginState): string | null {
|
||||
function getPluginStateInfoText(state?: PluginState): PopperContent<any> | null {
|
||||
switch (state) {
|
||||
case PluginState.alpha:
|
||||
return 'Plugin in alpha state. Means work in progress and updates may include breaking changes.';
|
||||
return (
|
||||
<div>
|
||||
<h5>Alpha Plugin</h5>
|
||||
<p>This plugin is a work in progress and updates may include breaking changes.</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
case PluginState.beta:
|
||||
return 'Plugin in beta state. Means there could be bugs and minor breaking changes.';
|
||||
return (
|
||||
<div>
|
||||
<h5>Beta Plugin</h5>
|
||||
<p>There could be bugs and minor breaking changes to this plugin.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -34,10 +45,11 @@ const PluginStateinfo: FC<Props> = props => {
|
||||
font-size: 13px;
|
||||
padding: 4px 8px;
|
||||
margin-left: 16px;
|
||||
cursor: help;
|
||||
`;
|
||||
|
||||
return (
|
||||
<Tooltip content={text}>
|
||||
<Tooltip content={text} theme={'info'} placement={'top'}>
|
||||
<div className={styles}>
|
||||
<i className="fa fa-warning" /> {props.state}
|
||||
</div>
|
||||
|
||||
@@ -105,7 +105,7 @@ exposeToPlugin('app/core/services/backend_srv', {
|
||||
});
|
||||
|
||||
exposeToPlugin('app/plugins/sdk', sdk);
|
||||
exposeToPlugin('@grafana/ui/src/utils/datemath', datemath);
|
||||
exposeToPlugin('app/core/utils/datemath', datemath);
|
||||
exposeToPlugin('app/core/utils/file_export', fileExport);
|
||||
exposeToPlugin('app/core/utils/flatten', flatten);
|
||||
exposeToPlugin('app/core/utils/kbn', kbn);
|
||||
|
||||
@@ -108,17 +108,18 @@
|
||||
<gf-form-dropdown model="ctrl.target.azureLogAnalytics.workspace" allow-custom="true" lookup-text="true"
|
||||
get-options="ctrl.workspaces" on-change="ctrl.refresh()" css-class="min-width-12">
|
||||
</gf-form-dropdown>
|
||||
<div class="gf-form">
|
||||
<div class="width-1"></div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button class="btn btn-primary width-10" ng-click="ctrl.refresh()">Run</button>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">(Run Query: Shift+Enter, Trigger Suggestion: Ctrl+Space)</label>
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
<div class="gf-form">
|
||||
<div class="width-1"></div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button class="btn btn-primary width-10" ng-click="ctrl.refresh()">Run</button>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">(Run Query: Shift+Enter, Trigger Suggestion: Ctrl+Space)</label>
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export default class InfluxDatasource {
|
||||
this.withCredentials = instanceSettings.withCredentials;
|
||||
this.interval = (instanceSettings.jsonData || {}).timeInterval;
|
||||
this.responseParser = new ResponseParser();
|
||||
this.httpMode = instanceSettings.jsonData.httpMode;
|
||||
this.httpMode = instanceSettings.jsonData.httpMode || 'GET';
|
||||
}
|
||||
|
||||
query(options) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import React, { PureComponent } from 'react';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
// Components
|
||||
import { Gauge, FieldDisplay, getFieldDisplayValues } from '@grafana/ui';
|
||||
import { Gauge, FieldDisplay, getFieldDisplayValues, VizOrientation } from '@grafana/ui';
|
||||
|
||||
// Types
|
||||
import { GaugeOptions } from './types';
|
||||
@@ -43,7 +43,7 @@ export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { height, width, options, data, renderCounter } = this.props;
|
||||
const { height, width, data, renderCounter } = this.props;
|
||||
return (
|
||||
<VizRepeater
|
||||
getValues={this.getValues}
|
||||
@@ -52,7 +52,7 @@ export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
|
||||
height={height}
|
||||
source={data}
|
||||
renderCounter={renderCounter}
|
||||
orientation={options.orientation}
|
||||
orientation={VizOrientation.Auto}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
174
public/app/plugins/panel/gettingstarted/GettingStarted.tsx
Normal file
174
public/app/plugins/panel/gettingstarted/GettingStarted.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { PanelProps } from '@grafana/ui/src/types';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
|
||||
interface Step {
|
||||
title: string;
|
||||
cta?: string;
|
||||
icon: string;
|
||||
href: string;
|
||||
target?: string;
|
||||
note?: string;
|
||||
check: () => Promise<boolean>;
|
||||
done?: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
checksDone: boolean;
|
||||
}
|
||||
|
||||
export class GettingStarted extends PureComponent<PanelProps, State> {
|
||||
stepIndex = 0;
|
||||
readonly steps: Step[];
|
||||
|
||||
constructor(props: PanelProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
checksDone: false,
|
||||
};
|
||||
|
||||
this.steps = [
|
||||
{
|
||||
title: 'Install Grafana',
|
||||
icon: 'icon-gf icon-gf-check',
|
||||
href: 'http://docs.grafana.org/',
|
||||
target: '_blank',
|
||||
note: 'Review the installation docs',
|
||||
check: () => Promise.resolve(true),
|
||||
},
|
||||
{
|
||||
title: 'Create your first data source',
|
||||
cta: 'Add data source',
|
||||
icon: 'gicon gicon-datasources',
|
||||
href: 'datasources/new?gettingstarted',
|
||||
check: () => {
|
||||
return new Promise(resolve => {
|
||||
resolve(
|
||||
getDatasourceSrv()
|
||||
.getMetricSources()
|
||||
.filter(item => {
|
||||
return item.meta.builtIn !== true;
|
||||
}).length > 0
|
||||
);
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Create your first dashboard',
|
||||
cta: 'New dashboard',
|
||||
icon: 'gicon gicon-dashboard',
|
||||
href: 'dashboard/new?gettingstarted',
|
||||
check: () => {
|
||||
return getBackendSrv()
|
||||
.search({ limit: 1 })
|
||||
.then(result => {
|
||||
return result.length > 0;
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Invite your team',
|
||||
cta: 'Add Users',
|
||||
icon: 'gicon gicon-team',
|
||||
href: 'org/users?gettingstarted',
|
||||
check: () => {
|
||||
return getBackendSrv()
|
||||
.get('/api/org/users')
|
||||
.then(res => {
|
||||
return res.length > 1;
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Install apps & plugins',
|
||||
cta: 'Explore plugin repository',
|
||||
icon: 'gicon gicon-plugins',
|
||||
href: 'https://grafana.com/plugins?utm_source=grafana_getting_started',
|
||||
check: () => {
|
||||
return getBackendSrv()
|
||||
.get('/api/plugins', { embedded: 0, core: 0 })
|
||||
.then(plugins => {
|
||||
return plugins.length > 0;
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.stepIndex = -1;
|
||||
return this.nextStep().then((res: any) => {
|
||||
this.setState({ checksDone: true });
|
||||
});
|
||||
}
|
||||
|
||||
nextStep() {
|
||||
if (this.stepIndex === this.steps.length - 1) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.stepIndex += 1;
|
||||
const currentStep = this.steps[this.stepIndex];
|
||||
return currentStep.check().then(passed => {
|
||||
if (passed) {
|
||||
currentStep.done = true;
|
||||
return this.nextStep();
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
dismiss = () => {
|
||||
const { id } = this.props;
|
||||
const dashboard = getDashboardSrv().getCurrent();
|
||||
const panel = dashboard.getPanelById(id);
|
||||
dashboard.removePanel(panel);
|
||||
getBackendSrv()
|
||||
.request({
|
||||
method: 'PUT',
|
||||
url: '/api/user/helpflags/1',
|
||||
showSuccessAlert: false,
|
||||
})
|
||||
.then((res: any) => {
|
||||
contextSrv.user.helpFlags1 = res.helpFlags1;
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { checksDone } = this.state;
|
||||
if (!checksDone) {
|
||||
return <div>checking...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="progress-tracker-container">
|
||||
<button className="progress-tracker-close-btn" onClick={this.dismiss}>
|
||||
<i className="fa fa-remove" />
|
||||
</button>
|
||||
<div className="progress-tracker">
|
||||
{this.steps.map(step => {
|
||||
return (
|
||||
<div className={step.done ? 'progress-step completed' : 'progress-step active'}>
|
||||
<a className="progress-link" href={step.href} target={step.target} title={step.note}>
|
||||
<span className="progress-marker" ng-className="step.cssClass">
|
||||
<i className={step.icon} />
|
||||
</span>
|
||||
<span className="progress-text">{step.title}</span>
|
||||
</a>
|
||||
<a className="btn-small progress-step-cta" href={step.href} target={step.target}>
|
||||
{step.cta}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Mode</span>
|
||||
<div class="gf-form-select-wrapper max-width-10">
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.mode" ng-options="f for f in ctrl.modes" ng-change="ctrl.refresh()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.panel.mode === 'recently viewed'">
|
||||
<span class="gf-form-label">
|
||||
<i class="grafana-tip fa fa-question-circle ng-scope" bs-tooltip="'WARNING: This list will be cleared when clearing browser cache'" data-original-title="" title=""></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline" ng-if="ctrl.panel.mode === 'search'">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Search options</span>
|
||||
<span class="gf-form-label">Query</span>
|
||||
|
||||
<input type="text" class="gf-form-input" placeholder="title query"
|
||||
ng-model="ctrl.panel.query" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label">Tags</span>
|
||||
|
||||
<bootstrap-tagsinput ng-model="ctrl.panel.tags" tagclass="label label-tag" placeholder="add tags" on-tags-updated="ctrl.refresh()">
|
||||
</bootstrap-tagsinput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Limit number to</span>
|
||||
<input class="gf-form-input" type="number" ng-model="ctrl.panel.limit" ng-model-onblur ng-change="ctrl.refresh()">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,16 +0,0 @@
|
||||
<div class="dashlist" ng-if="ctrl.checksDone">
|
||||
<div class="dashlist-section">
|
||||
<button class="dashlist-cta-close-btn" ng-click="ctrl.dismiss()">
|
||||
<i class="fa fa-remove"></i>
|
||||
</button>
|
||||
<ul class="progress-tracker">
|
||||
<li class="progress-step" ng-repeat="step in ctrl.steps" ng-class="step.cssClass">
|
||||
<a class="progress-link" ng-href="{{step.href}}" target="{{step.target}}" title="{{step.note}}">
|
||||
<span class="progress-marker" ng-class="step.cssClass"><i class="{{step.icon}}"></i></span>
|
||||
<span class="progress-text" ng-href="{{step.href}}" target="{{step.target}}">{{step.title}}</span>
|
||||
</a>
|
||||
<a class="btn-small progress-step-cta" ng-href="{{step.href}}" target="{{step.target}}">{{step.cta}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,118 +1,5 @@
|
||||
import { PanelCtrl } from 'app/plugins/sdk';
|
||||
import { PanelPlugin } from '@grafana/ui';
|
||||
import { GettingStarted } from './GettingStarted';
|
||||
|
||||
import { contextSrv } from 'app/core/core';
|
||||
|
||||
class GettingStartedPanelCtrl extends PanelCtrl {
|
||||
static templateUrl = 'public/app/plugins/panel/gettingstarted/module.html';
|
||||
checksDone: boolean;
|
||||
stepIndex: number;
|
||||
steps: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope, $injector, private backendSrv, datasourceSrv, private $q) {
|
||||
super($scope, $injector);
|
||||
|
||||
this.stepIndex = 0;
|
||||
this.steps = [];
|
||||
|
||||
this.steps.push({
|
||||
title: 'Install Grafana',
|
||||
icon: 'icon-gf icon-gf-check',
|
||||
href: 'http://docs.grafana.org/',
|
||||
target: '_blank',
|
||||
note: 'Review the installation docs',
|
||||
check: () => $q.when(true),
|
||||
});
|
||||
|
||||
this.steps.push({
|
||||
title: 'Create your first data source',
|
||||
cta: 'Add data source',
|
||||
icon: 'gicon gicon-datasources',
|
||||
href: 'datasources/new?gettingstarted',
|
||||
check: () => {
|
||||
return $q.when(
|
||||
datasourceSrv.getMetricSources().filter(item => {
|
||||
return item.meta.builtIn !== true;
|
||||
}).length > 0
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
this.steps.push({
|
||||
title: 'Create your first dashboard',
|
||||
cta: 'New dashboard',
|
||||
icon: 'gicon gicon-dashboard',
|
||||
href: 'dashboard/new?gettingstarted',
|
||||
check: () => {
|
||||
return this.backendSrv.search({ limit: 1 }).then(result => {
|
||||
return result.length > 0;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
this.steps.push({
|
||||
title: 'Invite your team',
|
||||
cta: 'Add Users',
|
||||
icon: 'gicon gicon-team',
|
||||
href: 'org/users?gettingstarted',
|
||||
check: () => {
|
||||
return this.backendSrv.get('/api/org/users').then(res => {
|
||||
return res.length > 1;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
this.steps.push({
|
||||
title: 'Install apps & plugins',
|
||||
cta: 'Explore plugin repository',
|
||||
icon: 'gicon gicon-plugins',
|
||||
href: 'https://grafana.com/plugins?utm_source=grafana_getting_started',
|
||||
check: () => {
|
||||
return this.backendSrv.get('/api/plugins', { embedded: 0, core: 0 }).then(plugins => {
|
||||
return plugins.length > 0;
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.stepIndex = -1;
|
||||
return this.nextStep().then(res => {
|
||||
this.checksDone = true;
|
||||
});
|
||||
}
|
||||
|
||||
nextStep() {
|
||||
if (this.stepIndex === this.steps.length - 1) {
|
||||
return this.$q.when();
|
||||
}
|
||||
|
||||
this.stepIndex += 1;
|
||||
const currentStep = this.steps[this.stepIndex];
|
||||
return currentStep.check().then(passed => {
|
||||
if (passed) {
|
||||
currentStep.cssClass = 'completed';
|
||||
return this.nextStep();
|
||||
}
|
||||
|
||||
currentStep.cssClass = 'active';
|
||||
return this.$q.when();
|
||||
});
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
this.dashboard.removePanel(this.panel, false);
|
||||
|
||||
this.backendSrv
|
||||
.request({
|
||||
method: 'PUT',
|
||||
url: '/api/user/helpflags/1',
|
||||
showSuccessAlert: false,
|
||||
})
|
||||
.then(res => {
|
||||
contextSrv.user.helpFlags1 = res.helpFlags1;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { GettingStartedPanelCtrl, GettingStartedPanelCtrl as PanelCtrl };
|
||||
// Simplest possible panel plugin
|
||||
export const plugin = new PanelPlugin(GettingStarted);
|
||||
|
||||
@@ -13,18 +13,22 @@ $marker-size-half: ($marker-size / 2);
|
||||
$path-height: 2px !default;
|
||||
$path-position: $marker-size-half - ($path-height / 2);
|
||||
|
||||
.dashlist-cta-close-btn {
|
||||
.progress-tracker-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progress-tracker-close-btn {
|
||||
color: $text-color-weak;
|
||||
float: right;
|
||||
padding: 0;
|
||||
margin: 0 2px 0 0;
|
||||
position: absolute;
|
||||
z-index: $panel-header-z-index;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
font-size: $font-size-lg;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
|
||||
i {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $white;
|
||||
}
|
||||
@@ -33,9 +37,9 @@ $path-position: $marker-size-half - ($path-height / 2);
|
||||
// Container element
|
||||
.progress-tracker {
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// Step container that creates lines between steps
|
||||
@@ -46,6 +50,7 @@ $path-position: $marker-size-half - ($path-height / 2);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: $text-color-weak;
|
||||
height: 84px;
|
||||
|
||||
// For a flexbox bug in firefox that wont allow the text overflow on the text
|
||||
min-width: $marker-size;
|
||||
@@ -54,7 +59,7 @@ $path-position: $marker-size-half - ($path-height / 2);
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
z-index: 0;
|
||||
top: $path-position;
|
||||
bottom: $path-position;
|
||||
right: -$marker-size-half;
|
||||
@@ -134,11 +139,10 @@ $path-position: $marker-size-half - ($path-height / 2);
|
||||
width: $marker-size;
|
||||
height: $marker-size;
|
||||
padding-bottom: 2px; // To align text within the marker
|
||||
z-index: 20;
|
||||
z-index: 1;
|
||||
background-color: $panel-bg;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-bottom: $spacer;
|
||||
color: $text-color-weak;
|
||||
font-size: 35px;
|
||||
vertical-align: sub;
|
||||
|
||||
@@ -126,6 +126,7 @@ $panel-header-no-title-zindex: 1;
|
||||
left: 0;
|
||||
width: $panel-header-height;
|
||||
height: $panel-header-height;
|
||||
z-index: $panel-header-no-title-zindex + 1;
|
||||
top: 0;
|
||||
|
||||
.fa {
|
||||
@@ -138,7 +139,8 @@ $panel-header-no-title-zindex: 1;
|
||||
|
||||
&--info {
|
||||
display: block;
|
||||
@include panel-corner-color(lighten($panel-corner, 4%));
|
||||
@include panel-corner-color(lighten($panel-corner, 6%));
|
||||
|
||||
.fa:before {
|
||||
content: '\f129';
|
||||
}
|
||||
@@ -146,7 +148,7 @@ $panel-header-no-title-zindex: 1;
|
||||
|
||||
&--links {
|
||||
display: block;
|
||||
@include panel-corner-color(lighten($panel-corner, 4%));
|
||||
@include panel-corner-color(lighten($panel-corner, 6%));
|
||||
.fa {
|
||||
left: 4px;
|
||||
}
|
||||
|
||||
@@ -6,16 +6,17 @@
|
||||
}
|
||||
|
||||
.singlestat-panel-value-container {
|
||||
line-height: 1;
|
||||
// line-height 0 is imporant here as the font-size is on this
|
||||
// level but overriden one level deeper and but the line-height: is still
|
||||
// based on the base font size on this level. Using line-height: 0 fixes that
|
||||
line-height: 0;
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-size: 3em;
|
||||
font-weight: $font-weight-semi-bold;
|
||||
// helps make the title feel more centered when there is a panel title
|
||||
padding-bottom: $panel-padding;
|
||||
font-size: 38px;
|
||||
}
|
||||
|
||||
// Helps
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
}
|
||||
|
||||
.table-panel-table-header-inner {
|
||||
padding: 0.45em 0 0.45em 1.1em;
|
||||
padding: 0.3em 0 0.45em 1.1em;
|
||||
text-align: left;
|
||||
color: $blue;
|
||||
position: absolute;
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
height: 100%; // Chrome 74 needs this to make the element scrollable
|
||||
|
||||
.search-item--indent {
|
||||
margin-left: 14px;
|
||||
@@ -258,10 +259,6 @@
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.search-dropdown__col_1 {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.search-filter-box {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-button--zoom,
|
||||
.navbar-button--refresh {
|
||||
.navbar-button--zoom {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
.panel-alert-icon:before {
|
||||
content: '\e611';
|
||||
position: relative;
|
||||
top: 5px;
|
||||
top: 1px;
|
||||
left: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ EXTRA_OPTS="$@"
|
||||
|
||||
# Right now we hack this in into the publish script.
|
||||
# Eventually we might want to keep a list of all previous releases somewhere.
|
||||
_releaseNoteUrl="https://community.grafana.com/t/release-notes-v6-0-x/14010"
|
||||
_whatsNewUrl="http://docs.grafana.org/guides/whats-new-in-v6-0/"
|
||||
_releaseNoteUrl="https://community.grafana.com/t/release-notes-v6-2-x/17037"
|
||||
_whatsNewUrl="https://grafana.com/docs/guides/whats-new-in-v6-2/"
|
||||
|
||||
./scripts/build/release_publisher/release_publisher \
|
||||
--wn ${_whatsNewUrl} \
|
||||
|
||||
@@ -174,6 +174,11 @@ var completeBuildArtifactConfigurations = []buildArtifact{
|
||||
arch: "amd64",
|
||||
urlPostfix: ".windows-amd64.zip",
|
||||
},
|
||||
{
|
||||
os: "win-installer",
|
||||
arch: "amd64",
|
||||
urlPostfix: ".windows-amd64.msi",
|
||||
},
|
||||
}
|
||||
|
||||
type artifactFilter struct {
|
||||
|
||||
@@ -74,6 +74,34 @@ func TestPreparingReleaseFromRemote(t *testing.T) {
|
||||
baseArchiveURL: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana",
|
||||
buildArtifacts: []buildArtifact{{"linux", "armv6", "_armhf.deb", "-rpi"}},
|
||||
},
|
||||
{
|
||||
version: "v5.4.0-pre1asdf",
|
||||
expectedVersion: "5.4.0-pre1asdf",
|
||||
whatsNewURL: "https://whatsnews.foo/",
|
||||
relNotesURL: "https://relnotes.foo/",
|
||||
nightly: true,
|
||||
expectedBeta: false,
|
||||
expectedStable: false,
|
||||
expectedArch: "amd64",
|
||||
expectedOs: "win-installer",
|
||||
expectedURL: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.4.0-pre1asdf.windows-amd64.msi",
|
||||
baseArchiveURL: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana",
|
||||
buildArtifacts: []buildArtifact{{"win-installer", "amd64", ".windows-amd64.msi", ""}},
|
||||
},
|
||||
{
|
||||
version: "v5.4.0-pre1asdf",
|
||||
expectedVersion: "5.4.0-pre1asdf",
|
||||
whatsNewURL: "https://whatsnews.foo/",
|
||||
relNotesURL: "https://relnotes.foo/",
|
||||
nightly: true,
|
||||
expectedBeta: false,
|
||||
expectedStable: false,
|
||||
expectedArch: "amd64",
|
||||
expectedOs: "win",
|
||||
expectedURL: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.4.0-pre1asdf.windows-amd64.zip",
|
||||
baseArchiveURL: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana",
|
||||
buildArtifacts: []buildArtifact{{"win", "amd64", ".windows-amd64.zip", ""}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range cases {
|
||||
|
||||
0
scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.windows-amd64.msi
vendored
Normal file
0
scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.windows-amd64.msi
vendored
Normal file
@@ -0,0 +1 @@
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||
Reference in New Issue
Block a user