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:
|
- run:
|
||||||
name: deploy to gcp
|
name: deploy to gcp
|
||||||
command: '/opt/google-cloud-sdk/bin/gsutil cp ./enterprise-dist/* gs://$GCP_BUCKET_NAME/enterprise/master'
|
command: '/opt/google-cloud-sdk/bin/gsutil cp ./enterprise-dist/* gs://$GCP_BUCKET_NAME/enterprise/master'
|
||||||
|
- run:
|
||||||
|
name: Deploy to grafana.com
|
||||||
|
command: 'cd enterprise-dist && ../scripts/build/release_publisher/release_publisher -apikey ${GRAFANA_COM_API_KEY} -enterprise -from-local'
|
||||||
|
|
||||||
|
|
||||||
deploy-enterprise-release:
|
deploy-enterprise-release:
|
||||||
@ -403,7 +406,7 @@ jobs:
|
|||||||
command: '/opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file=/tmp/gcpkey.json'
|
command: '/opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file=/tmp/gcpkey.json'
|
||||||
- run:
|
- run:
|
||||||
name: deploy to gcp
|
name: deploy to gcp
|
||||||
command: '/opt/google-cloud-sdk/bin/gsutil cp ./dist/* gs://R/oss/release'
|
command: '/opt/google-cloud-sdk/bin/gsutil cp ./dist/* gs://$GCP_BUCKET_NAME/oss/release'
|
||||||
- run:
|
- run:
|
||||||
name: Deploy to Grafana.com
|
name: Deploy to Grafana.com
|
||||||
command: './scripts/build/publish.sh'
|
command: './scripts/build/publish.sh'
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,6 +8,7 @@ awsconfig
|
|||||||
/dist
|
/dist
|
||||||
/public/build
|
/public/build
|
||||||
/public/views/index.html
|
/public/views/index.html
|
||||||
|
/public/views/error.html
|
||||||
/emails/dist
|
/emails/dist
|
||||||
/public_gen
|
/public_gen
|
||||||
/public/vendor/npm
|
/public/vendor/npm
|
||||||
|
@ -12,11 +12,14 @@
|
|||||||
### Minor
|
### Minor
|
||||||
|
|
||||||
* **Cloudwatch**: Show all available CloudWatch regions [#12308](https://github.com/grafana/grafana/issues/12308), thx [@mtanda](https://github.com/mtanda)
|
* **Cloudwatch**: Show all available CloudWatch regions [#12308](https://github.com/grafana/grafana/issues/12308), thx [@mtanda](https://github.com/mtanda)
|
||||||
|
* **Cloudwatch**: AWS/Connect metrics and dimensions [#13970](https://github.com/grafana/grafana/pull/13970), thx [@zcoffy](https://github.com/zcoffy)
|
||||||
* **Postgres**: Add delta window function to postgres query builder [#13925](https://github.com/grafana/grafana/issues/13925), thx [svenklemm](https://github.com/svenklemm)
|
* **Postgres**: Add delta window function to postgres query builder [#13925](https://github.com/grafana/grafana/issues/13925), thx [svenklemm](https://github.com/svenklemm)
|
||||||
* **Units**: New clock time format, to format ms or second values as for example `01h:59m`, [#13635](https://github.com/grafana/grafana/issues/13635), thx [@franciscocpg](https://github.com/franciscocpg)
|
* **Units**: New clock time format, to format ms or second values as for example `01h:59m`, [#13635](https://github.com/grafana/grafana/issues/13635), thx [@franciscocpg](https://github.com/franciscocpg)
|
||||||
* **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu)
|
* **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu)
|
||||||
* **DingDing**: Can't receive DingDing alert when alert is triggered [#13723](https://github.com/grafana/grafana/issues/13723), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
|
* **DingDing**: Can't receive DingDing alert when alert is triggered [#13723](https://github.com/grafana/grafana/issues/13723), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
|
||||||
* **Internal metrics**: Renamed `grafana_info` to `grafana_build_info` and added branch, goversion and revision [#13876](https://github.com/grafana/grafana/pull/13876)
|
* **Internal metrics**: Renamed `grafana_info` to `grafana_build_info` and added branch, goversion and revision [#13876](https://github.com/grafana/grafana/pull/13876)
|
||||||
|
* **Alerting**: Increaste default duration for queries [#13945](https://github.com/grafana/grafana/pull/13945)
|
||||||
|
* **Table**: Fix CSS alpha background-color applied twice in table cell with link [#13606](https://github.com/grafana/grafana/issues/13606), thx [@grisme](https://github.com/grisme)
|
||||||
|
|
||||||
### Breaking changes
|
### Breaking changes
|
||||||
|
|
||||||
@ -24,7 +27,10 @@
|
|||||||
|
|
||||||
# 5.3.3 (unreleased)
|
# 5.3.3 (unreleased)
|
||||||
|
|
||||||
|
* **Alerting**: Delete alerts when parent folder was deleted [#13322](https://github.com/grafana/grafana/issues/13322)
|
||||||
* **MySQL**: Fix `$__timeFilter()` should respect local time zone [#13769](https://github.com/grafana/grafana/issues/13769)
|
* **MySQL**: Fix `$__timeFilter()` should respect local time zone [#13769](https://github.com/grafana/grafana/issues/13769)
|
||||||
|
* **Dashboard**: Fix datasource selection in panel by enter key [#13932](https://github.com/grafana/grafana/issues/13932)
|
||||||
|
* **Graph**: Fix table legend height when positioned below graph and using Internet Explorer 11 [#13903](https://github.com/grafana/grafana/issues/13903)
|
||||||
|
|
||||||
# 5.3.2 (2018-10-24)
|
# 5.3.2 (2018-10-24)
|
||||||
|
|
||||||
|
28
build.go
28
build.go
@ -41,8 +41,8 @@ var (
|
|||||||
race bool
|
race bool
|
||||||
phjsToRelease string
|
phjsToRelease string
|
||||||
workingDir string
|
workingDir string
|
||||||
includeBuildNumber bool = true
|
includeBuildId bool = true
|
||||||
buildNumber int = 0
|
buildId string = "0"
|
||||||
binaries []string = []string{"grafana-server", "grafana-cli"}
|
binaries []string = []string{"grafana-server", "grafana-cli"}
|
||||||
isDev bool = false
|
isDev bool = false
|
||||||
enterprise bool = false
|
enterprise bool = false
|
||||||
@ -54,6 +54,8 @@ func main() {
|
|||||||
|
|
||||||
ensureGoPath()
|
ensureGoPath()
|
||||||
|
|
||||||
|
var buildIdRaw string
|
||||||
|
|
||||||
flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH")
|
flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH")
|
||||||
flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS")
|
flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS")
|
||||||
flag.StringVar(&gocc, "cc", "", "CC")
|
flag.StringVar(&gocc, "cc", "", "CC")
|
||||||
@ -61,12 +63,14 @@ func main() {
|
|||||||
flag.StringVar(&pkgArch, "pkg-arch", "", "PKG ARCH")
|
flag.StringVar(&pkgArch, "pkg-arch", "", "PKG ARCH")
|
||||||
flag.StringVar(&phjsToRelease, "phjs", "", "PhantomJS binary")
|
flag.StringVar(&phjsToRelease, "phjs", "", "PhantomJS binary")
|
||||||
flag.BoolVar(&race, "race", race, "Use race detector")
|
flag.BoolVar(&race, "race", race, "Use race detector")
|
||||||
flag.BoolVar(&includeBuildNumber, "includeBuildNumber", includeBuildNumber, "IncludeBuildNumber in package name")
|
flag.BoolVar(&includeBuildId, "includeBuildId", includeBuildId, "IncludeBuildId in package name")
|
||||||
flag.BoolVar(&enterprise, "enterprise", enterprise, "Build enterprise version of Grafana")
|
flag.BoolVar(&enterprise, "enterprise", enterprise, "Build enterprise version of Grafana")
|
||||||
flag.IntVar(&buildNumber, "buildNumber", 0, "Build number from CI system")
|
flag.StringVar(&buildIdRaw, "buildId", "0", "Build ID from CI system")
|
||||||
flag.BoolVar(&isDev, "dev", isDev, "optimal for development, skips certain steps")
|
flag.BoolVar(&isDev, "dev", isDev, "optimal for development, skips certain steps")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
buildId = shortenBuildId(buildIdRaw)
|
||||||
|
|
||||||
readVersionFromPackageJson()
|
readVersionFromPackageJson()
|
||||||
|
|
||||||
if pkgArch == "" {
|
if pkgArch == "" {
|
||||||
@ -197,9 +201,9 @@ func readVersionFromPackageJson() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// add timestamp to iteration
|
// add timestamp to iteration
|
||||||
if includeBuildNumber {
|
if includeBuildId {
|
||||||
if buildNumber != 0 {
|
if buildId != "0" {
|
||||||
linuxPackageIteration = fmt.Sprintf("%d%s", buildNumber, linuxPackageIteration)
|
linuxPackageIteration = fmt.Sprintf("%s%s", buildId, linuxPackageIteration)
|
||||||
} else {
|
} else {
|
||||||
linuxPackageIteration = fmt.Sprintf("%d%s", time.Now().Unix(), linuxPackageIteration)
|
linuxPackageIteration = fmt.Sprintf("%d%s", time.Now().Unix(), linuxPackageIteration)
|
||||||
}
|
}
|
||||||
@ -392,7 +396,7 @@ func grunt(params ...string) {
|
|||||||
|
|
||||||
func gruntBuildArg(task string) []string {
|
func gruntBuildArg(task string) []string {
|
||||||
args := []string{task}
|
args := []string{task}
|
||||||
if includeBuildNumber {
|
if includeBuildId {
|
||||||
args = append(args, fmt.Sprintf("--pkgVer=%v-%v", linuxPackageVersion, linuxPackageIteration))
|
args = append(args, fmt.Sprintf("--pkgVer=%v-%v", linuxPackageVersion, linuxPackageIteration))
|
||||||
} else {
|
} else {
|
||||||
args = append(args, fmt.Sprintf("--pkgVer=%v", version))
|
args = append(args, fmt.Sprintf("--pkgVer=%v", version))
|
||||||
@ -632,3 +636,11 @@ func shaFile(file string) error {
|
|||||||
|
|
||||||
return out.Close()
|
return out.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shortenBuildId(buildId string) string {
|
||||||
|
buildId = strings.Replace(buildId, "-", "", -1)
|
||||||
|
if (len(buildId) < 9) {
|
||||||
|
return buildId
|
||||||
|
}
|
||||||
|
return buildId[0:8]
|
||||||
|
}
|
||||||
|
@ -404,6 +404,112 @@
|
|||||||
"title": "Column style thresholds & units",
|
"title": "Column style thresholds & units",
|
||||||
"transform": "timeseries_to_columns",
|
"transform": "timeseries_to_columns",
|
||||||
"type": "table"
|
"type": "table"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": [],
|
||||||
|
"datasource": "gdev-testdata",
|
||||||
|
"fontSize": "100%",
|
||||||
|
"gridPos": {
|
||||||
|
"h": 10,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 26
|
||||||
|
},
|
||||||
|
"id": 6,
|
||||||
|
"links": [],
|
||||||
|
"pageSize": 20,
|
||||||
|
"scroll": true,
|
||||||
|
"showHeader": true,
|
||||||
|
"sort": {
|
||||||
|
"col": 0,
|
||||||
|
"desc": true
|
||||||
|
},
|
||||||
|
"styles": [
|
||||||
|
{
|
||||||
|
"alias": "Time",
|
||||||
|
"dateFormat": "YYYY-MM-DD HH:mm:ss",
|
||||||
|
"pattern": "Time",
|
||||||
|
"type": "date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "",
|
||||||
|
"colorMode": "cell",
|
||||||
|
"colors": [
|
||||||
|
"rgba(245, 54, 54, 0.5)",
|
||||||
|
"rgba(237, 129, 40, 0.5)",
|
||||||
|
"rgba(50, 172, 45, 0.5)"
|
||||||
|
],
|
||||||
|
"dateFormat": "YYYY-MM-DD HH:mm:ss",
|
||||||
|
"decimals": 2,
|
||||||
|
"link": true,
|
||||||
|
"linkTargetBlank": true,
|
||||||
|
"linkTooltip": "",
|
||||||
|
"linkUrl": "http://www.grafana.com",
|
||||||
|
"mappingType": 1,
|
||||||
|
"pattern": "ColorCell",
|
||||||
|
"thresholds": [
|
||||||
|
"5",
|
||||||
|
"10"
|
||||||
|
],
|
||||||
|
"type": "number",
|
||||||
|
"unit": "currencyUSD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "",
|
||||||
|
"colorMode": "value",
|
||||||
|
"colors": [
|
||||||
|
"rgba(245, 54, 54, 0.5)",
|
||||||
|
"rgba(237, 129, 40, 0.5)",
|
||||||
|
"rgba(50, 172, 45, 0.5)"
|
||||||
|
],
|
||||||
|
"dateFormat": "YYYY-MM-DD HH:mm:ss",
|
||||||
|
"decimals": 2,
|
||||||
|
"link": true,
|
||||||
|
"linkUrl": "http://www.grafana.com",
|
||||||
|
"mappingType": 1,
|
||||||
|
"pattern": "ColorValue",
|
||||||
|
"thresholds": [
|
||||||
|
"5",
|
||||||
|
"10"
|
||||||
|
],
|
||||||
|
"type": "number",
|
||||||
|
"unit": "Bps"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "",
|
||||||
|
"colorMode": null,
|
||||||
|
"colors": [
|
||||||
|
"rgba(245, 54, 54, 0.9)",
|
||||||
|
"rgba(237, 129, 40, 0.89)",
|
||||||
|
"rgba(50, 172, 45, 0.97)"
|
||||||
|
],
|
||||||
|
"decimals": 2,
|
||||||
|
"pattern": "/.*/",
|
||||||
|
"thresholds": [],
|
||||||
|
"type": "number",
|
||||||
|
"unit": "short"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"alias": "ColorValue",
|
||||||
|
"expr": "",
|
||||||
|
"format": "table",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"refId": "A",
|
||||||
|
"scenarioId": "csv_metric_values",
|
||||||
|
"stringInput": "null,1,20,90,30,5,0,20,10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "ColorCell",
|
||||||
|
"refId": "B",
|
||||||
|
"scenarioId": "csv_metric_values",
|
||||||
|
"stringInput": "null,5,1,2,3,4,5,10,20"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Column style thresholds and links",
|
||||||
|
"transform": "timeseries_to_columns",
|
||||||
|
"type": "table"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"refresh": false,
|
"refresh": false,
|
||||||
@ -449,5 +555,5 @@
|
|||||||
"timezone": "browser",
|
"timezone": "browser",
|
||||||
"title": "Panel Tests - Table",
|
"title": "Panel Tests - Table",
|
||||||
"uid": "pttable",
|
"uid": "pttable",
|
||||||
"version": 1
|
"version": 2
|
||||||
}
|
}
|
@ -60,7 +60,8 @@ Here is a minimal policy example:
|
|||||||
"Effect": "Allow",
|
"Effect": "Allow",
|
||||||
"Action": [
|
"Action": [
|
||||||
"cloudwatch:ListMetrics",
|
"cloudwatch:ListMetrics",
|
||||||
"cloudwatch:GetMetricStatistics"
|
"cloudwatch:GetMetricStatistics",
|
||||||
|
"cloudwatch:GetMetricData"
|
||||||
],
|
],
|
||||||
"Resource": "*"
|
"Resource": "*"
|
||||||
},
|
},
|
||||||
|
@ -18,7 +18,7 @@ Grafana v5.3 brings new features, many enhancements and bug fixes. This article
|
|||||||
- [TV mode]({{< relref "#tv-and-kiosk-mode" >}}) is improved and more accessible
|
- [TV mode]({{< relref "#tv-and-kiosk-mode" >}}) is improved and more accessible
|
||||||
- [Alerting]({{< relref "#notification-reminders" >}}) with notification reminders
|
- [Alerting]({{< relref "#notification-reminders" >}}) with notification reminders
|
||||||
- [Postgres]({{< relref "#postgres-query-builder" >}}) gets a new query builder!
|
- [Postgres]({{< relref "#postgres-query-builder" >}}) gets a new query builder!
|
||||||
- [OAuth]({{< relref "#improved-oauth-support-for-gitlab" >}}) support for Gitlab is improved
|
- [OAuth]({{< relref "#improved-oauth-support-for-gitlab" >}}) support for GitLab is improved
|
||||||
- [Annotations]({{< relref "#annotations" >}}) with template variable filtering
|
- [Annotations]({{< relref "#annotations" >}}) with template variable filtering
|
||||||
- [Variables]({{< relref "#variables" >}}) with free text support
|
- [Variables]({{< relref "#variables" >}}) with free text support
|
||||||
|
|
||||||
@ -69,9 +69,9 @@ Grafana 5.3 comes with a new graphical query builder for Postgres. This brings P
|
|||||||
|
|
||||||
{{< docs-imagebox img="/img/docs/v53/postgres_query_still.png" class="docs-image--no-shadow" animated-gif="/img/docs/v53/postgres_query.gif" >}}
|
{{< docs-imagebox img="/img/docs/v53/postgres_query_still.png" class="docs-image--no-shadow" animated-gif="/img/docs/v53/postgres_query.gif" >}}
|
||||||
|
|
||||||
## Improved OAuth Support for Gitlab
|
## Improved OAuth Support for GitLab
|
||||||
|
|
||||||
Grafana 5.3 comes with a new OAuth integration for Gitlab that enables configuration to only allow users that are a member of certain Gitlab groups to authenticate. This makes it possible to use Gitlab OAuth with Grafana in a shared environment without giving everyone access to Grafana.
|
Grafana 5.3 comes with a new OAuth integration for GitLab that enables configuration to only allow users that are a member of certain GitLab groups to authenticate. This makes it possible to use GitLab OAuth with Grafana in a shared environment without giving everyone access to Grafana.
|
||||||
Learn how to enable and configure it in the [documentation](/auth/gitlab/).
|
Learn how to enable and configure it in the [documentation](/auth/gitlab/).
|
||||||
|
|
||||||
## Annotations
|
## Annotations
|
||||||
|
@ -290,7 +290,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
|||||||
"sendReminder": true,
|
"sendReminder": true,
|
||||||
"frequency": "15m",
|
"frequency": "15m",
|
||||||
"settings": {
|
"settings": {
|
||||||
"addresses: "carl@grafana.com;dev@grafana.com"
|
"addresses": "carl@grafana.com;dev@grafana.com"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -134,12 +134,16 @@ func AlertTest(c *m.ReqContext, dto dtos.AlertTestCommand) Response {
|
|||||||
OrgId: c.OrgId,
|
OrgId: c.OrgId,
|
||||||
Dashboard: dto.Dashboard,
|
Dashboard: dto.Dashboard,
|
||||||
PanelId: dto.PanelId,
|
PanelId: dto.PanelId,
|
||||||
|
User: c.SignedInUser,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bus.Dispatch(&backendCmd); err != nil {
|
if err := bus.Dispatch(&backendCmd); err != nil {
|
||||||
if validationErr, ok := err.(alerting.ValidationError); ok {
|
if validationErr, ok := err.(alerting.ValidationError); ok {
|
||||||
return Error(422, validationErr.Error(), nil)
|
return Error(422, validationErr.Error(), nil)
|
||||||
}
|
}
|
||||||
|
if err == m.ErrDataSourceAccessDenied {
|
||||||
|
return Error(403, "Access denied to datasource", err)
|
||||||
|
}
|
||||||
return Error(500, "Failed to test rule", err)
|
return Error(500, "Failed to test rule", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,62 +1,22 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/pluginproxy"
|
"github.com/grafana/grafana/pkg/api/pluginproxy"
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
|
||||||
"github.com/grafana/grafana/pkg/metrics"
|
"github.com/grafana/grafana/pkg/metrics"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
const HeaderNameNoBackendCache = "X-Grafana-NoCache"
|
|
||||||
|
|
||||||
func (hs *HTTPServer) getDatasourceFromCache(id int64, c *m.ReqContext) (*m.DataSource, error) {
|
|
||||||
userPermissionsQuery := m.GetDataSourcePermissionsForUserQuery{
|
|
||||||
User: c.SignedInUser,
|
|
||||||
}
|
|
||||||
if err := bus.Dispatch(&userPermissionsQuery); err != nil {
|
|
||||||
if err != bus.ErrHandlerNotFound {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
permissionType, exists := userPermissionsQuery.Result[id]
|
|
||||||
if exists && permissionType != m.DsPermissionQuery {
|
|
||||||
return nil, errors.New("User not allowed to access datasource")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true"
|
|
||||||
cacheKey := fmt.Sprintf("ds-%d", id)
|
|
||||||
|
|
||||||
if !nocache {
|
|
||||||
if cached, found := hs.cache.Get(cacheKey); found {
|
|
||||||
ds := cached.(*m.DataSource)
|
|
||||||
if ds.OrgId == c.OrgId {
|
|
||||||
return ds, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
query := m.GetDataSourceByIdQuery{Id: id, OrgId: c.OrgId}
|
|
||||||
if err := bus.Dispatch(&query); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
hs.cache.Set(cacheKey, query.Result, time.Second*5)
|
|
||||||
return query.Result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) {
|
func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) {
|
||||||
c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer)
|
c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer)
|
||||||
|
|
||||||
dsId := c.ParamsInt64(":id")
|
dsId := c.ParamsInt64(":id")
|
||||||
ds, err := hs.getDatasourceFromCache(dsId, c)
|
ds, err := hs.DatasourceCache.GetDatasource(dsId, c.SignedInUser, c.SkipCache)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if err == m.ErrDataSourceAccessDenied {
|
||||||
|
c.JsonApiErr(403, "Access denied to datasource", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
c.JsonApiErr(500, "Unable to load datasource meta data", err)
|
c.JsonApiErr(500, "Unable to load datasource meta data", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,6 @@ import (
|
|||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
|
||||||
gocache "github.com/patrickmn/go-cache"
|
|
||||||
macaron "gopkg.in/macaron.v1"
|
macaron "gopkg.in/macaron.v1"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/live"
|
"github.com/grafana/grafana/pkg/api/live"
|
||||||
@ -28,6 +27,8 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/registry"
|
"github.com/grafana/grafana/pkg/registry"
|
||||||
|
"github.com/grafana/grafana/pkg/services/cache"
|
||||||
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
"github.com/grafana/grafana/pkg/services/hooks"
|
"github.com/grafana/grafana/pkg/services/hooks"
|
||||||
"github.com/grafana/grafana/pkg/services/rendering"
|
"github.com/grafana/grafana/pkg/services/rendering"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
@ -46,7 +47,6 @@ type HTTPServer struct {
|
|||||||
macaron *macaron.Macaron
|
macaron *macaron.Macaron
|
||||||
context context.Context
|
context context.Context
|
||||||
streamManager *live.StreamManager
|
streamManager *live.StreamManager
|
||||||
cache *gocache.Cache
|
|
||||||
httpSrv *http.Server
|
httpSrv *http.Server
|
||||||
|
|
||||||
RouteRegister routing.RouteRegister `inject:""`
|
RouteRegister routing.RouteRegister `inject:""`
|
||||||
@ -54,11 +54,12 @@ type HTTPServer struct {
|
|||||||
RenderService rendering.Service `inject:""`
|
RenderService rendering.Service `inject:""`
|
||||||
Cfg *setting.Cfg `inject:""`
|
Cfg *setting.Cfg `inject:""`
|
||||||
HooksService *hooks.HooksService `inject:""`
|
HooksService *hooks.HooksService `inject:""`
|
||||||
|
CacheService *cache.CacheService `inject:""`
|
||||||
|
DatasourceCache datasources.CacheService `inject:""`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hs *HTTPServer) Init() error {
|
func (hs *HTTPServer) Init() error {
|
||||||
hs.log = log.New("http.server")
|
hs.log = log.New("http.server")
|
||||||
hs.cache = gocache.New(5*time.Minute, 10*time.Minute)
|
|
||||||
|
|
||||||
hs.streamManager = live.NewStreamManager()
|
hs.streamManager = live.NewStreamManager()
|
||||||
hs.macaron = hs.newMacaron()
|
hs.macaron = hs.newMacaron()
|
||||||
@ -231,6 +232,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
|
|||||||
m.Use(middleware.ValidateHostHeader(setting.Domain))
|
m.Use(middleware.ValidateHostHeader(setting.Domain))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.Use(middleware.HandleNoCacheHeader())
|
||||||
m.Use(middleware.AddDefaultResponseHeaders())
|
m.Use(middleware.AddDefaultResponseHeaders())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,8 +25,11 @@ func (hs *HTTPServer) QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) R
|
|||||||
return Error(400, "Query missing datasourceId", nil)
|
return Error(400, "Query missing datasourceId", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
ds, err := hs.getDatasourceFromCache(datasourceId, c)
|
ds, err := hs.DatasourceCache.GetDatasource(datasourceId, c.SignedInUser, c.SkipCache)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if err == m.ErrDataSourceAccessDenied {
|
||||||
|
return Error(403, "Access denied to datasource", err)
|
||||||
|
}
|
||||||
return Error(500, "Unable to load datasource meta data", err)
|
return Error(500, "Unable to load datasource meta data", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,13 +15,21 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/api"
|
"github.com/grafana/grafana/pkg/api"
|
||||||
"github.com/grafana/grafana/pkg/api/routing"
|
"github.com/grafana/grafana/pkg/api/routing"
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
_ "github.com/grafana/grafana/pkg/extensions"
|
|
||||||
"github.com/grafana/grafana/pkg/log"
|
|
||||||
"github.com/grafana/grafana/pkg/login"
|
"github.com/grafana/grafana/pkg/login"
|
||||||
_ "github.com/grafana/grafana/pkg/metrics"
|
|
||||||
"github.com/grafana/grafana/pkg/middleware"
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
_ "github.com/grafana/grafana/pkg/plugins"
|
|
||||||
"github.com/grafana/grafana/pkg/registry"
|
"github.com/grafana/grafana/pkg/registry"
|
||||||
|
"github.com/grafana/grafana/pkg/social"
|
||||||
|
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/log"
|
||||||
|
"github.com/grafana/grafana/pkg/services/cache"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
|
||||||
|
// self registering services
|
||||||
|
_ "github.com/grafana/grafana/pkg/extensions"
|
||||||
|
_ "github.com/grafana/grafana/pkg/metrics"
|
||||||
|
_ "github.com/grafana/grafana/pkg/plugins"
|
||||||
_ "github.com/grafana/grafana/pkg/services/alerting"
|
_ "github.com/grafana/grafana/pkg/services/alerting"
|
||||||
_ "github.com/grafana/grafana/pkg/services/cleanup"
|
_ "github.com/grafana/grafana/pkg/services/cleanup"
|
||||||
_ "github.com/grafana/grafana/pkg/services/notifications"
|
_ "github.com/grafana/grafana/pkg/services/notifications"
|
||||||
@ -29,10 +37,7 @@ import (
|
|||||||
_ "github.com/grafana/grafana/pkg/services/rendering"
|
_ "github.com/grafana/grafana/pkg/services/rendering"
|
||||||
_ "github.com/grafana/grafana/pkg/services/search"
|
_ "github.com/grafana/grafana/pkg/services/search"
|
||||||
_ "github.com/grafana/grafana/pkg/services/sqlstore"
|
_ "github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
|
||||||
"github.com/grafana/grafana/pkg/social" // self registering services
|
|
||||||
_ "github.com/grafana/grafana/pkg/tracing"
|
_ "github.com/grafana/grafana/pkg/tracing"
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewGrafanaServer() *GrafanaServerImpl {
|
func NewGrafanaServer() *GrafanaServerImpl {
|
||||||
@ -72,6 +77,7 @@ func (g *GrafanaServerImpl) Run() error {
|
|||||||
serviceGraph.Provide(&inject.Object{Value: bus.GetBus()})
|
serviceGraph.Provide(&inject.Object{Value: bus.GetBus()})
|
||||||
serviceGraph.Provide(&inject.Object{Value: g.cfg})
|
serviceGraph.Provide(&inject.Object{Value: g.cfg})
|
||||||
serviceGraph.Provide(&inject.Object{Value: routing.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)})
|
serviceGraph.Provide(&inject.Object{Value: routing.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)})
|
||||||
|
serviceGraph.Provide(&inject.Object{Value: cache.New(5*time.Minute, 10*time.Minute)})
|
||||||
|
|
||||||
// self registered services
|
// self registered services
|
||||||
services := registry.GetServices()
|
services := registry.GetServices()
|
||||||
@ -138,7 +144,6 @@ func (g *GrafanaServerImpl) Run() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendSystemdNotification("READY=1")
|
sendSystemdNotification("READY=1")
|
||||||
|
|
||||||
return g.childRoutines.Wait()
|
return g.childRoutines.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package login
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
)
|
)
|
||||||
|
14
pkg/middleware/headers.go
Normal file
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(),
|
Session: session.GetSession(),
|
||||||
IsSignedIn: false,
|
IsSignedIn: false,
|
||||||
AllowAnonymous: false,
|
AllowAnonymous: false,
|
||||||
|
SkipCache: false,
|
||||||
Logger: log.New("context"),
|
Logger: log.New("context"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestMiddlewareContext(t *testing.T) {
|
func TestMiddlewareContext(t *testing.T) {
|
||||||
|
setting.ERR_TEMPLATE_NAME = "error-template"
|
||||||
|
|
||||||
Convey("Given the grafana middleware", t, func() {
|
Convey("Given the grafana middleware", t, func() {
|
||||||
middlewareScenario("middleware should add context to injector", func(sc *scenarioContext) {
|
middlewareScenario("middleware should add context to injector", func(sc *scenarioContext) {
|
||||||
|
@ -138,7 +138,7 @@ func Recovery() macaron.Handler {
|
|||||||
|
|
||||||
c.JSON(500, resp)
|
c.JSON(500, resp)
|
||||||
} else {
|
} else {
|
||||||
c.HTML(500, "error")
|
c.HTML(500, setting.ERR_TEMPLATE_NAME)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -8,11 +8,14 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/session"
|
"github.com/grafana/grafana/pkg/services/session"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
"gopkg.in/macaron.v1"
|
"gopkg.in/macaron.v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRecoveryMiddleware(t *testing.T) {
|
func TestRecoveryMiddleware(t *testing.T) {
|
||||||
|
setting.ERR_TEMPLATE_NAME = "error-template"
|
||||||
|
|
||||||
Convey("Given an api route that panics", t, func() {
|
Convey("Given an api route that panics", t, func() {
|
||||||
apiURL := "/api/whatever"
|
apiURL := "/api/whatever"
|
||||||
recoveryScenario("recovery middleware should return json", apiURL, func(sc *scenarioContext) {
|
recoveryScenario("recovery middleware should return json", apiURL, func(sc *scenarioContext) {
|
||||||
@ -50,6 +53,7 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) {
|
|||||||
sc := &scenarioContext{
|
sc := &scenarioContext{
|
||||||
url: url,
|
url: url,
|
||||||
}
|
}
|
||||||
|
|
||||||
viewsPath, _ := filepath.Abs("../../public/views")
|
viewsPath, _ := filepath.Abs("../../public/views")
|
||||||
|
|
||||||
sc.m = macaron.New()
|
sc.m = macaron.New()
|
||||||
|
@ -215,13 +215,14 @@ type AlertStateInfoDTO struct {
|
|||||||
// "Internal" commands
|
// "Internal" commands
|
||||||
|
|
||||||
type UpdateDashboardAlertsCommand struct {
|
type UpdateDashboardAlertsCommand struct {
|
||||||
UserId int64
|
|
||||||
OrgId int64
|
OrgId int64
|
||||||
Dashboard *Dashboard
|
Dashboard *Dashboard
|
||||||
|
User *SignedInUser
|
||||||
}
|
}
|
||||||
|
|
||||||
type ValidateDashboardAlertsCommand struct {
|
type ValidateDashboardAlertsCommand struct {
|
||||||
UserId int64
|
UserId int64
|
||||||
OrgId int64
|
OrgId int64
|
||||||
Dashboard *Dashboard
|
Dashboard *Dashboard
|
||||||
|
User *SignedInUser
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ type ReqContext struct {
|
|||||||
IsSignedIn bool
|
IsSignedIn bool
|
||||||
IsRenderCall bool
|
IsRenderCall bool
|
||||||
AllowAnonymous bool
|
AllowAnonymous bool
|
||||||
|
SkipCache bool
|
||||||
Logger log.Logger
|
Logger log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,7 +37,7 @@ func (ctx *ReqContext) Handle(status int, title string, err error) {
|
|||||||
ctx.Data["AppSubUrl"] = setting.AppSubUrl
|
ctx.Data["AppSubUrl"] = setting.AppSubUrl
|
||||||
ctx.Data["Theme"] = "dark"
|
ctx.Data["Theme"] = "dark"
|
||||||
|
|
||||||
ctx.HTML(status, "error")
|
ctx.HTML(status, setting.ERR_TEMPLATE_NAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *ReqContext) JsonOK(message string) {
|
func (ctx *ReqContext) JsonOK(message string) {
|
||||||
|
@ -207,11 +207,6 @@ func (p DsPermissionType) String() string {
|
|||||||
return names[int(p)]
|
return names[int(p)]
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetDataSourcePermissionsForUserQuery struct {
|
|
||||||
User *SignedInUser
|
|
||||||
Result map[int64]DsPermissionType
|
|
||||||
}
|
|
||||||
|
|
||||||
type DatasourcesPermissionFilterQuery struct {
|
type DatasourcesPermissionFilterQuery struct {
|
||||||
User *SignedInUser
|
User *SignedInUser
|
||||||
Datasources []*DataSource
|
Datasources []*DataSource
|
||||||
|
@ -165,6 +165,7 @@ type SignedInUser struct {
|
|||||||
IsAnonymous bool
|
IsAnonymous bool
|
||||||
HelpFlags1 HelpFlags1
|
HelpFlags1 HelpFlags1
|
||||||
LastSeenAt time.Time
|
LastSeenAt time.Time
|
||||||
|
Teams []int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *SignedInUser) ShouldUpdateLastSeenAt() bool {
|
func (u *SignedInUser) ShouldUpdateLastSeenAt() bool {
|
||||||
|
@ -29,11 +29,42 @@ func Register(descriptor *Descriptor) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetServices() []*Descriptor {
|
func GetServices() []*Descriptor {
|
||||||
sort.Slice(services, func(i, j int) bool {
|
slice := getServicesWithOverrides()
|
||||||
return services[i].InitPriority > services[j].InitPriority
|
|
||||||
|
sort.Slice(slice, func(i, j int) bool {
|
||||||
|
return slice[i].InitPriority > slice[j].InitPriority
|
||||||
})
|
})
|
||||||
|
|
||||||
return services
|
return slice
|
||||||
|
}
|
||||||
|
|
||||||
|
type OverrideServiceFunc func(descriptor Descriptor) (*Descriptor, bool)
|
||||||
|
|
||||||
|
var overrides []OverrideServiceFunc
|
||||||
|
|
||||||
|
func RegisterOverride(fn OverrideServiceFunc) {
|
||||||
|
overrides = append(overrides, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getServicesWithOverrides() []*Descriptor {
|
||||||
|
slice := []*Descriptor{}
|
||||||
|
for _, s := range services {
|
||||||
|
var descriptor *Descriptor
|
||||||
|
for _, fn := range overrides {
|
||||||
|
if newDescriptor, override := fn(*s); override {
|
||||||
|
descriptor = newDescriptor
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if descriptor != nil {
|
||||||
|
slice = append(slice, descriptor)
|
||||||
|
} else {
|
||||||
|
slice = append(slice, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return slice
|
||||||
}
|
}
|
||||||
|
|
||||||
// Service interface is the lowest common shape that services
|
// Service interface is the lowest common shape that services
|
||||||
|
@ -11,7 +11,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error {
|
func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error {
|
||||||
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
|
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId, cmd.User)
|
||||||
|
|
||||||
return extractor.ValidateAlerts()
|
return extractor.ValidateAlerts()
|
||||||
}
|
}
|
||||||
@ -19,11 +19,11 @@ func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error {
|
|||||||
func updateDashboardAlerts(cmd *m.UpdateDashboardAlertsCommand) error {
|
func updateDashboardAlerts(cmd *m.UpdateDashboardAlertsCommand) error {
|
||||||
saveAlerts := m.SaveAlertsCommand{
|
saveAlerts := m.SaveAlertsCommand{
|
||||||
OrgId: cmd.OrgId,
|
OrgId: cmd.OrgId,
|
||||||
UserId: cmd.UserId,
|
UserId: cmd.User.UserId,
|
||||||
DashboardId: cmd.Dashboard.Id,
|
DashboardId: cmd.Dashboard.Id,
|
||||||
}
|
}
|
||||||
|
|
||||||
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
|
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId, cmd.User)
|
||||||
|
|
||||||
alerts, err := extractor.GetAlerts()
|
alerts, err := extractor.GetAlerts()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -52,6 +52,24 @@ func TestSimpleReducer(t *testing.T) {
|
|||||||
So(result, ShouldEqual, float64(1))
|
So(result, ShouldEqual, float64(1))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("median should ignore null values", func() {
|
||||||
|
reducer := NewSimpleReducer("median")
|
||||||
|
series := &tsdb.TimeSeries{
|
||||||
|
Name: "test time serie",
|
||||||
|
}
|
||||||
|
|
||||||
|
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1))
|
||||||
|
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2))
|
||||||
|
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 3))
|
||||||
|
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(float64(1)), 4))
|
||||||
|
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(float64(2)), 5))
|
||||||
|
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(float64(3)), 6))
|
||||||
|
|
||||||
|
result := reducer.Reduce(series)
|
||||||
|
So(result.Valid, ShouldEqual, true)
|
||||||
|
So(result.Float64, ShouldEqual, float64(2))
|
||||||
|
})
|
||||||
|
|
||||||
Convey("avg", func() {
|
Convey("avg", func() {
|
||||||
result := testReducer("avg", 1, 2, 3)
|
result := testReducer("avg", 1, 2, 3)
|
||||||
So(result, ShouldEqual, float64(2))
|
So(result, ShouldEqual, float64(2))
|
||||||
|
@ -13,14 +13,16 @@ import (
|
|||||||
|
|
||||||
// DashAlertExtractor extracts alerts from the dashboard json
|
// DashAlertExtractor extracts alerts from the dashboard json
|
||||||
type DashAlertExtractor struct {
|
type DashAlertExtractor struct {
|
||||||
|
User *m.SignedInUser
|
||||||
Dash *m.Dashboard
|
Dash *m.Dashboard
|
||||||
OrgID int64
|
OrgID int64
|
||||||
log log.Logger
|
log log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDashAlertExtractor returns a new DashAlertExtractor
|
// NewDashAlertExtractor returns a new DashAlertExtractor
|
||||||
func NewDashAlertExtractor(dash *m.Dashboard, orgID int64) *DashAlertExtractor {
|
func NewDashAlertExtractor(dash *m.Dashboard, orgID int64, user *m.SignedInUser) *DashAlertExtractor {
|
||||||
return &DashAlertExtractor{
|
return &DashAlertExtractor{
|
||||||
|
User: user,
|
||||||
Dash: dash,
|
Dash: dash,
|
||||||
OrgID: orgID,
|
OrgID: orgID,
|
||||||
log: log.New("alerting.extractor"),
|
log: log.New("alerting.extractor"),
|
||||||
@ -149,6 +151,21 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
|
|||||||
return nil, ValidationError{Reason: fmt.Sprintf("Data source used by alert rule not found, alertName=%v, datasource=%s", alert.Name, dsName)}
|
return nil, ValidationError{Reason: fmt.Sprintf("Data source used by alert rule not found, alertName=%v, datasource=%s", alert.Name, dsName)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dsFilterQuery := m.DatasourcesPermissionFilterQuery{
|
||||||
|
User: e.User,
|
||||||
|
Datasources: []*m.DataSource{datasource},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bus.Dispatch(&dsFilterQuery); err != nil {
|
||||||
|
if err != bus.ErrHandlerNotFound {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if len(dsFilterQuery.Result) == 0 {
|
||||||
|
return nil, m.ErrDataSourceAccessDenied
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id)
|
jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id)
|
||||||
|
|
||||||
if interval, err := panel.Get("interval").String(); err == nil {
|
if interval, err := panel.Get("interval").String(); err == nil {
|
||||||
|
@ -69,7 +69,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
|||||||
So(getTarget(dashJson), ShouldEqual, "")
|
So(getTarget(dashJson), ShouldEqual, "")
|
||||||
})
|
})
|
||||||
|
|
||||||
extractor := NewDashAlertExtractor(dash, 1)
|
extractor := NewDashAlertExtractor(dash, 1, nil)
|
||||||
_, _ = extractor.GetAlerts()
|
_, _ = extractor.GetAlerts()
|
||||||
|
|
||||||
Convey("Dashboard json should not be updated after extracting rules", func() {
|
Convey("Dashboard json should not be updated after extracting rules", func() {
|
||||||
@ -83,7 +83,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
|||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
dash := m.NewDashboardFromJson(dashJson)
|
dash := m.NewDashboardFromJson(dashJson)
|
||||||
extractor := NewDashAlertExtractor(dash, 1)
|
extractor := NewDashAlertExtractor(dash, 1, nil)
|
||||||
|
|
||||||
alerts, err := extractor.GetAlerts()
|
alerts, err := extractor.GetAlerts()
|
||||||
|
|
||||||
@ -146,7 +146,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
|||||||
dashJson, err := simplejson.NewJson(panelWithoutId)
|
dashJson, err := simplejson.NewJson(panelWithoutId)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
dash := m.NewDashboardFromJson(dashJson)
|
dash := m.NewDashboardFromJson(dashJson)
|
||||||
extractor := NewDashAlertExtractor(dash, 1)
|
extractor := NewDashAlertExtractor(dash, 1, nil)
|
||||||
|
|
||||||
_, err = extractor.GetAlerts()
|
_, err = extractor.GetAlerts()
|
||||||
|
|
||||||
@ -162,7 +162,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
|||||||
dashJson, err := simplejson.NewJson(panelWithIdZero)
|
dashJson, err := simplejson.NewJson(panelWithIdZero)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
dash := m.NewDashboardFromJson(dashJson)
|
dash := m.NewDashboardFromJson(dashJson)
|
||||||
extractor := NewDashAlertExtractor(dash, 1)
|
extractor := NewDashAlertExtractor(dash, 1, nil)
|
||||||
|
|
||||||
_, err = extractor.GetAlerts()
|
_, err = extractor.GetAlerts()
|
||||||
|
|
||||||
@ -178,7 +178,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
|||||||
dashJson, err := simplejson.NewJson(json)
|
dashJson, err := simplejson.NewJson(json)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
dash := m.NewDashboardFromJson(dashJson)
|
dash := m.NewDashboardFromJson(dashJson)
|
||||||
extractor := NewDashAlertExtractor(dash, 1)
|
extractor := NewDashAlertExtractor(dash, 1, nil)
|
||||||
|
|
||||||
alerts, err := extractor.GetAlerts()
|
alerts, err := extractor.GetAlerts()
|
||||||
|
|
||||||
@ -198,7 +198,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
|||||||
dashJson, err := simplejson.NewJson(json)
|
dashJson, err := simplejson.NewJson(json)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
dash := m.NewDashboardFromJson(dashJson)
|
dash := m.NewDashboardFromJson(dashJson)
|
||||||
extractor := NewDashAlertExtractor(dash, 1)
|
extractor := NewDashAlertExtractor(dash, 1, nil)
|
||||||
|
|
||||||
alerts, err := extractor.GetAlerts()
|
alerts, err := extractor.GetAlerts()
|
||||||
|
|
||||||
@ -228,7 +228,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
|||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
dash := m.NewDashboardFromJson(dashJson)
|
dash := m.NewDashboardFromJson(dashJson)
|
||||||
extractor := NewDashAlertExtractor(dash, 1)
|
extractor := NewDashAlertExtractor(dash, 1, nil)
|
||||||
|
|
||||||
alerts, err := extractor.GetAlerts()
|
alerts, err := extractor.GetAlerts()
|
||||||
|
|
||||||
@ -248,7 +248,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
|||||||
dashJSON, err := simplejson.NewJson(json)
|
dashJSON, err := simplejson.NewJson(json)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
dash := m.NewDashboardFromJson(dashJSON)
|
dash := m.NewDashboardFromJson(dashJSON)
|
||||||
extractor := NewDashAlertExtractor(dash, 1)
|
extractor := NewDashAlertExtractor(dash, 1, nil)
|
||||||
|
|
||||||
err = extractor.ValidateAlerts()
|
err = extractor.ValidateAlerts()
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ type AlertTestCommand struct {
|
|||||||
Dashboard *simplejson.Json
|
Dashboard *simplejson.Json
|
||||||
PanelId int64
|
PanelId int64
|
||||||
OrgId int64
|
OrgId int64
|
||||||
|
User *m.SignedInUser
|
||||||
|
|
||||||
Result *EvalContext
|
Result *EvalContext
|
||||||
}
|
}
|
||||||
@ -25,7 +26,7 @@ func handleAlertTestCommand(cmd *AlertTestCommand) error {
|
|||||||
|
|
||||||
dash := m.NewDashboardFromJson(cmd.Dashboard)
|
dash := m.NewDashboardFromJson(cmd.Dashboard)
|
||||||
|
|
||||||
extractor := NewDashAlertExtractor(dash, cmd.OrgId)
|
extractor := NewDashAlertExtractor(dash, cmd.OrgId, cmd.User)
|
||||||
alerts, err := extractor.GetAlerts()
|
alerts, err := extractor.GetAlerts()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
17
pkg/services/cache/cache.go
vendored
Normal file
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{
|
validateAlertsCmd := models.ValidateDashboardAlertsCommand{
|
||||||
OrgId: dto.OrgId,
|
OrgId: dto.OrgId,
|
||||||
Dashboard: dash,
|
Dashboard: dash,
|
||||||
|
User: dto.User,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bus.Dispatch(&validateAlertsCmd); err != nil {
|
if err := bus.Dispatch(&validateAlertsCmd); err != nil {
|
||||||
@ -159,8 +160,8 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO,
|
|||||||
func (dr *dashboardServiceImpl) updateAlerting(cmd *models.SaveDashboardCommand, dto *SaveDashboardDTO) error {
|
func (dr *dashboardServiceImpl) updateAlerting(cmd *models.SaveDashboardCommand, dto *SaveDashboardDTO) error {
|
||||||
alertCmd := models.UpdateDashboardAlertsCommand{
|
alertCmd := models.UpdateDashboardAlertsCommand{
|
||||||
OrgId: dto.OrgId,
|
OrgId: dto.OrgId,
|
||||||
UserId: dto.User.UserId,
|
|
||||||
Dashboard: cmd.Result,
|
Dashboard: cmd.Result,
|
||||||
|
User: dto.User,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bus.Dispatch(&alertCmd); err != nil {
|
if err := bus.Dispatch(&alertCmd); err != nil {
|
||||||
|
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 {
|
if dashboard.IsFolder {
|
||||||
deletes = append(deletes, "DELETE FROM dashboard_provisioning WHERE dashboard_id in (select id from dashboard where folder_id = ?)")
|
deletes = append(deletes, "DELETE FROM dashboard_provisioning WHERE dashboard_id in (select id from dashboard where folder_id = ?)")
|
||||||
deletes = append(deletes, "DELETE FROM dashboard WHERE folder_id = ?")
|
deletes = append(deletes, "DELETE FROM dashboard WHERE folder_id = ?")
|
||||||
|
|
||||||
|
dashIds := []struct {
|
||||||
|
Id int64
|
||||||
|
}{}
|
||||||
|
err := sess.SQL("select id from dashboard where folder_id = ?", dashboard.Id).Find(&dashIds)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range dashIds {
|
||||||
|
if err := deleteAlertDefinition(id.Id, sess); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := deleteAlertDefinition(dashboard.Id, sess); err != nil {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, sql := range deletes {
|
for _, sql := range deletes {
|
||||||
@ -337,10 +355,6 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := deleteAlertDefinition(dashboard.Id, sess); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/registry"
|
"github.com/grafana/grafana/pkg/registry"
|
||||||
"github.com/grafana/grafana/pkg/services/annotations"
|
"github.com/grafana/grafana/pkg/services/annotations"
|
||||||
|
"github.com/grafana/grafana/pkg/services/cache"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
|
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
|
||||||
@ -49,6 +50,7 @@ func init() {
|
|||||||
type SqlStore struct {
|
type SqlStore struct {
|
||||||
Cfg *setting.Cfg `inject:""`
|
Cfg *setting.Cfg `inject:""`
|
||||||
Bus bus.Bus `inject:""`
|
Bus bus.Bus `inject:""`
|
||||||
|
CacheService *cache.CacheService `inject:""`
|
||||||
|
|
||||||
dbCfg DatabaseConfig
|
dbCfg DatabaseConfig
|
||||||
engine *xorm.Engine
|
engine *xorm.Engine
|
||||||
@ -148,9 +150,11 @@ func (ss *SqlStore) Init() error {
|
|||||||
|
|
||||||
// Init repo instances
|
// Init repo instances
|
||||||
annotations.SetRepository(&SqlAnnotationRepo{})
|
annotations.SetRepository(&SqlAnnotationRepo{})
|
||||||
|
|
||||||
ss.Bus.SetTransactionManager(ss)
|
ss.Bus.SetTransactionManager(ss)
|
||||||
|
|
||||||
|
// Register handlers
|
||||||
|
ss.addUserQueryAndCommandHandlers()
|
||||||
|
|
||||||
// ensure admin user
|
// ensure admin user
|
||||||
if ss.skipEnsureAdmin {
|
if ss.skipEnsureAdmin {
|
||||||
return nil
|
return nil
|
||||||
@ -322,6 +326,7 @@ func InitTestDB(t *testing.T) *SqlStore {
|
|||||||
sqlstore := &SqlStore{}
|
sqlstore := &SqlStore{}
|
||||||
sqlstore.skipEnsureAdmin = true
|
sqlstore.skipEnsureAdmin = true
|
||||||
sqlstore.Bus = bus.New()
|
sqlstore.Bus = bus.New()
|
||||||
|
sqlstore.CacheService = cache.New(5*time.Minute, 10*time.Minute)
|
||||||
|
|
||||||
dbType := migrator.SQLITE
|
dbType := migrator.SQLITE
|
||||||
|
|
||||||
|
@ -15,8 +15,9 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func (ss *SqlStore) addUserQueryAndCommandHandlers() {
|
||||||
//bus.AddHandler("sql", CreateUser)
|
ss.Bus.AddHandler(ss.GetSignedInUserWithCache)
|
||||||
|
|
||||||
bus.AddHandler("sql", GetUserById)
|
bus.AddHandler("sql", GetUserById)
|
||||||
bus.AddHandler("sql", UpdateUser)
|
bus.AddHandler("sql", UpdateUser)
|
||||||
bus.AddHandler("sql", ChangeUserPassword)
|
bus.AddHandler("sql", ChangeUserPassword)
|
||||||
@ -25,7 +26,6 @@ func init() {
|
|||||||
bus.AddHandler("sql", SetUsingOrg)
|
bus.AddHandler("sql", SetUsingOrg)
|
||||||
bus.AddHandler("sql", UpdateUserLastSeenAt)
|
bus.AddHandler("sql", UpdateUserLastSeenAt)
|
||||||
bus.AddHandler("sql", GetUserProfile)
|
bus.AddHandler("sql", GetUserProfile)
|
||||||
bus.AddHandler("sql", GetSignedInUser)
|
|
||||||
bus.AddHandler("sql", SearchUsers)
|
bus.AddHandler("sql", SearchUsers)
|
||||||
bus.AddHandler("sql", GetUserOrgList)
|
bus.AddHandler("sql", GetUserOrgList)
|
||||||
bus.AddHandler("sql", DeleteUser)
|
bus.AddHandler("sql", DeleteUser)
|
||||||
@ -345,6 +345,22 @@ func GetUserOrgList(query *m.GetUserOrgListQuery) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ss *SqlStore) GetSignedInUserWithCache(query *m.GetSignedInUserQuery) error {
|
||||||
|
cacheKey := fmt.Sprintf("signed-in-user-%d-%d", query.UserId, query.OrgId)
|
||||||
|
if cached, found := ss.CacheService.Get(cacheKey); found {
|
||||||
|
query.Result = cached.(*m.SignedInUser)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := GetSignedInUser(query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ss.CacheService.Set(cacheKey, query.Result, time.Second*5)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func GetSignedInUser(query *m.GetSignedInUserQuery) error {
|
func GetSignedInUser(query *m.GetSignedInUserQuery) error {
|
||||||
orgId := "u.org_id"
|
orgId := "u.org_id"
|
||||||
if query.OrgId > 0 {
|
if query.OrgId > 0 {
|
||||||
@ -389,6 +405,17 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error {
|
|||||||
user.OrgName = "Org missing"
|
user.OrgName = "Org missing"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTeamsByUserQuery := &m.GetTeamsByUserQuery{OrgId: user.OrgId, UserId: user.UserId}
|
||||||
|
err = GetTeamsByUser(getTeamsByUserQuery)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Teams = make([]int64, len(getTeamsByUserQuery.Result))
|
||||||
|
for i, t := range getTeamsByUserQuery.Result {
|
||||||
|
user.Teams[i] = t.Id
|
||||||
|
}
|
||||||
|
|
||||||
query.Result = &user
|
query.Result = &user
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,10 @@ const (
|
|||||||
APP_NAME_ENTERPRISE = "Grafana Enterprise"
|
APP_NAME_ENTERPRISE = "Grafana Enterprise"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ERR_TEMPLATE_NAME = "error"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// App settings.
|
// App settings.
|
||||||
Env = DEV
|
Env = DEV
|
||||||
|
@ -46,6 +46,7 @@ func init() {
|
|||||||
"AWS/Billing": {"EstimatedCharges"},
|
"AWS/Billing": {"EstimatedCharges"},
|
||||||
"AWS/CloudFront": {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"},
|
"AWS/CloudFront": {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"},
|
||||||
"AWS/CloudSearch": {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"},
|
"AWS/CloudSearch": {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"},
|
||||||
|
"AWS/Connect": {"CallsBreachingConcurrencyQuota", "CallBackNotDialableNumber", "CallRecordingUploadError", "CallsPerInterval", "ConcurrentCalls", "ConcurrentCallsPercentage", "ContactFlowErrors", "ContactFlowFatalErrors", "LongestQueueWaitTime", "MissedCalls", "MisconfiguredPhoneNumbers", "PublicSigningKeyUsage", "QueueCapacityExceededError", "QueueSize", "ThrottledCalls", "ToInstancePacketLossRate"},
|
||||||
"AWS/DMS": {"FreeableMemory", "WriteIOPS", "ReadIOPS", "WriteThroughput", "ReadThroughput", "WriteLatency", "ReadLatency", "SwapUsage", "NetworkTransmitThroughput", "NetworkReceiveThroughput", "FullLoadThroughputBandwidthSource", "FullLoadThroughputBandwidthTarget", "FullLoadThroughputRowsSource", "FullLoadThroughputRowsTarget", "CDCIncomingChanges", "CDCChangesMemorySource", "CDCChangesMemoryTarget", "CDCChangesDiskSource", "CDCChangesDiskTarget", "CDCThroughputBandwidthTarget", "CDCThroughputRowsSource", "CDCThroughputRowsTarget", "CDCLatencySource", "CDCLatencyTarget"},
|
"AWS/DMS": {"FreeableMemory", "WriteIOPS", "ReadIOPS", "WriteThroughput", "ReadThroughput", "WriteLatency", "ReadLatency", "SwapUsage", "NetworkTransmitThroughput", "NetworkReceiveThroughput", "FullLoadThroughputBandwidthSource", "FullLoadThroughputBandwidthTarget", "FullLoadThroughputRowsSource", "FullLoadThroughputRowsTarget", "CDCIncomingChanges", "CDCChangesMemorySource", "CDCChangesMemoryTarget", "CDCChangesDiskSource", "CDCChangesDiskTarget", "CDCThroughputBandwidthTarget", "CDCThroughputRowsSource", "CDCThroughputRowsTarget", "CDCLatencySource", "CDCLatencyTarget"},
|
||||||
"AWS/DX": {"ConnectionState", "ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionCRCErrorCount", "ConnectionLightLevelTx", "ConnectionLightLevelRx"},
|
"AWS/DX": {"ConnectionState", "ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionCRCErrorCount", "ConnectionLightLevelTx", "ConnectionLightLevelRx"},
|
||||||
"AWS/DynamoDB": {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReturnedBytes", "ReturnedItemCount", "ReturnedRecordsCount", "SuccessfulRequestLatency", "SystemErrors", "TimeToLiveDeletedItemCount", "ThrottledRequests", "UserErrors", "WriteThrottleEvents"},
|
"AWS/DynamoDB": {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReturnedBytes", "ReturnedItemCount", "ReturnedRecordsCount", "SuccessfulRequestLatency", "SystemErrors", "TimeToLiveDeletedItemCount", "ThrottledRequests", "UserErrors", "WriteThrottleEvents"},
|
||||||
@ -120,6 +121,7 @@ func init() {
|
|||||||
"AWS/Billing": {"ServiceName", "LinkedAccount", "Currency"},
|
"AWS/Billing": {"ServiceName", "LinkedAccount", "Currency"},
|
||||||
"AWS/CloudFront": {"DistributionId", "Region"},
|
"AWS/CloudFront": {"DistributionId", "Region"},
|
||||||
"AWS/CloudSearch": {},
|
"AWS/CloudSearch": {},
|
||||||
|
"AWS/Connect": {"InstanceId", "MetricGroup", "Participant", "QueueName", "Stream Type", "Type of Connection"},
|
||||||
"AWS/DMS": {"ReplicationInstanceIdentifier", "ReplicationTaskIdentifier"},
|
"AWS/DMS": {"ReplicationInstanceIdentifier", "ReplicationTaskIdentifier"},
|
||||||
"AWS/DX": {"ConnectionId"},
|
"AWS/DX": {"ConnectionId"},
|
||||||
"AWS/DynamoDB": {"TableName", "GlobalSecondaryIndexName", "Operation", "StreamLabel"},
|
"AWS/DynamoDB": {"TableName", "GlobalSecondaryIndexName", "Operation", "StreamLabel"},
|
||||||
|
@ -88,7 +88,7 @@ export class FormDropdownCtrl {
|
|||||||
if (evt.keyCode === 13) {
|
if (evt.keyCode === 13) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.inputElement.blur();
|
this.inputElement.blur();
|
||||||
}, 100);
|
}, 300);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -166,7 +166,7 @@ export class AlertTabCtrl {
|
|||||||
|
|
||||||
alert.noDataState = alert.noDataState || config.alertingNoDataOrNullValues;
|
alert.noDataState = alert.noDataState || config.alertingNoDataOrNullValues;
|
||||||
alert.executionErrorState = alert.executionErrorState || config.alertingErrorOrTimeout;
|
alert.executionErrorState = alert.executionErrorState || config.alertingErrorOrTimeout;
|
||||||
alert.frequency = alert.frequency || '60s';
|
alert.frequency = alert.frequency || '1m';
|
||||||
alert.handler = alert.handler || 1;
|
alert.handler = alert.handler || 1;
|
||||||
alert.notifications = alert.notifications || [];
|
alert.notifications = alert.notifications || [];
|
||||||
|
|
||||||
@ -217,7 +217,7 @@ export class AlertTabCtrl {
|
|||||||
buildDefaultCondition() {
|
buildDefaultCondition() {
|
||||||
return {
|
return {
|
||||||
type: 'query',
|
type: 'query',
|
||||||
query: { params: ['A', '5m', 'now'] },
|
query: { params: ['A', '15m', 'now'] },
|
||||||
reducer: { type: 'avg', params: [] },
|
reducer: { type: 'avg', params: [] },
|
||||||
evaluator: { type: 'gt', params: [null] },
|
evaluator: { type: 'gt', params: [null] },
|
||||||
operator: { type: 'and' },
|
operator: { type: 'and' },
|
||||||
|
@ -21,6 +21,7 @@ export interface Props {
|
|||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
refreshCounter: number;
|
refreshCounter: number;
|
||||||
|
renderCounter: number;
|
||||||
timeRange?: TimeRange;
|
timeRange?: TimeRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,11 +31,13 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
refreshCounter: 0,
|
refreshCounter: 0,
|
||||||
|
renderCounter: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.panel.events.on('refresh', this.onRefresh);
|
this.props.panel.events.on('refresh', this.onRefresh);
|
||||||
|
this.props.panel.events.on('render', this.onRender);
|
||||||
this.props.dashboard.panelInitialized(this.props.panel);
|
this.props.dashboard.panelInitialized(this.props.panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,6 +55,13 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onRender = () => {
|
||||||
|
console.log('onRender');
|
||||||
|
this.setState({
|
||||||
|
renderCounter: this.state.renderCounter + 1,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
get isVisible() {
|
get isVisible() {
|
||||||
return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
|
return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
|
||||||
}
|
}
|
||||||
@ -59,9 +69,11 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
render() {
|
render() {
|
||||||
const { panel, dashboard } = this.props;
|
const { panel, dashboard } = this.props;
|
||||||
const { datasource, targets } = panel;
|
const { datasource, targets } = panel;
|
||||||
const { refreshCounter, timeRange } = this.state;
|
const { timeRange, renderCounter, refreshCounter } = this.state;
|
||||||
const PanelComponent = this.props.component;
|
const PanelComponent = this.props.component;
|
||||||
|
|
||||||
|
console.log('Panel chrome render');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="panel-container">
|
<div className="panel-container">
|
||||||
<PanelHeader panel={panel} dashboard={dashboard} />
|
<PanelHeader panel={panel} dashboard={dashboard} />
|
||||||
@ -74,7 +86,16 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
refreshCounter={refreshCounter}
|
refreshCounter={refreshCounter}
|
||||||
>
|
>
|
||||||
{({ loading, timeSeries }) => {
|
{({ loading, timeSeries }) => {
|
||||||
return <PanelComponent loading={loading} timeSeries={timeSeries} timeRange={timeRange} />;
|
console.log('panelcrome inner render');
|
||||||
|
return (
|
||||||
|
<PanelComponent
|
||||||
|
loading={loading}
|
||||||
|
timeSeries={timeSeries}
|
||||||
|
timeRange={timeRange}
|
||||||
|
options={panel.getOptions()}
|
||||||
|
renderCounter={renderCounter}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
</DataPanel>
|
</DataPanel>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import React from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { QueriesTab } from './QueriesTab';
|
||||||
|
import { VizTypePicker } from './VizTypePicker';
|
||||||
|
|
||||||
|
import { store } from 'app/store/store';
|
||||||
|
import { updateLocation } from 'app/core/actions';
|
||||||
|
|
||||||
import { PanelModel } from '../panel_model';
|
import { PanelModel } from '../panel_model';
|
||||||
import { DashboardModel } from '../dashboard_model';
|
import { DashboardModel } from '../dashboard_model';
|
||||||
import { store } from 'app/store/store';
|
|
||||||
import { QueriesTab } from './QueriesTab';
|
|
||||||
import { PanelPlugin, PluginExports } from 'app/types/plugins';
|
import { PanelPlugin, PluginExports } from 'app/types/plugins';
|
||||||
import { VizTypePicker } from './VizTypePicker';
|
|
||||||
import { updateLocation } from 'app/core/actions';
|
|
||||||
|
|
||||||
interface PanelEditorProps {
|
interface PanelEditorProps {
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
@ -22,7 +25,7 @@ interface PanelEditorTab {
|
|||||||
icon: string;
|
icon: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PanelEditor extends React.Component<PanelEditorProps, any> {
|
export class PanelEditor extends PureComponent<PanelEditorProps> {
|
||||||
tabs: PanelEditorTab[];
|
tabs: PanelEditorTab[];
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -39,16 +42,21 @@ export class PanelEditor extends React.Component<PanelEditorProps, any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderPanelOptions() {
|
renderPanelOptions() {
|
||||||
const { pluginExports } = this.props;
|
const { pluginExports, panel } = this.props;
|
||||||
|
|
||||||
if (pluginExports.PanelOptions) {
|
if (pluginExports.PanelOptionsComponent) {
|
||||||
const PanelOptions = pluginExports.PanelOptions;
|
const OptionsComponent = pluginExports.PanelOptionsComponent;
|
||||||
return <PanelOptions />;
|
return <OptionsComponent options={panel.getOptions()} onChange={this.onPanelOptionsChanged} />;
|
||||||
} else {
|
} else {
|
||||||
return <p>Visualization has no options</p>;
|
return <p>Visualization has no options</p>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onPanelOptionsChanged = (options: any) => {
|
||||||
|
this.props.panel.updateOptions(options);
|
||||||
|
this.forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
renderVizTab() {
|
renderVizTab() {
|
||||||
return (
|
return (
|
||||||
<div className="viz-editor">
|
<div className="viz-editor">
|
||||||
@ -70,6 +78,7 @@ export class PanelEditor extends React.Component<PanelEditorProps, any> {
|
|||||||
partial: true,
|
partial: true,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -60,6 +60,21 @@ export class PanelModel {
|
|||||||
_.defaultsDeep(this, _.cloneDeep(defaults));
|
_.defaultsDeep(this, _.cloneDeep(defaults));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOptions() {
|
||||||
|
return this[this.getOptionsKey()] || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOptions(options: object) {
|
||||||
|
const update: any = {};
|
||||||
|
update[this.getOptionsKey()] = options;
|
||||||
|
Object.assign(this, update);
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOptionsKey() {
|
||||||
|
return this.type + 'Options';
|
||||||
|
}
|
||||||
|
|
||||||
getSaveModel() {
|
getSaveModel() {
|
||||||
const model: any = {};
|
const model: any = {};
|
||||||
for (const property in this) {
|
for (const property in this) {
|
||||||
@ -121,10 +136,6 @@ export class PanelModel {
|
|||||||
this.events.emit('panel-initialized');
|
this.events.emit('panel-initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
initEditMode() {
|
|
||||||
this.events.emit('panel-init-edit-mode');
|
|
||||||
}
|
|
||||||
|
|
||||||
changeType(pluginId: string) {
|
changeType(pluginId: string) {
|
||||||
this.type = pluginId;
|
this.type = pluginId;
|
||||||
|
|
||||||
|
@ -32,9 +32,9 @@ export class SettingsCtrl {
|
|||||||
|
|
||||||
this.$scope.$on('$destroy', () => {
|
this.$scope.$on('$destroy', () => {
|
||||||
this.dashboard.updateSubmenuVisibility();
|
this.dashboard.updateSubmenuVisibility();
|
||||||
this.dashboard.startRefresh();
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.$rootScope.appEvent('dash-scroll', { restore: true });
|
this.$rootScope.appEvent('dash-scroll', { restore: true });
|
||||||
|
this.dashboard.startRefresh();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div ng-show="mode === 'email-sent'">
|
<div ng-show="mode === 'email-sent'">
|
||||||
An email with a reset link as been sent to the email address. <br>
|
An email with a reset link has been sent to the email address. <br>
|
||||||
You should receive it shortly.
|
You should receive it shortly.
|
||||||
<div class="p-t-1">
|
<div class="p-t-1">
|
||||||
<a href="login" class="btn btn-success p-t-1">
|
<a href="login" class="btn btn-success p-t-1">
|
||||||
|
@ -95,9 +95,9 @@ class LoggingQueryField extends React.PureComponent<LoggingQueryFieldProps, Logg
|
|||||||
this.languageProvider
|
this.languageProvider
|
||||||
.start()
|
.start()
|
||||||
.then(remaining => {
|
.then(remaining => {
|
||||||
remaining.map(task => task.then(this.onReceiveMetrics).catch(() => {}));
|
remaining.map(task => task.then(this.onUpdateLanguage).catch(() => {}));
|
||||||
})
|
})
|
||||||
.then(() => this.onReceiveMetrics());
|
.then(() => this.onUpdateLanguage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,7 +119,7 @@ class LoggingQueryField extends React.PureComponent<LoggingQueryFieldProps, Logg
|
|||||||
|
|
||||||
this.languageProvider
|
this.languageProvider
|
||||||
.fetchLabelValues(targetOption.value)
|
.fetchLabelValues(targetOption.value)
|
||||||
.then(this.onReceiveMetrics)
|
.then(this.onUpdateLanguage)
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -147,7 +147,7 @@ class LoggingQueryField extends React.PureComponent<LoggingQueryFieldProps, Logg
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onReceiveMetrics = () => {
|
onUpdateLanguage = () => {
|
||||||
Prism.languages[PRISM_SYNTAX] = this.languageProvider.getSyntax();
|
Prism.languages[PRISM_SYNTAX] = this.languageProvider.getSyntax();
|
||||||
const { logLabelOptions } = this.languageProvider;
|
const { logLabelOptions } = this.languageProvider;
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -47,7 +47,6 @@ export default class LoggingLanguageProvider extends LanguageProvider {
|
|||||||
this.datasource = datasource;
|
this.datasource = datasource;
|
||||||
this.labelKeys = {};
|
this.labelKeys = {};
|
||||||
this.labelValues = {};
|
this.labelValues = {};
|
||||||
this.started = false;
|
|
||||||
|
|
||||||
Object.assign(this, initialValues);
|
Object.assign(this, initialValues);
|
||||||
}
|
}
|
||||||
@ -63,11 +62,10 @@ export default class LoggingLanguageProvider extends LanguageProvider {
|
|||||||
};
|
};
|
||||||
|
|
||||||
start = () => {
|
start = () => {
|
||||||
if (!this.started) {
|
if (!this.startTask) {
|
||||||
this.started = true;
|
this.startTask = this.fetchLogLabels();
|
||||||
return this.fetchLogLabels();
|
|
||||||
}
|
}
|
||||||
return Promise.resolve([]);
|
return this.startTask;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Keep this DOM-free for testing
|
// Keep this DOM-free for testing
|
||||||
|
@ -134,9 +134,9 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
|||||||
this.languageProvider
|
this.languageProvider
|
||||||
.start()
|
.start()
|
||||||
.then(remaining => {
|
.then(remaining => {
|
||||||
remaining.map(task => task.then(this.onReceiveMetrics).catch(() => {}));
|
remaining.map(task => task.then(this.onUpdateLanguage).catch(() => {}));
|
||||||
})
|
})
|
||||||
.then(() => this.onReceiveMetrics());
|
.then(() => this.onUpdateLanguage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,7 +176,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onReceiveMetrics = () => {
|
onUpdateLanguage = () => {
|
||||||
const { histogramMetrics, metrics } = this.languageProvider;
|
const { histogramMetrics, metrics } = this.languageProvider;
|
||||||
if (!metrics) {
|
if (!metrics) {
|
||||||
return;
|
return;
|
||||||
|
@ -46,7 +46,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
|||||||
labelKeys?: { [index: string]: string[] }; // metric -> [labelKey,...]
|
labelKeys?: { [index: string]: string[] }; // metric -> [labelKey,...]
|
||||||
labelValues?: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...]
|
labelValues?: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...]
|
||||||
metrics?: string[];
|
metrics?: string[];
|
||||||
started: boolean;
|
startTask: Promise<any>;
|
||||||
|
|
||||||
constructor(datasource: any, initialValues?: any) {
|
constructor(datasource: any, initialValues?: any) {
|
||||||
super();
|
super();
|
||||||
@ -56,7 +56,6 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
|||||||
this.labelKeys = {};
|
this.labelKeys = {};
|
||||||
this.labelValues = {};
|
this.labelValues = {};
|
||||||
this.metrics = [];
|
this.metrics = [];
|
||||||
this.started = false;
|
|
||||||
|
|
||||||
Object.assign(this, initialValues);
|
Object.assign(this, initialValues);
|
||||||
}
|
}
|
||||||
@ -72,11 +71,10 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
|||||||
};
|
};
|
||||||
|
|
||||||
start = () => {
|
start = () => {
|
||||||
if (!this.started) {
|
if (!this.startTask) {
|
||||||
this.started = true;
|
this.startTask = this.fetchMetricNames().then(() => [this.fetchHistogramMetrics()]);
|
||||||
return this.fetchMetricNames().then(() => [this.fetchHistogramMetrics()]);
|
|
||||||
}
|
}
|
||||||
return Promise.resolve([]);
|
return this.startTask;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Keep this DOM-free for testing
|
// Keep this DOM-free for testing
|
||||||
@ -156,7 +154,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAggregationCompletionItems({ value }: TypeaheadInput): TypeaheadOutput {
|
getAggregationCompletionItems({ value }: TypeaheadInput): TypeaheadOutput {
|
||||||
let refresher: Promise<any> = null;
|
const refresher: Promise<any> = null;
|
||||||
const suggestions: CompletionItemGroup[] = [];
|
const suggestions: CompletionItemGroup[] = [];
|
||||||
|
|
||||||
// Stitch all query lines together to support multi-line queries
|
// Stitch all query lines together to support multi-line queries
|
||||||
@ -172,12 +170,30 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
|||||||
return text;
|
return text;
|
||||||
}, '');
|
}, '');
|
||||||
|
|
||||||
const leftSide = queryText.slice(0, queryOffset);
|
// Try search for selector part on the left-hand side, such as `sum (m) by (l)`
|
||||||
const openParensAggregationIndex = leftSide.lastIndexOf('(');
|
const openParensAggregationIndex = queryText.lastIndexOf('(', queryOffset);
|
||||||
const openParensSelectorIndex = leftSide.slice(0, openParensAggregationIndex).lastIndexOf('(');
|
let openParensSelectorIndex = queryText.lastIndexOf('(', openParensAggregationIndex - 1);
|
||||||
const closeParensSelectorIndex = leftSide.slice(openParensSelectorIndex).indexOf(')') + openParensSelectorIndex;
|
let closeParensSelectorIndex = queryText.indexOf(')', openParensSelectorIndex);
|
||||||
|
|
||||||
let selectorString = leftSide.slice(openParensSelectorIndex + 1, closeParensSelectorIndex);
|
// Try search for selector part of an alternate aggregation clause, such as `sum by (l) (m)`
|
||||||
|
if (openParensSelectorIndex === -1) {
|
||||||
|
const closeParensAggregationIndex = queryText.indexOf(')', queryOffset);
|
||||||
|
closeParensSelectorIndex = queryText.indexOf(')', closeParensAggregationIndex + 1);
|
||||||
|
openParensSelectorIndex = queryText.lastIndexOf('(', closeParensSelectorIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
refresher,
|
||||||
|
suggestions,
|
||||||
|
context: 'context-aggregation',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Suggestions are useless for alternative aggregation clauses without a selector in context
|
||||||
|
if (openParensSelectorIndex === -1) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectorString = queryText.slice(openParensSelectorIndex + 1, closeParensSelectorIndex);
|
||||||
|
|
||||||
// Range vector syntax not accounted for by subsequent parse so discard it if present
|
// Range vector syntax not accounted for by subsequent parse so discard it if present
|
||||||
selectorString = selectorString.replace(/\[[^\]]+\]$/, '');
|
selectorString = selectorString.replace(/\[[^\]]+\]$/, '');
|
||||||
@ -188,14 +204,10 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
|||||||
if (labelKeys) {
|
if (labelKeys) {
|
||||||
suggestions.push({ label: 'Labels', items: labelKeys.map(wrapLabel) });
|
suggestions.push({ label: 'Labels', items: labelKeys.map(wrapLabel) });
|
||||||
} else {
|
} else {
|
||||||
refresher = this.fetchSeriesLabels(selector);
|
result.refresher = this.fetchSeriesLabels(selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return result;
|
||||||
refresher,
|
|
||||||
suggestions,
|
|
||||||
context: 'context-aggregation',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getLabelCompletionItems({ text, wrapperClasses, labelKey, value }: TypeaheadInput): TypeaheadOutput {
|
getLabelCompletionItems({ text, wrapperClasses, labelKey, value }: TypeaheadInput): TypeaheadOutput {
|
||||||
|
@ -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 _ from 'lodash';
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
// Components
|
|
||||||
import Graph from 'app/viz/Graph';
|
import Graph from 'app/viz/Graph';
|
||||||
import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
|
|
||||||
import { Switch } from 'app/core/components/Switch/Switch';
|
import { Switch } from 'app/core/components/Switch/Switch';
|
||||||
|
|
||||||
// Types
|
import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
|
||||||
import { PanelProps, NullValueMode } from 'app/types';
|
import { PanelProps, PanelOptionsProps, NullValueMode } from 'app/types';
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
showBars: boolean;
|
showBars: boolean;
|
||||||
|
showLines: boolean;
|
||||||
|
showPoints: boolean;
|
||||||
|
|
||||||
|
onChange: (options: Options) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props extends PanelProps {
|
interface Props extends PanelProps<Options> {}
|
||||||
options: Options;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Graph2 extends PureComponent<Props> {
|
export class Graph2 extends PureComponent<Props> {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -25,27 +24,52 @@ export class Graph2 extends PureComponent<Props> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { timeSeries, timeRange } = this.props;
|
const { timeSeries, timeRange } = this.props;
|
||||||
|
const { showLines, showBars, showPoints } = this.props.options;
|
||||||
|
|
||||||
const vmSeries = getTimeSeriesVMs({
|
const vmSeries = getTimeSeriesVMs({
|
||||||
timeSeries: timeSeries,
|
timeSeries: timeSeries,
|
||||||
nullValueMode: NullValueMode.Ignore,
|
nullValueMode: NullValueMode.Ignore,
|
||||||
});
|
});
|
||||||
|
|
||||||
return <Graph timeSeries={vmSeries} timeRange={timeRange} />;
|
return (
|
||||||
|
<Graph
|
||||||
|
timeSeries={vmSeries}
|
||||||
|
timeRange={timeRange}
|
||||||
|
showLines={showLines}
|
||||||
|
showPoints={showPoints}
|
||||||
|
showBars={showBars}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TextOptions extends PureComponent<any> {
|
export class GraphOptions extends PureComponent<PanelOptionsProps<Options>> {
|
||||||
onChange = () => {};
|
onToggleLines = () => {
|
||||||
|
this.props.onChange({ ...this.props.options, showLines: !this.props.options.showLines });
|
||||||
|
};
|
||||||
|
|
||||||
|
onToggleBars = () => {
|
||||||
|
this.props.onChange({ ...this.props.options, showBars: !this.props.options.showBars });
|
||||||
|
};
|
||||||
|
|
||||||
|
onTogglePoints = () => {
|
||||||
|
this.props.onChange({ ...this.props.options, showPoints: !this.props.options.showPoints });
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { showBars, showPoints, showLines } = this.props.options;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div>
|
||||||
<div className="section gf-form-group">
|
<div className="section gf-form-group">
|
||||||
<h5 className="section-heading">Draw Modes</h5>
|
<h5 className="page-heading">Draw Modes</h5>
|
||||||
<Switch label="Lines" checked={true} onChange={this.onChange} />
|
<Switch label="Lines" labelClass="width-5" checked={showLines} onChange={this.onToggleLines} />
|
||||||
|
<Switch label="Bars" labelClass="width-5" checked={showBars} onChange={this.onToggleBars} />
|
||||||
|
<Switch label="Points" labelClass="width-5" checked={showPoints} onChange={this.onTogglePoints} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Graph2 as PanelComponent, TextOptions as PanelOptions };
|
export { Graph2 as PanelComponent, GraphOptions as PanelOptionsComponent };
|
||||||
|
@ -211,16 +211,17 @@ export class TableRenderer {
|
|||||||
value = this.formatColumnValue(columnIndex, value);
|
value = this.formatColumnValue(columnIndex, value);
|
||||||
|
|
||||||
const column = this.table.columns[columnIndex];
|
const column = this.table.columns[columnIndex];
|
||||||
let style = '';
|
let cellStyle = '';
|
||||||
|
let textStyle = '';
|
||||||
const cellClasses = [];
|
const cellClasses = [];
|
||||||
let cellClass = '';
|
let cellClass = '';
|
||||||
|
|
||||||
if (this.colorState.cell) {
|
if (this.colorState.cell) {
|
||||||
style = ' style="background-color:' + this.colorState.cell + '"';
|
cellStyle = ' style="background-color:' + this.colorState.cell + '"';
|
||||||
cellClasses.push('table-panel-color-cell');
|
cellClasses.push('table-panel-color-cell');
|
||||||
this.colorState.cell = null;
|
this.colorState.cell = null;
|
||||||
} else if (this.colorState.value) {
|
} else if (this.colorState.value) {
|
||||||
style = ' style="color:' + this.colorState.value + '"';
|
textStyle = ' style="color:' + this.colorState.value + '"';
|
||||||
this.colorState.value = null;
|
this.colorState.value = null;
|
||||||
}
|
}
|
||||||
// because of the fixed table headers css only solution
|
// because of the fixed table headers css only solution
|
||||||
@ -232,7 +233,7 @@ export class TableRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
style = ' style="display:none;"';
|
cellStyle = ' style="display:none;"';
|
||||||
column.hidden = true;
|
column.hidden = true;
|
||||||
} else {
|
} else {
|
||||||
column.hidden = false;
|
column.hidden = false;
|
||||||
@ -258,7 +259,7 @@ export class TableRenderer {
|
|||||||
cellClasses.push('table-panel-cell-link');
|
cellClasses.push('table-panel-cell-link');
|
||||||
|
|
||||||
columnHtml += `
|
columnHtml += `
|
||||||
<a href="${cellLink}" target="${cellTarget}" data-link-tooltip data-original-title="${cellLinkTooltip}" data-placement="right"${style}>
|
<a href="${cellLink}" target="${cellTarget}" data-link-tooltip data-original-title="${cellLinkTooltip}" data-placement="right"${textStyle}>
|
||||||
${value}
|
${value}
|
||||||
</a>
|
</a>
|
||||||
`;
|
`;
|
||||||
@ -283,7 +284,7 @@ export class TableRenderer {
|
|||||||
cellClass = ' class="' + cellClasses.join(' ') + '"';
|
cellClass = ' class="' + cellClasses.join(' ') + '"';
|
||||||
}
|
}
|
||||||
|
|
||||||
columnHtml = '<td' + cellClass + style + '>' + columnHtml + '</td>';
|
columnHtml = '<td' + cellClass + cellStyle + textStyle + '>' + columnHtml + '</td>';
|
||||||
return columnHtml;
|
return columnHtml;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,10 +86,11 @@ export abstract class LanguageProvider {
|
|||||||
datasource: any;
|
datasource: any;
|
||||||
request: (url) => Promise<any>;
|
request: (url) => Promise<any>;
|
||||||
/**
|
/**
|
||||||
* Returns a promise that resolves with a task list when main syntax is loaded.
|
* Returns startTask that resolves with a task list when main syntax is loaded.
|
||||||
* Task list consists of secondary promises that load more detailed language features.
|
* Task list consists of secondary promises that load more detailed language features.
|
||||||
*/
|
*/
|
||||||
start: () => Promise<any[]>;
|
start: () => Promise<any[]>;
|
||||||
|
startTask?: Promise<any[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TypeaheadInput {
|
export interface TypeaheadInput {
|
||||||
|
@ -20,7 +20,7 @@ import {
|
|||||||
DataQueryResponse,
|
DataQueryResponse,
|
||||||
DataQueryOptions,
|
DataQueryOptions,
|
||||||
} from './series';
|
} from './series';
|
||||||
import { PanelProps } from './panel';
|
import { PanelProps, PanelOptionsProps } from './panel';
|
||||||
import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins';
|
import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins';
|
||||||
import { Organization, OrganizationPreferences, OrganizationState } from './organization';
|
import { Organization, OrganizationPreferences, OrganizationState } from './organization';
|
||||||
import {
|
import {
|
||||||
@ -69,6 +69,7 @@ export {
|
|||||||
TimeRange,
|
TimeRange,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
PanelProps,
|
PanelProps,
|
||||||
|
PanelOptionsProps,
|
||||||
TimeSeries,
|
TimeSeries,
|
||||||
TimeSeriesVM,
|
TimeSeriesVM,
|
||||||
TimeSeriesVMs,
|
TimeSeriesVMs,
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
import { LoadingState, TimeSeries, TimeRange } from './series';
|
import { LoadingState, TimeSeries, TimeRange } from './series';
|
||||||
|
|
||||||
export interface PanelProps {
|
export interface PanelProps<T = any> {
|
||||||
timeSeries: TimeSeries[];
|
timeSeries: TimeSeries[];
|
||||||
timeRange: TimeRange;
|
timeRange: TimeRange;
|
||||||
loading: LoadingState;
|
loading: LoadingState;
|
||||||
|
options: T;
|
||||||
|
renderCounter: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PanelOptionsProps<T = any> {
|
||||||
|
options: T;
|
||||||
|
onChange: (options: T) => void;
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
|
import { ComponentClass } from 'react';
|
||||||
|
import { PanelProps, PanelOptionsProps } from './panel';
|
||||||
|
|
||||||
export interface PluginExports {
|
export interface PluginExports {
|
||||||
PanelCtrl?;
|
|
||||||
PanelComponent?: any;
|
|
||||||
Datasource?: any;
|
Datasource?: any;
|
||||||
QueryCtrl?: any;
|
QueryCtrl?: any;
|
||||||
ConfigCtrl?: any;
|
ConfigCtrl?: any;
|
||||||
AnnotationsQueryCtrl?: any;
|
AnnotationsQueryCtrl?: any;
|
||||||
PanelOptions?: any;
|
|
||||||
ExploreQueryField?: any;
|
ExploreQueryField?: any;
|
||||||
ExploreStartPage?: any;
|
ExploreStartPage?: any;
|
||||||
|
|
||||||
|
// Panel plugin
|
||||||
|
PanelCtrl?;
|
||||||
|
PanelComponent?: ComponentClass<PanelProps>;
|
||||||
|
PanelOptionsComponent: ComponentClass<PanelOptionsProps>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PanelPlugin {
|
export interface PanelPlugin {
|
||||||
|
@ -8,6 +8,111 @@ import 'vendor/flot/jquery.flot.time';
|
|||||||
// Types
|
// Types
|
||||||
import { TimeRange, TimeSeriesVMs } from 'app/types';
|
import { TimeRange, TimeSeriesVMs } from 'app/types';
|
||||||
|
|
||||||
|
interface GraphProps {
|
||||||
|
timeSeries: TimeSeriesVMs;
|
||||||
|
timeRange: TimeRange;
|
||||||
|
showLines?: boolean;
|
||||||
|
showPoints?: boolean;
|
||||||
|
showBars?: boolean;
|
||||||
|
size?: { width: number; height: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Graph extends PureComponent<GraphProps> {
|
||||||
|
static defaultProps = {
|
||||||
|
showLines: true,
|
||||||
|
showPoints: false,
|
||||||
|
showBars: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
element: any;
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: GraphProps) {
|
||||||
|
if (
|
||||||
|
prevProps.timeSeries !== this.props.timeSeries ||
|
||||||
|
prevProps.timeRange !== this.props.timeRange ||
|
||||||
|
prevProps.size !== this.props.size
|
||||||
|
) {
|
||||||
|
this.draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
const { size, timeSeries, timeRange, showLines, showBars, showPoints } = this.props;
|
||||||
|
|
||||||
|
if (!size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ticks = (size.width || 0) / 100;
|
||||||
|
const min = timeRange.from.valueOf();
|
||||||
|
const max = timeRange.to.valueOf();
|
||||||
|
|
||||||
|
const flotOptions = {
|
||||||
|
legend: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
series: {
|
||||||
|
lines: {
|
||||||
|
show: showLines,
|
||||||
|
linewidth: 1,
|
||||||
|
zero: false,
|
||||||
|
},
|
||||||
|
points: {
|
||||||
|
show: showPoints,
|
||||||
|
fill: 1,
|
||||||
|
fillColor: false,
|
||||||
|
radius: 2,
|
||||||
|
},
|
||||||
|
bars: {
|
||||||
|
show: showBars,
|
||||||
|
fill: 1,
|
||||||
|
barWidth: 1,
|
||||||
|
zero: false,
|
||||||
|
lineWidth: 0,
|
||||||
|
},
|
||||||
|
shadowSize: 0,
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
mode: 'time',
|
||||||
|
min: min,
|
||||||
|
max: max,
|
||||||
|
label: 'Datetime',
|
||||||
|
ticks: ticks,
|
||||||
|
timeformat: time_format(ticks, min, max),
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
minBorderMargin: 0,
|
||||||
|
markings: [],
|
||||||
|
backgroundColor: null,
|
||||||
|
borderWidth: 0,
|
||||||
|
// hoverable: true,
|
||||||
|
clickable: true,
|
||||||
|
color: '#a1a1a1',
|
||||||
|
margin: { left: 0, right: 0 },
|
||||||
|
labelMarginX: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
$.plot(this.element, timeSeries, flotOptions);
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Graph rendering error', err, flotOptions, timeSeries);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="graph-panel">
|
||||||
|
<div className="graph-panel__chart" ref={e => (this.element = e)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Copied from graph.ts
|
// Copied from graph.ts
|
||||||
function time_format(ticks, min, max) {
|
function time_format(ticks, min, max) {
|
||||||
if (min && max && ticks) {
|
if (min && max && ticks) {
|
||||||
@ -34,91 +139,4 @@ function time_format(ticks, min, max) {
|
|||||||
return '%H:%M';
|
return '%H:%M';
|
||||||
}
|
}
|
||||||
|
|
||||||
const FLOT_OPTIONS = {
|
|
||||||
legend: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
series: {
|
|
||||||
lines: {
|
|
||||||
linewidth: 1,
|
|
||||||
zero: false,
|
|
||||||
},
|
|
||||||
shadowSize: 0,
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
minBorderMargin: 0,
|
|
||||||
markings: [],
|
|
||||||
backgroundColor: null,
|
|
||||||
borderWidth: 0,
|
|
||||||
// hoverable: true,
|
|
||||||
clickable: true,
|
|
||||||
color: '#a1a1a1',
|
|
||||||
margin: { left: 0, right: 0 },
|
|
||||||
labelMarginX: 0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
interface GraphProps {
|
|
||||||
timeSeries: TimeSeriesVMs;
|
|
||||||
timeRange: TimeRange;
|
|
||||||
size?: { width: number; height: number };
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Graph extends PureComponent<GraphProps> {
|
|
||||||
element: any;
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps: GraphProps) {
|
|
||||||
if (
|
|
||||||
prevProps.timeSeries !== this.props.timeSeries ||
|
|
||||||
prevProps.timeRange !== this.props.timeRange ||
|
|
||||||
prevProps.size !== this.props.size
|
|
||||||
) {
|
|
||||||
this.draw();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.draw();
|
|
||||||
}
|
|
||||||
|
|
||||||
draw() {
|
|
||||||
const { size, timeSeries, timeRange } = this.props;
|
|
||||||
|
|
||||||
if (!size) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ticks = (size.width || 0) / 100;
|
|
||||||
const min = timeRange.from.valueOf();
|
|
||||||
const max = timeRange.to.valueOf();
|
|
||||||
|
|
||||||
const dynamicOptions = {
|
|
||||||
xaxis: {
|
|
||||||
mode: 'time',
|
|
||||||
min: min,
|
|
||||||
max: max,
|
|
||||||
label: 'Datetime',
|
|
||||||
ticks: ticks,
|
|
||||||
timeformat: time_format(ticks, min, max),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
...FLOT_OPTIONS,
|
|
||||||
...dynamicOptions,
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('plot', timeSeries, options);
|
|
||||||
$.plot(this.element, timeSeries, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="graph-panel">
|
|
||||||
<div className="graph-panel__chart" ref={e => (this.element = e)} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withSize()(Graph);
|
export default withSize()(Graph);
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
<base href="[[.AppSubUrl]]/" />
|
<base href="[[.AppSubUrl]]/" />
|
||||||
|
|
||||||
<link rel="stylesheet" href="public/build/grafana.[[ .Theme ]].css?v[[ .BuildVersion ]]">
|
<link rel="stylesheet" href="public/build/grafana.[[ .Theme ]].<%= webpack.hash %>.css">
|
||||||
|
|
||||||
<link rel="icon" type="image/png" href="public/img/fav32.png">
|
<link rel="icon" type="image/png" href="public/img/fav32.png">
|
||||||
<link rel="mask-icon" href="public/img/grafana_mask_icon.svg" color="#F05A28">
|
<link rel="mask-icon" href="public/img/grafana_mask_icon.svg" color="#F05A28">
|
@ -15,7 +15,7 @@
|
|||||||
<link rel="mask-icon" href="public/img/grafana_mask_icon.svg" color="#F05A28">
|
<link rel="mask-icon" href="public/img/grafana_mask_icon.svg" color="#F05A28">
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="public/img/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="public/img/apple-touch-icon.png">
|
||||||
|
|
||||||
<link rel="stylesheet" href="public/build/grafana.[[ .Theme ]].css?v[[ .BuildVersion ]]+[[ .BuildCommit ]]">
|
<link rel="stylesheet" href="public/build/grafana.[[ .Theme ]].<%= webpack.hash %>.css">
|
||||||
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
@ -22,10 +22,10 @@ echo "current dir: $(pwd)"
|
|||||||
|
|
||||||
if [ "$CIRCLE_TAG" != "" ]; then
|
if [ "$CIRCLE_TAG" != "" ]; then
|
||||||
echo "Building releases from tag $CIRCLE_TAG"
|
echo "Building releases from tag $CIRCLE_TAG"
|
||||||
OPT="-includeBuildNumber=false ${EXTRA_OPTS}"
|
OPT="-includeBuildId=false ${EXTRA_OPTS}"
|
||||||
else
|
else
|
||||||
echo "Building incremental build for $CIRCLE_BRANCH"
|
echo "Building incremental build for $CIRCLE_BRANCH"
|
||||||
OPT="-buildNumber=${CIRCLE_BUILD_NUM} ${EXTRA_OPTS}"
|
OPT="-buildId=${CIRCLE_WORKFLOW_ID} ${EXTRA_OPTS}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Build arguments: $OPT"
|
echo "Build arguments: $OPT"
|
||||||
|
@ -18,10 +18,10 @@ echo "current dir: $(pwd)"
|
|||||||
|
|
||||||
if [ "$CIRCLE_TAG" != "" ]; then
|
if [ "$CIRCLE_TAG" != "" ]; then
|
||||||
echo "Building releases from tag $CIRCLE_TAG"
|
echo "Building releases from tag $CIRCLE_TAG"
|
||||||
OPT="-includeBuildNumber=false ${EXTRA_OPTS}"
|
OPT="-includeBuildId=false ${EXTRA_OPTS}"
|
||||||
else
|
else
|
||||||
echo "Building incremental build for $CIRCLE_BRANCH"
|
echo "Building incremental build for $CIRCLE_BRANCH"
|
||||||
OPT="-buildNumber=${CIRCLE_BUILD_NUM} ${EXTRA_OPTS}"
|
OPT="-buildId=${CIRCLE_WORKFLOW_ID} ${EXTRA_OPTS}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Build arguments: $OPT"
|
echo "Build arguments: $OPT"
|
||||||
|
@ -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
|
# 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"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
var baseUri string = "https://grafana.com/api"
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var version string
|
var version string
|
||||||
var whatsNewUrl string
|
var whatsNewUrl string
|
||||||
var releaseNotesUrl string
|
var releaseNotesUrl string
|
||||||
var dryRun bool
|
var dryRun bool
|
||||||
|
var enterprise bool
|
||||||
|
var fromLocal bool
|
||||||
|
var nightly bool
|
||||||
var apiKey string
|
var apiKey string
|
||||||
|
|
||||||
flag.StringVar(&version, "version", "", "Grafana version (ex: --version v5.2.0-beta1)")
|
flag.StringVar(&version, "version", "", "Grafana version (ex: --version v5.2.0-beta1)")
|
||||||
@ -21,20 +22,69 @@ func main() {
|
|||||||
flag.StringVar(&releaseNotesUrl, "rn", "", "Grafana version (ex: --rn https://community.grafana.com/t/release-notes-v5-2-x/7894)")
|
flag.StringVar(&releaseNotesUrl, "rn", "", "Grafana version (ex: --rn https://community.grafana.com/t/release-notes-v5-2-x/7894)")
|
||||||
flag.StringVar(&apiKey, "apikey", "", "Grafana.com API key (ex: --apikey ABCDEF)")
|
flag.StringVar(&apiKey, "apikey", "", "Grafana.com API key (ex: --apikey ABCDEF)")
|
||||||
flag.BoolVar(&dryRun, "dry-run", false, "--dry-run")
|
flag.BoolVar(&dryRun, "dry-run", false, "--dry-run")
|
||||||
|
flag.BoolVar(&enterprise, "enterprise", false, "--enterprise")
|
||||||
|
flag.BoolVar(&fromLocal, "from-local", false, "--from-local (builds will be tagged as nightly)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
nightly = fromLocal
|
||||||
|
|
||||||
if len(os.Args) == 1 {
|
if len(os.Args) == 1 {
|
||||||
fmt.Println("Usage: go run publisher.go main.go --version <v> --wn <what's new url> --rn <release notes url> --apikey <api key> --dry-run false")
|
fmt.Println("Usage: go run publisher.go main.go --version <v> --wn <what's new url> --rn <release notes url> --apikey <api key> --dry-run false --enterprise false --nightly false")
|
||||||
fmt.Println("example: go run publisher.go main.go --version v5.2.0-beta2 --wn http://docs.grafana.org/guides/whats-new-in-v5-2/ --rn https://community.grafana.com/t/release-notes-v5-2-x/7894 --apikey ASDF123 --dry-run true")
|
fmt.Println("example: go run publisher.go main.go --version v5.2.0-beta2 --wn http://docs.grafana.org/guides/whats-new-in-v5-2/ --rn https://community.grafana.com/t/release-notes-v5-2-x/7894 --apikey ASDF123 --dry-run --enterprise")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if dryRun {
|
if dryRun {
|
||||||
log.Println("Dry-run has been enabled.")
|
log.Println("Dry-run has been enabled.")
|
||||||
}
|
}
|
||||||
|
var baseUrl string
|
||||||
|
var builder releaseBuilder
|
||||||
|
var product string
|
||||||
|
|
||||||
p := publisher{apiKey: apiKey}
|
if fromLocal {
|
||||||
if err := p.doRelease(version, whatsNewUrl, releaseNotesUrl, dryRun); err != nil {
|
path, _ := os.Getwd()
|
||||||
|
builder = releaseLocalSources{
|
||||||
|
path: path,
|
||||||
|
artifactConfigurations: buildArtifactConfigurations,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
builder = releaseFromExternalContent{
|
||||||
|
getter: getHttpContents{},
|
||||||
|
rawVersion: version,
|
||||||
|
artifactConfigurations: buildArtifactConfigurations,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
archiveProviderRoot := "https://s3-us-west-2.amazonaws.com"
|
||||||
|
|
||||||
|
if enterprise {
|
||||||
|
product = "grafana-enterprise"
|
||||||
|
baseUrl = createBaseUrl(archiveProviderRoot, "grafana-enterprise-releases", product, nightly)
|
||||||
|
} else {
|
||||||
|
product = "grafana"
|
||||||
|
baseUrl = createBaseUrl(archiveProviderRoot, "grafana-releases", product, nightly)
|
||||||
|
}
|
||||||
|
|
||||||
|
p := publisher{
|
||||||
|
apiKey: apiKey,
|
||||||
|
apiUri: "https://grafana.com/api",
|
||||||
|
product: product,
|
||||||
|
dryRun: dryRun,
|
||||||
|
enterprise: enterprise,
|
||||||
|
baseArchiveUrl: baseUrl,
|
||||||
|
builder: builder,
|
||||||
|
}
|
||||||
|
if err := p.doRelease(whatsNewUrl, releaseNotesUrl, nightly); err != nil {
|
||||||
log.Fatalf("error: %v", err)
|
log.Fatalf("error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
func createBaseUrl(root string, bucketName string, product string, nightly bool) string {
|
||||||
|
var subPath string
|
||||||
|
if nightly {
|
||||||
|
subPath = "master"
|
||||||
|
} else {
|
||||||
|
subPath = "release"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s/%s/%s/%s", root, bucketName, subPath, product)
|
||||||
|
}
|
||||||
|
@ -13,52 +13,46 @@ import (
|
|||||||
|
|
||||||
type publisher struct {
|
type publisher struct {
|
||||||
apiKey string
|
apiKey string
|
||||||
|
apiUri string
|
||||||
|
product string
|
||||||
|
dryRun bool
|
||||||
|
enterprise bool
|
||||||
|
baseArchiveUrl string
|
||||||
|
builder releaseBuilder
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *publisher) doRelease(version string, whatsNewUrl string, releaseNotesUrl string, dryRun bool) error {
|
type releaseBuilder interface {
|
||||||
currentRelease, err := newRelease(version, whatsNewUrl, releaseNotesUrl, buildArtifactConfigurations, getHttpContents{})
|
prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string, nightly bool) (*release, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *publisher) doRelease(whatsNewUrl string, releaseNotesUrl string, nightly bool) error {
|
||||||
|
currentRelease, err := p.builder.prepareRelease(p.baseArchiveUrl, whatsNewUrl, releaseNotesUrl, nightly)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if dryRun {
|
|
||||||
relJson, err := json.Marshal(currentRelease)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Println(string(relJson))
|
|
||||||
|
|
||||||
for _, b := range currentRelease.Builds {
|
|
||||||
artifactJson, err := json.Marshal(b)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Println(string(artifactJson))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := p.postRelease(currentRelease); err != nil {
|
if err := p.postRelease(currentRelease); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *publisher) postRelease(r *release) error {
|
func (p *publisher) postRelease(r *release) error {
|
||||||
err := p.postRequest("/grafana/versions", r, fmt.Sprintf("Create Release %s", r.Version))
|
err := p.postRequest("/versions", r, fmt.Sprintf("Create Release %s", r.Version))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = p.postRequest("/grafana/versions/"+r.Version, r, fmt.Sprintf("Update Release %s", r.Version))
|
err = p.postRequest("/versions/"+r.Version, r, fmt.Sprintf("Update Release %s", r.Version))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, b := range r.Builds {
|
for _, b := range r.Builds {
|
||||||
err = p.postRequest(fmt.Sprintf("/grafana/versions/%s/packages", r.Version), b, fmt.Sprintf("Create Build %s %s", b.Os, b.Arch))
|
err = p.postRequest(fmt.Sprintf("/versions/%s/packages", r.Version), b, fmt.Sprintf("Create Build %s %s", b.Os, b.Arch))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = p.postRequest(fmt.Sprintf("/grafana/versions/%s/packages/%s/%s", r.Version, b.Arch, b.Os), b, fmt.Sprintf("Update Build %s %s", b.Os, b.Arch))
|
err = p.postRequest(fmt.Sprintf("/versions/%s/packages/%s/%s", r.Version, b.Arch, b.Os), b, fmt.Sprintf("Update Build %s %s", b.Os, b.Arch))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -67,15 +61,13 @@ func (p *publisher) postRelease(r *release) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseArhiveUrl = "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana"
|
|
||||||
|
|
||||||
type buildArtifact struct {
|
type buildArtifact struct {
|
||||||
os string
|
os string
|
||||||
arch string
|
arch string
|
||||||
urlPostfix string
|
urlPostfix string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t buildArtifact) getUrl(version string, isBeta bool) string {
|
func (t buildArtifact) getUrl(baseArchiveUrl, version string, isBeta bool) string {
|
||||||
prefix := "-"
|
prefix := "-"
|
||||||
rhelReleaseExtra := ""
|
rhelReleaseExtra := ""
|
||||||
|
|
||||||
@ -87,7 +79,7 @@ func (t buildArtifact) getUrl(version string, isBeta bool) string {
|
|||||||
rhelReleaseExtra = "-1"
|
rhelReleaseExtra = "-1"
|
||||||
}
|
}
|
||||||
|
|
||||||
url := strings.Join([]string{baseArhiveUrl, prefix, version, rhelReleaseExtra, t.urlPostfix}, "")
|
url := strings.Join([]string{baseArchiveUrl, prefix, version, rhelReleaseExtra, t.urlPostfix}, "")
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,48 +141,32 @@ var buildArtifactConfigurations = []buildArtifact{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRelease(rawVersion string, whatsNewUrl string, releaseNotesUrl string, artifactConfigurations []buildArtifact, getter urlGetter) (*release, error) {
|
func newBuild(baseArchiveUrl string, ba buildArtifact, version string, isBeta bool, sha256 string) build {
|
||||||
version := rawVersion[1:]
|
|
||||||
now := time.Now()
|
|
||||||
isBeta := strings.Contains(version, "beta")
|
|
||||||
|
|
||||||
builds := []build{}
|
|
||||||
for _, ba := range artifactConfigurations {
|
|
||||||
sha256, err := getter.getContents(fmt.Sprintf("%s.sha256", ba.getUrl(version, isBeta)))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
builds = append(builds, newBuild(ba, version, isBeta, sha256))
|
|
||||||
}
|
|
||||||
|
|
||||||
r := release{
|
|
||||||
Version: version,
|
|
||||||
ReleaseDate: time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local),
|
|
||||||
Stable: !isBeta,
|
|
||||||
Beta: isBeta,
|
|
||||||
Nightly: false,
|
|
||||||
WhatsNewUrl: whatsNewUrl,
|
|
||||||
ReleaseNotesUrl: releaseNotesUrl,
|
|
||||||
Builds: builds,
|
|
||||||
}
|
|
||||||
return &r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newBuild(ba buildArtifact, version string, isBeta bool, sha256 string) build {
|
|
||||||
return build{
|
return build{
|
||||||
Os: ba.os,
|
Os: ba.os,
|
||||||
Url: ba.getUrl(version, isBeta),
|
Url: ba.getUrl(baseArchiveUrl, version, isBeta),
|
||||||
Sha256: sha256,
|
Sha256: sha256,
|
||||||
Arch: ba.arch,
|
Arch: ba.arch,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *publisher) apiUrl(url string) string {
|
||||||
|
return fmt.Sprintf("%s/%s%s", p.apiUri, p.product, url)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *publisher) postRequest(url string, obj interface{}, desc string) error {
|
func (p *publisher) postRequest(url string, obj interface{}, desc string) error {
|
||||||
jsonBytes, err := json.Marshal(obj)
|
jsonBytes, err := json.Marshal(obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
req, err := http.NewRequest(http.MethodPost, baseUri+url, bytes.NewReader(jsonBytes))
|
|
||||||
|
if p.dryRun {
|
||||||
|
log.Println(fmt.Sprintf("POST to %s:", p.apiUrl(url)))
|
||||||
|
log.Println(string(jsonBytes))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, p.apiUrl(url), bytes.NewReader(jsonBytes))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -243,24 +219,3 @@ type build struct {
|
|||||||
Sha256 string `json:"sha256"`
|
Sha256 string `json:"sha256"`
|
||||||
Arch string `json:"arch"`
|
Arch string `json:"arch"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type urlGetter interface {
|
|
||||||
getContents(url string) (string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type getHttpContents struct{}
|
|
||||||
|
|
||||||
func (getHttpContents) getContents(url string) (string, error) {
|
|
||||||
response, err := http.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer response.Body.Close()
|
|
||||||
all, err := ioutil.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(all), nil
|
|
||||||
}
|
|
||||||
|
@ -2,7 +2,7 @@ package main
|
|||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
func TestNewRelease(t *testing.T) {
|
func TestPreparingReleaseFromRemote(t *testing.T) {
|
||||||
versionIn := "v5.2.0-beta1"
|
versionIn := "v5.2.0-beta1"
|
||||||
expectedVersion := "5.2.0-beta1"
|
expectedVersion := "5.2.0-beta1"
|
||||||
whatsNewUrl := "https://whatsnews.foo/"
|
whatsNewUrl := "https://whatsnews.foo/"
|
||||||
@ -11,7 +11,15 @@ func TestNewRelease(t *testing.T) {
|
|||||||
expectedOs := "linux"
|
expectedOs := "linux"
|
||||||
buildArtifacts := []buildArtifact{{expectedOs,expectedArch, ".linux-amd64.tar.gz"}}
|
buildArtifacts := []buildArtifact{{expectedOs,expectedArch, ".linux-amd64.tar.gz"}}
|
||||||
|
|
||||||
rel, _ := newRelease(versionIn, whatsNewUrl, relNotesUrl, buildArtifacts, mockHttpGetter{})
|
var builder releaseBuilder
|
||||||
|
|
||||||
|
builder = releaseFromExternalContent{
|
||||||
|
getter: mockHttpGetter{},
|
||||||
|
rawVersion: versionIn,
|
||||||
|
artifactConfigurations: buildArtifactConfigurations,
|
||||||
|
}
|
||||||
|
|
||||||
|
rel, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana", whatsNewUrl, relNotesUrl, false)
|
||||||
|
|
||||||
if !rel.Beta || rel.Stable {
|
if !rel.Beta || rel.Stable {
|
||||||
t.Errorf("%s should have been tagged as beta (not stable), but wasn't .", versionIn)
|
t.Errorf("%s should have been tagged as beta (not stable), but wasn't .", versionIn)
|
||||||
@ -41,3 +49,71 @@ type mockHttpGetter struct{}
|
|||||||
func (mockHttpGetter) getContents(url string) (string, error) {
|
func (mockHttpGetter) getContents(url string) (string, error) {
|
||||||
return url, nil
|
return url, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func TestPreparingReleaseFromLocal(t *testing.T) {
|
||||||
|
whatsNewUrl := "https://whatsnews.foo/"
|
||||||
|
relNotesUrl := "https://relnotes.foo/"
|
||||||
|
expectedVersion := "5.4.0-123pre1"
|
||||||
|
expectedBuilds := 4
|
||||||
|
|
||||||
|
var builder releaseBuilder
|
||||||
|
testDataPath := "testdata"
|
||||||
|
builder = releaseLocalSources{
|
||||||
|
path: testDataPath,
|
||||||
|
artifactConfigurations: buildArtifactConfigurations,
|
||||||
|
}
|
||||||
|
|
||||||
|
relAll, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-enterprise-releases/master/grafana-enterprise", whatsNewUrl, relNotesUrl, true)
|
||||||
|
|
||||||
|
if relAll.Stable || !relAll.Nightly {
|
||||||
|
t.Error("Expected a nightly release but wasn't.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if relAll.ReleaseNotesUrl != relNotesUrl {
|
||||||
|
t.Errorf("expected releaseNotesUrl to be %s, but it was %s", relNotesUrl, relAll.ReleaseNotesUrl)
|
||||||
|
}
|
||||||
|
if relAll.WhatsNewUrl != whatsNewUrl {
|
||||||
|
t.Errorf("expected whatsNewUrl to be %s, but it was %s", whatsNewUrl, relAll.WhatsNewUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
if relAll.Beta {
|
||||||
|
t.Errorf("Expected release to be nightly, not beta.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if relAll.Version != expectedVersion {
|
||||||
|
t.Errorf("Expected version=%s, but got=%s", expectedVersion, relAll.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(relAll.Builds) != expectedBuilds {
|
||||||
|
t.Errorf("Expected %v builds, but was %v", expectedBuilds, len(relAll.Builds))
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedArch := "amd64"
|
||||||
|
expectedOs := "win"
|
||||||
|
|
||||||
|
builder = releaseLocalSources{
|
||||||
|
path: testDataPath,
|
||||||
|
artifactConfigurations: []buildArtifact{{
|
||||||
|
os: expectedOs,
|
||||||
|
arch: expectedArch,
|
||||||
|
urlPostfix: ".windows-amd64.zip",
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
relOne, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-enterprise-releases/master/grafana-enterprise", whatsNewUrl, relNotesUrl, true)
|
||||||
|
|
||||||
|
if len(relOne.Builds) != 1 {
|
||||||
|
t.Errorf("Expected 1 artifact, but was %v", len(relOne.Builds))
|
||||||
|
}
|
||||||
|
|
||||||
|
build := relOne.Builds[0]
|
||||||
|
|
||||||
|
if build.Arch != expectedArch {
|
||||||
|
t.Fatalf("Expected arch to be %s, but was %s", expectedArch, build.Arch)
|
||||||
|
}
|
||||||
|
|
||||||
|
if build.Os != expectedOs {
|
||||||
|
t.Fatalf("Expected os to be %s, but was %s", expectedOs, build.Os)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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$/,
|
test: /\.html$/,
|
||||||
exclude: /index\.template.html/,
|
exclude: /(index|error)\-template\.html/,
|
||||||
use: [
|
use: [
|
||||||
{ loader: 'ngtemplate-loader?relativeTo=' + (path.resolve(__dirname, '../../public')) + '&prefix=public' },
|
{ loader: 'ngtemplate-loader?relativeTo=' + (path.resolve(__dirname, '../../public')) + '&prefix=public' },
|
||||||
{
|
{
|
||||||
|
@ -80,11 +80,16 @@ module.exports = merge(common, {
|
|||||||
plugins: [
|
plugins: [
|
||||||
new CleanWebpackPlugin('../../public/build', { allowExternal: true }),
|
new CleanWebpackPlugin('../../public/build', { allowExternal: true }),
|
||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
filename: "grafana.[name].css"
|
filename: "grafana.[name].[hash].css"
|
||||||
|
}),
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
filename: path.resolve(__dirname, '../../public/views/error.html'),
|
||||||
|
template: path.resolve(__dirname, '../../public/views/error-template.html'),
|
||||||
|
inject: 'false',
|
||||||
}),
|
}),
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
filename: path.resolve(__dirname, '../../public/views/index.html'),
|
filename: path.resolve(__dirname, '../../public/views/index.html'),
|
||||||
template: path.resolve(__dirname, '../../public/views/index.template.html'),
|
template: path.resolve(__dirname, '../../public/views/index-template.html'),
|
||||||
inject: 'body',
|
inject: 'body',
|
||||||
chunks: ['manifest', 'vendor', 'app'],
|
chunks: ['manifest', 'vendor', 'app'],
|
||||||
}),
|
}),
|
||||||
|
@ -83,7 +83,7 @@ module.exports = merge(common, {
|
|||||||
new CleanWebpackPlugin('../public/build', { allowExternal: true }),
|
new CleanWebpackPlugin('../public/build', { allowExternal: true }),
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
filename: path.resolve(__dirname, '../../public/views/index.html'),
|
filename: path.resolve(__dirname, '../../public/views/index.html'),
|
||||||
template: path.resolve(__dirname, '../../public/views/index.template.html'),
|
template: path.resolve(__dirname, '../../public/views/index-template.html'),
|
||||||
inject: 'body',
|
inject: 'body',
|
||||||
alwaysWriteToDisk: true,
|
alwaysWriteToDisk: true,
|
||||||
}),
|
}),
|
||||||
|
@ -71,15 +71,20 @@ module.exports = merge(common, {
|
|||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
filename: "grafana.[name].css"
|
filename: "grafana.[name].[hash].css"
|
||||||
}),
|
}),
|
||||||
new ngAnnotatePlugin(),
|
new ngAnnotatePlugin(),
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
filename: path.resolve(__dirname, '../../public/views/index.html'),
|
filename: path.resolve(__dirname, '../../public/views/index.html'),
|
||||||
template: path.resolve(__dirname, '../../public/views/index.template.html'),
|
template: path.resolve(__dirname, '../../public/views/index-template.html'),
|
||||||
inject: 'body',
|
inject: 'body',
|
||||||
chunks: ['vendor', 'app'],
|
chunks: ['vendor', 'app'],
|
||||||
}),
|
}),
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
filename: path.resolve(__dirname, '../../public/views/error.html'),
|
||||||
|
template: path.resolve(__dirname, '../../public/views/error-template.html'),
|
||||||
|
inject: false,
|
||||||
|
}),
|
||||||
function () {
|
function () {
|
||||||
this.hooks.done.tap('Done', function (stats) {
|
this.hooks.done.tap('Done', function (stats) {
|
||||||
if (stats.compilation.errors && stats.compilation.errors.length) {
|
if (stats.compilation.errors && stats.compilation.errors.length) {
|
||||||
|
Loading…
Reference in New Issue
Block a user