Merge branch 'master' into data-source-settings-to-react

This commit is contained in:
Peter Holmberg 2018-11-07 09:50:17 +01:00
commit e25b2d0ab6
79 changed files with 1091 additions and 394 deletions

View File

@ -335,6 +335,9 @@ jobs:
- run: - run:
name: deploy to gcp name: deploy to gcp
command: '/opt/google-cloud-sdk/bin/gsutil cp ./enterprise-dist/* gs://$GCP_BUCKET_NAME/enterprise/master' command: '/opt/google-cloud-sdk/bin/gsutil cp ./enterprise-dist/* gs://$GCP_BUCKET_NAME/enterprise/master'
- run:
name: Deploy to grafana.com
command: 'cd enterprise-dist && ../scripts/build/release_publisher/release_publisher -apikey ${GRAFANA_COM_API_KEY} -enterprise -from-local'
deploy-enterprise-release: deploy-enterprise-release:
@ -403,7 +406,7 @@ jobs:
command: '/opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file=/tmp/gcpkey.json' command: '/opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file=/tmp/gcpkey.json'
- run: - run:
name: deploy to gcp name: deploy to gcp
command: '/opt/google-cloud-sdk/bin/gsutil cp ./dist/* gs://R/oss/release' command: '/opt/google-cloud-sdk/bin/gsutil cp ./dist/* gs://$GCP_BUCKET_NAME/oss/release'
- run: - run:
name: Deploy to Grafana.com name: Deploy to Grafana.com
command: './scripts/build/publish.sh' command: './scripts/build/publish.sh'

1
.gitignore vendored
View File

@ -8,6 +8,7 @@ awsconfig
/dist /dist
/public/build /public/build
/public/views/index.html /public/views/index.html
/public/views/error.html
/emails/dist /emails/dist
/public_gen /public_gen
/public/vendor/npm /public/vendor/npm

View File

@ -12,11 +12,14 @@
### Minor ### Minor
* **Cloudwatch**: Show all available CloudWatch regions [#12308](https://github.com/grafana/grafana/issues/12308), thx [@mtanda](https://github.com/mtanda) * **Cloudwatch**: Show all available CloudWatch regions [#12308](https://github.com/grafana/grafana/issues/12308), thx [@mtanda](https://github.com/mtanda)
* **Cloudwatch**: AWS/Connect metrics and dimensions [#13970](https://github.com/grafana/grafana/pull/13970), thx [@zcoffy](https://github.com/zcoffy)
* **Postgres**: Add delta window function to postgres query builder [#13925](https://github.com/grafana/grafana/issues/13925), thx [svenklemm](https://github.com/svenklemm) * **Postgres**: Add delta window function to postgres query builder [#13925](https://github.com/grafana/grafana/issues/13925), thx [svenklemm](https://github.com/svenklemm)
* **Units**: New clock time format, to format ms or second values as for example `01h:59m`, [#13635](https://github.com/grafana/grafana/issues/13635), thx [@franciscocpg](https://github.com/franciscocpg) * **Units**: New clock time format, to format ms or second values as for example `01h:59m`, [#13635](https://github.com/grafana/grafana/issues/13635), thx [@franciscocpg](https://github.com/franciscocpg)
* **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu) * **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu)
* **DingDing**: Can't receive DingDing alert when alert is triggered [#13723](https://github.com/grafana/grafana/issues/13723), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino) * **DingDing**: Can't receive DingDing alert when alert is triggered [#13723](https://github.com/grafana/grafana/issues/13723), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
* **Internal metrics**: Renamed `grafana_info` to `grafana_build_info` and added branch, goversion and revision [#13876](https://github.com/grafana/grafana/pull/13876) * **Internal metrics**: Renamed `grafana_info` to `grafana_build_info` and added branch, goversion and revision [#13876](https://github.com/grafana/grafana/pull/13876)
* **Alerting**: Increaste default duration for queries [#13945](https://github.com/grafana/grafana/pull/13945)
* **Table**: Fix CSS alpha background-color applied twice in table cell with link [#13606](https://github.com/grafana/grafana/issues/13606), thx [@grisme](https://github.com/grisme)
### Breaking changes ### Breaking changes
@ -24,7 +27,10 @@
# 5.3.3 (unreleased) # 5.3.3 (unreleased)
* **Alerting**: Delete alerts when parent folder was deleted [#13322](https://github.com/grafana/grafana/issues/13322)
* **MySQL**: Fix `$__timeFilter()` should respect local time zone [#13769](https://github.com/grafana/grafana/issues/13769) * **MySQL**: Fix `$__timeFilter()` should respect local time zone [#13769](https://github.com/grafana/grafana/issues/13769)
* **Dashboard**: Fix datasource selection in panel by enter key [#13932](https://github.com/grafana/grafana/issues/13932)
* **Graph**: Fix table legend height when positioned below graph and using Internet Explorer 11 [#13903](https://github.com/grafana/grafana/issues/13903)
# 5.3.2 (2018-10-24) # 5.3.2 (2018-10-24)

View File

@ -41,8 +41,8 @@ var (
race bool race bool
phjsToRelease string phjsToRelease string
workingDir string workingDir string
includeBuildNumber bool = true includeBuildId bool = true
buildNumber int = 0 buildId string = "0"
binaries []string = []string{"grafana-server", "grafana-cli"} binaries []string = []string{"grafana-server", "grafana-cli"}
isDev bool = false isDev bool = false
enterprise bool = false enterprise bool = false
@ -54,6 +54,8 @@ func main() {
ensureGoPath() ensureGoPath()
var buildIdRaw string
flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH") flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH")
flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS") flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS")
flag.StringVar(&gocc, "cc", "", "CC") flag.StringVar(&gocc, "cc", "", "CC")
@ -61,12 +63,14 @@ func main() {
flag.StringVar(&pkgArch, "pkg-arch", "", "PKG ARCH") flag.StringVar(&pkgArch, "pkg-arch", "", "PKG ARCH")
flag.StringVar(&phjsToRelease, "phjs", "", "PhantomJS binary") flag.StringVar(&phjsToRelease, "phjs", "", "PhantomJS binary")
flag.BoolVar(&race, "race", race, "Use race detector") flag.BoolVar(&race, "race", race, "Use race detector")
flag.BoolVar(&includeBuildNumber, "includeBuildNumber", includeBuildNumber, "IncludeBuildNumber in package name") flag.BoolVar(&includeBuildId, "includeBuildId", includeBuildId, "IncludeBuildId in package name")
flag.BoolVar(&enterprise, "enterprise", enterprise, "Build enterprise version of Grafana") flag.BoolVar(&enterprise, "enterprise", enterprise, "Build enterprise version of Grafana")
flag.IntVar(&buildNumber, "buildNumber", 0, "Build number from CI system") flag.StringVar(&buildIdRaw, "buildId", "0", "Build ID from CI system")
flag.BoolVar(&isDev, "dev", isDev, "optimal for development, skips certain steps") flag.BoolVar(&isDev, "dev", isDev, "optimal for development, skips certain steps")
flag.Parse() flag.Parse()
buildId = shortenBuildId(buildIdRaw)
readVersionFromPackageJson() readVersionFromPackageJson()
if pkgArch == "" { if pkgArch == "" {
@ -197,9 +201,9 @@ func readVersionFromPackageJson() {
} }
// add timestamp to iteration // add timestamp to iteration
if includeBuildNumber { if includeBuildId {
if buildNumber != 0 { if buildId != "0" {
linuxPackageIteration = fmt.Sprintf("%d%s", buildNumber, linuxPackageIteration) linuxPackageIteration = fmt.Sprintf("%s%s", buildId, linuxPackageIteration)
} else { } else {
linuxPackageIteration = fmt.Sprintf("%d%s", time.Now().Unix(), linuxPackageIteration) linuxPackageIteration = fmt.Sprintf("%d%s", time.Now().Unix(), linuxPackageIteration)
} }
@ -392,7 +396,7 @@ func grunt(params ...string) {
func gruntBuildArg(task string) []string { func gruntBuildArg(task string) []string {
args := []string{task} args := []string{task}
if includeBuildNumber { if includeBuildId {
args = append(args, fmt.Sprintf("--pkgVer=%v-%v", linuxPackageVersion, linuxPackageIteration)) args = append(args, fmt.Sprintf("--pkgVer=%v-%v", linuxPackageVersion, linuxPackageIteration))
} else { } else {
args = append(args, fmt.Sprintf("--pkgVer=%v", version)) args = append(args, fmt.Sprintf("--pkgVer=%v", version))
@ -632,3 +636,11 @@ func shaFile(file string) error {
return out.Close() return out.Close()
} }
func shortenBuildId(buildId string) string {
buildId = strings.Replace(buildId, "-", "", -1)
if (len(buildId) < 9) {
return buildId
}
return buildId[0:8]
}

View File

@ -404,6 +404,112 @@
"title": "Column style thresholds & units", "title": "Column style thresholds & units",
"transform": "timeseries_to_columns", "transform": "timeseries_to_columns",
"type": "table" "type": "table"
},
{
"columns": [],
"datasource": "gdev-testdata",
"fontSize": "100%",
"gridPos": {
"h": 10,
"w": 24,
"x": 0,
"y": 26
},
"id": 6,
"links": [],
"pageSize": 20,
"scroll": true,
"showHeader": true,
"sort": {
"col": 0,
"desc": true
},
"styles": [
{
"alias": "Time",
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"pattern": "Time",
"type": "date"
},
{
"alias": "",
"colorMode": "cell",
"colors": [
"rgba(245, 54, 54, 0.5)",
"rgba(237, 129, 40, 0.5)",
"rgba(50, 172, 45, 0.5)"
],
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"decimals": 2,
"link": true,
"linkTargetBlank": true,
"linkTooltip": "",
"linkUrl": "http://www.grafana.com",
"mappingType": 1,
"pattern": "ColorCell",
"thresholds": [
"5",
"10"
],
"type": "number",
"unit": "currencyUSD"
},
{
"alias": "",
"colorMode": "value",
"colors": [
"rgba(245, 54, 54, 0.5)",
"rgba(237, 129, 40, 0.5)",
"rgba(50, 172, 45, 0.5)"
],
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"decimals": 2,
"link": true,
"linkUrl": "http://www.grafana.com",
"mappingType": 1,
"pattern": "ColorValue",
"thresholds": [
"5",
"10"
],
"type": "number",
"unit": "Bps"
},
{
"alias": "",
"colorMode": null,
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"decimals": 2,
"pattern": "/.*/",
"thresholds": [],
"type": "number",
"unit": "short"
}
],
"targets": [
{
"alias": "ColorValue",
"expr": "",
"format": "table",
"intervalFactor": 1,
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "null,1,20,90,30,5,0,20,10"
},
{
"alias": "ColorCell",
"refId": "B",
"scenarioId": "csv_metric_values",
"stringInput": "null,5,1,2,3,4,5,10,20"
}
],
"title": "Column style thresholds and links",
"transform": "timeseries_to_columns",
"type": "table"
} }
], ],
"refresh": false, "refresh": false,
@ -449,5 +555,5 @@
"timezone": "browser", "timezone": "browser",
"title": "Panel Tests - Table", "title": "Panel Tests - Table",
"uid": "pttable", "uid": "pttable",
"version": 1 "version": 2
} }

View File

@ -60,7 +60,8 @@ Here is a minimal policy example:
"Effect": "Allow", "Effect": "Allow",
"Action": [ "Action": [
"cloudwatch:ListMetrics", "cloudwatch:ListMetrics",
"cloudwatch:GetMetricStatistics" "cloudwatch:GetMetricStatistics",
"cloudwatch:GetMetricData"
], ],
"Resource": "*" "Resource": "*"
}, },

View File

@ -18,7 +18,7 @@ Grafana v5.3 brings new features, many enhancements and bug fixes. This article
- [TV mode]({{< relref "#tv-and-kiosk-mode" >}}) is improved and more accessible - [TV mode]({{< relref "#tv-and-kiosk-mode" >}}) is improved and more accessible
- [Alerting]({{< relref "#notification-reminders" >}}) with notification reminders - [Alerting]({{< relref "#notification-reminders" >}}) with notification reminders
- [Postgres]({{< relref "#postgres-query-builder" >}}) gets a new query builder! - [Postgres]({{< relref "#postgres-query-builder" >}}) gets a new query builder!
- [OAuth]({{< relref "#improved-oauth-support-for-gitlab" >}}) support for Gitlab is improved - [OAuth]({{< relref "#improved-oauth-support-for-gitlab" >}}) support for GitLab is improved
- [Annotations]({{< relref "#annotations" >}}) with template variable filtering - [Annotations]({{< relref "#annotations" >}}) with template variable filtering
- [Variables]({{< relref "#variables" >}}) with free text support - [Variables]({{< relref "#variables" >}}) with free text support
@ -69,9 +69,9 @@ Grafana 5.3 comes with a new graphical query builder for Postgres. This brings P
{{< docs-imagebox img="/img/docs/v53/postgres_query_still.png" class="docs-image--no-shadow" animated-gif="/img/docs/v53/postgres_query.gif" >}} {{< docs-imagebox img="/img/docs/v53/postgres_query_still.png" class="docs-image--no-shadow" animated-gif="/img/docs/v53/postgres_query.gif" >}}
## Improved OAuth Support for Gitlab ## Improved OAuth Support for GitLab
Grafana 5.3 comes with a new OAuth integration for Gitlab that enables configuration to only allow users that are a member of certain Gitlab groups to authenticate. This makes it possible to use Gitlab OAuth with Grafana in a shared environment without giving everyone access to Grafana. Grafana 5.3 comes with a new OAuth integration for GitLab that enables configuration to only allow users that are a member of certain GitLab groups to authenticate. This makes it possible to use GitLab OAuth with Grafana in a shared environment without giving everyone access to Grafana.
Learn how to enable and configure it in the [documentation](/auth/gitlab/). Learn how to enable and configure it in the [documentation](/auth/gitlab/).
## Annotations ## Annotations

View File

@ -290,7 +290,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
"sendReminder": true, "sendReminder": true,
"frequency": "15m", "frequency": "15m",
"settings": { "settings": {
"addresses: "carl@grafana.com;dev@grafana.com" "addresses": "carl@grafana.com;dev@grafana.com"
} }
} }
``` ```

View File

@ -134,12 +134,16 @@ func AlertTest(c *m.ReqContext, dto dtos.AlertTestCommand) Response {
OrgId: c.OrgId, OrgId: c.OrgId,
Dashboard: dto.Dashboard, Dashboard: dto.Dashboard,
PanelId: dto.PanelId, PanelId: dto.PanelId,
User: c.SignedInUser,
} }
if err := bus.Dispatch(&backendCmd); err != nil { if err := bus.Dispatch(&backendCmd); err != nil {
if validationErr, ok := err.(alerting.ValidationError); ok { if validationErr, ok := err.(alerting.ValidationError); ok {
return Error(422, validationErr.Error(), nil) return Error(422, validationErr.Error(), nil)
} }
if err == m.ErrDataSourceAccessDenied {
return Error(403, "Access denied to datasource", err)
}
return Error(500, "Failed to test rule", err) return Error(500, "Failed to test rule", err)
} }

View File

@ -1,62 +1,22 @@
package api package api
import ( import (
"fmt"
"github.com/pkg/errors"
"time"
"github.com/grafana/grafana/pkg/api/pluginproxy" "github.com/grafana/grafana/pkg/api/pluginproxy"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/metrics"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
) )
const HeaderNameNoBackendCache = "X-Grafana-NoCache"
func (hs *HTTPServer) getDatasourceFromCache(id int64, c *m.ReqContext) (*m.DataSource, error) {
userPermissionsQuery := m.GetDataSourcePermissionsForUserQuery{
User: c.SignedInUser,
}
if err := bus.Dispatch(&userPermissionsQuery); err != nil {
if err != bus.ErrHandlerNotFound {
return nil, err
}
} else {
permissionType, exists := userPermissionsQuery.Result[id]
if exists && permissionType != m.DsPermissionQuery {
return nil, errors.New("User not allowed to access datasource")
}
}
nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true"
cacheKey := fmt.Sprintf("ds-%d", id)
if !nocache {
if cached, found := hs.cache.Get(cacheKey); found {
ds := cached.(*m.DataSource)
if ds.OrgId == c.OrgId {
return ds, nil
}
}
}
query := m.GetDataSourceByIdQuery{Id: id, OrgId: c.OrgId}
if err := bus.Dispatch(&query); err != nil {
return nil, err
}
hs.cache.Set(cacheKey, query.Result, time.Second*5)
return query.Result, nil
}
func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) { func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) {
c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer) c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer)
dsId := c.ParamsInt64(":id") dsId := c.ParamsInt64(":id")
ds, err := hs.getDatasourceFromCache(dsId, c) ds, err := hs.DatasourceCache.GetDatasource(dsId, c.SignedInUser, c.SkipCache)
if err != nil { if err != nil {
if err == m.ErrDataSourceAccessDenied {
c.JsonApiErr(403, "Access denied to datasource", err)
return
}
c.JsonApiErr(500, "Unable to load datasource meta data", err) c.JsonApiErr(500, "Unable to load datasource meta data", err)
return return
} }

View File

@ -16,7 +16,6 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
gocache "github.com/patrickmn/go-cache"
macaron "gopkg.in/macaron.v1" macaron "gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/api/live" "github.com/grafana/grafana/pkg/api/live"
@ -28,6 +27,8 @@ import (
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/cache"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/hooks" "github.com/grafana/grafana/pkg/services/hooks"
"github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
@ -46,7 +47,6 @@ type HTTPServer struct {
macaron *macaron.Macaron macaron *macaron.Macaron
context context.Context context context.Context
streamManager *live.StreamManager streamManager *live.StreamManager
cache *gocache.Cache
httpSrv *http.Server httpSrv *http.Server
RouteRegister routing.RouteRegister `inject:""` RouteRegister routing.RouteRegister `inject:""`
@ -54,11 +54,12 @@ type HTTPServer struct {
RenderService rendering.Service `inject:""` RenderService rendering.Service `inject:""`
Cfg *setting.Cfg `inject:""` Cfg *setting.Cfg `inject:""`
HooksService *hooks.HooksService `inject:""` HooksService *hooks.HooksService `inject:""`
CacheService *cache.CacheService `inject:""`
DatasourceCache datasources.CacheService `inject:""`
} }
func (hs *HTTPServer) Init() error { func (hs *HTTPServer) Init() error {
hs.log = log.New("http.server") hs.log = log.New("http.server")
hs.cache = gocache.New(5*time.Minute, 10*time.Minute)
hs.streamManager = live.NewStreamManager() hs.streamManager = live.NewStreamManager()
hs.macaron = hs.newMacaron() hs.macaron = hs.newMacaron()
@ -231,6 +232,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
m.Use(middleware.ValidateHostHeader(setting.Domain)) m.Use(middleware.ValidateHostHeader(setting.Domain))
} }
m.Use(middleware.HandleNoCacheHeader())
m.Use(middleware.AddDefaultResponseHeaders()) m.Use(middleware.AddDefaultResponseHeaders())
} }

View File

@ -25,8 +25,11 @@ func (hs *HTTPServer) QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) R
return Error(400, "Query missing datasourceId", nil) return Error(400, "Query missing datasourceId", nil)
} }
ds, err := hs.getDatasourceFromCache(datasourceId, c) ds, err := hs.DatasourceCache.GetDatasource(datasourceId, c.SignedInUser, c.SkipCache)
if err != nil { if err != nil {
if err == m.ErrDataSourceAccessDenied {
return Error(403, "Access denied to datasource", err)
}
return Error(500, "Unable to load datasource meta data", err) return Error(500, "Unable to load datasource meta data", err)
} }

View File

@ -15,13 +15,21 @@ import (
"github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/api"
"github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
_ "github.com/grafana/grafana/pkg/extensions"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/login"
_ "github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware"
_ "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/social"
"golang.org/x/sync/errgroup"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/services/cache"
"github.com/grafana/grafana/pkg/setting"
// self registering services
_ "github.com/grafana/grafana/pkg/extensions"
_ "github.com/grafana/grafana/pkg/metrics"
_ "github.com/grafana/grafana/pkg/plugins"
_ "github.com/grafana/grafana/pkg/services/alerting" _ "github.com/grafana/grafana/pkg/services/alerting"
_ "github.com/grafana/grafana/pkg/services/cleanup" _ "github.com/grafana/grafana/pkg/services/cleanup"
_ "github.com/grafana/grafana/pkg/services/notifications" _ "github.com/grafana/grafana/pkg/services/notifications"
@ -29,10 +37,7 @@ import (
_ "github.com/grafana/grafana/pkg/services/rendering" _ "github.com/grafana/grafana/pkg/services/rendering"
_ "github.com/grafana/grafana/pkg/services/search" _ "github.com/grafana/grafana/pkg/services/search"
_ "github.com/grafana/grafana/pkg/services/sqlstore" _ "github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/social" // self registering services
_ "github.com/grafana/grafana/pkg/tracing" _ "github.com/grafana/grafana/pkg/tracing"
"golang.org/x/sync/errgroup"
) )
func NewGrafanaServer() *GrafanaServerImpl { func NewGrafanaServer() *GrafanaServerImpl {
@ -72,6 +77,7 @@ func (g *GrafanaServerImpl) Run() error {
serviceGraph.Provide(&inject.Object{Value: bus.GetBus()}) serviceGraph.Provide(&inject.Object{Value: bus.GetBus()})
serviceGraph.Provide(&inject.Object{Value: g.cfg}) serviceGraph.Provide(&inject.Object{Value: g.cfg})
serviceGraph.Provide(&inject.Object{Value: routing.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)}) serviceGraph.Provide(&inject.Object{Value: routing.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)})
serviceGraph.Provide(&inject.Object{Value: cache.New(5*time.Minute, 10*time.Minute)})
// self registered services // self registered services
services := registry.GetServices() services := registry.GetServices()
@ -138,7 +144,6 @@ func (g *GrafanaServerImpl) Run() error {
} }
sendSystemdNotification("READY=1") sendSystemdNotification("READY=1")
return g.childRoutines.Wait() return g.childRoutines.Wait()
} }

View File

@ -2,6 +2,7 @@ package login
import ( import (
"errors" "errors"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
) )

14
pkg/middleware/headers.go Normal file
View File

@ -0,0 +1,14 @@
package middleware
import (
m "github.com/grafana/grafana/pkg/models"
macaron "gopkg.in/macaron.v1"
)
const HeaderNameNoBackendCache = "X-Grafana-NoCache"
func HandleNoCacheHeader() macaron.Handler {
return func(ctx *m.ReqContext) {
ctx.SkipCache = ctx.Req.Header.Get(HeaderNameNoBackendCache) == "true"
}
}

View File

@ -29,6 +29,7 @@ func GetContextHandler() macaron.Handler {
Session: session.GetSession(), Session: session.GetSession(),
IsSignedIn: false, IsSignedIn: false,
AllowAnonymous: false, AllowAnonymous: false,
SkipCache: false,
Logger: log.New("context"), Logger: log.New("context"),
} }

View File

@ -18,6 +18,7 @@ import (
) )
func TestMiddlewareContext(t *testing.T) { func TestMiddlewareContext(t *testing.T) {
setting.ERR_TEMPLATE_NAME = "error-template"
Convey("Given the grafana middleware", t, func() { Convey("Given the grafana middleware", t, func() {
middlewareScenario("middleware should add context to injector", func(sc *scenarioContext) { middlewareScenario("middleware should add context to injector", func(sc *scenarioContext) {

View File

@ -138,7 +138,7 @@ func Recovery() macaron.Handler {
c.JSON(500, resp) c.JSON(500, resp)
} else { } else {
c.HTML(500, "error") c.HTML(500, setting.ERR_TEMPLATE_NAME)
} }
} }
}() }()

View File

@ -8,11 +8,14 @@ import (
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
"gopkg.in/macaron.v1" "gopkg.in/macaron.v1"
) )
func TestRecoveryMiddleware(t *testing.T) { func TestRecoveryMiddleware(t *testing.T) {
setting.ERR_TEMPLATE_NAME = "error-template"
Convey("Given an api route that panics", t, func() { Convey("Given an api route that panics", t, func() {
apiURL := "/api/whatever" apiURL := "/api/whatever"
recoveryScenario("recovery middleware should return json", apiURL, func(sc *scenarioContext) { recoveryScenario("recovery middleware should return json", apiURL, func(sc *scenarioContext) {
@ -50,6 +53,7 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) {
sc := &scenarioContext{ sc := &scenarioContext{
url: url, url: url,
} }
viewsPath, _ := filepath.Abs("../../public/views") viewsPath, _ := filepath.Abs("../../public/views")
sc.m = macaron.New() sc.m = macaron.New()

View File

@ -215,13 +215,14 @@ type AlertStateInfoDTO struct {
// "Internal" commands // "Internal" commands
type UpdateDashboardAlertsCommand struct { type UpdateDashboardAlertsCommand struct {
UserId int64
OrgId int64 OrgId int64
Dashboard *Dashboard Dashboard *Dashboard
User *SignedInUser
} }
type ValidateDashboardAlertsCommand struct { type ValidateDashboardAlertsCommand struct {
UserId int64 UserId int64
OrgId int64 OrgId int64
Dashboard *Dashboard Dashboard *Dashboard
User *SignedInUser
} }

View File

@ -20,6 +20,7 @@ type ReqContext struct {
IsSignedIn bool IsSignedIn bool
IsRenderCall bool IsRenderCall bool
AllowAnonymous bool AllowAnonymous bool
SkipCache bool
Logger log.Logger Logger log.Logger
} }
@ -36,7 +37,7 @@ func (ctx *ReqContext) Handle(status int, title string, err error) {
ctx.Data["AppSubUrl"] = setting.AppSubUrl ctx.Data["AppSubUrl"] = setting.AppSubUrl
ctx.Data["Theme"] = "dark" ctx.Data["Theme"] = "dark"
ctx.HTML(status, "error") ctx.HTML(status, setting.ERR_TEMPLATE_NAME)
} }
func (ctx *ReqContext) JsonOK(message string) { func (ctx *ReqContext) JsonOK(message string) {

View File

@ -207,11 +207,6 @@ func (p DsPermissionType) String() string {
return names[int(p)] return names[int(p)]
} }
type GetDataSourcePermissionsForUserQuery struct {
User *SignedInUser
Result map[int64]DsPermissionType
}
type DatasourcesPermissionFilterQuery struct { type DatasourcesPermissionFilterQuery struct {
User *SignedInUser User *SignedInUser
Datasources []*DataSource Datasources []*DataSource

View File

@ -165,6 +165,7 @@ type SignedInUser struct {
IsAnonymous bool IsAnonymous bool
HelpFlags1 HelpFlags1 HelpFlags1 HelpFlags1
LastSeenAt time.Time LastSeenAt time.Time
Teams []int64
} }
func (u *SignedInUser) ShouldUpdateLastSeenAt() bool { func (u *SignedInUser) ShouldUpdateLastSeenAt() bool {

View File

@ -29,11 +29,42 @@ func Register(descriptor *Descriptor) {
} }
func GetServices() []*Descriptor { func GetServices() []*Descriptor {
sort.Slice(services, func(i, j int) bool { slice := getServicesWithOverrides()
return services[i].InitPriority > services[j].InitPriority
sort.Slice(slice, func(i, j int) bool {
return slice[i].InitPriority > slice[j].InitPriority
}) })
return services return slice
}
type OverrideServiceFunc func(descriptor Descriptor) (*Descriptor, bool)
var overrides []OverrideServiceFunc
func RegisterOverride(fn OverrideServiceFunc) {
overrides = append(overrides, fn)
}
func getServicesWithOverrides() []*Descriptor {
slice := []*Descriptor{}
for _, s := range services {
var descriptor *Descriptor
for _, fn := range overrides {
if newDescriptor, override := fn(*s); override {
descriptor = newDescriptor
break
}
}
if descriptor != nil {
slice = append(slice, descriptor)
} else {
slice = append(slice, s)
}
}
return slice
} }
// Service interface is the lowest common shape that services // Service interface is the lowest common shape that services

View File

@ -11,7 +11,7 @@ func init() {
} }
func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error { func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error {
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId) extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId, cmd.User)
return extractor.ValidateAlerts() return extractor.ValidateAlerts()
} }
@ -19,11 +19,11 @@ func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error {
func updateDashboardAlerts(cmd *m.UpdateDashboardAlertsCommand) error { func updateDashboardAlerts(cmd *m.UpdateDashboardAlertsCommand) error {
saveAlerts := m.SaveAlertsCommand{ saveAlerts := m.SaveAlertsCommand{
OrgId: cmd.OrgId, OrgId: cmd.OrgId,
UserId: cmd.UserId, UserId: cmd.User.UserId,
DashboardId: cmd.Dashboard.Id, DashboardId: cmd.Dashboard.Id,
} }
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId) extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId, cmd.User)
alerts, err := extractor.GetAlerts() alerts, err := extractor.GetAlerts()
if err != nil { if err != nil {

View File

@ -52,6 +52,24 @@ func TestSimpleReducer(t *testing.T) {
So(result, ShouldEqual, float64(1)) So(result, ShouldEqual, float64(1))
}) })
Convey("median should ignore null values", func() {
reducer := NewSimpleReducer("median")
series := &tsdb.TimeSeries{
Name: "test time serie",
}
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1))
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2))
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 3))
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(float64(1)), 4))
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(float64(2)), 5))
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(float64(3)), 6))
result := reducer.Reduce(series)
So(result.Valid, ShouldEqual, true)
So(result.Float64, ShouldEqual, float64(2))
})
Convey("avg", func() { Convey("avg", func() {
result := testReducer("avg", 1, 2, 3) result := testReducer("avg", 1, 2, 3)
So(result, ShouldEqual, float64(2)) So(result, ShouldEqual, float64(2))

View File

@ -13,14 +13,16 @@ import (
// DashAlertExtractor extracts alerts from the dashboard json // DashAlertExtractor extracts alerts from the dashboard json
type DashAlertExtractor struct { type DashAlertExtractor struct {
User *m.SignedInUser
Dash *m.Dashboard Dash *m.Dashboard
OrgID int64 OrgID int64
log log.Logger log log.Logger
} }
// NewDashAlertExtractor returns a new DashAlertExtractor // NewDashAlertExtractor returns a new DashAlertExtractor
func NewDashAlertExtractor(dash *m.Dashboard, orgID int64) *DashAlertExtractor { func NewDashAlertExtractor(dash *m.Dashboard, orgID int64, user *m.SignedInUser) *DashAlertExtractor {
return &DashAlertExtractor{ return &DashAlertExtractor{
User: user,
Dash: dash, Dash: dash,
OrgID: orgID, OrgID: orgID,
log: log.New("alerting.extractor"), log: log.New("alerting.extractor"),
@ -149,6 +151,21 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
return nil, ValidationError{Reason: fmt.Sprintf("Data source used by alert rule not found, alertName=%v, datasource=%s", alert.Name, dsName)} return nil, ValidationError{Reason: fmt.Sprintf("Data source used by alert rule not found, alertName=%v, datasource=%s", alert.Name, dsName)}
} }
dsFilterQuery := m.DatasourcesPermissionFilterQuery{
User: e.User,
Datasources: []*m.DataSource{datasource},
}
if err := bus.Dispatch(&dsFilterQuery); err != nil {
if err != bus.ErrHandlerNotFound {
return nil, err
}
} else {
if len(dsFilterQuery.Result) == 0 {
return nil, m.ErrDataSourceAccessDenied
}
}
jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id) jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id)
if interval, err := panel.Get("interval").String(); err == nil { if interval, err := panel.Get("interval").String(); err == nil {

View File

@ -69,7 +69,7 @@ func TestAlertRuleExtraction(t *testing.T) {
So(getTarget(dashJson), ShouldEqual, "") So(getTarget(dashJson), ShouldEqual, "")
}) })
extractor := NewDashAlertExtractor(dash, 1) extractor := NewDashAlertExtractor(dash, 1, nil)
_, _ = extractor.GetAlerts() _, _ = extractor.GetAlerts()
Convey("Dashboard json should not be updated after extracting rules", func() { Convey("Dashboard json should not be updated after extracting rules", func() {
@ -83,7 +83,7 @@ func TestAlertRuleExtraction(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
dash := m.NewDashboardFromJson(dashJson) dash := m.NewDashboardFromJson(dashJson)
extractor := NewDashAlertExtractor(dash, 1) extractor := NewDashAlertExtractor(dash, 1, nil)
alerts, err := extractor.GetAlerts() alerts, err := extractor.GetAlerts()
@ -146,7 +146,7 @@ func TestAlertRuleExtraction(t *testing.T) {
dashJson, err := simplejson.NewJson(panelWithoutId) dashJson, err := simplejson.NewJson(panelWithoutId)
So(err, ShouldBeNil) So(err, ShouldBeNil)
dash := m.NewDashboardFromJson(dashJson) dash := m.NewDashboardFromJson(dashJson)
extractor := NewDashAlertExtractor(dash, 1) extractor := NewDashAlertExtractor(dash, 1, nil)
_, err = extractor.GetAlerts() _, err = extractor.GetAlerts()
@ -162,7 +162,7 @@ func TestAlertRuleExtraction(t *testing.T) {
dashJson, err := simplejson.NewJson(panelWithIdZero) dashJson, err := simplejson.NewJson(panelWithIdZero)
So(err, ShouldBeNil) So(err, ShouldBeNil)
dash := m.NewDashboardFromJson(dashJson) dash := m.NewDashboardFromJson(dashJson)
extractor := NewDashAlertExtractor(dash, 1) extractor := NewDashAlertExtractor(dash, 1, nil)
_, err = extractor.GetAlerts() _, err = extractor.GetAlerts()
@ -178,7 +178,7 @@ func TestAlertRuleExtraction(t *testing.T) {
dashJson, err := simplejson.NewJson(json) dashJson, err := simplejson.NewJson(json)
So(err, ShouldBeNil) So(err, ShouldBeNil)
dash := m.NewDashboardFromJson(dashJson) dash := m.NewDashboardFromJson(dashJson)
extractor := NewDashAlertExtractor(dash, 1) extractor := NewDashAlertExtractor(dash, 1, nil)
alerts, err := extractor.GetAlerts() alerts, err := extractor.GetAlerts()
@ -198,7 +198,7 @@ func TestAlertRuleExtraction(t *testing.T) {
dashJson, err := simplejson.NewJson(json) dashJson, err := simplejson.NewJson(json)
So(err, ShouldBeNil) So(err, ShouldBeNil)
dash := m.NewDashboardFromJson(dashJson) dash := m.NewDashboardFromJson(dashJson)
extractor := NewDashAlertExtractor(dash, 1) extractor := NewDashAlertExtractor(dash, 1, nil)
alerts, err := extractor.GetAlerts() alerts, err := extractor.GetAlerts()
@ -228,7 +228,7 @@ func TestAlertRuleExtraction(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
dash := m.NewDashboardFromJson(dashJson) dash := m.NewDashboardFromJson(dashJson)
extractor := NewDashAlertExtractor(dash, 1) extractor := NewDashAlertExtractor(dash, 1, nil)
alerts, err := extractor.GetAlerts() alerts, err := extractor.GetAlerts()
@ -248,7 +248,7 @@ func TestAlertRuleExtraction(t *testing.T) {
dashJSON, err := simplejson.NewJson(json) dashJSON, err := simplejson.NewJson(json)
So(err, ShouldBeNil) So(err, ShouldBeNil)
dash := m.NewDashboardFromJson(dashJSON) dash := m.NewDashboardFromJson(dashJSON)
extractor := NewDashAlertExtractor(dash, 1) extractor := NewDashAlertExtractor(dash, 1, nil)
err = extractor.ValidateAlerts() err = extractor.ValidateAlerts()

View File

@ -13,6 +13,7 @@ type AlertTestCommand struct {
Dashboard *simplejson.Json Dashboard *simplejson.Json
PanelId int64 PanelId int64
OrgId int64 OrgId int64
User *m.SignedInUser
Result *EvalContext Result *EvalContext
} }
@ -25,7 +26,7 @@ func handleAlertTestCommand(cmd *AlertTestCommand) error {
dash := m.NewDashboardFromJson(cmd.Dashboard) dash := m.NewDashboardFromJson(cmd.Dashboard)
extractor := NewDashAlertExtractor(dash, cmd.OrgId) extractor := NewDashAlertExtractor(dash, cmd.OrgId, cmd.User)
alerts, err := extractor.GetAlerts() alerts, err := extractor.GetAlerts()
if err != nil { if err != nil {
return err return err

17
pkg/services/cache/cache.go vendored Normal file
View File

@ -0,0 +1,17 @@
package cache
import (
"time"
gocache "github.com/patrickmn/go-cache"
)
type CacheService struct {
*gocache.Cache
}
func New(defaultExpiration, cleanupInterval time.Duration) *CacheService {
return &CacheService{
Cache: gocache.New(defaultExpiration, cleanupInterval),
}
}

View File

@ -90,6 +90,7 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO,
validateAlertsCmd := models.ValidateDashboardAlertsCommand{ validateAlertsCmd := models.ValidateDashboardAlertsCommand{
OrgId: dto.OrgId, OrgId: dto.OrgId,
Dashboard: dash, Dashboard: dash,
User: dto.User,
} }
if err := bus.Dispatch(&validateAlertsCmd); err != nil { if err := bus.Dispatch(&validateAlertsCmd); err != nil {
@ -159,8 +160,8 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO,
func (dr *dashboardServiceImpl) updateAlerting(cmd *models.SaveDashboardCommand, dto *SaveDashboardDTO) error { func (dr *dashboardServiceImpl) updateAlerting(cmd *models.SaveDashboardCommand, dto *SaveDashboardDTO) error {
alertCmd := models.UpdateDashboardAlertsCommand{ alertCmd := models.UpdateDashboardAlertsCommand{
OrgId: dto.OrgId, OrgId: dto.OrgId,
UserId: dto.User.UserId,
Dashboard: cmd.Result, Dashboard: cmd.Result,
User: dto.User,
} }
if err := bus.Dispatch(&alertCmd); err != nil { if err := bus.Dispatch(&alertCmd); err != nil {

View File

@ -0,0 +1,53 @@
package datasources
import (
"fmt"
"time"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/cache"
)
type CacheService interface {
GetDatasource(datasourceID int64, user *m.SignedInUser, skipCache bool) (*m.DataSource, error)
}
type CacheServiceImpl struct {
Bus bus.Bus `inject:""`
CacheService *cache.CacheService `inject:""`
}
func init() {
registry.Register(&registry.Descriptor{
Name: "DatasourceCacheService",
Instance: &CacheServiceImpl{},
InitPriority: registry.Low,
})
}
func (dc *CacheServiceImpl) Init() error {
return nil
}
func (dc *CacheServiceImpl) GetDatasource(datasourceID int64, user *m.SignedInUser, skipCache bool) (*m.DataSource, error) {
cacheKey := fmt.Sprintf("ds-%d", datasourceID)
if !skipCache {
if cached, found := dc.CacheService.Get(cacheKey); found {
ds := cached.(*m.DataSource)
if ds.OrgId == user.OrgId {
return ds, nil
}
}
}
query := m.GetDataSourceByIdQuery{Id: datasourceID, OrgId: user.OrgId}
if err := dc.Bus.Dispatch(&query); err != nil {
return nil, err
}
dc.CacheService.Set(cacheKey, query.Result, time.Second*5)
return query.Result, nil
}

View File

@ -327,6 +327,24 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
if dashboard.IsFolder { if dashboard.IsFolder {
deletes = append(deletes, "DELETE FROM dashboard_provisioning WHERE dashboard_id in (select id from dashboard where folder_id = ?)") deletes = append(deletes, "DELETE FROM dashboard_provisioning WHERE dashboard_id in (select id from dashboard where folder_id = ?)")
deletes = append(deletes, "DELETE FROM dashboard WHERE folder_id = ?") deletes = append(deletes, "DELETE FROM dashboard WHERE folder_id = ?")
dashIds := []struct {
Id int64
}{}
err := sess.SQL("select id from dashboard where folder_id = ?", dashboard.Id).Find(&dashIds)
if err != nil {
return err
}
for _, id := range dashIds {
if err := deleteAlertDefinition(id.Id, sess); err != nil {
return nil
}
}
}
if err := deleteAlertDefinition(dashboard.Id, sess); err != nil {
return nil
} }
for _, sql := range deletes { for _, sql := range deletes {
@ -337,10 +355,6 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
} }
} }
if err := deleteAlertDefinition(dashboard.Id, sess); err != nil {
return nil
}
return nil return nil
}) })
} }

View File

@ -16,6 +16,7 @@ import (
m "github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/cache"
"github.com/grafana/grafana/pkg/services/sqlstore/migrations" "github.com/grafana/grafana/pkg/services/sqlstore/migrations"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
@ -49,6 +50,7 @@ func init() {
type SqlStore struct { type SqlStore struct {
Cfg *setting.Cfg `inject:""` Cfg *setting.Cfg `inject:""`
Bus bus.Bus `inject:""` Bus bus.Bus `inject:""`
CacheService *cache.CacheService `inject:""`
dbCfg DatabaseConfig dbCfg DatabaseConfig
engine *xorm.Engine engine *xorm.Engine
@ -148,9 +150,11 @@ func (ss *SqlStore) Init() error {
// Init repo instances // Init repo instances
annotations.SetRepository(&SqlAnnotationRepo{}) annotations.SetRepository(&SqlAnnotationRepo{})
ss.Bus.SetTransactionManager(ss) ss.Bus.SetTransactionManager(ss)
// Register handlers
ss.addUserQueryAndCommandHandlers()
// ensure admin user // ensure admin user
if ss.skipEnsureAdmin { if ss.skipEnsureAdmin {
return nil return nil
@ -322,6 +326,7 @@ func InitTestDB(t *testing.T) *SqlStore {
sqlstore := &SqlStore{} sqlstore := &SqlStore{}
sqlstore.skipEnsureAdmin = true sqlstore.skipEnsureAdmin = true
sqlstore.Bus = bus.New() sqlstore.Bus = bus.New()
sqlstore.CacheService = cache.New(5*time.Minute, 10*time.Minute)
dbType := migrator.SQLITE dbType := migrator.SQLITE

View File

@ -15,8 +15,9 @@ import (
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
func init() { func (ss *SqlStore) addUserQueryAndCommandHandlers() {
//bus.AddHandler("sql", CreateUser) ss.Bus.AddHandler(ss.GetSignedInUserWithCache)
bus.AddHandler("sql", GetUserById) bus.AddHandler("sql", GetUserById)
bus.AddHandler("sql", UpdateUser) bus.AddHandler("sql", UpdateUser)
bus.AddHandler("sql", ChangeUserPassword) bus.AddHandler("sql", ChangeUserPassword)
@ -25,7 +26,6 @@ func init() {
bus.AddHandler("sql", SetUsingOrg) bus.AddHandler("sql", SetUsingOrg)
bus.AddHandler("sql", UpdateUserLastSeenAt) bus.AddHandler("sql", UpdateUserLastSeenAt)
bus.AddHandler("sql", GetUserProfile) bus.AddHandler("sql", GetUserProfile)
bus.AddHandler("sql", GetSignedInUser)
bus.AddHandler("sql", SearchUsers) bus.AddHandler("sql", SearchUsers)
bus.AddHandler("sql", GetUserOrgList) bus.AddHandler("sql", GetUserOrgList)
bus.AddHandler("sql", DeleteUser) bus.AddHandler("sql", DeleteUser)
@ -345,6 +345,22 @@ func GetUserOrgList(query *m.GetUserOrgListQuery) error {
return err return err
} }
func (ss *SqlStore) GetSignedInUserWithCache(query *m.GetSignedInUserQuery) error {
cacheKey := fmt.Sprintf("signed-in-user-%d-%d", query.UserId, query.OrgId)
if cached, found := ss.CacheService.Get(cacheKey); found {
query.Result = cached.(*m.SignedInUser)
return nil
}
err := GetSignedInUser(query)
if err != nil {
return err
}
ss.CacheService.Set(cacheKey, query.Result, time.Second*5)
return nil
}
func GetSignedInUser(query *m.GetSignedInUserQuery) error { func GetSignedInUser(query *m.GetSignedInUserQuery) error {
orgId := "u.org_id" orgId := "u.org_id"
if query.OrgId > 0 { if query.OrgId > 0 {
@ -389,6 +405,17 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error {
user.OrgName = "Org missing" user.OrgName = "Org missing"
} }
getTeamsByUserQuery := &m.GetTeamsByUserQuery{OrgId: user.OrgId, UserId: user.UserId}
err = GetTeamsByUser(getTeamsByUserQuery)
if err != nil {
return err
}
user.Teams = make([]int64, len(getTeamsByUserQuery.Result))
for i, t := range getTeamsByUserQuery.Result {
user.Teams[i] = t.Id
}
query.Result = &user query.Result = &user
return err return err
} }

View File

@ -38,6 +38,10 @@ const (
APP_NAME_ENTERPRISE = "Grafana Enterprise" APP_NAME_ENTERPRISE = "Grafana Enterprise"
) )
var (
ERR_TEMPLATE_NAME = "error"
)
var ( var (
// App settings. // App settings.
Env = DEV Env = DEV

View File

@ -46,6 +46,7 @@ func init() {
"AWS/Billing": {"EstimatedCharges"}, "AWS/Billing": {"EstimatedCharges"},
"AWS/CloudFront": {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"}, "AWS/CloudFront": {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"},
"AWS/CloudSearch": {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"}, "AWS/CloudSearch": {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"},
"AWS/Connect": {"CallsBreachingConcurrencyQuota", "CallBackNotDialableNumber", "CallRecordingUploadError", "CallsPerInterval", "ConcurrentCalls", "ConcurrentCallsPercentage", "ContactFlowErrors", "ContactFlowFatalErrors", "LongestQueueWaitTime", "MissedCalls", "MisconfiguredPhoneNumbers", "PublicSigningKeyUsage", "QueueCapacityExceededError", "QueueSize", "ThrottledCalls", "ToInstancePacketLossRate"},
"AWS/DMS": {"FreeableMemory", "WriteIOPS", "ReadIOPS", "WriteThroughput", "ReadThroughput", "WriteLatency", "ReadLatency", "SwapUsage", "NetworkTransmitThroughput", "NetworkReceiveThroughput", "FullLoadThroughputBandwidthSource", "FullLoadThroughputBandwidthTarget", "FullLoadThroughputRowsSource", "FullLoadThroughputRowsTarget", "CDCIncomingChanges", "CDCChangesMemorySource", "CDCChangesMemoryTarget", "CDCChangesDiskSource", "CDCChangesDiskTarget", "CDCThroughputBandwidthTarget", "CDCThroughputRowsSource", "CDCThroughputRowsTarget", "CDCLatencySource", "CDCLatencyTarget"}, "AWS/DMS": {"FreeableMemory", "WriteIOPS", "ReadIOPS", "WriteThroughput", "ReadThroughput", "WriteLatency", "ReadLatency", "SwapUsage", "NetworkTransmitThroughput", "NetworkReceiveThroughput", "FullLoadThroughputBandwidthSource", "FullLoadThroughputBandwidthTarget", "FullLoadThroughputRowsSource", "FullLoadThroughputRowsTarget", "CDCIncomingChanges", "CDCChangesMemorySource", "CDCChangesMemoryTarget", "CDCChangesDiskSource", "CDCChangesDiskTarget", "CDCThroughputBandwidthTarget", "CDCThroughputRowsSource", "CDCThroughputRowsTarget", "CDCLatencySource", "CDCLatencyTarget"},
"AWS/DX": {"ConnectionState", "ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionCRCErrorCount", "ConnectionLightLevelTx", "ConnectionLightLevelRx"}, "AWS/DX": {"ConnectionState", "ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionCRCErrorCount", "ConnectionLightLevelTx", "ConnectionLightLevelRx"},
"AWS/DynamoDB": {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReturnedBytes", "ReturnedItemCount", "ReturnedRecordsCount", "SuccessfulRequestLatency", "SystemErrors", "TimeToLiveDeletedItemCount", "ThrottledRequests", "UserErrors", "WriteThrottleEvents"}, "AWS/DynamoDB": {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReturnedBytes", "ReturnedItemCount", "ReturnedRecordsCount", "SuccessfulRequestLatency", "SystemErrors", "TimeToLiveDeletedItemCount", "ThrottledRequests", "UserErrors", "WriteThrottleEvents"},
@ -120,6 +121,7 @@ func init() {
"AWS/Billing": {"ServiceName", "LinkedAccount", "Currency"}, "AWS/Billing": {"ServiceName", "LinkedAccount", "Currency"},
"AWS/CloudFront": {"DistributionId", "Region"}, "AWS/CloudFront": {"DistributionId", "Region"},
"AWS/CloudSearch": {}, "AWS/CloudSearch": {},
"AWS/Connect": {"InstanceId", "MetricGroup", "Participant", "QueueName", "Stream Type", "Type of Connection"},
"AWS/DMS": {"ReplicationInstanceIdentifier", "ReplicationTaskIdentifier"}, "AWS/DMS": {"ReplicationInstanceIdentifier", "ReplicationTaskIdentifier"},
"AWS/DX": {"ConnectionId"}, "AWS/DX": {"ConnectionId"},
"AWS/DynamoDB": {"TableName", "GlobalSecondaryIndexName", "Operation", "StreamLabel"}, "AWS/DynamoDB": {"TableName", "GlobalSecondaryIndexName", "Operation", "StreamLabel"},

View File

@ -88,7 +88,7 @@ export class FormDropdownCtrl {
if (evt.keyCode === 13) { if (evt.keyCode === 13) {
setTimeout(() => { setTimeout(() => {
this.inputElement.blur(); this.inputElement.blur();
}, 100); }, 300);
} }
}); });

View File

@ -166,7 +166,7 @@ export class AlertTabCtrl {
alert.noDataState = alert.noDataState || config.alertingNoDataOrNullValues; alert.noDataState = alert.noDataState || config.alertingNoDataOrNullValues;
alert.executionErrorState = alert.executionErrorState || config.alertingErrorOrTimeout; alert.executionErrorState = alert.executionErrorState || config.alertingErrorOrTimeout;
alert.frequency = alert.frequency || '60s'; alert.frequency = alert.frequency || '1m';
alert.handler = alert.handler || 1; alert.handler = alert.handler || 1;
alert.notifications = alert.notifications || []; alert.notifications = alert.notifications || [];
@ -217,7 +217,7 @@ export class AlertTabCtrl {
buildDefaultCondition() { buildDefaultCondition() {
return { return {
type: 'query', type: 'query',
query: { params: ['A', '5m', 'now'] }, query: { params: ['A', '15m', 'now'] },
reducer: { type: 'avg', params: [] }, reducer: { type: 'avg', params: [] },
evaluator: { type: 'gt', params: [null] }, evaluator: { type: 'gt', params: [null] },
operator: { type: 'and' }, operator: { type: 'and' },

View File

@ -21,6 +21,7 @@ export interface Props {
export interface State { export interface State {
refreshCounter: number; refreshCounter: number;
renderCounter: number;
timeRange?: TimeRange; timeRange?: TimeRange;
} }
@ -30,11 +31,13 @@ export class PanelChrome extends PureComponent<Props, State> {
this.state = { this.state = {
refreshCounter: 0, refreshCounter: 0,
renderCounter: 0,
}; };
} }
componentDidMount() { componentDidMount() {
this.props.panel.events.on('refresh', this.onRefresh); this.props.panel.events.on('refresh', this.onRefresh);
this.props.panel.events.on('render', this.onRender);
this.props.dashboard.panelInitialized(this.props.panel); this.props.dashboard.panelInitialized(this.props.panel);
} }
@ -52,6 +55,13 @@ export class PanelChrome extends PureComponent<Props, State> {
}); });
}; };
onRender = () => {
console.log('onRender');
this.setState({
renderCounter: this.state.renderCounter + 1,
});
};
get isVisible() { get isVisible() {
return !this.props.dashboard.otherPanelInFullscreen(this.props.panel); return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
} }
@ -59,9 +69,11 @@ export class PanelChrome extends PureComponent<Props, State> {
render() { render() {
const { panel, dashboard } = this.props; const { panel, dashboard } = this.props;
const { datasource, targets } = panel; const { datasource, targets } = panel;
const { refreshCounter, timeRange } = this.state; const { timeRange, renderCounter, refreshCounter } = this.state;
const PanelComponent = this.props.component; const PanelComponent = this.props.component;
console.log('Panel chrome render');
return ( return (
<div className="panel-container"> <div className="panel-container">
<PanelHeader panel={panel} dashboard={dashboard} /> <PanelHeader panel={panel} dashboard={dashboard} />
@ -74,7 +86,16 @@ export class PanelChrome extends PureComponent<Props, State> {
refreshCounter={refreshCounter} refreshCounter={refreshCounter}
> >
{({ loading, timeSeries }) => { {({ loading, timeSeries }) => {
return <PanelComponent loading={loading} timeSeries={timeSeries} timeRange={timeRange} />; console.log('panelcrome inner render');
return (
<PanelComponent
loading={loading}
timeSeries={timeSeries}
timeRange={timeRange}
options={panel.getOptions()}
renderCounter={renderCounter}
/>
);
}} }}
</DataPanel> </DataPanel>
</div> </div>

View File

@ -1,12 +1,15 @@
import React from 'react'; import React, { PureComponent } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { QueriesTab } from './QueriesTab';
import { VizTypePicker } from './VizTypePicker';
import { store } from 'app/store/store';
import { updateLocation } from 'app/core/actions';
import { PanelModel } from '../panel_model'; import { PanelModel } from '../panel_model';
import { DashboardModel } from '../dashboard_model'; import { DashboardModel } from '../dashboard_model';
import { store } from 'app/store/store';
import { QueriesTab } from './QueriesTab';
import { PanelPlugin, PluginExports } from 'app/types/plugins'; import { PanelPlugin, PluginExports } from 'app/types/plugins';
import { VizTypePicker } from './VizTypePicker';
import { updateLocation } from 'app/core/actions';
interface PanelEditorProps { interface PanelEditorProps {
panel: PanelModel; panel: PanelModel;
@ -22,7 +25,7 @@ interface PanelEditorTab {
icon: string; icon: string;
} }
export class PanelEditor extends React.Component<PanelEditorProps, any> { export class PanelEditor extends PureComponent<PanelEditorProps> {
tabs: PanelEditorTab[]; tabs: PanelEditorTab[];
constructor(props) { constructor(props) {
@ -39,16 +42,21 @@ export class PanelEditor extends React.Component<PanelEditorProps, any> {
} }
renderPanelOptions() { renderPanelOptions() {
const { pluginExports } = this.props; const { pluginExports, panel } = this.props;
if (pluginExports.PanelOptions) { if (pluginExports.PanelOptionsComponent) {
const PanelOptions = pluginExports.PanelOptions; const OptionsComponent = pluginExports.PanelOptionsComponent;
return <PanelOptions />; return <OptionsComponent options={panel.getOptions()} onChange={this.onPanelOptionsChanged} />;
} else { } else {
return <p>Visualization has no options</p>; return <p>Visualization has no options</p>;
} }
} }
onPanelOptionsChanged = (options: any) => {
this.props.panel.updateOptions(options);
this.forceUpdate();
};
renderVizTab() { renderVizTab() {
return ( return (
<div className="viz-editor"> <div className="viz-editor">
@ -70,6 +78,7 @@ export class PanelEditor extends React.Component<PanelEditorProps, any> {
partial: true, partial: true,
}) })
); );
this.forceUpdate();
}; };
render() { render() {

View File

@ -60,6 +60,21 @@ export class PanelModel {
_.defaultsDeep(this, _.cloneDeep(defaults)); _.defaultsDeep(this, _.cloneDeep(defaults));
} }
getOptions() {
return this[this.getOptionsKey()] || {};
}
updateOptions(options: object) {
const update: any = {};
update[this.getOptionsKey()] = options;
Object.assign(this, update);
this.render();
}
private getOptionsKey() {
return this.type + 'Options';
}
getSaveModel() { getSaveModel() {
const model: any = {}; const model: any = {};
for (const property in this) { for (const property in this) {
@ -121,10 +136,6 @@ export class PanelModel {
this.events.emit('panel-initialized'); this.events.emit('panel-initialized');
} }
initEditMode() {
this.events.emit('panel-init-edit-mode');
}
changeType(pluginId: string) { changeType(pluginId: string) {
this.type = pluginId; this.type = pluginId;

View File

@ -32,9 +32,9 @@ export class SettingsCtrl {
this.$scope.$on('$destroy', () => { this.$scope.$on('$destroy', () => {
this.dashboard.updateSubmenuVisibility(); this.dashboard.updateSubmenuVisibility();
this.dashboard.startRefresh();
setTimeout(() => { setTimeout(() => {
this.$rootScope.appEvent('dash-scroll', { restore: true }); this.$rootScope.appEvent('dash-scroll', { restore: true });
this.dashboard.startRefresh();
}); });
}); });

View File

@ -19,7 +19,7 @@
</div> </div>
</form> </form>
<div ng-show="mode === 'email-sent'"> <div ng-show="mode === 'email-sent'">
An email with a reset link as been sent to the email address. <br> An email with a reset link has been sent to the email address. <br>
You should receive it shortly. You should receive it shortly.
<div class="p-t-1"> <div class="p-t-1">
<a href="login" class="btn btn-success p-t-1"> <a href="login" class="btn btn-success p-t-1">

View File

@ -95,9 +95,9 @@ class LoggingQueryField extends React.PureComponent<LoggingQueryFieldProps, Logg
this.languageProvider this.languageProvider
.start() .start()
.then(remaining => { .then(remaining => {
remaining.map(task => task.then(this.onReceiveMetrics).catch(() => {})); remaining.map(task => task.then(this.onUpdateLanguage).catch(() => {}));
}) })
.then(() => this.onReceiveMetrics()); .then(() => this.onUpdateLanguage());
} }
} }
@ -119,7 +119,7 @@ class LoggingQueryField extends React.PureComponent<LoggingQueryFieldProps, Logg
this.languageProvider this.languageProvider
.fetchLabelValues(targetOption.value) .fetchLabelValues(targetOption.value)
.then(this.onReceiveMetrics) .then(this.onUpdateLanguage)
.catch(() => {}); .catch(() => {});
}; };
@ -147,7 +147,7 @@ class LoggingQueryField extends React.PureComponent<LoggingQueryFieldProps, Logg
} }
}; };
onReceiveMetrics = () => { onUpdateLanguage = () => {
Prism.languages[PRISM_SYNTAX] = this.languageProvider.getSyntax(); Prism.languages[PRISM_SYNTAX] = this.languageProvider.getSyntax();
const { logLabelOptions } = this.languageProvider; const { logLabelOptions } = this.languageProvider;
this.setState({ this.setState({

View File

@ -47,7 +47,6 @@ export default class LoggingLanguageProvider extends LanguageProvider {
this.datasource = datasource; this.datasource = datasource;
this.labelKeys = {}; this.labelKeys = {};
this.labelValues = {}; this.labelValues = {};
this.started = false;
Object.assign(this, initialValues); Object.assign(this, initialValues);
} }
@ -63,11 +62,10 @@ export default class LoggingLanguageProvider extends LanguageProvider {
}; };
start = () => { start = () => {
if (!this.started) { if (!this.startTask) {
this.started = true; this.startTask = this.fetchLogLabels();
return this.fetchLogLabels();
} }
return Promise.resolve([]); return this.startTask;
}; };
// Keep this DOM-free for testing // Keep this DOM-free for testing

View File

@ -134,9 +134,9 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
this.languageProvider this.languageProvider
.start() .start()
.then(remaining => { .then(remaining => {
remaining.map(task => task.then(this.onReceiveMetrics).catch(() => {})); remaining.map(task => task.then(this.onUpdateLanguage).catch(() => {}));
}) })
.then(() => this.onReceiveMetrics()); .then(() => this.onUpdateLanguage());
} }
} }
@ -176,7 +176,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
} }
}; };
onReceiveMetrics = () => { onUpdateLanguage = () => {
const { histogramMetrics, metrics } = this.languageProvider; const { histogramMetrics, metrics } = this.languageProvider;
if (!metrics) { if (!metrics) {
return; return;

View File

@ -46,7 +46,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
labelKeys?: { [index: string]: string[] }; // metric -> [labelKey,...] labelKeys?: { [index: string]: string[] }; // metric -> [labelKey,...]
labelValues?: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...] labelValues?: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...]
metrics?: string[]; metrics?: string[];
started: boolean; startTask: Promise<any>;
constructor(datasource: any, initialValues?: any) { constructor(datasource: any, initialValues?: any) {
super(); super();
@ -56,7 +56,6 @@ export default class PromQlLanguageProvider extends LanguageProvider {
this.labelKeys = {}; this.labelKeys = {};
this.labelValues = {}; this.labelValues = {};
this.metrics = []; this.metrics = [];
this.started = false;
Object.assign(this, initialValues); Object.assign(this, initialValues);
} }
@ -72,11 +71,10 @@ export default class PromQlLanguageProvider extends LanguageProvider {
}; };
start = () => { start = () => {
if (!this.started) { if (!this.startTask) {
this.started = true; this.startTask = this.fetchMetricNames().then(() => [this.fetchHistogramMetrics()]);
return this.fetchMetricNames().then(() => [this.fetchHistogramMetrics()]);
} }
return Promise.resolve([]); return this.startTask;
}; };
// Keep this DOM-free for testing // Keep this DOM-free for testing
@ -156,7 +154,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
} }
getAggregationCompletionItems({ value }: TypeaheadInput): TypeaheadOutput { getAggregationCompletionItems({ value }: TypeaheadInput): TypeaheadOutput {
let refresher: Promise<any> = null; const refresher: Promise<any> = null;
const suggestions: CompletionItemGroup[] = []; const suggestions: CompletionItemGroup[] = [];
// Stitch all query lines together to support multi-line queries // Stitch all query lines together to support multi-line queries
@ -172,12 +170,30 @@ export default class PromQlLanguageProvider extends LanguageProvider {
return text; return text;
}, ''); }, '');
const leftSide = queryText.slice(0, queryOffset); // Try search for selector part on the left-hand side, such as `sum (m) by (l)`
const openParensAggregationIndex = leftSide.lastIndexOf('('); const openParensAggregationIndex = queryText.lastIndexOf('(', queryOffset);
const openParensSelectorIndex = leftSide.slice(0, openParensAggregationIndex).lastIndexOf('('); let openParensSelectorIndex = queryText.lastIndexOf('(', openParensAggregationIndex - 1);
const closeParensSelectorIndex = leftSide.slice(openParensSelectorIndex).indexOf(')') + openParensSelectorIndex; let closeParensSelectorIndex = queryText.indexOf(')', openParensSelectorIndex);
let selectorString = leftSide.slice(openParensSelectorIndex + 1, closeParensSelectorIndex); // Try search for selector part of an alternate aggregation clause, such as `sum by (l) (m)`
if (openParensSelectorIndex === -1) {
const closeParensAggregationIndex = queryText.indexOf(')', queryOffset);
closeParensSelectorIndex = queryText.indexOf(')', closeParensAggregationIndex + 1);
openParensSelectorIndex = queryText.lastIndexOf('(', closeParensSelectorIndex);
}
const result = {
refresher,
suggestions,
context: 'context-aggregation',
};
// Suggestions are useless for alternative aggregation clauses without a selector in context
if (openParensSelectorIndex === -1) {
return result;
}
let selectorString = queryText.slice(openParensSelectorIndex + 1, closeParensSelectorIndex);
// Range vector syntax not accounted for by subsequent parse so discard it if present // Range vector syntax not accounted for by subsequent parse so discard it if present
selectorString = selectorString.replace(/\[[^\]]+\]$/, ''); selectorString = selectorString.replace(/\[[^\]]+\]$/, '');
@ -188,14 +204,10 @@ export default class PromQlLanguageProvider extends LanguageProvider {
if (labelKeys) { if (labelKeys) {
suggestions.push({ label: 'Labels', items: labelKeys.map(wrapLabel) }); suggestions.push({ label: 'Labels', items: labelKeys.map(wrapLabel) });
} else { } else {
refresher = this.fetchSeriesLabels(selector); result.refresher = this.fetchSeriesLabels(selector);
} }
return { return result;
refresher,
suggestions,
context: 'context-aggregation',
};
} }
getLabelCompletionItems({ text, wrapperClasses, labelKey, value }: TypeaheadInput): TypeaheadOutput { getLabelCompletionItems({ text, wrapperClasses, labelKey, value }: TypeaheadInput): TypeaheadOutput {

View File

@ -269,5 +269,48 @@ describe('Language completion provider', () => {
}, },
]); ]);
}); });
it('returns no suggestions inside an unclear aggregation context using alternate syntax', () => {
const instance = new LanguageProvider(datasource, {
labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] },
});
const value = Plain.deserialize('sum by ()');
const range = value.selection.merge({
anchorOffset: 8,
});
const valueWithSelection = value.change().select(range).value;
const result = instance.provideCompletionItems({
text: '',
prefix: '',
wrapperClasses: ['context-aggregation'],
value: valueWithSelection,
});
expect(result.context).toBe('context-aggregation');
expect(result.suggestions).toEqual([]);
});
it('returns label suggestions inside an aggregation context using alternate syntax', () => {
const instance = new LanguageProvider(datasource, {
labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] },
});
const value = Plain.deserialize('sum by () (metric)');
const range = value.selection.merge({
anchorOffset: 8,
});
const valueWithSelection = value.change().select(range).value;
const result = instance.provideCompletionItems({
text: '',
prefix: '',
wrapperClasses: ['context-aggregation'],
value: valueWithSelection,
});
expect(result.context).toBe('context-aggregation');
expect(result.suggestions).toEqual([
{
items: [{ label: 'label1' }, { label: 'label2' }, { label: 'label3' }],
label: 'Labels',
},
]);
});
}); });
}); });

View File

@ -1,22 +1,21 @@
// Libraries
import _ from 'lodash'; import _ from 'lodash';
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
// Components
import Graph from 'app/viz/Graph'; import Graph from 'app/viz/Graph';
import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
import { Switch } from 'app/core/components/Switch/Switch'; import { Switch } from 'app/core/components/Switch/Switch';
// Types import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
import { PanelProps, NullValueMode } from 'app/types'; import { PanelProps, PanelOptionsProps, NullValueMode } from 'app/types';
interface Options { interface Options {
showBars: boolean; showBars: boolean;
showLines: boolean;
showPoints: boolean;
onChange: (options: Options) => void;
} }
interface Props extends PanelProps { interface Props extends PanelProps<Options> {}
options: Options;
}
export class Graph2 extends PureComponent<Props> { export class Graph2 extends PureComponent<Props> {
constructor(props) { constructor(props) {
@ -25,27 +24,52 @@ export class Graph2 extends PureComponent<Props> {
render() { render() {
const { timeSeries, timeRange } = this.props; const { timeSeries, timeRange } = this.props;
const { showLines, showBars, showPoints } = this.props.options;
const vmSeries = getTimeSeriesVMs({ const vmSeries = getTimeSeriesVMs({
timeSeries: timeSeries, timeSeries: timeSeries,
nullValueMode: NullValueMode.Ignore, nullValueMode: NullValueMode.Ignore,
}); });
return <Graph timeSeries={vmSeries} timeRange={timeRange} />; return (
<Graph
timeSeries={vmSeries}
timeRange={timeRange}
showLines={showLines}
showPoints={showPoints}
showBars={showBars}
/>
);
} }
} }
export class TextOptions extends PureComponent<any> { export class GraphOptions extends PureComponent<PanelOptionsProps<Options>> {
onChange = () => {}; onToggleLines = () => {
this.props.onChange({ ...this.props.options, showLines: !this.props.options.showLines });
};
onToggleBars = () => {
this.props.onChange({ ...this.props.options, showBars: !this.props.options.showBars });
};
onTogglePoints = () => {
this.props.onChange({ ...this.props.options, showPoints: !this.props.options.showPoints });
};
render() { render() {
const { showBars, showPoints, showLines } = this.props.options;
return ( return (
<div>
<div className="section gf-form-group"> <div className="section gf-form-group">
<h5 className="section-heading">Draw Modes</h5> <h5 className="page-heading">Draw Modes</h5>
<Switch label="Lines" checked={true} onChange={this.onChange} /> <Switch label="Lines" labelClass="width-5" checked={showLines} onChange={this.onToggleLines} />
<Switch label="Bars" labelClass="width-5" checked={showBars} onChange={this.onToggleBars} />
<Switch label="Points" labelClass="width-5" checked={showPoints} onChange={this.onTogglePoints} />
</div>
</div> </div>
); );
} }
} }
export { Graph2 as PanelComponent, TextOptions as PanelOptions }; export { Graph2 as PanelComponent, GraphOptions as PanelOptionsComponent };

View File

@ -211,16 +211,17 @@ export class TableRenderer {
value = this.formatColumnValue(columnIndex, value); value = this.formatColumnValue(columnIndex, value);
const column = this.table.columns[columnIndex]; const column = this.table.columns[columnIndex];
let style = ''; let cellStyle = '';
let textStyle = '';
const cellClasses = []; const cellClasses = [];
let cellClass = ''; let cellClass = '';
if (this.colorState.cell) { if (this.colorState.cell) {
style = ' style="background-color:' + this.colorState.cell + '"'; cellStyle = ' style="background-color:' + this.colorState.cell + '"';
cellClasses.push('table-panel-color-cell'); cellClasses.push('table-panel-color-cell');
this.colorState.cell = null; this.colorState.cell = null;
} else if (this.colorState.value) { } else if (this.colorState.value) {
style = ' style="color:' + this.colorState.value + '"'; textStyle = ' style="color:' + this.colorState.value + '"';
this.colorState.value = null; this.colorState.value = null;
} }
// because of the fixed table headers css only solution // because of the fixed table headers css only solution
@ -232,7 +233,7 @@ export class TableRenderer {
} }
if (value === undefined) { if (value === undefined) {
style = ' style="display:none;"'; cellStyle = ' style="display:none;"';
column.hidden = true; column.hidden = true;
} else { } else {
column.hidden = false; column.hidden = false;
@ -258,7 +259,7 @@ export class TableRenderer {
cellClasses.push('table-panel-cell-link'); cellClasses.push('table-panel-cell-link');
columnHtml += ` columnHtml += `
<a href="${cellLink}" target="${cellTarget}" data-link-tooltip data-original-title="${cellLinkTooltip}" data-placement="right"${style}> <a href="${cellLink}" target="${cellTarget}" data-link-tooltip data-original-title="${cellLinkTooltip}" data-placement="right"${textStyle}>
${value} ${value}
</a> </a>
`; `;
@ -283,7 +284,7 @@ export class TableRenderer {
cellClass = ' class="' + cellClasses.join(' ') + '"'; cellClass = ' class="' + cellClasses.join(' ') + '"';
} }
columnHtml = '<td' + cellClass + style + '>' + columnHtml + '</td>'; columnHtml = '<td' + cellClass + cellStyle + textStyle + '>' + columnHtml + '</td>';
return columnHtml; return columnHtml;
} }

View File

@ -86,10 +86,11 @@ export abstract class LanguageProvider {
datasource: any; datasource: any;
request: (url) => Promise<any>; request: (url) => Promise<any>;
/** /**
* Returns a promise that resolves with a task list when main syntax is loaded. * Returns startTask that resolves with a task list when main syntax is loaded.
* Task list consists of secondary promises that load more detailed language features. * Task list consists of secondary promises that load more detailed language features.
*/ */
start: () => Promise<any[]>; start: () => Promise<any[]>;
startTask?: Promise<any[]>;
} }
export interface TypeaheadInput { export interface TypeaheadInput {

View File

@ -20,7 +20,7 @@ import {
DataQueryResponse, DataQueryResponse,
DataQueryOptions, DataQueryOptions,
} from './series'; } from './series';
import { PanelProps } from './panel'; import { PanelProps, PanelOptionsProps } from './panel';
import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins'; import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins';
import { Organization, OrganizationPreferences, OrganizationState } from './organization'; import { Organization, OrganizationPreferences, OrganizationState } from './organization';
import { import {
@ -69,6 +69,7 @@ export {
TimeRange, TimeRange,
LoadingState, LoadingState,
PanelProps, PanelProps,
PanelOptionsProps,
TimeSeries, TimeSeries,
TimeSeriesVM, TimeSeriesVM,
TimeSeriesVMs, TimeSeriesVMs,

View File

@ -1,7 +1,14 @@
import { LoadingState, TimeSeries, TimeRange } from './series'; import { LoadingState, TimeSeries, TimeRange } from './series';
export interface PanelProps { export interface PanelProps<T = any> {
timeSeries: TimeSeries[]; timeSeries: TimeSeries[];
timeRange: TimeRange; timeRange: TimeRange;
loading: LoadingState; loading: LoadingState;
options: T;
renderCounter: number;
}
export interface PanelOptionsProps<T = any> {
options: T;
onChange: (options: T) => void;
} }

View File

@ -1,13 +1,18 @@
import { ComponentClass } from 'react';
import { PanelProps, PanelOptionsProps } from './panel';
export interface PluginExports { export interface PluginExports {
PanelCtrl?;
PanelComponent?: any;
Datasource?: any; Datasource?: any;
QueryCtrl?: any; QueryCtrl?: any;
ConfigCtrl?: any; ConfigCtrl?: any;
AnnotationsQueryCtrl?: any; AnnotationsQueryCtrl?: any;
PanelOptions?: any;
ExploreQueryField?: any; ExploreQueryField?: any;
ExploreStartPage?: any; ExploreStartPage?: any;
// Panel plugin
PanelCtrl?;
PanelComponent?: ComponentClass<PanelProps>;
PanelOptionsComponent: ComponentClass<PanelOptionsProps>;
} }
export interface PanelPlugin { export interface PanelPlugin {

View File

@ -8,6 +8,111 @@ import 'vendor/flot/jquery.flot.time';
// Types // Types
import { TimeRange, TimeSeriesVMs } from 'app/types'; import { TimeRange, TimeSeriesVMs } from 'app/types';
interface GraphProps {
timeSeries: TimeSeriesVMs;
timeRange: TimeRange;
showLines?: boolean;
showPoints?: boolean;
showBars?: boolean;
size?: { width: number; height: number };
}
export class Graph extends PureComponent<GraphProps> {
static defaultProps = {
showLines: true,
showPoints: false,
showBars: false,
};
element: any;
componentDidUpdate(prevProps: GraphProps) {
if (
prevProps.timeSeries !== this.props.timeSeries ||
prevProps.timeRange !== this.props.timeRange ||
prevProps.size !== this.props.size
) {
this.draw();
}
}
componentDidMount() {
this.draw();
}
draw() {
const { size, timeSeries, timeRange, showLines, showBars, showPoints } = this.props;
if (!size) {
return;
}
const ticks = (size.width || 0) / 100;
const min = timeRange.from.valueOf();
const max = timeRange.to.valueOf();
const flotOptions = {
legend: {
show: false,
},
series: {
lines: {
show: showLines,
linewidth: 1,
zero: false,
},
points: {
show: showPoints,
fill: 1,
fillColor: false,
radius: 2,
},
bars: {
show: showBars,
fill: 1,
barWidth: 1,
zero: false,
lineWidth: 0,
},
shadowSize: 0,
},
xaxis: {
mode: 'time',
min: min,
max: max,
label: 'Datetime',
ticks: ticks,
timeformat: time_format(ticks, min, max),
},
grid: {
minBorderMargin: 0,
markings: [],
backgroundColor: null,
borderWidth: 0,
// hoverable: true,
clickable: true,
color: '#a1a1a1',
margin: { left: 0, right: 0 },
labelMarginX: 0,
},
};
try {
$.plot(this.element, timeSeries, flotOptions);
} catch (err) {
console.log('Graph rendering error', err, flotOptions, timeSeries);
}
}
render() {
return (
<div className="graph-panel">
<div className="graph-panel__chart" ref={e => (this.element = e)} />
</div>
);
}
}
// Copied from graph.ts // Copied from graph.ts
function time_format(ticks, min, max) { function time_format(ticks, min, max) {
if (min && max && ticks) { if (min && max && ticks) {
@ -34,91 +139,4 @@ function time_format(ticks, min, max) {
return '%H:%M'; return '%H:%M';
} }
const FLOT_OPTIONS = {
legend: {
show: false,
},
series: {
lines: {
linewidth: 1,
zero: false,
},
shadowSize: 0,
},
grid: {
minBorderMargin: 0,
markings: [],
backgroundColor: null,
borderWidth: 0,
// hoverable: true,
clickable: true,
color: '#a1a1a1',
margin: { left: 0, right: 0 },
labelMarginX: 0,
},
};
interface GraphProps {
timeSeries: TimeSeriesVMs;
timeRange: TimeRange;
size?: { width: number; height: number };
}
export class Graph extends PureComponent<GraphProps> {
element: any;
componentDidUpdate(prevProps: GraphProps) {
if (
prevProps.timeSeries !== this.props.timeSeries ||
prevProps.timeRange !== this.props.timeRange ||
prevProps.size !== this.props.size
) {
this.draw();
}
}
componentDidMount() {
this.draw();
}
draw() {
const { size, timeSeries, timeRange } = this.props;
if (!size) {
return;
}
const ticks = (size.width || 0) / 100;
const min = timeRange.from.valueOf();
const max = timeRange.to.valueOf();
const dynamicOptions = {
xaxis: {
mode: 'time',
min: min,
max: max,
label: 'Datetime',
ticks: ticks,
timeformat: time_format(ticks, min, max),
},
};
const options = {
...FLOT_OPTIONS,
...dynamicOptions,
};
console.log('plot', timeSeries, options);
$.plot(this.element, timeSeries, options);
}
render() {
return (
<div className="graph-panel">
<div className="graph-panel__chart" ref={e => (this.element = e)} />
</div>
);
}
}
export default withSize()(Graph); export default withSize()(Graph);

View File

@ -10,7 +10,7 @@
<base href="[[.AppSubUrl]]/" /> <base href="[[.AppSubUrl]]/" />
<link rel="stylesheet" href="public/build/grafana.[[ .Theme ]].css?v[[ .BuildVersion ]]"> <link rel="stylesheet" href="public/build/grafana.[[ .Theme ]].<%= webpack.hash %>.css">
<link rel="icon" type="image/png" href="public/img/fav32.png"> <link rel="icon" type="image/png" href="public/img/fav32.png">
<link rel="mask-icon" href="public/img/grafana_mask_icon.svg" color="#F05A28"> <link rel="mask-icon" href="public/img/grafana_mask_icon.svg" color="#F05A28">

View File

@ -15,7 +15,7 @@
<link rel="mask-icon" href="public/img/grafana_mask_icon.svg" color="#F05A28"> <link rel="mask-icon" href="public/img/grafana_mask_icon.svg" color="#F05A28">
<link rel="apple-touch-icon" sizes="180x180" href="public/img/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="public/img/apple-touch-icon.png">
<link rel="stylesheet" href="public/build/grafana.[[ .Theme ]].css?v[[ .BuildVersion ]]+[[ .BuildCommit ]]"> <link rel="stylesheet" href="public/build/grafana.[[ .Theme ]].<%= webpack.hash %>.css">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black">

View File

@ -22,10 +22,10 @@ echo "current dir: $(pwd)"
if [ "$CIRCLE_TAG" != "" ]; then if [ "$CIRCLE_TAG" != "" ]; then
echo "Building releases from tag $CIRCLE_TAG" echo "Building releases from tag $CIRCLE_TAG"
OPT="-includeBuildNumber=false ${EXTRA_OPTS}" OPT="-includeBuildId=false ${EXTRA_OPTS}"
else else
echo "Building incremental build for $CIRCLE_BRANCH" echo "Building incremental build for $CIRCLE_BRANCH"
OPT="-buildNumber=${CIRCLE_BUILD_NUM} ${EXTRA_OPTS}" OPT="-buildId=${CIRCLE_WORKFLOW_ID} ${EXTRA_OPTS}"
fi fi
echo "Build arguments: $OPT" echo "Build arguments: $OPT"

View File

@ -18,10 +18,10 @@ echo "current dir: $(pwd)"
if [ "$CIRCLE_TAG" != "" ]; then if [ "$CIRCLE_TAG" != "" ]; then
echo "Building releases from tag $CIRCLE_TAG" echo "Building releases from tag $CIRCLE_TAG"
OPT="-includeBuildNumber=false ${EXTRA_OPTS}" OPT="-includeBuildId=false ${EXTRA_OPTS}"
else else
echo "Building incremental build for $CIRCLE_BRANCH" echo "Building incremental build for $CIRCLE_BRANCH"
OPT="-buildNumber=${CIRCLE_BUILD_NUM} ${EXTRA_OPTS}" OPT="-buildId=${CIRCLE_WORKFLOW_ID} ${EXTRA_OPTS}"
fi fi
echo "Build arguments: $OPT" echo "Build arguments: $OPT"

View File

@ -1,14 +0,0 @@
#!/bin/bash
mkdir -p dist
echo "Circle branch: ${CIRCLE_BRANCH}"
echo "Circle tag: ${CIRCLE_TAG}"
docker run -i -t --name gfbuild \
-v $(pwd):/go/src/github.com/grafana/grafana \
-e "CIRCLE_BRANCH=${CIRCLE_BRANCH}" \
-e "CIRCLE_TAG=${CIRCLE_TAG}" \
-e "CIRCLE_BUILD_NUM=${CIRCLE_BUILD_NUM}" \
grafana/buildcontainer
sudo chown -R ${USER:=$(/usr/bin/id -run)}:$USER dist

View File

@ -1,4 +1,4 @@
#/bin/sh #!/bin/sh
# no relation to publish.go # no relation to publish.go

View File

@ -0,0 +1,62 @@
package main
import (
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"
)
type releaseFromExternalContent struct {
getter urlGetter
rawVersion string
artifactConfigurations []buildArtifact
}
func (re releaseFromExternalContent) prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string, nightly bool) (*release, error) {
version := re.rawVersion[1:]
isBeta := strings.Contains(version, "beta")
builds := []build{}
for _, ba := range re.artifactConfigurations {
sha256, err := re.getter.getContents(fmt.Sprintf("%s.sha256", ba.getUrl(baseArchiveUrl, version, isBeta)))
if err != nil {
return nil, err
}
builds = append(builds, newBuild(baseArchiveUrl, ba, version, isBeta, sha256))
}
r := release{
Version: version,
ReleaseDate: time.Now().UTC(),
Stable: !isBeta && !nightly,
Beta: isBeta,
Nightly: nightly,
WhatsNewUrl: whatsNewUrl,
ReleaseNotesUrl: releaseNotesUrl,
Builds: builds,
}
return &r, nil
}
type urlGetter interface {
getContents(url string) (string, error)
}
type getHttpContents struct{}
func (getHttpContents) getContents(url string) (string, error) {
response, err := http.Get(url)
if err != nil {
return "", err
}
defer response.Body.Close()
all, err := ioutil.ReadAll(response.Body)
if err != nil {
return "", err
}
return string(all), nil
}

View File

@ -0,0 +1,91 @@
package main
import (
"fmt"
"github.com/pkg/errors"
"io/ioutil"
"log"
"os"
"path/filepath"
"regexp"
"strings"
"time"
)
type releaseLocalSources struct {
path string
artifactConfigurations []buildArtifact
}
func (r releaseLocalSources) prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string, nightly bool) (*release, error) {
buildData := r.findBuilds(baseArchiveUrl)
rel := release{
Version: buildData.version,
ReleaseDate: time.Now().UTC(),
Stable: false,
Beta: false,
Nightly: nightly,
WhatsNewUrl: whatsNewUrl,
ReleaseNotesUrl: releaseNotesUrl,
Builds: buildData.builds,
}
return &rel, nil
}
type buildData struct {
version string
builds []build
}
func (r releaseLocalSources) findBuilds(baseArchiveUrl string) buildData {
data := buildData{}
filepath.Walk(r.path, createBuildWalker(r.path, &data, r.artifactConfigurations, baseArchiveUrl))
return data
}
func createBuildWalker(path string, data *buildData, archiveTypes []buildArtifact, baseArchiveUrl string) func(path string, f os.FileInfo, err error) error {
return func(path string, f os.FileInfo, err error) error {
if err != nil {
log.Printf("error: %v", err)
}
if f.Name() == path || strings.HasSuffix(f.Name(), ".sha256") {
return nil
}
for _, archive := range archiveTypes {
if strings.HasSuffix(f.Name(), archive.urlPostfix) {
shaBytes, err := ioutil.ReadFile(path + ".sha256")
if err != nil {
log.Fatalf("Failed to read sha256 file %v", err)
}
version, err := grabVersion(f.Name(), archive.urlPostfix)
if err != nil {
log.Println(err)
continue
}
data.version = version
data.builds = append(data.builds, build{
Os: archive.os,
Url: archive.getUrl(baseArchiveUrl, version, false),
Sha256: string(shaBytes),
Arch: archive.arch,
})
return nil
}
}
return nil
}
}
func grabVersion(name string, suffix string) (string, error) {
match := regexp.MustCompile(fmt.Sprintf(`grafana(-enterprise)?[-_](.*)%s`, suffix)).FindSubmatch([]byte(name))
if len(match) > 0 {
return string(match[2]), nil
}
return "", errors.New("No version found.")
}

View File

@ -7,13 +7,14 @@ import (
"os" "os"
) )
var baseUri string = "https://grafana.com/api"
func main() { func main() {
var version string var version string
var whatsNewUrl string var whatsNewUrl string
var releaseNotesUrl string var releaseNotesUrl string
var dryRun bool var dryRun bool
var enterprise bool
var fromLocal bool
var nightly bool
var apiKey string var apiKey string
flag.StringVar(&version, "version", "", "Grafana version (ex: --version v5.2.0-beta1)") flag.StringVar(&version, "version", "", "Grafana version (ex: --version v5.2.0-beta1)")
@ -21,20 +22,69 @@ func main() {
flag.StringVar(&releaseNotesUrl, "rn", "", "Grafana version (ex: --rn https://community.grafana.com/t/release-notes-v5-2-x/7894)") flag.StringVar(&releaseNotesUrl, "rn", "", "Grafana version (ex: --rn https://community.grafana.com/t/release-notes-v5-2-x/7894)")
flag.StringVar(&apiKey, "apikey", "", "Grafana.com API key (ex: --apikey ABCDEF)") flag.StringVar(&apiKey, "apikey", "", "Grafana.com API key (ex: --apikey ABCDEF)")
flag.BoolVar(&dryRun, "dry-run", false, "--dry-run") flag.BoolVar(&dryRun, "dry-run", false, "--dry-run")
flag.BoolVar(&enterprise, "enterprise", false, "--enterprise")
flag.BoolVar(&fromLocal, "from-local", false, "--from-local (builds will be tagged as nightly)")
flag.Parse() flag.Parse()
nightly = fromLocal
if len(os.Args) == 1 { if len(os.Args) == 1 {
fmt.Println("Usage: go run publisher.go main.go --version <v> --wn <what's new url> --rn <release notes url> --apikey <api key> --dry-run false") fmt.Println("Usage: go run publisher.go main.go --version <v> --wn <what's new url> --rn <release notes url> --apikey <api key> --dry-run false --enterprise false --nightly false")
fmt.Println("example: go run publisher.go main.go --version v5.2.0-beta2 --wn http://docs.grafana.org/guides/whats-new-in-v5-2/ --rn https://community.grafana.com/t/release-notes-v5-2-x/7894 --apikey ASDF123 --dry-run true") fmt.Println("example: go run publisher.go main.go --version v5.2.0-beta2 --wn http://docs.grafana.org/guides/whats-new-in-v5-2/ --rn https://community.grafana.com/t/release-notes-v5-2-x/7894 --apikey ASDF123 --dry-run --enterprise")
os.Exit(1) os.Exit(1)
} }
if dryRun { if dryRun {
log.Println("Dry-run has been enabled.") log.Println("Dry-run has been enabled.")
} }
var baseUrl string
var builder releaseBuilder
var product string
p := publisher{apiKey: apiKey} if fromLocal {
if err := p.doRelease(version, whatsNewUrl, releaseNotesUrl, dryRun); err != nil { path, _ := os.Getwd()
builder = releaseLocalSources{
path: path,
artifactConfigurations: buildArtifactConfigurations,
}
} else {
builder = releaseFromExternalContent{
getter: getHttpContents{},
rawVersion: version,
artifactConfigurations: buildArtifactConfigurations,
}
}
archiveProviderRoot := "https://s3-us-west-2.amazonaws.com"
if enterprise {
product = "grafana-enterprise"
baseUrl = createBaseUrl(archiveProviderRoot, "grafana-enterprise-releases", product, nightly)
} else {
product = "grafana"
baseUrl = createBaseUrl(archiveProviderRoot, "grafana-releases", product, nightly)
}
p := publisher{
apiKey: apiKey,
apiUri: "https://grafana.com/api",
product: product,
dryRun: dryRun,
enterprise: enterprise,
baseArchiveUrl: baseUrl,
builder: builder,
}
if err := p.doRelease(whatsNewUrl, releaseNotesUrl, nightly); err != nil {
log.Fatalf("error: %v", err) log.Fatalf("error: %v", err)
} }
} }
func createBaseUrl(root string, bucketName string, product string, nightly bool) string {
var subPath string
if nightly {
subPath = "master"
} else {
subPath = "release"
}
return fmt.Sprintf("%s/%s/%s/%s", root, bucketName, subPath, product)
}

View File

@ -13,52 +13,46 @@ import (
type publisher struct { type publisher struct {
apiKey string apiKey string
apiUri string
product string
dryRun bool
enterprise bool
baseArchiveUrl string
builder releaseBuilder
} }
func (p *publisher) doRelease(version string, whatsNewUrl string, releaseNotesUrl string, dryRun bool) error { type releaseBuilder interface {
currentRelease, err := newRelease(version, whatsNewUrl, releaseNotesUrl, buildArtifactConfigurations, getHttpContents{}) prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string, nightly bool) (*release, error)
}
func (p *publisher) doRelease(whatsNewUrl string, releaseNotesUrl string, nightly bool) error {
currentRelease, err := p.builder.prepareRelease(p.baseArchiveUrl, whatsNewUrl, releaseNotesUrl, nightly)
if err != nil { if err != nil {
return err return err
} }
if dryRun {
relJson, err := json.Marshal(currentRelease)
if err != nil {
return err
}
log.Println(string(relJson))
for _, b := range currentRelease.Builds {
artifactJson, err := json.Marshal(b)
if err != nil {
return err
}
log.Println(string(artifactJson))
}
} else {
if err := p.postRelease(currentRelease); err != nil { if err := p.postRelease(currentRelease); err != nil {
return err return err
} }
}
return nil return nil
} }
func (p *publisher) postRelease(r *release) error { func (p *publisher) postRelease(r *release) error {
err := p.postRequest("/grafana/versions", r, fmt.Sprintf("Create Release %s", r.Version)) err := p.postRequest("/versions", r, fmt.Sprintf("Create Release %s", r.Version))
if err != nil { if err != nil {
return err return err
} }
err = p.postRequest("/grafana/versions/"+r.Version, r, fmt.Sprintf("Update Release %s", r.Version)) err = p.postRequest("/versions/"+r.Version, r, fmt.Sprintf("Update Release %s", r.Version))
if err != nil { if err != nil {
return err return err
} }
for _, b := range r.Builds { for _, b := range r.Builds {
err = p.postRequest(fmt.Sprintf("/grafana/versions/%s/packages", r.Version), b, fmt.Sprintf("Create Build %s %s", b.Os, b.Arch)) err = p.postRequest(fmt.Sprintf("/versions/%s/packages", r.Version), b, fmt.Sprintf("Create Build %s %s", b.Os, b.Arch))
if err != nil { if err != nil {
return err return err
} }
err = p.postRequest(fmt.Sprintf("/grafana/versions/%s/packages/%s/%s", r.Version, b.Arch, b.Os), b, fmt.Sprintf("Update Build %s %s", b.Os, b.Arch)) err = p.postRequest(fmt.Sprintf("/versions/%s/packages/%s/%s", r.Version, b.Arch, b.Os), b, fmt.Sprintf("Update Build %s %s", b.Os, b.Arch))
if err != nil { if err != nil {
return err return err
} }
@ -67,15 +61,13 @@ func (p *publisher) postRelease(r *release) error {
return nil return nil
} }
const baseArhiveUrl = "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana"
type buildArtifact struct { type buildArtifact struct {
os string os string
arch string arch string
urlPostfix string urlPostfix string
} }
func (t buildArtifact) getUrl(version string, isBeta bool) string { func (t buildArtifact) getUrl(baseArchiveUrl, version string, isBeta bool) string {
prefix := "-" prefix := "-"
rhelReleaseExtra := "" rhelReleaseExtra := ""
@ -87,7 +79,7 @@ func (t buildArtifact) getUrl(version string, isBeta bool) string {
rhelReleaseExtra = "-1" rhelReleaseExtra = "-1"
} }
url := strings.Join([]string{baseArhiveUrl, prefix, version, rhelReleaseExtra, t.urlPostfix}, "") url := strings.Join([]string{baseArchiveUrl, prefix, version, rhelReleaseExtra, t.urlPostfix}, "")
return url return url
} }
@ -149,48 +141,32 @@ var buildArtifactConfigurations = []buildArtifact{
}, },
} }
func newRelease(rawVersion string, whatsNewUrl string, releaseNotesUrl string, artifactConfigurations []buildArtifact, getter urlGetter) (*release, error) { func newBuild(baseArchiveUrl string, ba buildArtifact, version string, isBeta bool, sha256 string) build {
version := rawVersion[1:]
now := time.Now()
isBeta := strings.Contains(version, "beta")
builds := []build{}
for _, ba := range artifactConfigurations {
sha256, err := getter.getContents(fmt.Sprintf("%s.sha256", ba.getUrl(version, isBeta)))
if err != nil {
return nil, err
}
builds = append(builds, newBuild(ba, version, isBeta, sha256))
}
r := release{
Version: version,
ReleaseDate: time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local),
Stable: !isBeta,
Beta: isBeta,
Nightly: false,
WhatsNewUrl: whatsNewUrl,
ReleaseNotesUrl: releaseNotesUrl,
Builds: builds,
}
return &r, nil
}
func newBuild(ba buildArtifact, version string, isBeta bool, sha256 string) build {
return build{ return build{
Os: ba.os, Os: ba.os,
Url: ba.getUrl(version, isBeta), Url: ba.getUrl(baseArchiveUrl, version, isBeta),
Sha256: sha256, Sha256: sha256,
Arch: ba.arch, Arch: ba.arch,
} }
} }
func (p *publisher) apiUrl(url string) string {
return fmt.Sprintf("%s/%s%s", p.apiUri, p.product, url)
}
func (p *publisher) postRequest(url string, obj interface{}, desc string) error { func (p *publisher) postRequest(url string, obj interface{}, desc string) error {
jsonBytes, err := json.Marshal(obj) jsonBytes, err := json.Marshal(obj)
if err != nil { if err != nil {
return err return err
} }
req, err := http.NewRequest(http.MethodPost, baseUri+url, bytes.NewReader(jsonBytes))
if p.dryRun {
log.Println(fmt.Sprintf("POST to %s:", p.apiUrl(url)))
log.Println(string(jsonBytes))
return nil
}
req, err := http.NewRequest(http.MethodPost, p.apiUrl(url), bytes.NewReader(jsonBytes))
if err != nil { if err != nil {
return err return err
} }
@ -243,24 +219,3 @@ type build struct {
Sha256 string `json:"sha256"` Sha256 string `json:"sha256"`
Arch string `json:"arch"` Arch string `json:"arch"`
} }
type urlGetter interface {
getContents(url string) (string, error)
}
type getHttpContents struct{}
func (getHttpContents) getContents(url string) (string, error) {
response, err := http.Get(url)
if err != nil {
return "", err
}
defer response.Body.Close()
all, err := ioutil.ReadAll(response.Body)
if err != nil {
return "", err
}
return string(all), nil
}

View File

@ -2,7 +2,7 @@ package main
import "testing" import "testing"
func TestNewRelease(t *testing.T) { func TestPreparingReleaseFromRemote(t *testing.T) {
versionIn := "v5.2.0-beta1" versionIn := "v5.2.0-beta1"
expectedVersion := "5.2.0-beta1" expectedVersion := "5.2.0-beta1"
whatsNewUrl := "https://whatsnews.foo/" whatsNewUrl := "https://whatsnews.foo/"
@ -11,7 +11,15 @@ func TestNewRelease(t *testing.T) {
expectedOs := "linux" expectedOs := "linux"
buildArtifacts := []buildArtifact{{expectedOs,expectedArch, ".linux-amd64.tar.gz"}} buildArtifacts := []buildArtifact{{expectedOs,expectedArch, ".linux-amd64.tar.gz"}}
rel, _ := newRelease(versionIn, whatsNewUrl, relNotesUrl, buildArtifacts, mockHttpGetter{}) var builder releaseBuilder
builder = releaseFromExternalContent{
getter: mockHttpGetter{},
rawVersion: versionIn,
artifactConfigurations: buildArtifactConfigurations,
}
rel, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana", whatsNewUrl, relNotesUrl, false)
if !rel.Beta || rel.Stable { if !rel.Beta || rel.Stable {
t.Errorf("%s should have been tagged as beta (not stable), but wasn't .", versionIn) t.Errorf("%s should have been tagged as beta (not stable), but wasn't .", versionIn)
@ -41,3 +49,71 @@ type mockHttpGetter struct{}
func (mockHttpGetter) getContents(url string) (string, error) { func (mockHttpGetter) getContents(url string) (string, error) {
return url, nil return url, nil
} }
func TestPreparingReleaseFromLocal(t *testing.T) {
whatsNewUrl := "https://whatsnews.foo/"
relNotesUrl := "https://relnotes.foo/"
expectedVersion := "5.4.0-123pre1"
expectedBuilds := 4
var builder releaseBuilder
testDataPath := "testdata"
builder = releaseLocalSources{
path: testDataPath,
artifactConfigurations: buildArtifactConfigurations,
}
relAll, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-enterprise-releases/master/grafana-enterprise", whatsNewUrl, relNotesUrl, true)
if relAll.Stable || !relAll.Nightly {
t.Error("Expected a nightly release but wasn't.")
}
if relAll.ReleaseNotesUrl != relNotesUrl {
t.Errorf("expected releaseNotesUrl to be %s, but it was %s", relNotesUrl, relAll.ReleaseNotesUrl)
}
if relAll.WhatsNewUrl != whatsNewUrl {
t.Errorf("expected whatsNewUrl to be %s, but it was %s", whatsNewUrl, relAll.WhatsNewUrl)
}
if relAll.Beta {
t.Errorf("Expected release to be nightly, not beta.")
}
if relAll.Version != expectedVersion {
t.Errorf("Expected version=%s, but got=%s", expectedVersion, relAll.Version)
}
if len(relAll.Builds) != expectedBuilds {
t.Errorf("Expected %v builds, but was %v", expectedBuilds, len(relAll.Builds))
}
expectedArch := "amd64"
expectedOs := "win"
builder = releaseLocalSources{
path: testDataPath,
artifactConfigurations: []buildArtifact{{
os: expectedOs,
arch: expectedArch,
urlPostfix: ".windows-amd64.zip",
}},
}
relOne, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-enterprise-releases/master/grafana-enterprise", whatsNewUrl, relNotesUrl, true)
if len(relOne.Builds) != 1 {
t.Errorf("Expected 1 artifact, but was %v", len(relOne.Builds))
}
build := relOne.Builds[0]
if build.Arch != expectedArch {
t.Fatalf("Expected arch to be %s, but was %s", expectedArch, build.Arch)
}
if build.Os != expectedOs {
t.Fatalf("Expected os to be %s, but was %s", expectedOs, build.Os)
}
}

View File

@ -0,0 +1 @@
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

View File

@ -0,0 +1 @@
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

View File

@ -0,0 +1 @@
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

View File

@ -0,0 +1 @@
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

View File

@ -47,7 +47,7 @@ module.exports = {
}, },
{ {
test: /\.html$/, test: /\.html$/,
exclude: /index\.template.html/, exclude: /(index|error)\-template\.html/,
use: [ use: [
{ loader: 'ngtemplate-loader?relativeTo=' + (path.resolve(__dirname, '../../public')) + '&prefix=public' }, { loader: 'ngtemplate-loader?relativeTo=' + (path.resolve(__dirname, '../../public')) + '&prefix=public' },
{ {

View File

@ -80,11 +80,16 @@ module.exports = merge(common, {
plugins: [ plugins: [
new CleanWebpackPlugin('../../public/build', { allowExternal: true }), new CleanWebpackPlugin('../../public/build', { allowExternal: true }),
new MiniCssExtractPlugin({ new MiniCssExtractPlugin({
filename: "grafana.[name].css" filename: "grafana.[name].[hash].css"
}),
new HtmlWebpackPlugin({
filename: path.resolve(__dirname, '../../public/views/error.html'),
template: path.resolve(__dirname, '../../public/views/error-template.html'),
inject: 'false',
}), }),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
filename: path.resolve(__dirname, '../../public/views/index.html'), filename: path.resolve(__dirname, '../../public/views/index.html'),
template: path.resolve(__dirname, '../../public/views/index.template.html'), template: path.resolve(__dirname, '../../public/views/index-template.html'),
inject: 'body', inject: 'body',
chunks: ['manifest', 'vendor', 'app'], chunks: ['manifest', 'vendor', 'app'],
}), }),

View File

@ -83,7 +83,7 @@ module.exports = merge(common, {
new CleanWebpackPlugin('../public/build', { allowExternal: true }), new CleanWebpackPlugin('../public/build', { allowExternal: true }),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
filename: path.resolve(__dirname, '../../public/views/index.html'), filename: path.resolve(__dirname, '../../public/views/index.html'),
template: path.resolve(__dirname, '../../public/views/index.template.html'), template: path.resolve(__dirname, '../../public/views/index-template.html'),
inject: 'body', inject: 'body',
alwaysWriteToDisk: true, alwaysWriteToDisk: true,
}), }),

View File

@ -71,15 +71,20 @@ module.exports = merge(common, {
plugins: [ plugins: [
new MiniCssExtractPlugin({ new MiniCssExtractPlugin({
filename: "grafana.[name].css" filename: "grafana.[name].[hash].css"
}), }),
new ngAnnotatePlugin(), new ngAnnotatePlugin(),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
filename: path.resolve(__dirname, '../../public/views/index.html'), filename: path.resolve(__dirname, '../../public/views/index.html'),
template: path.resolve(__dirname, '../../public/views/index.template.html'), template: path.resolve(__dirname, '../../public/views/index-template.html'),
inject: 'body', inject: 'body',
chunks: ['vendor', 'app'], chunks: ['vendor', 'app'],
}), }),
new HtmlWebpackPlugin({
filename: path.resolve(__dirname, '../../public/views/error.html'),
template: path.resolve(__dirname, '../../public/views/error-template.html'),
inject: false,
}),
function () { function () {
this.hooks.done.tap('Done', function (stats) { this.hooks.done.tap('Done', function (stats) {
if (stats.compilation.errors && stats.compilation.errors.length) { if (stats.compilation.errors && stats.compilation.errors.length) {