mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into data-source-settings-to-react
This commit is contained in:
commit
e25b2d0ab6
@ -335,6 +335,9 @@ jobs:
|
||||
- run:
|
||||
name: deploy to gcp
|
||||
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:
|
||||
@ -403,7 +406,7 @@ jobs:
|
||||
command: '/opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file=/tmp/gcpkey.json'
|
||||
- run:
|
||||
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:
|
||||
name: Deploy to Grafana.com
|
||||
command: './scripts/build/publish.sh'
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,6 +8,7 @@ awsconfig
|
||||
/dist
|
||||
/public/build
|
||||
/public/views/index.html
|
||||
/public/views/error.html
|
||||
/emails/dist
|
||||
/public_gen
|
||||
/public/vendor/npm
|
||||
|
@ -12,11 +12,14 @@
|
||||
### Minor
|
||||
|
||||
* **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)
|
||||
* **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)
|
||||
* **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)
|
||||
* **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
|
||||
|
||||
@ -24,7 +27,10 @@
|
||||
|
||||
# 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)
|
||||
* **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)
|
||||
|
||||
|
28
build.go
28
build.go
@ -41,8 +41,8 @@ var (
|
||||
race bool
|
||||
phjsToRelease string
|
||||
workingDir string
|
||||
includeBuildNumber bool = true
|
||||
buildNumber int = 0
|
||||
includeBuildId bool = true
|
||||
buildId string = "0"
|
||||
binaries []string = []string{"grafana-server", "grafana-cli"}
|
||||
isDev bool = false
|
||||
enterprise bool = false
|
||||
@ -54,6 +54,8 @@ func main() {
|
||||
|
||||
ensureGoPath()
|
||||
|
||||
var buildIdRaw string
|
||||
|
||||
flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH")
|
||||
flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS")
|
||||
flag.StringVar(&gocc, "cc", "", "CC")
|
||||
@ -61,12 +63,14 @@ func main() {
|
||||
flag.StringVar(&pkgArch, "pkg-arch", "", "PKG ARCH")
|
||||
flag.StringVar(&phjsToRelease, "phjs", "", "PhantomJS binary")
|
||||
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.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.Parse()
|
||||
|
||||
buildId = shortenBuildId(buildIdRaw)
|
||||
|
||||
readVersionFromPackageJson()
|
||||
|
||||
if pkgArch == "" {
|
||||
@ -197,9 +201,9 @@ func readVersionFromPackageJson() {
|
||||
}
|
||||
|
||||
// add timestamp to iteration
|
||||
if includeBuildNumber {
|
||||
if buildNumber != 0 {
|
||||
linuxPackageIteration = fmt.Sprintf("%d%s", buildNumber, linuxPackageIteration)
|
||||
if includeBuildId {
|
||||
if buildId != "0" {
|
||||
linuxPackageIteration = fmt.Sprintf("%s%s", buildId, linuxPackageIteration)
|
||||
} else {
|
||||
linuxPackageIteration = fmt.Sprintf("%d%s", time.Now().Unix(), linuxPackageIteration)
|
||||
}
|
||||
@ -392,7 +396,7 @@ func grunt(params ...string) {
|
||||
|
||||
func gruntBuildArg(task string) []string {
|
||||
args := []string{task}
|
||||
if includeBuildNumber {
|
||||
if includeBuildId {
|
||||
args = append(args, fmt.Sprintf("--pkgVer=%v-%v", linuxPackageVersion, linuxPackageIteration))
|
||||
} else {
|
||||
args = append(args, fmt.Sprintf("--pkgVer=%v", version))
|
||||
@ -632,3 +636,11 @@ func shaFile(file string) error {
|
||||
|
||||
return out.Close()
|
||||
}
|
||||
|
||||
func shortenBuildId(buildId string) string {
|
||||
buildId = strings.Replace(buildId, "-", "", -1)
|
||||
if (len(buildId) < 9) {
|
||||
return buildId
|
||||
}
|
||||
return buildId[0:8]
|
||||
}
|
||||
|
@ -404,6 +404,112 @@
|
||||
"title": "Column style thresholds & units",
|
||||
"transform": "timeseries_to_columns",
|
||||
"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,
|
||||
@ -449,5 +555,5 @@
|
||||
"timezone": "browser",
|
||||
"title": "Panel Tests - Table",
|
||||
"uid": "pttable",
|
||||
"version": 1
|
||||
}
|
||||
"version": 2
|
||||
}
|
@ -60,7 +60,8 @@ Here is a minimal policy example:
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"cloudwatch:ListMetrics",
|
||||
"cloudwatch:GetMetricStatistics"
|
||||
"cloudwatch:GetMetricStatistics",
|
||||
"cloudwatch:GetMetricData"
|
||||
],
|
||||
"Resource": "*"
|
||||
},
|
||||
|
@ -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
|
||||
- [Alerting]({{< relref "#notification-reminders" >}}) with notification reminders
|
||||
- [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
|
||||
- [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" >}}
|
||||
|
||||
## 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/).
|
||||
|
||||
## Annotations
|
||||
|
@ -290,7 +290,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
"sendReminder": true,
|
||||
"frequency": "15m",
|
||||
"settings": {
|
||||
"addresses: "carl@grafana.com;dev@grafana.com"
|
||||
"addresses": "carl@grafana.com;dev@grafana.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -134,12 +134,16 @@ func AlertTest(c *m.ReqContext, dto dtos.AlertTestCommand) Response {
|
||||
OrgId: c.OrgId,
|
||||
Dashboard: dto.Dashboard,
|
||||
PanelId: dto.PanelId,
|
||||
User: c.SignedInUser,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&backendCmd); err != nil {
|
||||
if validationErr, ok := err.(alerting.ValidationError); ok {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -1,62 +1,22 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/pluginproxy"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"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) {
|
||||
c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer)
|
||||
|
||||
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 == m.ErrDataSourceAccessDenied {
|
||||
c.JsonApiErr(403, "Access denied to datasource", err)
|
||||
return
|
||||
}
|
||||
c.JsonApiErr(500, "Unable to load datasource meta data", err)
|
||||
return
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ import (
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
|
||||
gocache "github.com/patrickmn/go-cache"
|
||||
macaron "gopkg.in/macaron.v1"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/live"
|
||||
@ -28,6 +27,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"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/rendering"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@ -46,19 +47,19 @@ type HTTPServer struct {
|
||||
macaron *macaron.Macaron
|
||||
context context.Context
|
||||
streamManager *live.StreamManager
|
||||
cache *gocache.Cache
|
||||
httpSrv *http.Server
|
||||
|
||||
RouteRegister routing.RouteRegister `inject:""`
|
||||
Bus bus.Bus `inject:""`
|
||||
RenderService rendering.Service `inject:""`
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
HooksService *hooks.HooksService `inject:""`
|
||||
RouteRegister routing.RouteRegister `inject:""`
|
||||
Bus bus.Bus `inject:""`
|
||||
RenderService rendering.Service `inject:""`
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
HooksService *hooks.HooksService `inject:""`
|
||||
CacheService *cache.CacheService `inject:""`
|
||||
DatasourceCache datasources.CacheService `inject:""`
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) Init() error {
|
||||
hs.log = log.New("http.server")
|
||||
hs.cache = gocache.New(5*time.Minute, 10*time.Minute)
|
||||
|
||||
hs.streamManager = live.NewStreamManager()
|
||||
hs.macaron = hs.newMacaron()
|
||||
@ -231,6 +232,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
|
||||
m.Use(middleware.ValidateHostHeader(setting.Domain))
|
||||
}
|
||||
|
||||
m.Use(middleware.HandleNoCacheHeader())
|
||||
m.Use(middleware.AddDefaultResponseHeaders())
|
||||
}
|
||||
|
||||
|
@ -25,8 +25,11 @@ func (hs *HTTPServer) QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) R
|
||||
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 == m.ErrDataSourceAccessDenied {
|
||||
return Error(403, "Access denied to datasource", err)
|
||||
}
|
||||
return Error(500, "Unable to load datasource meta data", err)
|
||||
}
|
||||
|
||||
|
@ -15,13 +15,21 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"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/metrics"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
_ "github.com/grafana/grafana/pkg/plugins"
|
||||
"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/cleanup"
|
||||
_ "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/search"
|
||||
_ "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"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func NewGrafanaServer() *GrafanaServerImpl {
|
||||
@ -72,6 +77,7 @@ func (g *GrafanaServerImpl) Run() error {
|
||||
serviceGraph.Provide(&inject.Object{Value: bus.GetBus()})
|
||||
serviceGraph.Provide(&inject.Object{Value: g.cfg})
|
||||
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
|
||||
services := registry.GetServices()
|
||||
@ -138,7 +144,6 @@ func (g *GrafanaServerImpl) Run() error {
|
||||
}
|
||||
|
||||
sendSystemdNotification("READY=1")
|
||||
|
||||
return g.childRoutines.Wait()
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ package login
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
14
pkg/middleware/headers.go
Normal file
14
pkg/middleware/headers.go
Normal 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"
|
||||
}
|
||||
}
|
@ -29,6 +29,7 @@ func GetContextHandler() macaron.Handler {
|
||||
Session: session.GetSession(),
|
||||
IsSignedIn: false,
|
||||
AllowAnonymous: false,
|
||||
SkipCache: false,
|
||||
Logger: log.New("context"),
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
)
|
||||
|
||||
func TestMiddlewareContext(t *testing.T) {
|
||||
setting.ERR_TEMPLATE_NAME = "error-template"
|
||||
|
||||
Convey("Given the grafana middleware", t, func() {
|
||||
middlewareScenario("middleware should add context to injector", func(sc *scenarioContext) {
|
||||
|
@ -138,7 +138,7 @@ func Recovery() macaron.Handler {
|
||||
|
||||
c.JSON(500, resp)
|
||||
} else {
|
||||
c.HTML(500, "error")
|
||||
c.HTML(500, setting.ERR_TEMPLATE_NAME)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
@ -8,11 +8,14 @@ import (
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/session"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
func TestRecoveryMiddleware(t *testing.T) {
|
||||
setting.ERR_TEMPLATE_NAME = "error-template"
|
||||
|
||||
Convey("Given an api route that panics", t, func() {
|
||||
apiURL := "/api/whatever"
|
||||
recoveryScenario("recovery middleware should return json", apiURL, func(sc *scenarioContext) {
|
||||
@ -50,6 +53,7 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) {
|
||||
sc := &scenarioContext{
|
||||
url: url,
|
||||
}
|
||||
|
||||
viewsPath, _ := filepath.Abs("../../public/views")
|
||||
|
||||
sc.m = macaron.New()
|
||||
|
@ -215,13 +215,14 @@ type AlertStateInfoDTO struct {
|
||||
// "Internal" commands
|
||||
|
||||
type UpdateDashboardAlertsCommand struct {
|
||||
UserId int64
|
||||
OrgId int64
|
||||
Dashboard *Dashboard
|
||||
User *SignedInUser
|
||||
}
|
||||
|
||||
type ValidateDashboardAlertsCommand struct {
|
||||
UserId int64
|
||||
OrgId int64
|
||||
Dashboard *Dashboard
|
||||
User *SignedInUser
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ type ReqContext struct {
|
||||
IsSignedIn bool
|
||||
IsRenderCall bool
|
||||
AllowAnonymous bool
|
||||
SkipCache bool
|
||||
Logger log.Logger
|
||||
}
|
||||
|
||||
@ -36,7 +37,7 @@ func (ctx *ReqContext) Handle(status int, title string, err error) {
|
||||
ctx.Data["AppSubUrl"] = setting.AppSubUrl
|
||||
ctx.Data["Theme"] = "dark"
|
||||
|
||||
ctx.HTML(status, "error")
|
||||
ctx.HTML(status, setting.ERR_TEMPLATE_NAME)
|
||||
}
|
||||
|
||||
func (ctx *ReqContext) JsonOK(message string) {
|
||||
|
@ -207,11 +207,6 @@ func (p DsPermissionType) String() string {
|
||||
return names[int(p)]
|
||||
}
|
||||
|
||||
type GetDataSourcePermissionsForUserQuery struct {
|
||||
User *SignedInUser
|
||||
Result map[int64]DsPermissionType
|
||||
}
|
||||
|
||||
type DatasourcesPermissionFilterQuery struct {
|
||||
User *SignedInUser
|
||||
Datasources []*DataSource
|
||||
|
@ -165,6 +165,7 @@ type SignedInUser struct {
|
||||
IsAnonymous bool
|
||||
HelpFlags1 HelpFlags1
|
||||
LastSeenAt time.Time
|
||||
Teams []int64
|
||||
}
|
||||
|
||||
func (u *SignedInUser) ShouldUpdateLastSeenAt() bool {
|
||||
|
@ -29,11 +29,42 @@ func Register(descriptor *Descriptor) {
|
||||
}
|
||||
|
||||
func GetServices() []*Descriptor {
|
||||
sort.Slice(services, func(i, j int) bool {
|
||||
return services[i].InitPriority > services[j].InitPriority
|
||||
slice := getServicesWithOverrides()
|
||||
|
||||
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
|
||||
|
@ -11,7 +11,7 @@ func init() {
|
||||
}
|
||||
|
||||
func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error {
|
||||
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
|
||||
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId, cmd.User)
|
||||
|
||||
return extractor.ValidateAlerts()
|
||||
}
|
||||
@ -19,11 +19,11 @@ func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error {
|
||||
func updateDashboardAlerts(cmd *m.UpdateDashboardAlertsCommand) error {
|
||||
saveAlerts := m.SaveAlertsCommand{
|
||||
OrgId: cmd.OrgId,
|
||||
UserId: cmd.UserId,
|
||||
UserId: cmd.User.UserId,
|
||||
DashboardId: cmd.Dashboard.Id,
|
||||
}
|
||||
|
||||
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
|
||||
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId, cmd.User)
|
||||
|
||||
alerts, err := extractor.GetAlerts()
|
||||
if err != nil {
|
||||
|
@ -52,6 +52,24 @@ func TestSimpleReducer(t *testing.T) {
|
||||
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() {
|
||||
result := testReducer("avg", 1, 2, 3)
|
||||
So(result, ShouldEqual, float64(2))
|
||||
|
@ -13,14 +13,16 @@ import (
|
||||
|
||||
// DashAlertExtractor extracts alerts from the dashboard json
|
||||
type DashAlertExtractor struct {
|
||||
User *m.SignedInUser
|
||||
Dash *m.Dashboard
|
||||
OrgID int64
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
// 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{
|
||||
User: user,
|
||||
Dash: dash,
|
||||
OrgID: orgID,
|
||||
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)}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if interval, err := panel.Get("interval").String(); err == nil {
|
||||
|
@ -69,7 +69,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
So(getTarget(dashJson), ShouldEqual, "")
|
||||
})
|
||||
|
||||
extractor := NewDashAlertExtractor(dash, 1)
|
||||
extractor := NewDashAlertExtractor(dash, 1, nil)
|
||||
_, _ = extractor.GetAlerts()
|
||||
|
||||
Convey("Dashboard json should not be updated after extracting rules", func() {
|
||||
@ -83,7 +83,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
dash := m.NewDashboardFromJson(dashJson)
|
||||
extractor := NewDashAlertExtractor(dash, 1)
|
||||
extractor := NewDashAlertExtractor(dash, 1, nil)
|
||||
|
||||
alerts, err := extractor.GetAlerts()
|
||||
|
||||
@ -146,7 +146,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
dashJson, err := simplejson.NewJson(panelWithoutId)
|
||||
So(err, ShouldBeNil)
|
||||
dash := m.NewDashboardFromJson(dashJson)
|
||||
extractor := NewDashAlertExtractor(dash, 1)
|
||||
extractor := NewDashAlertExtractor(dash, 1, nil)
|
||||
|
||||
_, err = extractor.GetAlerts()
|
||||
|
||||
@ -162,7 +162,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
dashJson, err := simplejson.NewJson(panelWithIdZero)
|
||||
So(err, ShouldBeNil)
|
||||
dash := m.NewDashboardFromJson(dashJson)
|
||||
extractor := NewDashAlertExtractor(dash, 1)
|
||||
extractor := NewDashAlertExtractor(dash, 1, nil)
|
||||
|
||||
_, err = extractor.GetAlerts()
|
||||
|
||||
@ -178,7 +178,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
dashJson, err := simplejson.NewJson(json)
|
||||
So(err, ShouldBeNil)
|
||||
dash := m.NewDashboardFromJson(dashJson)
|
||||
extractor := NewDashAlertExtractor(dash, 1)
|
||||
extractor := NewDashAlertExtractor(dash, 1, nil)
|
||||
|
||||
alerts, err := extractor.GetAlerts()
|
||||
|
||||
@ -198,7 +198,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
dashJson, err := simplejson.NewJson(json)
|
||||
So(err, ShouldBeNil)
|
||||
dash := m.NewDashboardFromJson(dashJson)
|
||||
extractor := NewDashAlertExtractor(dash, 1)
|
||||
extractor := NewDashAlertExtractor(dash, 1, nil)
|
||||
|
||||
alerts, err := extractor.GetAlerts()
|
||||
|
||||
@ -228,7 +228,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
dash := m.NewDashboardFromJson(dashJson)
|
||||
extractor := NewDashAlertExtractor(dash, 1)
|
||||
extractor := NewDashAlertExtractor(dash, 1, nil)
|
||||
|
||||
alerts, err := extractor.GetAlerts()
|
||||
|
||||
@ -248,7 +248,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
dashJSON, err := simplejson.NewJson(json)
|
||||
So(err, ShouldBeNil)
|
||||
dash := m.NewDashboardFromJson(dashJSON)
|
||||
extractor := NewDashAlertExtractor(dash, 1)
|
||||
extractor := NewDashAlertExtractor(dash, 1, nil)
|
||||
|
||||
err = extractor.ValidateAlerts()
|
||||
|
||||
|
@ -13,6 +13,7 @@ type AlertTestCommand struct {
|
||||
Dashboard *simplejson.Json
|
||||
PanelId int64
|
||||
OrgId int64
|
||||
User *m.SignedInUser
|
||||
|
||||
Result *EvalContext
|
||||
}
|
||||
@ -25,7 +26,7 @@ func handleAlertTestCommand(cmd *AlertTestCommand) error {
|
||||
|
||||
dash := m.NewDashboardFromJson(cmd.Dashboard)
|
||||
|
||||
extractor := NewDashAlertExtractor(dash, cmd.OrgId)
|
||||
extractor := NewDashAlertExtractor(dash, cmd.OrgId, cmd.User)
|
||||
alerts, err := extractor.GetAlerts()
|
||||
if err != nil {
|
||||
return err
|
||||
|
17
pkg/services/cache/cache.go
vendored
Normal file
17
pkg/services/cache/cache.go
vendored
Normal 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),
|
||||
}
|
||||
}
|
@ -90,6 +90,7 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO,
|
||||
validateAlertsCmd := models.ValidateDashboardAlertsCommand{
|
||||
OrgId: dto.OrgId,
|
||||
Dashboard: dash,
|
||||
User: dto.User,
|
||||
}
|
||||
|
||||
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 {
|
||||
alertCmd := models.UpdateDashboardAlertsCommand{
|
||||
OrgId: dto.OrgId,
|
||||
UserId: dto.User.UserId,
|
||||
Dashboard: cmd.Result,
|
||||
User: dto.User,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&alertCmd); err != nil {
|
||||
|
53
pkg/services/datasources/cache.go
Normal file
53
pkg/services/datasources/cache.go
Normal 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(®istry.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
|
||||
}
|
@ -327,6 +327,24 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
|
||||
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 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 {
|
||||
@ -337,10 +355,6 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := deleteAlertDefinition(dashboard.Id, sess); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"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/migrator"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
|
||||
@ -47,8 +48,9 @@ func init() {
|
||||
}
|
||||
|
||||
type SqlStore struct {
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
Bus bus.Bus `inject:""`
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
Bus bus.Bus `inject:""`
|
||||
CacheService *cache.CacheService `inject:""`
|
||||
|
||||
dbCfg DatabaseConfig
|
||||
engine *xorm.Engine
|
||||
@ -148,9 +150,11 @@ func (ss *SqlStore) Init() error {
|
||||
|
||||
// Init repo instances
|
||||
annotations.SetRepository(&SqlAnnotationRepo{})
|
||||
|
||||
ss.Bus.SetTransactionManager(ss)
|
||||
|
||||
// Register handlers
|
||||
ss.addUserQueryAndCommandHandlers()
|
||||
|
||||
// ensure admin user
|
||||
if ss.skipEnsureAdmin {
|
||||
return nil
|
||||
@ -322,6 +326,7 @@ func InitTestDB(t *testing.T) *SqlStore {
|
||||
sqlstore := &SqlStore{}
|
||||
sqlstore.skipEnsureAdmin = true
|
||||
sqlstore.Bus = bus.New()
|
||||
sqlstore.CacheService = cache.New(5*time.Minute, 10*time.Minute)
|
||||
|
||||
dbType := migrator.SQLITE
|
||||
|
||||
|
@ -15,8 +15,9 @@ import (
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
//bus.AddHandler("sql", CreateUser)
|
||||
func (ss *SqlStore) addUserQueryAndCommandHandlers() {
|
||||
ss.Bus.AddHandler(ss.GetSignedInUserWithCache)
|
||||
|
||||
bus.AddHandler("sql", GetUserById)
|
||||
bus.AddHandler("sql", UpdateUser)
|
||||
bus.AddHandler("sql", ChangeUserPassword)
|
||||
@ -25,7 +26,6 @@ func init() {
|
||||
bus.AddHandler("sql", SetUsingOrg)
|
||||
bus.AddHandler("sql", UpdateUserLastSeenAt)
|
||||
bus.AddHandler("sql", GetUserProfile)
|
||||
bus.AddHandler("sql", GetSignedInUser)
|
||||
bus.AddHandler("sql", SearchUsers)
|
||||
bus.AddHandler("sql", GetUserOrgList)
|
||||
bus.AddHandler("sql", DeleteUser)
|
||||
@ -345,6 +345,22 @@ func GetUserOrgList(query *m.GetUserOrgListQuery) error {
|
||||
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 {
|
||||
orgId := "u.org_id"
|
||||
if query.OrgId > 0 {
|
||||
@ -389,6 +405,17 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error {
|
||||
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
|
||||
return err
|
||||
}
|
||||
|
@ -38,6 +38,10 @@ const (
|
||||
APP_NAME_ENTERPRISE = "Grafana Enterprise"
|
||||
)
|
||||
|
||||
var (
|
||||
ERR_TEMPLATE_NAME = "error"
|
||||
)
|
||||
|
||||
var (
|
||||
// App settings.
|
||||
Env = DEV
|
||||
|
@ -46,6 +46,7 @@ func init() {
|
||||
"AWS/Billing": {"EstimatedCharges"},
|
||||
"AWS/CloudFront": {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"},
|
||||
"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/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"},
|
||||
@ -120,6 +121,7 @@ func init() {
|
||||
"AWS/Billing": {"ServiceName", "LinkedAccount", "Currency"},
|
||||
"AWS/CloudFront": {"DistributionId", "Region"},
|
||||
"AWS/CloudSearch": {},
|
||||
"AWS/Connect": {"InstanceId", "MetricGroup", "Participant", "QueueName", "Stream Type", "Type of Connection"},
|
||||
"AWS/DMS": {"ReplicationInstanceIdentifier", "ReplicationTaskIdentifier"},
|
||||
"AWS/DX": {"ConnectionId"},
|
||||
"AWS/DynamoDB": {"TableName", "GlobalSecondaryIndexName", "Operation", "StreamLabel"},
|
||||
|
@ -88,7 +88,7 @@ export class FormDropdownCtrl {
|
||||
if (evt.keyCode === 13) {
|
||||
setTimeout(() => {
|
||||
this.inputElement.blur();
|
||||
}, 100);
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -166,7 +166,7 @@ export class AlertTabCtrl {
|
||||
|
||||
alert.noDataState = alert.noDataState || config.alertingNoDataOrNullValues;
|
||||
alert.executionErrorState = alert.executionErrorState || config.alertingErrorOrTimeout;
|
||||
alert.frequency = alert.frequency || '60s';
|
||||
alert.frequency = alert.frequency || '1m';
|
||||
alert.handler = alert.handler || 1;
|
||||
alert.notifications = alert.notifications || [];
|
||||
|
||||
@ -217,7 +217,7 @@ export class AlertTabCtrl {
|
||||
buildDefaultCondition() {
|
||||
return {
|
||||
type: 'query',
|
||||
query: { params: ['A', '5m', 'now'] },
|
||||
query: { params: ['A', '15m', 'now'] },
|
||||
reducer: { type: 'avg', params: [] },
|
||||
evaluator: { type: 'gt', params: [null] },
|
||||
operator: { type: 'and' },
|
||||
|
@ -21,6 +21,7 @@ export interface Props {
|
||||
|
||||
export interface State {
|
||||
refreshCounter: number;
|
||||
renderCounter: number;
|
||||
timeRange?: TimeRange;
|
||||
}
|
||||
|
||||
@ -30,11 +31,13 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
|
||||
this.state = {
|
||||
refreshCounter: 0,
|
||||
renderCounter: 0,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.panel.events.on('refresh', this.onRefresh);
|
||||
this.props.panel.events.on('render', this.onRender);
|
||||
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() {
|
||||
return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
|
||||
}
|
||||
@ -59,9 +69,11 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
render() {
|
||||
const { panel, dashboard } = this.props;
|
||||
const { datasource, targets } = panel;
|
||||
const { refreshCounter, timeRange } = this.state;
|
||||
const { timeRange, renderCounter, refreshCounter } = this.state;
|
||||
const PanelComponent = this.props.component;
|
||||
|
||||
console.log('Panel chrome render');
|
||||
|
||||
return (
|
||||
<div className="panel-container">
|
||||
<PanelHeader panel={panel} dashboard={dashboard} />
|
||||
@ -74,7 +86,16 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
refreshCounter={refreshCounter}
|
||||
>
|
||||
{({ 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>
|
||||
</div>
|
||||
|
@ -1,12 +1,15 @@
|
||||
import React from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
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 { DashboardModel } from '../dashboard_model';
|
||||
import { store } from 'app/store/store';
|
||||
import { QueriesTab } from './QueriesTab';
|
||||
import { PanelPlugin, PluginExports } from 'app/types/plugins';
|
||||
import { VizTypePicker } from './VizTypePicker';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
|
||||
interface PanelEditorProps {
|
||||
panel: PanelModel;
|
||||
@ -22,7 +25,7 @@ interface PanelEditorTab {
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export class PanelEditor extends React.Component<PanelEditorProps, any> {
|
||||
export class PanelEditor extends PureComponent<PanelEditorProps> {
|
||||
tabs: PanelEditorTab[];
|
||||
|
||||
constructor(props) {
|
||||
@ -39,16 +42,21 @@ export class PanelEditor extends React.Component<PanelEditorProps, any> {
|
||||
}
|
||||
|
||||
renderPanelOptions() {
|
||||
const { pluginExports } = this.props;
|
||||
const { pluginExports, panel } = this.props;
|
||||
|
||||
if (pluginExports.PanelOptions) {
|
||||
const PanelOptions = pluginExports.PanelOptions;
|
||||
return <PanelOptions />;
|
||||
if (pluginExports.PanelOptionsComponent) {
|
||||
const OptionsComponent = pluginExports.PanelOptionsComponent;
|
||||
return <OptionsComponent options={panel.getOptions()} onChange={this.onPanelOptionsChanged} />;
|
||||
} else {
|
||||
return <p>Visualization has no options</p>;
|
||||
}
|
||||
}
|
||||
|
||||
onPanelOptionsChanged = (options: any) => {
|
||||
this.props.panel.updateOptions(options);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
renderVizTab() {
|
||||
return (
|
||||
<div className="viz-editor">
|
||||
@ -70,6 +78,7 @@ export class PanelEditor extends React.Component<PanelEditorProps, any> {
|
||||
partial: true,
|
||||
})
|
||||
);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -60,6 +60,21 @@ export class PanelModel {
|
||||
_.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() {
|
||||
const model: any = {};
|
||||
for (const property in this) {
|
||||
@ -121,10 +136,6 @@ export class PanelModel {
|
||||
this.events.emit('panel-initialized');
|
||||
}
|
||||
|
||||
initEditMode() {
|
||||
this.events.emit('panel-init-edit-mode');
|
||||
}
|
||||
|
||||
changeType(pluginId: string) {
|
||||
this.type = pluginId;
|
||||
|
||||
|
@ -32,9 +32,9 @@ export class SettingsCtrl {
|
||||
|
||||
this.$scope.$on('$destroy', () => {
|
||||
this.dashboard.updateSubmenuVisibility();
|
||||
this.dashboard.startRefresh();
|
||||
setTimeout(() => {
|
||||
this.$rootScope.appEvent('dash-scroll', { restore: true });
|
||||
this.dashboard.startRefresh();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
||||
</div>
|
||||
</form>
|
||||
<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.
|
||||
<div class="p-t-1">
|
||||
<a href="login" class="btn btn-success p-t-1">
|
||||
|
@ -95,9 +95,9 @@ class LoggingQueryField extends React.PureComponent<LoggingQueryFieldProps, Logg
|
||||
this.languageProvider
|
||||
.start()
|
||||
.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
|
||||
.fetchLabelValues(targetOption.value)
|
||||
.then(this.onReceiveMetrics)
|
||||
.then(this.onUpdateLanguage)
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
@ -147,7 +147,7 @@ class LoggingQueryField extends React.PureComponent<LoggingQueryFieldProps, Logg
|
||||
}
|
||||
};
|
||||
|
||||
onReceiveMetrics = () => {
|
||||
onUpdateLanguage = () => {
|
||||
Prism.languages[PRISM_SYNTAX] = this.languageProvider.getSyntax();
|
||||
const { logLabelOptions } = this.languageProvider;
|
||||
this.setState({
|
||||
|
@ -47,7 +47,6 @@ export default class LoggingLanguageProvider extends LanguageProvider {
|
||||
this.datasource = datasource;
|
||||
this.labelKeys = {};
|
||||
this.labelValues = {};
|
||||
this.started = false;
|
||||
|
||||
Object.assign(this, initialValues);
|
||||
}
|
||||
@ -63,11 +62,10 @@ export default class LoggingLanguageProvider extends LanguageProvider {
|
||||
};
|
||||
|
||||
start = () => {
|
||||
if (!this.started) {
|
||||
this.started = true;
|
||||
return this.fetchLogLabels();
|
||||
if (!this.startTask) {
|
||||
this.startTask = this.fetchLogLabels();
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
return this.startTask;
|
||||
};
|
||||
|
||||
// Keep this DOM-free for testing
|
||||
|
@ -134,9 +134,9 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
this.languageProvider
|
||||
.start()
|
||||
.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;
|
||||
if (!metrics) {
|
||||
return;
|
||||
|
@ -46,7 +46,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
labelKeys?: { [index: string]: string[] }; // metric -> [labelKey,...]
|
||||
labelValues?: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...]
|
||||
metrics?: string[];
|
||||
started: boolean;
|
||||
startTask: Promise<any>;
|
||||
|
||||
constructor(datasource: any, initialValues?: any) {
|
||||
super();
|
||||
@ -56,7 +56,6 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
this.labelKeys = {};
|
||||
this.labelValues = {};
|
||||
this.metrics = [];
|
||||
this.started = false;
|
||||
|
||||
Object.assign(this, initialValues);
|
||||
}
|
||||
@ -72,11 +71,10 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
};
|
||||
|
||||
start = () => {
|
||||
if (!this.started) {
|
||||
this.started = true;
|
||||
return this.fetchMetricNames().then(() => [this.fetchHistogramMetrics()]);
|
||||
if (!this.startTask) {
|
||||
this.startTask = this.fetchMetricNames().then(() => [this.fetchHistogramMetrics()]);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
return this.startTask;
|
||||
};
|
||||
|
||||
// Keep this DOM-free for testing
|
||||
@ -156,7 +154,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
}
|
||||
|
||||
getAggregationCompletionItems({ value }: TypeaheadInput): TypeaheadOutput {
|
||||
let refresher: Promise<any> = null;
|
||||
const refresher: Promise<any> = null;
|
||||
const suggestions: CompletionItemGroup[] = [];
|
||||
|
||||
// Stitch all query lines together to support multi-line queries
|
||||
@ -172,12 +170,30 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
return text;
|
||||
}, '');
|
||||
|
||||
const leftSide = queryText.slice(0, queryOffset);
|
||||
const openParensAggregationIndex = leftSide.lastIndexOf('(');
|
||||
const openParensSelectorIndex = leftSide.slice(0, openParensAggregationIndex).lastIndexOf('(');
|
||||
const closeParensSelectorIndex = leftSide.slice(openParensSelectorIndex).indexOf(')') + openParensSelectorIndex;
|
||||
// Try search for selector part on the left-hand side, such as `sum (m) by (l)`
|
||||
const openParensAggregationIndex = queryText.lastIndexOf('(', queryOffset);
|
||||
let openParensSelectorIndex = queryText.lastIndexOf('(', openParensAggregationIndex - 1);
|
||||
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
|
||||
selectorString = selectorString.replace(/\[[^\]]+\]$/, '');
|
||||
@ -188,14 +204,10 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
if (labelKeys) {
|
||||
suggestions.push({ label: 'Labels', items: labelKeys.map(wrapLabel) });
|
||||
} else {
|
||||
refresher = this.fetchSeriesLabels(selector);
|
||||
result.refresher = this.fetchSeriesLabels(selector);
|
||||
}
|
||||
|
||||
return {
|
||||
refresher,
|
||||
suggestions,
|
||||
context: 'context-aggregation',
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
getLabelCompletionItems({ text, wrapperClasses, labelKey, value }: TypeaheadInput): TypeaheadOutput {
|
||||
|
@ -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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,22 +1,21 @@
|
||||
// Libraries
|
||||
import _ from 'lodash';
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Components
|
||||
import Graph from 'app/viz/Graph';
|
||||
import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
|
||||
import { Switch } from 'app/core/components/Switch/Switch';
|
||||
|
||||
// Types
|
||||
import { PanelProps, NullValueMode } from 'app/types';
|
||||
import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
|
||||
import { PanelProps, PanelOptionsProps, NullValueMode } from 'app/types';
|
||||
|
||||
interface Options {
|
||||
showBars: boolean;
|
||||
showLines: boolean;
|
||||
showPoints: boolean;
|
||||
|
||||
onChange: (options: Options) => void;
|
||||
}
|
||||
|
||||
interface Props extends PanelProps {
|
||||
options: Options;
|
||||
}
|
||||
interface Props extends PanelProps<Options> {}
|
||||
|
||||
export class Graph2 extends PureComponent<Props> {
|
||||
constructor(props) {
|
||||
@ -25,27 +24,52 @@ export class Graph2 extends PureComponent<Props> {
|
||||
|
||||
render() {
|
||||
const { timeSeries, timeRange } = this.props;
|
||||
const { showLines, showBars, showPoints } = this.props.options;
|
||||
|
||||
const vmSeries = getTimeSeriesVMs({
|
||||
timeSeries: timeSeries,
|
||||
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> {
|
||||
onChange = () => {};
|
||||
export class GraphOptions extends PureComponent<PanelOptionsProps<Options>> {
|
||||
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() {
|
||||
const { showBars, showPoints, showLines } = this.props.options;
|
||||
|
||||
return (
|
||||
<div className="section gf-form-group">
|
||||
<h5 className="section-heading">Draw Modes</h5>
|
||||
<Switch label="Lines" checked={true} onChange={this.onChange} />
|
||||
<div>
|
||||
<div className="section gf-form-group">
|
||||
<h5 className="page-heading">Draw Modes</h5>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { Graph2 as PanelComponent, TextOptions as PanelOptions };
|
||||
export { Graph2 as PanelComponent, GraphOptions as PanelOptionsComponent };
|
||||
|
@ -211,16 +211,17 @@ export class TableRenderer {
|
||||
value = this.formatColumnValue(columnIndex, value);
|
||||
|
||||
const column = this.table.columns[columnIndex];
|
||||
let style = '';
|
||||
let cellStyle = '';
|
||||
let textStyle = '';
|
||||
const cellClasses = [];
|
||||
let cellClass = '';
|
||||
|
||||
if (this.colorState.cell) {
|
||||
style = ' style="background-color:' + this.colorState.cell + '"';
|
||||
cellStyle = ' style="background-color:' + this.colorState.cell + '"';
|
||||
cellClasses.push('table-panel-color-cell');
|
||||
this.colorState.cell = null;
|
||||
} else if (this.colorState.value) {
|
||||
style = ' style="color:' + this.colorState.value + '"';
|
||||
textStyle = ' style="color:' + this.colorState.value + '"';
|
||||
this.colorState.value = null;
|
||||
}
|
||||
// because of the fixed table headers css only solution
|
||||
@ -232,7 +233,7 @@ export class TableRenderer {
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
style = ' style="display:none;"';
|
||||
cellStyle = ' style="display:none;"';
|
||||
column.hidden = true;
|
||||
} else {
|
||||
column.hidden = false;
|
||||
@ -258,7 +259,7 @@ export class TableRenderer {
|
||||
cellClasses.push('table-panel-cell-link');
|
||||
|
||||
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}
|
||||
</a>
|
||||
`;
|
||||
@ -283,7 +284,7 @@ export class TableRenderer {
|
||||
cellClass = ' class="' + cellClasses.join(' ') + '"';
|
||||
}
|
||||
|
||||
columnHtml = '<td' + cellClass + style + '>' + columnHtml + '</td>';
|
||||
columnHtml = '<td' + cellClass + cellStyle + textStyle + '>' + columnHtml + '</td>';
|
||||
return columnHtml;
|
||||
}
|
||||
|
||||
|
@ -86,10 +86,11 @@ export abstract class LanguageProvider {
|
||||
datasource: 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.
|
||||
*/
|
||||
start: () => Promise<any[]>;
|
||||
startTask?: Promise<any[]>;
|
||||
}
|
||||
|
||||
export interface TypeaheadInput {
|
||||
|
@ -20,7 +20,7 @@ import {
|
||||
DataQueryResponse,
|
||||
DataQueryOptions,
|
||||
} from './series';
|
||||
import { PanelProps } from './panel';
|
||||
import { PanelProps, PanelOptionsProps } from './panel';
|
||||
import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins';
|
||||
import { Organization, OrganizationPreferences, OrganizationState } from './organization';
|
||||
import {
|
||||
@ -69,6 +69,7 @@ export {
|
||||
TimeRange,
|
||||
LoadingState,
|
||||
PanelProps,
|
||||
PanelOptionsProps,
|
||||
TimeSeries,
|
||||
TimeSeriesVM,
|
||||
TimeSeriesVMs,
|
||||
|
@ -1,7 +1,14 @@
|
||||
import { LoadingState, TimeSeries, TimeRange } from './series';
|
||||
|
||||
export interface PanelProps {
|
||||
export interface PanelProps<T = any> {
|
||||
timeSeries: TimeSeries[];
|
||||
timeRange: TimeRange;
|
||||
loading: LoadingState;
|
||||
options: T;
|
||||
renderCounter: number;
|
||||
}
|
||||
|
||||
export interface PanelOptionsProps<T = any> {
|
||||
options: T;
|
||||
onChange: (options: T) => void;
|
||||
}
|
||||
|
@ -1,13 +1,18 @@
|
||||
import { ComponentClass } from 'react';
|
||||
import { PanelProps, PanelOptionsProps } from './panel';
|
||||
|
||||
export interface PluginExports {
|
||||
PanelCtrl?;
|
||||
PanelComponent?: any;
|
||||
Datasource?: any;
|
||||
QueryCtrl?: any;
|
||||
ConfigCtrl?: any;
|
||||
AnnotationsQueryCtrl?: any;
|
||||
PanelOptions?: any;
|
||||
ExploreQueryField?: any;
|
||||
ExploreStartPage?: any;
|
||||
|
||||
// Panel plugin
|
||||
PanelCtrl?;
|
||||
PanelComponent?: ComponentClass<PanelProps>;
|
||||
PanelOptionsComponent: ComponentClass<PanelOptionsProps>;
|
||||
}
|
||||
|
||||
export interface PanelPlugin {
|
||||
|
@ -8,6 +8,111 @@ import 'vendor/flot/jquery.flot.time';
|
||||
// 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
|
||||
function time_format(ticks, min, max) {
|
||||
if (min && max && ticks) {
|
||||
@ -34,91 +139,4 @@ function time_format(ticks, min, max) {
|
||||
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);
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
<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="mask-icon" href="public/img/grafana_mask_icon.svg" color="#F05A28">
|
@ -15,7 +15,7 @@
|
||||
<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="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-status-bar-style" content="black">
|
@ -22,10 +22,10 @@ echo "current dir: $(pwd)"
|
||||
|
||||
if [ "$CIRCLE_TAG" != "" ]; then
|
||||
echo "Building releases from tag $CIRCLE_TAG"
|
||||
OPT="-includeBuildNumber=false ${EXTRA_OPTS}"
|
||||
OPT="-includeBuildId=false ${EXTRA_OPTS}"
|
||||
else
|
||||
echo "Building incremental build for $CIRCLE_BRANCH"
|
||||
OPT="-buildNumber=${CIRCLE_BUILD_NUM} ${EXTRA_OPTS}"
|
||||
OPT="-buildId=${CIRCLE_WORKFLOW_ID} ${EXTRA_OPTS}"
|
||||
fi
|
||||
|
||||
echo "Build arguments: $OPT"
|
||||
|
@ -18,10 +18,10 @@ echo "current dir: $(pwd)"
|
||||
|
||||
if [ "$CIRCLE_TAG" != "" ]; then
|
||||
echo "Building releases from tag $CIRCLE_TAG"
|
||||
OPT="-includeBuildNumber=false ${EXTRA_OPTS}"
|
||||
OPT="-includeBuildId=false ${EXTRA_OPTS}"
|
||||
else
|
||||
echo "Building incremental build for $CIRCLE_BRANCH"
|
||||
OPT="-buildNumber=${CIRCLE_BUILD_NUM} ${EXTRA_OPTS}"
|
||||
OPT="-buildId=${CIRCLE_WORKFLOW_ID} ${EXTRA_OPTS}"
|
||||
fi
|
||||
|
||||
echo "Build arguments: $OPT"
|
||||
|
@ -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
|
@ -1,4 +1,4 @@
|
||||
#/bin/sh
|
||||
#!/bin/sh
|
||||
|
||||
# no relation to publish.go
|
||||
|
||||
|
62
scripts/build/release_publisher/externalrelease.go
Normal file
62
scripts/build/release_publisher/externalrelease.go
Normal 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
|
||||
}
|
91
scripts/build/release_publisher/localrelease.go
Normal file
91
scripts/build/release_publisher/localrelease.go
Normal 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.")
|
||||
}
|
@ -7,13 +7,14 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
var baseUri string = "https://grafana.com/api"
|
||||
|
||||
func main() {
|
||||
var version string
|
||||
var whatsNewUrl string
|
||||
var releaseNotesUrl string
|
||||
var dryRun bool
|
||||
var enterprise bool
|
||||
var fromLocal bool
|
||||
var nightly bool
|
||||
var apiKey string
|
||||
|
||||
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(&apiKey, "apikey", "", "Grafana.com API key (ex: --apikey ABCDEF)")
|
||||
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()
|
||||
|
||||
nightly = fromLocal
|
||||
|
||||
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("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("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 --enterprise")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
log.Println("Dry-run has been enabled.")
|
||||
}
|
||||
var baseUrl string
|
||||
var builder releaseBuilder
|
||||
var product string
|
||||
|
||||
p := publisher{apiKey: apiKey}
|
||||
if err := p.doRelease(version, whatsNewUrl, releaseNotesUrl, dryRun); err != nil {
|
||||
if fromLocal {
|
||||
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)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
@ -12,53 +12,47 @@ import (
|
||||
)
|
||||
|
||||
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 {
|
||||
currentRelease, err := newRelease(version, whatsNewUrl, releaseNotesUrl, buildArtifactConfigurations, getHttpContents{})
|
||||
type releaseBuilder interface {
|
||||
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 {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
if err := p.postRelease(currentRelease); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
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 {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@ -67,15 +61,13 @@ func (p *publisher) postRelease(r *release) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
const baseArhiveUrl = "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana"
|
||||
|
||||
type buildArtifact struct {
|
||||
os string
|
||||
arch string
|
||||
urlPostfix string
|
||||
}
|
||||
|
||||
func (t buildArtifact) getUrl(version string, isBeta bool) string {
|
||||
func (t buildArtifact) getUrl(baseArchiveUrl, version string, isBeta bool) string {
|
||||
prefix := "-"
|
||||
rhelReleaseExtra := ""
|
||||
|
||||
@ -87,7 +79,7 @@ func (t buildArtifact) getUrl(version string, isBeta bool) string {
|
||||
rhelReleaseExtra = "-1"
|
||||
}
|
||||
|
||||
url := strings.Join([]string{baseArhiveUrl, prefix, version, rhelReleaseExtra, t.urlPostfix}, "")
|
||||
url := strings.Join([]string{baseArchiveUrl, prefix, version, rhelReleaseExtra, t.urlPostfix}, "")
|
||||
return url
|
||||
}
|
||||
|
||||
@ -149,48 +141,32 @@ var buildArtifactConfigurations = []buildArtifact{
|
||||
},
|
||||
}
|
||||
|
||||
func newRelease(rawVersion string, whatsNewUrl string, releaseNotesUrl string, artifactConfigurations []buildArtifact, getter urlGetter) (*release, error) {
|
||||
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 {
|
||||
func newBuild(baseArchiveUrl string, ba buildArtifact, version string, isBeta bool, sha256 string) build {
|
||||
return build{
|
||||
Os: ba.os,
|
||||
Url: ba.getUrl(version, isBeta),
|
||||
Url: ba.getUrl(baseArchiveUrl, version, isBeta),
|
||||
Sha256: sha256,
|
||||
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 {
|
||||
jsonBytes, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@ -243,24 +219,3 @@ type build struct {
|
||||
Sha256 string `json:"sha256"`
|
||||
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
|
||||
}
|
||||
|
@ -2,16 +2,24 @@ package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNewRelease(t *testing.T) {
|
||||
func TestPreparingReleaseFromRemote(t *testing.T) {
|
||||
versionIn := "v5.2.0-beta1"
|
||||
expectedVersion := "5.2.0-beta1"
|
||||
whatsNewUrl := "https://whatsnews.foo/"
|
||||
relNotesUrl := "https://relnotes.foo/"
|
||||
expectedArch := "amd64"
|
||||
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 {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
0
scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz
vendored
Normal file
0
scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.linux-amd64.tar.gz
vendored
Normal file
@ -0,0 +1 @@
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
0
scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.windows-amd64.zip
vendored
Normal file
0
scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.windows-amd64.zip
vendored
Normal file
@ -0,0 +1 @@
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
0
scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.x86_64.rpm
vendored
Normal file
0
scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.x86_64.rpm
vendored
Normal file
1
scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.x86_64.rpm.sha256
vendored
Normal file
1
scripts/build/release_publisher/testdata/grafana-enterprise-5.4.0-123pre1.x86_64.rpm.sha256
vendored
Normal file
@ -0,0 +1 @@
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
0
scripts/build/release_publisher/testdata/grafana-enterprise_5.4.0-123pre1_amd64.deb
vendored
Normal file
0
scripts/build/release_publisher/testdata/grafana-enterprise_5.4.0-123pre1_amd64.deb
vendored
Normal file
1
scripts/build/release_publisher/testdata/grafana-enterprise_5.4.0-123pre1_amd64.deb.sha256
vendored
Normal file
1
scripts/build/release_publisher/testdata/grafana-enterprise_5.4.0-123pre1_amd64.deb.sha256
vendored
Normal file
@ -0,0 +1 @@
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
@ -47,7 +47,7 @@ module.exports = {
|
||||
},
|
||||
{
|
||||
test: /\.html$/,
|
||||
exclude: /index\.template.html/,
|
||||
exclude: /(index|error)\-template\.html/,
|
||||
use: [
|
||||
{ loader: 'ngtemplate-loader?relativeTo=' + (path.resolve(__dirname, '../../public')) + '&prefix=public' },
|
||||
{
|
||||
|
@ -80,11 +80,16 @@ module.exports = merge(common, {
|
||||
plugins: [
|
||||
new CleanWebpackPlugin('../../public/build', { allowExternal: true }),
|
||||
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({
|
||||
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',
|
||||
chunks: ['manifest', 'vendor', 'app'],
|
||||
}),
|
||||
|
@ -83,7 +83,7 @@ module.exports = merge(common, {
|
||||
new CleanWebpackPlugin('../public/build', { allowExternal: true }),
|
||||
new HtmlWebpackPlugin({
|
||||
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',
|
||||
alwaysWriteToDisk: true,
|
||||
}),
|
||||
|
@ -71,15 +71,20 @@ module.exports = merge(common, {
|
||||
|
||||
plugins: [
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "grafana.[name].css"
|
||||
filename: "grafana.[name].[hash].css"
|
||||
}),
|
||||
new ngAnnotatePlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
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',
|
||||
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 () {
|
||||
this.hooks.done.tap('Done', function (stats) {
|
||||
if (stats.compilation.errors && stats.compilation.errors.length) {
|
||||
|
Loading…
Reference in New Issue
Block a user