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

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

View File

@ -335,6 +335,9 @@ jobs:
- run:
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
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,7 +18,7 @@ Grafana v5.3 brings new features, many enhancements and bug fixes. This article
- [TV mode]({{< relref "#tv-and-kiosk-mode" >}}) is improved and more accessible
- [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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@ -8,11 +8,14 @@ import (
"github.com/grafana/grafana/pkg/bus"
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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -90,6 +90,7 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO,
validateAlertsCmd := models.ValidateDashboardAlertsCommand{
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 {

View File

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

View File

@ -327,6 +327,24 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
if dashboard.IsFolder {
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
})
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,22 +1,21 @@
// Libraries
import _ from 'lodash';
import 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 };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,13 +7,14 @@ import (
"os"
)
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)
}

View File

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

View File

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

View File

@ -0,0 +1 @@
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

View File

@ -0,0 +1 @@
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

View File

@ -0,0 +1 @@
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

View File

@ -0,0 +1 @@
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

View File

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

View File

@ -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'],
}),

View File

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

View File

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