Compare commits

...

26 Commits

Author SHA1 Message Date
Torkel Ödegaard
7824f66cd3 Explore: fixed cherrypick / merge issue 2019-05-15 13:10:59 +02:00
Torkel Ödegaard
71c1e8a731 Release: Bumped version to v6.2.0-beta2 2019-05-15 12:21:21 +02:00
Carl Bergquist
74bc94bcec Remotecache: Avoid race condition in Set causing error on insert. (#17082)
* remotecache: avoid race condition in set

since set called the database twice without transactions another
operation could insert a value before the first operation completed.
which would raise an error on insert since the data have been inserted
by the other request.

closes #17079

(cherry picked from commit aed3d0d3ad)
2019-05-15 12:14:16 +02:00
Brian Gann
f566da0ff6 Build: Support publishing MSI to grafana.com (#17073)
* add test for msi, and support for publishing msi
* update arch and os in test
* Build: Fixed issues with os naming

(cherry picked from commit d0ea98f6bd)
2019-05-15 12:14:16 +02:00
Torkel Ödegaard
216aff96fd Panels: Fixed alert icon position in panel header (#17070)
(cherry picked from commit 238a929262)
2019-05-15 12:14:15 +02:00
Torkel Ödegaard
9de11c25a6 Gauge: Fix switching orientation issue when switching from BarGauge to Gauge (#17064)
(cherry picked from commit 68ad93f410)
2019-05-15 12:14:15 +02:00
Torkel Ödegaard
599e1030d8 Dashboard: Fixes lazy loading & expanding collapsed rows on mobile (#17055)
* Dashboard: Fixes lazy loading & expanding collapsing rows on mobile

Fixes #16978

* Updated dashboard tags

(cherry picked from commit 74a31bd9b4)
2019-05-15 12:14:15 +02:00
Daniel Lee
d62da61d8a fix: Azure Monitor adds missing closing div tag to query editor (#17057)
(cherry picked from commit 4bc1a66fe4)
2019-05-15 12:14:15 +02:00
Johannes Schill
a2ca973925 Search: Set element height to 100% to avoid Chrome 74's overflow (#17054)
Fixes #16981

(cherry picked from commit ceb21bd653)
2019-05-15 12:14:15 +02:00
Johannes Schill
98da29fd7b Dashboard: Fixes scrolling issues for Edge browser (#17033)
* Fix: Only set scrollTop on CustomScroll element when it's needed and move arrow functions out of the props

* Fix: Update snapshots

* Minor refactoring to reuse same functions when rendering custom scrollbar

Fixes #16796

(cherry picked from commit 1001cd7ac3)
2019-05-15 12:14:15 +02:00
Torkel Ödegaard
888ff61d30 Dashboard: show refresh button in kiosk mode (#17032)
Fixes #16945

(cherry picked from commit 3ce2f3b58d)
2019-05-15 12:14:15 +02:00
Torkel Ödegaard
c1be3adf3b Gauge: tweaks to background color and height usage (#17019)
(cherry picked from commit 597c380ead)
2019-05-15 12:14:15 +02:00
Marcus Efraimsson
ede9d9964d Explore: Fix empty result from datasource should render logs container (#16999)
Make sure to return an empty logs model instead of undefined so that 
the logs container renders an empty graph and log result in Explore.

Fixes #16997

(cherry picked from commit 8eb78ea931)
2019-05-15 12:14:15 +02:00
Hugo Häggmark
5cd69e8d39 Explore: Fixes zoom exception in Loki/Graph (#16991)
Fixes: #16986
(cherry picked from commit d5a35f3631)
2019-05-15 12:14:14 +02:00
Torkel Ödegaard
ceb3672482 Panels: Fixed error panel tooltip (#16993)
Fixes #16989

(cherry picked from commit 5573d28582)
2019-05-15 12:13:29 +02:00
Will Medlar
e519a9d2c4 Docker: Prevent a permission denied error when writing files to the default provisioning directory (#16831)
* build: Grant ownership of provisioning directory to runtime user

(cherry picked from commit 5e44f001fb)
2019-05-15 12:07:59 +02:00
Ryan McKinley
2a3d6604c0 GettingStarted: convert to react panel plugin (#16985)
* getting started

* getting started

(cherry picked from commit f617cd8975)
2019-05-15 11:53:46 +02:00
Marcus Efraimsson
9cf0ea5395 plugins: fix how datemath utils are exposed to plugins (#16976)
Fixes a regression accidentally introduced by #16890 so that datemath 
utils are exposed to plugins in a backward-compatible way.

Fixes #16962

(cherry picked from commit 0c1530c7a8)
2019-05-15 11:53:35 +02:00
Torkel Ödegaard
db37e138bf GettingStarted: Fixes layout issues, fixes #16926 (#16941)
(cherry picked from commit a9e01d8b04)
2019-05-15 11:53:27 +02:00
Torkel Ödegaard
a5a6d43f47 PanelModel: Fix crash after window resize, fixes #16933 (#16942)
(cherry picked from commit f58ab7945b)
2019-05-15 11:52:31 +02:00
Torkel Ödegaard
d9950aa4f1 Singlestat: fixed centering issue for very small panels (#16944)
(cherry picked from commit e97853abc9)
2019-05-15 11:52:23 +02:00
Stephen SORRIAUX
6e9a395063 InfluxDB: Fix HTTP method should default to GET (#16949)
Fixes #16929

(cherry picked from commit e7a9afe983)
2019-05-15 11:52:14 +02:00
Ryan McKinley
7374aafb90 AppPlugin: Menu Edit Url Fix (#16934)
(cherry picked from commit 17ce1273ca)
2019-05-15 11:52:02 +02:00
Ryan McKinley
b54e9880b4 Plugins: update beta notice style (#16928)
(cherry picked from commit b08cf1e7ac)
2019-05-15 11:51:50 +02:00
Brian Gann
09672a287f Plugins: Support templated urls in routes (#16599)
This adds support for using templated/dynamic urls in routes.
* refactor interpolateString into utils and add interpolation support for app plugin routes.
* cleanup and add error check for url parse failure
* add docs for interpolated route urls

Closes #16835

(cherry picked from commit b07d0b1026)
2019-05-15 11:50:34 +02:00
Andrej Ocenas
9d877d670e release 6.2.0-beta1 2019-05-07 16:08:46 +02:00
54 changed files with 3066 additions and 369 deletions

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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.

View File

@@ -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"

View File

@@ -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>

View File

@@ -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",

View File

@@ -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,

View File

@@ -32,6 +32,7 @@ export interface PanelData {
}
export interface PanelProps<T = any> {
id: number; // ID within the current dashboard
data: PanelData;
// TODO: annotation?: PanelData;

View File

@@ -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

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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")
})

View File

@@ -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))
}

View File

@@ -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

View 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
}

View 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")
})
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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', () => {

View File

@@ -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",

View File

@@ -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"
>

View File

@@ -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}
>

View File

@@ -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();
});
});
});

View File

@@ -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;

View File

@@ -1,4 +0,0 @@
import { react2AngularDirective } from 'app/core/utils/react2angular';
import DashboardGrid from './DashboardGrid';
react2AngularDirective('dashboardGrid', DashboardGrid, [['dashboard', { watchDepth: 'reference' }]]);

View File

@@ -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)}

View File

@@ -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">

View File

@@ -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)>
`;

View File

@@ -1,5 +1,3 @@
import './dashgrid/DashboardGridDirective';
// Services
import './services/UnsavedChangesSrv';
import './services/DashboardLoaderSrv';

View File

@@ -324,7 +324,7 @@ export class PanelModel {
}
hasTitle() {
return !!this.title.length;
return this.title && this.title.length > 0;
}
destroy() {

View File

@@ -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}

View File

@@ -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)(

View File

@@ -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>

View File

@@ -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);

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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}
/>
);
}

View 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>
);
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -16,8 +16,7 @@
}
}
.navbar-button--zoom,
.navbar-button--refresh {
.navbar-button--zoom {
display: none;
}
}

View File

@@ -32,7 +32,7 @@
.panel-alert-icon:before {
content: '\e611';
position: relative;
top: 5px;
top: 1px;
left: -3px;
}
}

View File

@@ -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} \

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -0,0 +1 @@
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855