From e1a15bab21f47c3ac72ef1e513272ecee0c00dcb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 May 2022 15:21:21 +0100 Subject: [PATCH 001/440] Update dependency @microsoft/api-extractor-model to v7.17.2 (#47917) Co-authored-by: Renovate Bot --- package.json | 2 +- yarn.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 42119f0a598..d56cdbe99d8 100644 --- a/package.json +++ b/package.json @@ -394,7 +394,7 @@ "resolutions": { "underscore": "1.13.2", "@types/slate": "0.47.2", - "@microsoft/api-extractor-model": "7.16.0", + "@microsoft/api-extractor-model": "7.17.2", "@rushstack/node-core-library": "3.45.1", "@rushstack/rig-package": "0.3.8", "@rushstack/ts-command-line": "4.10.7", diff --git a/yarn.lock b/yarn.lock index b8ef2975030..b968a6f77fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6252,14 +6252,14 @@ __metadata: languageName: node linkType: hard -"@microsoft/api-extractor-model@npm:7.16.0": - version: 7.16.0 - resolution: "@microsoft/api-extractor-model@npm:7.16.0" +"@microsoft/api-extractor-model@npm:7.17.2": + version: 7.17.2 + resolution: "@microsoft/api-extractor-model@npm:7.17.2" dependencies: - "@microsoft/tsdoc": 0.13.2 - "@microsoft/tsdoc-config": ~0.15.2 - "@rushstack/node-core-library": 3.45.1 - checksum: ba1bf057f5eed213aea45887a93c0cdabd0d7c9da2644aeb7460748fa33a01f04b91507923e7ea511452a7dfb849498089a2c3a844986ffc4c2abf8ab6d831a6 + "@microsoft/tsdoc": 0.14.1 + "@microsoft/tsdoc-config": ~0.16.1 + "@rushstack/node-core-library": 3.45.4 + checksum: 94c1c63674d85bf69cff9abbf94a1b2d2f5b6e3b651b8483d8949e39424cb29df156b589a297ca19f85ad1ff6380389e58a03198e7fe1ec34b59d5cae2166de7 languageName: node linkType: hard From 5dabb55b3955e0335740ac920b71ad987979c5ec Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Tue, 3 May 2022 15:28:40 +0100 Subject: [PATCH 002/440] Navigation: Enable new navigation by default (#48447) --- conf/defaults.ini | 3 + .../trace-view-scrolling.spec.ts | 2 +- .../src/selectors/pages.ts | 2 +- .../CustomScrollbar/CustomScrollbar.tsx | 3 + .../components/NavBar/Next/NavBarNext.tsx | 56 ++++++++++--------- .../NavBar/Next/NavBarScrollContainer.tsx | 1 + public/app/features/explore/Explore.tsx | 1 + 7 files changed, 39 insertions(+), 29 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index ff30a4d640a..a36fdede73c 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -1152,6 +1152,9 @@ commandPalette = true # Use dynamic labels in CloudWatch datasource cloudWatchDynamicLabels = false +# New expandable navigation +newNavigation = true + # feature1 = true # feature2 = false diff --git a/e2e/various-suite/trace-view-scrolling.spec.ts b/e2e/various-suite/trace-view-scrolling.spec.ts index 69a8bb191f8..3342281afe5 100644 --- a/e2e/various-suite/trace-view-scrolling.spec.ts +++ b/e2e/various-suite/trace-view-scrolling.spec.ts @@ -26,7 +26,7 @@ describe('Trace view', () => { e2e.components.TraceViewer.spanBar() .its('length') .then((oldLength) => { - e2e.pages.Explore.General.scrollBar().scrollTo('center'); + e2e.pages.Explore.General.scrollView().children('.scrollbar-view').scrollTo('center'); // After scrolling we should load more spans e2e.components.TraceViewer.spanBar().its('length').should('be.gt', oldLength); diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index 6ac5d7fbe6e..6dff1d14cac 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -183,7 +183,7 @@ export const Pages = { container: 'data-testid Explore', graph: 'Explore Graph', table: 'Explore Table', - scrollBar: () => '.scrollbar-view', + scrollView: 'data-testid explorer scroll view', }, }, SoloPanel: { diff --git a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx index a5091495894..f860dccebd0 100644 --- a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx +++ b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx @@ -12,6 +12,7 @@ export type ScrollbarPosition = positionValues; interface Props { className?: string; + testId?: string; autoHide?: boolean; autoHideTimeout?: number; autoHeightMax?: string; @@ -33,6 +34,7 @@ export const CustomScrollbar: FC = ({ autoHideTimeout = 200, setScrollTop, className, + testId, autoHeightMin = '0', autoHeightMax = '100%', hideTracksWhenNotNeeded = false, @@ -118,6 +120,7 @@ export const CustomScrollbar: FC = ({ return ( { -
    - - - + + + - + +
      @@ -130,21 +130,21 @@ export const NavBarNext = React.memo(() => { {link.img && {`${link.text}} ))} - - {configItems.map((link, index) => ( - - {link.icon && } - {link.img && {`${link.text}} - - ))} -
    + {configItems.map((link, index) => ( + + {link.icon && } + {link.img && {`${link.text}} + + ))} +
+ @@ -210,9 +210,6 @@ const getStyles = (theme: GrafanaTheme2) => ({ display: 'flex', flexDirection: 'column', height: '100%', - '> *': { - height: theme.spacing(6), - }, [theme.breakpoints.down('md')]: { visibility: 'hidden', @@ -222,7 +219,12 @@ const getStyles = (theme: GrafanaTheme2) => ({ alignItems: 'stretch', display: 'flex', flexShrink: 0, + height: theme.spacing(6), justifyContent: 'stretch', + + [theme.breakpoints.down('md')]: { + visibility: 'hidden', + }, }), grafanaLogoInner: css({ alignItems: 'center', diff --git a/public/app/core/components/NavBar/Next/NavBarScrollContainer.tsx b/public/app/core/components/NavBar/Next/NavBarScrollContainer.tsx index 91cc70fb680..a1d14bb2c9b 100644 --- a/public/app/core/components/NavBar/Next/NavBarScrollContainer.tsx +++ b/public/app/core/components/NavBar/Next/NavBarScrollContainer.tsx @@ -77,6 +77,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ bottom: 0, }), scrollContent: css({ + flex: 1, position: 'relative', }), // override the scroll container position so that the scroll indicators diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 319d3801189..26f65b7eaf8 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -353,6 +353,7 @@ export class Explore extends React.PureComponent { return ( (this.scrollElement = scrollElement || undefined)} > From b231cbcf86ce11224640fbc37d162ec252ef6988 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Tue, 3 May 2022 15:57:24 +0100 Subject: [PATCH 003/440] Fix lockfile... (#48643) --- yarn.lock | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/yarn.lock b/yarn.lock index b968a6f77fb..76590239444 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6285,18 +6285,6 @@ __metadata: languageName: node linkType: hard -"@microsoft/tsdoc-config@npm:~0.15.2": - version: 0.15.2 - resolution: "@microsoft/tsdoc-config@npm:0.15.2" - dependencies: - "@microsoft/tsdoc": 0.13.2 - ajv: ~6.12.6 - jju: ~1.4.0 - resolve: ~1.19.0 - checksum: 85eb7808d4e4541199437f39e6aed235aaece0a6d0fd05c0b923067d494d20baca483fc6871880d09630f6d4e62b8bb99af0fde503eb2b2ded1b7ae5f74dfaf3 - languageName: node - linkType: hard - "@microsoft/tsdoc-config@npm:~0.16.1": version: 0.16.1 resolution: "@microsoft/tsdoc-config@npm:0.16.1" @@ -6316,13 +6304,6 @@ __metadata: languageName: node linkType: hard -"@microsoft/tsdoc@npm:0.13.2": - version: 0.13.2 - resolution: "@microsoft/tsdoc@npm:0.13.2" - checksum: 70948c5647495ef99752ff500e0f612c1fcf3476ea663ace19937e4d2f86fd78f0ad92ea5876d67e06b421f347d571b3d9e49c444935dc267768d5afd15581f8 - languageName: node - linkType: hard - "@microsoft/tsdoc@npm:0.14.1": version: 0.14.1 resolution: "@microsoft/tsdoc@npm:0.14.1" From 88eeb878a44933d7617c669a11b574323eb35b5c Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Tue, 3 May 2022 08:52:19 -0700 Subject: [PATCH 004/440] API: add stars HTTP endpoint (#48612) Co-authored-by: Ying WANG --- pkg/api/api.go | 1 + pkg/api/stars.go | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/pkg/api/api.go b/pkg/api/api.go index 560cd323037..595a8faa6d4 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -156,6 +156,7 @@ func (hs *HTTPServer) registerRoutes() { userRoute.Get("/orgs", routing.Wrap(hs.GetSignedInUserOrgList)) userRoute.Get("/teams", routing.Wrap(hs.GetSignedInUserTeamList)) + userRoute.Get("/stars", routing.Wrap(hs.GetStars)) userRoute.Post("/stars/dashboard/:id", routing.Wrap(hs.StarDashboard)) userRoute.Delete("/stars/dashboard/:id", routing.Wrap(hs.UnstarDashboard)) diff --git a/pkg/api/stars.go b/pkg/api/stars.go index e3594fc0eb7..fde1af2d4ab 100644 --- a/pkg/api/stars.go +++ b/pkg/api/stars.go @@ -9,6 +9,32 @@ import ( "github.com/grafana/grafana/pkg/web" ) +func (hs *HTTPServer) GetStars(c *models.ReqContext) response.Response { + query := models.GetUserStarsQuery{ + UserId: c.SignedInUser.UserId, + } + + err := hs.SQLStore.GetUserStars(c.Req.Context(), &query) + if err != nil { + return response.Error(500, "Failed to get user stars", err) + } + + iuserstars := query.Result + uids := []string{} + for dashboardId := range iuserstars { + query := &models.GetDashboardQuery{ + Id: dashboardId, + OrgId: c.OrgId, + } + err := hs.SQLStore.GetDashboard(c.Req.Context(), query) + if err != nil { + return response.Error(500, "Failed to get dashboard", err) + } + uids = append(uids, query.Result.Uid) + } + return response.JSON(200, uids) +} + func (hs *HTTPServer) StarDashboard(c *models.ReqContext) response.Response { id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { From 4ecd57f49c75d72927be62aba23a15dca02f8eb6 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Tue, 3 May 2022 18:02:20 +0200 Subject: [PATCH 005/440] Plugins: Introduce HTTP 207 Multi Status response to api/ds/query (#48550) * feature toggles * return HTTP 207 from ds/query * add ft check * add API test * add 207 check for qr * change to OR * revert check * add explicit toggle check for cloudwatch * remove unused import * remove from defaults.ini * add status codes to md and update swagger * new fangled http api tests pattern * update swagger * Update docs/sources/http_api/data_source.md Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> * add missing word and reformat Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> --- docs/sources/http_api/data_source.md | 10 ++++ .../src/types/featureToggles.gen.ts | 1 + pkg/api/docs/definitions/ds.go | 1 + pkg/api/metrics.go | 13 +++-- pkg/api/metrics_test.go | 51 +++++++++++++++++++ pkg/api/plugins_test.go | 12 ++++- pkg/services/featuremgmt/registry.go | 5 ++ pkg/services/featuremgmt/toggles_gen.go | 4 ++ public/api-merged.json | 46 +++++++++-------- public/api-spec.json | 46 +++++++++-------- .../datasource/cloudwatch/datasource.ts | 6 +++ 11 files changed, 150 insertions(+), 45 deletions(-) diff --git a/docs/sources/http_api/data_source.md b/docs/sources/http_api/data_source.md index 15c78d89d99..3261f0a6ebc 100644 --- a/docs/sources/http_api/data_source.md +++ b/docs/sources/http_api/data_source.md @@ -691,6 +691,16 @@ In addition, specific properties of each data source should be added in a reques } ``` +#### Status codes + +| Code | Description | +| ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 200 | All data source queries returned a successful response. | +| 400 | Bad request due to invalid JSON, missing content type, missing or invalid fields, etc. Or one or more data source queries were unsuccessful. Refer to the body for more details. | +| 403 | Access denied. | +| 404 | Either the data source or plugin required to fulfil the request could not be found. | +| 500 | Unexpected error. Refer to the body and/or server logs for more details. | + ## Deprecated resources The following resources have been deprecated. They will be removed in a future release. diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index bdba00d64ef..c00c802c03c 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -58,4 +58,5 @@ export interface FeatureToggles { commandPalette?: boolean; savedItems?: boolean; cloudWatchDynamicLabels?: boolean; + datasourceQueryMultiStatus?: boolean; } diff --git a/pkg/api/docs/definitions/ds.go b/pkg/api/docs/definitions/ds.go index a77449ba073..e37d465e453 100644 --- a/pkg/api/docs/definitions/ds.go +++ b/pkg/api/docs/definitions/ds.go @@ -14,6 +14,7 @@ import ( // // Responses: // 200: queryDataResponse +// 207: queryDataResponse // 401: unauthorisedError // 400: badRequestError // 403: forbiddenError diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index c37f371ce00..f9baf5a1373 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -31,7 +31,7 @@ func (hs *HTTPServer) QueryMetricsV2(c *models.ReqContext) response.Response { if err != nil { return hs.handleQueryMetricsError(err) } - return toJsonStreamingResponse(resp) + return hs.toJsonStreamingResponse(resp) } func (hs *HTTPServer) handleQueryMetricsError(err error) *response.NormalResponse { @@ -147,7 +147,7 @@ func (hs *HTTPServer) QueryMetricsFromDashboard(c *models.ReqContext) response.R if err != nil { return hs.handleQueryMetricsError(err) } - return toJsonStreamingResponse(resp) + return hs.toJsonStreamingResponse(resp) } // QueryMetrics returns query metrics @@ -198,11 +198,16 @@ func (hs *HTTPServer) QueryMetrics(c *models.ReqContext) response.Response { return response.JSON(statusCode, &legacyResp) } -func toJsonStreamingResponse(qdr *backend.QueryDataResponse) response.Response { +func (hs *HTTPServer) toJsonStreamingResponse(qdr *backend.QueryDataResponse) response.Response { + statusWhenError := http.StatusBadRequest + if hs.Features.IsEnabled(featuremgmt.FlagDatasourceQueryMultiStatus) { + statusWhenError = http.StatusMultiStatus + } + statusCode := http.StatusOK for _, res := range qdr.Responses { if res.Error != nil { - statusCode = http.StatusBadRequest + statusCode = statusWhenError } } diff --git a/pkg/api/metrics_test.go b/pkg/api/metrics_test.go index 0b4472b0d52..c7acbe032df 100644 --- a/pkg/api/metrics_test.go +++ b/pkg/api/metrics_test.go @@ -12,6 +12,7 @@ import ( acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" + "github.com/grafana/grafana/pkg/web/webtest" "golang.org/x/oauth2" @@ -19,12 +20,14 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes" datasources "github.com/grafana/grafana/pkg/services/datasources/service" "github.com/grafana/grafana/pkg/services/query" "github.com/grafana/grafana/pkg/services/secrets/fakes" "github.com/grafana/grafana/pkg/services/secrets/kvstore" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var ( @@ -500,3 +503,51 @@ func TestAPIEndpoint_Metrics_ParseDashboardQueryParams(t *testing.T) { }) } } + +// `/ds/query` endpoint test +func TestAPIEndpoint_Metrics_QueryMetricsV2(t *testing.T) { + qds := query.ProvideService( + nil, + nil, + nil, + &fakePluginRequestValidator{}, + &fakeDatasources.FakeDataSourceService{}, + &fakePluginClient{ + QueryDataHandlerFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + resp := backend.Responses{ + "A": backend.DataResponse{ + Error: fmt.Errorf("query failed"), + }, + } + return &backend.QueryDataResponse{Responses: resp}, nil + }, + }, + &fakeOAuthTokenService{}, + ) + serverFeatureEnabled := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.queryDataService = qds + hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagDatasourceQueryMultiStatus, true) + }) + serverFeatureDisabled := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.queryDataService = qds + hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagDatasourceQueryMultiStatus, false) + }) + + t.Run("Status code is 400 when data source response has an error and feature toggle is disabled", func(t *testing.T) { + req := serverFeatureDisabled.NewPostRequest("/api/ds/query", strings.NewReader(queryDatasourceInput)) + webtest.RequestWithSignedInUser(req, &models.SignedInUser{UserId: 1, OrgId: 1, OrgRole: models.ROLE_VIEWER}) + resp, err := serverFeatureDisabled.SendJSON(req) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("Status code is 207 when data source response has an error and feature toggle is enabled", func(t *testing.T) { + req := serverFeatureEnabled.NewPostRequest("/api/ds/query", strings.NewReader(queryDatasourceInput)) + webtest.RequestWithSignedInUser(req, &models.SignedInUser{UserId: 1, OrgId: 1, OrgRole: models.ROLE_VIEWER}) + resp, err := serverFeatureEnabled.SendJSON(req) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + require.Equal(t, http.StatusMultiStatus, resp.StatusCode) + }) +} diff --git a/pkg/api/plugins_test.go b/pkg/api/plugins_test.go index 3504d51e4cf..a28ec025750 100644 --- a/pkg/api/plugins_test.go +++ b/pkg/api/plugins_test.go @@ -249,9 +249,11 @@ type fakePluginClient struct { plugins.Client req *backend.CallResourceRequest + + backend.QueryDataHandlerFunc } -func (c *fakePluginClient) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { +func (c *fakePluginClient) CallResource(_ context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { c.req = req bytes, err := json.Marshal(map[string]interface{}{ "message": "hello", @@ -266,3 +268,11 @@ func (c *fakePluginClient) CallResource(ctx context.Context, req *backend.CallRe Body: bytes, }) } + +func (c *fakePluginClient) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + if c.QueryDataHandlerFunc != nil { + return c.QueryDataHandlerFunc.QueryData(ctx, req) + } + + return backend.NewQueryDataResponse(), nil +} diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 82618ed1872..04386a7940b 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -236,5 +236,10 @@ var ( Description: "Use dynamic labels instead of alias patterns in CloudWatch datasource", State: FeatureStateStable, }, + { + Name: "datasourceQueryMultiStatus", + Description: "Introduce HTTP 207 Multi Status for api/ds/query", + State: FeatureStateAlpha, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index db7b68edcbc..7309dce29ef 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -174,4 +174,8 @@ const ( // FlagCloudWatchDynamicLabels // Use dynamic labels instead of alias patterns in CloudWatch datasource FlagCloudWatchDynamicLabels = "cloudWatchDynamicLabels" + + // FlagDatasourceQueryMultiStatus + // Introduce HTTP 207 Multi Status for api/ds/query + FlagDatasourceQueryMultiStatus = "datasourceQueryMultiStatus" ) diff --git a/public/api-merged.json b/public/api-merged.json index 4fa6dd8353d..adcdd972323 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -4692,15 +4692,15 @@ "parameters": [ { "type": "string", - "x-go-name": "DatasourceID", - "name": "datasource_id", + "x-go-name": "PermissionID", + "name": "permissionId", "in": "path", "required": true }, { "type": "string", - "x-go-name": "PermissionID", - "name": "permissionId", + "x-go-name": "DatasourceID", + "name": "datasource_id", "in": "path", "required": true } @@ -4745,6 +4745,9 @@ "200": { "$ref": "#/responses/queryDataResponse" }, + "207": { + "$ref": "#/responses/queryDataResponse" + }, "400": { "$ref": "#/responses/badRequestError" }, @@ -8258,14 +8261,6 @@ "summary": "Add External Group.", "operationId": "addTeamGroupApi", "parameters": [ - { - "type": "integer", - "format": "int64", - "x-go-name": "TeamID", - "name": "teamId", - "in": "path", - "required": true - }, { "x-go-name": "Body", "name": "body", @@ -8274,6 +8269,14 @@ "schema": { "$ref": "#/definitions/TeamGroupMapping" } + }, + { + "type": "integer", + "format": "int64", + "x-go-name": "TeamID", + "name": "teamId", + "in": "path", + "required": true } ], "responses": { @@ -8307,16 +8310,16 @@ { "type": "integer", "format": "int64", - "x-go-name": "TeamID", - "name": "teamId", + "x-go-name": "GroupID", + "name": "groupId", "in": "path", "required": true }, { "type": "integer", "format": "int64", - "x-go-name": "GroupID", - "name": "groupId", + "x-go-name": "TeamID", + "name": "teamId", "in": "path", "required": true } @@ -10534,6 +10537,9 @@ "ApiKeyDTO": { "type": "object", "properties": { + "accessControl": { + "$ref": "#/definitions/Metadata" + }, "expiration": { "type": "string", "format": "date-time", @@ -10555,7 +10561,7 @@ "x-go-name": "Role" } }, - "x-go-package": "github.com/grafana/grafana/pkg/models" + "x-go-package": "github.com/grafana/grafana/pkg/api/dtos" }, "ApiRuleNode": { "type": "object", @@ -13632,7 +13638,7 @@ "properties": { "id": { "type": "string", - "x-go-name": "Id" + "x-go-name": "ID" }, "target": { "type": "string", @@ -13647,7 +13653,7 @@ "x-go-name": "Url" } }, - "x-go-package": "github.com/grafana/grafana/pkg/models" + "x-go-package": "github.com/grafana/grafana/pkg/services/preference" }, "NavbarPreference": { "type": "object", @@ -14739,7 +14745,7 @@ "x-go-name": "HomeTab" } }, - "x-go-package": "github.com/grafana/grafana/pkg/models" + "x-go-package": "github.com/grafana/grafana/pkg/services/preference" }, "Receiver": { "type": "object", diff --git a/public/api-spec.json b/public/api-spec.json index 5f993fcd6da..c9f744551dd 100644 --- a/public/api-spec.json +++ b/public/api-spec.json @@ -3754,15 +3754,15 @@ "parameters": [ { "type": "string", - "x-go-name": "DatasourceID", - "name": "datasource_id", + "x-go-name": "PermissionID", + "name": "permissionId", "in": "path", "required": true }, { "type": "string", - "x-go-name": "PermissionID", - "name": "permissionId", + "x-go-name": "DatasourceID", + "name": "datasource_id", "in": "path", "required": true } @@ -3807,6 +3807,9 @@ "200": { "$ref": "#/responses/queryDataResponse" }, + "207": { + "$ref": "#/responses/queryDataResponse" + }, "400": { "$ref": "#/responses/badRequestError" }, @@ -6667,14 +6670,6 @@ "summary": "Add External Group.", "operationId": "addTeamGroupApi", "parameters": [ - { - "type": "integer", - "format": "int64", - "x-go-name": "TeamID", - "name": "teamId", - "in": "path", - "required": true - }, { "x-go-name": "Body", "name": "body", @@ -6683,6 +6678,14 @@ "schema": { "$ref": "#/definitions/TeamGroupMapping" } + }, + { + "type": "integer", + "format": "int64", + "x-go-name": "TeamID", + "name": "teamId", + "in": "path", + "required": true } ], "responses": { @@ -6716,16 +6719,16 @@ { "type": "integer", "format": "int64", - "x-go-name": "TeamID", - "name": "teamId", + "x-go-name": "GroupID", + "name": "groupId", "in": "path", "required": true }, { "type": "integer", "format": "int64", - "x-go-name": "GroupID", - "name": "groupId", + "x-go-name": "TeamID", + "name": "teamId", "in": "path", "required": true } @@ -8626,6 +8629,9 @@ "ApiKeyDTO": { "type": "object", "properties": { + "accessControl": { + "$ref": "#/definitions/Metadata" + }, "expiration": { "type": "string", "format": "date-time", @@ -8647,7 +8653,7 @@ "x-go-name": "Role" } }, - "x-go-package": "github.com/grafana/grafana/pkg/models" + "x-go-package": "github.com/grafana/grafana/pkg/api/dtos" }, "BrandingOptionsDTO": { "type": "object", @@ -10715,7 +10721,7 @@ "properties": { "id": { "type": "string", - "x-go-name": "Id" + "x-go-name": "ID" }, "target": { "type": "string", @@ -10730,7 +10736,7 @@ "x-go-name": "Url" } }, - "x-go-package": "github.com/grafana/grafana/pkg/models" + "x-go-package": "github.com/grafana/grafana/pkg/services/preference" }, "NavbarPreference": { "type": "object", @@ -11191,7 +11197,7 @@ "x-go-name": "HomeTab" } }, - "x-go-package": "github.com/grafana/grafana/pkg/models" + "x-go-package": "github.com/grafana/grafana/pkg/services/preference" }, "RecordingRuleJSON": { "description": "RecordingRuleJSON is the external representation of a recording rule", diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 8b87e9a95a1..064ec3d7f5c 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -29,6 +29,8 @@ import { VariableWithMultiSupport } from 'app/features/variables/types'; import { store } from 'app/store/store'; import { AppNotificationTimeout } from 'app/types'; +import config from '../../../core/config'; + import { CloudWatchAnnotationSupport } from './annotationSupport'; import { SQLCompletionItemProvider } from './cloudwatch-sql/completion/CompletionItemProvider'; import { ThrottlingErrorMessage } from './components/ThrottlingErrorMessage'; @@ -669,6 +671,10 @@ export class CloudWatchDatasource return this.awsRequest(DS_QUERY_ENDPOINT, requestParams, headers).pipe( map((response) => resultsToDataFrames({ data: response })), catchError((err: FetchError) => { + if (config.featureToggles.datasourceQueryMultiStatus && err.status === 207) { + throw err; + } + if (err.status === 400) { throw err; } From 46e53cf42cbe062975699d04ed6057859118fdb9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 May 2022 17:17:55 +0100 Subject: [PATCH 006/440] Update dependency @rollup/plugin-node-resolve to v13.3.0 (#48645) Co-authored-by: Renovate Bot --- packages/grafana-data/package.json | 2 +- packages/grafana-e2e-selectors/package.json | 2 +- packages/grafana-e2e/package.json | 2 +- packages/grafana-runtime/package.json | 2 +- packages/grafana-schema/package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 33 +++++++++++++-------- 7 files changed, 27 insertions(+), 18 deletions(-) diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index ad1ebcc68f0..27fa4ca2950 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -45,7 +45,7 @@ "@grafana/tsconfig": "^1.2.0-rc1", "@rollup/plugin-commonjs": "21.0.2", "@rollup/plugin-json": "4.1.0", - "@rollup/plugin-node-resolve": "13.1.3", + "@rollup/plugin-node-resolve": "13.3.0", "@swc/helpers": "0.3.8", "@testing-library/dom": "8.13.0", "@testing-library/jest-dom": "5.16.2", diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 776d09e5500..d3897524868 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -25,7 +25,7 @@ }, "devDependencies": { "@rollup/plugin-commonjs": "21.0.2", - "@rollup/plugin-node-resolve": "13.1.3", + "@rollup/plugin-node-resolve": "13.3.0", "@types/node": "16.11.26", "rimraf": "3.0.2", "rollup": "2.70.1", diff --git a/packages/grafana-e2e/package.json b/packages/grafana-e2e/package.json index 6fb70e16064..05056bb0f8f 100644 --- a/packages/grafana-e2e/package.json +++ b/packages/grafana-e2e/package.json @@ -32,7 +32,7 @@ }, "devDependencies": { "@rollup/plugin-commonjs": "21.0.2", - "@rollup/plugin-node-resolve": "13.1.3", + "@rollup/plugin-node-resolve": "13.3.0", "@types/chrome-remote-interface": "0.31.4", "@types/lodash": "4.14.181", "@types/node": "16.11.26", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index e81c2487311..135c761f0be 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -37,7 +37,7 @@ "devDependencies": { "@grafana/tsconfig": "^1.2.0-rc1", "@rollup/plugin-commonjs": "21.0.2", - "@rollup/plugin-node-resolve": "13.1.3", + "@rollup/plugin-node-resolve": "13.3.0", "@testing-library/dom": "8.13.0", "@testing-library/react": "12.1.4", "@testing-library/user-event": "14.1.1", diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index 16db7c725b7..917785c200e 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -25,7 +25,7 @@ "@grafana/tsconfig": "^1.2.0-rc1", "@rollup/plugin-commonjs": "21.0.2", "@rollup/plugin-json": "4.1.0", - "@rollup/plugin-node-resolve": "13.1.3", + "@rollup/plugin-node-resolve": "13.3.0", "@swc/helpers": "0.3.8", "rimraf": "3.0.2", "rollup": "2.70.1", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index d9f181a9bd6..de47d607a3b 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -98,7 +98,7 @@ "@grafana/tsconfig": "^1.2.0-rc1", "@mdx-js/react": "1.6.22", "@rollup/plugin-commonjs": "21.0.2", - "@rollup/plugin-node-resolve": "13.1.3", + "@rollup/plugin-node-resolve": "13.3.0", "@storybook/addon-a11y": "6.4.21", "@storybook/addon-actions": "6.4.21", "@storybook/addon-docs": "6.4.21", diff --git a/yarn.lock b/yarn.lock index 76590239444..38ba14a2547 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3991,7 +3991,7 @@ __metadata: "@grafana/tsconfig": ^1.2.0-rc1 "@rollup/plugin-commonjs": 21.0.2 "@rollup/plugin-json": 4.1.0 - "@rollup/plugin-node-resolve": 13.1.3 + "@rollup/plugin-node-resolve": 13.3.0 "@swc/helpers": 0.3.8 "@testing-library/dom": 8.13.0 "@testing-library/jest-dom": 5.16.2 @@ -4046,7 +4046,7 @@ __metadata: dependencies: "@grafana/tsconfig": ^1.2.0-rc1 "@rollup/plugin-commonjs": 21.0.2 - "@rollup/plugin-node-resolve": 13.1.3 + "@rollup/plugin-node-resolve": 13.3.0 "@types/node": 16.11.26 rimraf: 3.0.2 rollup: 2.70.1 @@ -4068,7 +4068,7 @@ __metadata: "@grafana/tsconfig": ^1.2.0-rc1 "@mochajs/json-file-reporter": ^1.2.0 "@rollup/plugin-commonjs": 21.0.2 - "@rollup/plugin-node-resolve": 13.1.3 + "@rollup/plugin-node-resolve": 13.3.0 "@types/chrome-remote-interface": 0.31.4 "@types/lodash": 4.14.181 "@types/node": 16.11.26 @@ -4176,7 +4176,7 @@ __metadata: "@grafana/tsconfig": ^1.2.0-rc1 "@grafana/ui": 9.0.0-pre "@rollup/plugin-commonjs": 21.0.2 - "@rollup/plugin-node-resolve": 13.1.3 + "@rollup/plugin-node-resolve": 13.3.0 "@sentry/browser": 6.19.1 "@testing-library/dom": 8.13.0 "@testing-library/react": 12.1.4 @@ -4210,7 +4210,7 @@ __metadata: "@grafana/tsconfig": ^1.2.0-rc1 "@rollup/plugin-commonjs": 21.0.2 "@rollup/plugin-json": 4.1.0 - "@rollup/plugin-node-resolve": 13.1.3 + "@rollup/plugin-node-resolve": 13.3.0 "@swc/helpers": 0.3.8 rimraf: 3.0.2 rollup: 2.70.1 @@ -4366,7 +4366,7 @@ __metadata: "@react-aria/overlays": 3.8.1 "@react-stately/menu": 3.2.6 "@rollup/plugin-commonjs": 21.0.2 - "@rollup/plugin-node-resolve": 13.1.3 + "@rollup/plugin-node-resolve": 13.3.0 "@sentry/browser": 6.19.1 "@storybook/addon-a11y": 6.4.21 "@storybook/addon-actions": 6.4.21 @@ -7308,19 +7308,19 @@ __metadata: languageName: node linkType: hard -"@rollup/plugin-node-resolve@npm:13.1.3": - version: 13.1.3 - resolution: "@rollup/plugin-node-resolve@npm:13.1.3" +"@rollup/plugin-node-resolve@npm:13.3.0": + version: 13.3.0 + resolution: "@rollup/plugin-node-resolve@npm:13.3.0" dependencies: "@rollup/pluginutils": ^3.1.0 "@types/resolve": 1.17.1 - builtin-modules: ^3.1.0 deepmerge: ^4.2.2 + is-builtin-module: ^3.1.0 is-module: ^1.0.0 resolve: ^1.19.0 peerDependencies: rollup: ^2.42.0 - checksum: c275843aef884ff15ed7edb8a3b8fd072a72d517632098f6e9c25ef2c00f7842559565cc77e16c59eb119b8c4e2d858a8b5a94701ca6f85ae6a4f60a6e31f0ab + checksum: ec5418e6b3c23a9e30683056b3010e9d325316dcfae93fbc673ae64dad8e56a2ce761c15c48f5e2dcfe0c822fdc4a4905ee6346e3dcf90603ba2260afef5a5e6 languageName: node linkType: hard @@ -14114,7 +14114,7 @@ __metadata: languageName: node linkType: hard -"builtin-modules@npm:^3.1.0": +"builtin-modules@npm:^3.0.0": version: 3.2.0 resolution: "builtin-modules@npm:3.2.0" checksum: 0265aa1ba78e1a16f4e18668d815cb43fb364e6a6b8aa9189c6f44c7b894a551a43b323c40206959d2d4b2568c1f2805607ad6c88adc306a776ce6904cca6715 @@ -22273,6 +22273,15 @@ __metadata: languageName: node linkType: hard +"is-builtin-module@npm:^3.1.0": + version: 3.1.0 + resolution: "is-builtin-module@npm:3.1.0" + dependencies: + builtin-modules: ^3.0.0 + checksum: f1e5dd2cd5f252d4d799b20a0c8c4f7e9c399c4d141749af76ca0121058d4062c3015d026f1b1409dd3d2a4ddfb9b15cf6eb9c370fed53fea8652ce35b5e95cb + languageName: node + linkType: hard + "is-callable@npm:^1.1.3, is-callable@npm:^1.1.4, is-callable@npm:^1.1.5, is-callable@npm:^1.2.4": version: 1.2.4 resolution: "is-callable@npm:1.2.4" From 09634b518c086dbe76e5f093a5f669b20fbb1bdb Mon Sep 17 00:00:00 2001 From: Joey Tawadrous <90795735+joey-grafana@users.noreply.github.com> Date: Tue, 3 May 2022 17:42:36 +0100 Subject: [PATCH 007/440] Traces Panel: Add new Traces Panel visualization (#47534) * Panel * Support tempo dash variables * Support tempo explore variables * Only show span links for explore * Cleanup * Added tests * apply variables to search * Tests for search variables * Handling no data * Interpolation and tests * TracesPanel tests * More tests * Fix for backend test * Manager integration test fix * Traces doc and updated visualizations index doc * Logs for this span * Search, scrollToTop, other improvements * Refactor to extract common code * Removed TopOfViewRefType optional * Remove topOfViewRef optional * Removed another optional and fixed tests * Test * Only show search bar if trace * Support traces panel in add to dashboard * Self review * Update betterer * Linter fixes * Updated traces doc * Ahh, moved the for more info too * Updated betterer.results * Added new icon * Updated expectedListResp.json --- .betterer.results | 6 +- docs/sources/visualizations/_index.md | 1 + docs/sources/visualizations/traces.md | 19 ++ .../TracePageSearchBar.test.js | 4 - .../TracePageHeader/TracePageSearchBar.tsx | 73 ++++++- .../SpanDetail/index.test.js | 3 + .../TraceTimelineViewer/SpanDetail/index.tsx | 47 +++-- .../src/TraceTimelineViewer/SpanDetailRow.tsx | 4 + .../VirtualizedTraceView.test.js | 6 + .../VirtualizedTraceView.tsx | 28 ++- .../src/TraceTimelineViewer/index.tsx | 9 +- .../manager/manager_integration_test.go | 1 + .../api/plugins/data/expectedListResp.json | 2 +- .../AddToDashboard/addToDashboard.test.ts | 11 +- .../explore/AddToDashboard/addToDashboard.ts | 3 + public/app/features/explore/Explore.tsx | 10 +- .../app/features/explore/ExploreToolbar.tsx | 6 +- .../explore/TraceView/TraceView.test.tsx | 17 +- .../features/explore/TraceView/TraceView.tsx | 187 +++++++++--------- .../TraceView/TraceViewContainer.test.tsx | 4 +- .../explore/TraceView/TraceViewContainer.tsx | 83 ++------ .../app/features/plugins/built_in_plugins.ts | 2 + .../tempo/QueryEditor/NativeSearch.tsx | 9 +- .../datasource/tempo/datasource.test.ts | 67 ++++++- .../plugins/datasource/tempo/datasource.ts | 57 +++++- .../plugins/panel/traces/TracesPanel.test.tsx | 22 +++ .../app/plugins/panel/traces/TracesPanel.tsx | 69 +++++++ .../plugins/panel/traces/img/traces-panel.svg | 1 + public/app/plugins/panel/traces/module.tsx | 5 + public/app/plugins/panel/traces/plugin.json | 17 ++ 30 files changed, 520 insertions(+), 253 deletions(-) create mode 100644 docs/sources/visualizations/traces.md create mode 100644 public/app/plugins/panel/traces/TracesPanel.test.tsx create mode 100644 public/app/plugins/panel/traces/TracesPanel.tsx create mode 100644 public/app/plugins/panel/traces/img/traces-panel.svg create mode 100644 public/app/plugins/panel/traces/module.tsx create mode 100644 public/app/plugins/panel/traces/plugin.json diff --git a/.betterer.results b/.betterer.results index 07281917d1d..65d99476a08 100644 --- a/.betterer.results +++ b/.betterer.results @@ -80,7 +80,7 @@ exports[`no enzyme tests`] = { "packages/jaeger-ui-components/src/TracePageHeader/TracePageHeader.test.js:3242042907": [ [14, 26, 13, "RegExp match", "2409514259"] ], - "packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.test.js:2807329716": [ + "packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.test.js:1062402339": [ [14, 19, 13, "RegExp match", "2409514259"] ], "packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/index.test.js:1734982398": [ @@ -113,7 +113,7 @@ exports[`no enzyme tests`] = { "packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/TextList.test.js:3006381933": [ [14, 19, 13, "RegExp match", "2409514259"] ], - "packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.test.js:3097530078": [ + "packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.test.js:2816619357": [ [16, 19, 13, "RegExp match", "2409514259"] ], "packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetailRow.test.js:2623922632": [ @@ -137,7 +137,7 @@ exports[`no enzyme tests`] = { "packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.test.js:1423129438": [ [15, 17, 13, "RegExp match", "2409514259"] ], - "packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.test.js:2326471104": [ + "packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.test.js:551014442": [ [13, 26, 13, "RegExp match", "2409514259"] ], "packages/jaeger-ui-components/src/TraceTimelineViewer/index.test.js:381298544": [ diff --git a/docs/sources/visualizations/_index.md b/docs/sources/visualizations/_index.md index a5119e5b543..3995009a37d 100644 --- a/docs/sources/visualizations/_index.md +++ b/docs/sources/visualizations/_index.md @@ -27,6 +27,7 @@ Grafana offers a variety of visualizations to support different use cases. This - [Table]({{< relref "./table/_index.md" >}}) is the main and only table visualization. - [Logs]({{< relref "./logs-panel.md" >}}) is the main visualization for logs. - [Node Graph]({{< relref "./node-graph.md" >}}) for directed graphs or networks. + - [Traces]({{< relref "./traces.md" >}}) is the main visualization for traces. - Widgets - [Dashboard list]({{< relref "./dashboard-list-panel.md" >}}) can list dashboards. - [Alert list]({{< relref "./alert-list-panel.md" >}}) can list alerts. diff --git a/docs/sources/visualizations/traces.md b/docs/sources/visualizations/traces.md new file mode 100644 index 00000000000..82b51084636 --- /dev/null +++ b/docs/sources/visualizations/traces.md @@ -0,0 +1,19 @@ ++++ +title = "Traces" +keywords = ["grafana", "dashboard", "documentation", "panels", "traces"] +weight = 850 ++++ + +# Traces panel + +> **Note:** This panel is currently in beta. Expect changes in future releases. + +_Traces_ are a visualization that enables you to track and log a request as it traverses the services in your infrastructure. + +For more information about traces and how to use them, refer to the following documentation: + +- [What are traces](https://grafana.com/docs/grafana-cloud/traces) +- [Tracing in expliore]({{< relref "../explore/trace-integration.md" >}}) +- [Getting started with Tempo](https://grafana.com/docs/tempo/latest/getting-started) + +{{< figure src="/static/img/docs/explore/explore-trace-view-full-8-0.png" class="docs-image--no-shadow" max-width= "900px" caption="Screenshot of the trace view" >}} diff --git a/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.test.js b/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.test.js index d7cf4baff07..539ecb2860a 100644 --- a/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.test.js +++ b/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.test.js @@ -25,8 +25,6 @@ import * as markers from './TracePageSearchBar.markers'; const defaultProps = { forwardedRef: React.createRef(), navigable: true, - nextResult: () => {}, - prevResult: () => {}, suffix: '', searchValue: 'something', }; @@ -59,8 +57,6 @@ describe('', () => { buttons.forEach((button) => { expect(button.prop('disabled')).toBe(false); }); - expect(wrapper.find('Button[icon="arrow-up"]').prop('onClick')).toBe(defaultProps.prevResult); - expect(wrapper.find('Button[icon="arrow-down"]').prop('onClick')).toBe(defaultProps.nextResult); }); it('only shows navigable buttons when navigable is true', () => { diff --git a/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx b/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx index cfa1e99a544..0d54d4e2419 100644 --- a/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx +++ b/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx @@ -14,8 +14,7 @@ import { css } from '@emotion/css'; import cx from 'classnames'; -import * as React from 'react'; -import { memo } from 'react'; +import React, { memo, Dispatch, SetStateAction } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Button, useStyles2 } from '@grafana/ui'; @@ -72,16 +71,27 @@ export const getStyles = (theme: GrafanaTheme2) => { }; type TracePageSearchBarProps = { - prevResult: () => void; - nextResult: () => void; navigable: boolean; searchValue: string; - onSearchValueChange: (value: string) => void; + setSearch: (value: string) => void; searchBarSuffix: string; + spanFindMatches: Set | undefined; + focusedSpanIdForSearch: string; + setSearchBarSuffix: Dispatch>; + setFocusedSpanIdForSearch: Dispatch>; }; export default memo(function TracePageSearchBar(props: TracePageSearchBarProps) { - const { navigable, nextResult, prevResult, onSearchValueChange, searchValue, searchBarSuffix } = props; + const { + navigable, + setSearch, + searchValue, + searchBarSuffix, + spanFindMatches, + focusedSpanIdForSearch, + setSearchBarSuffix, + setFocusedSpanIdForSearch, + } = props; const styles = useStyles2(getStyles); const suffix = searchValue ? ( @@ -98,11 +108,60 @@ export default memo(function TracePageSearchBar(props: TracePageSearchBarProps) suffix, }; + const setTraceSearch = (value: string) => { + setFocusedSpanIdForSearch(''); + setSearchBarSuffix(''); + setSearch(value); + }; + + const nextResult = () => { + const spanMatches = Array.from(spanFindMatches!); + const prevMatchedIndex = spanMatches.indexOf(focusedSpanIdForSearch) + ? spanMatches.indexOf(focusedSpanIdForSearch) + : 0; + + // new query || at end, go to start + if (prevMatchedIndex === -1 || prevMatchedIndex === spanMatches.length - 1) { + setFocusedSpanIdForSearch(spanMatches[0]); + setSearchBarSuffix(getSearchBarSuffix(1)); + return; + } + + // get next + setFocusedSpanIdForSearch(spanMatches[prevMatchedIndex + 1]); + setSearchBarSuffix(getSearchBarSuffix(prevMatchedIndex + 2)); + }; + + const prevResult = () => { + const spanMatches = Array.from(spanFindMatches!); + const prevMatchedIndex = spanMatches.indexOf(focusedSpanIdForSearch) + ? spanMatches.indexOf(focusedSpanIdForSearch) + : 0; + + // new query || at start, go to end + if (prevMatchedIndex === -1 || prevMatchedIndex === 0) { + setFocusedSpanIdForSearch(spanMatches[spanMatches.length - 1]); + setSearchBarSuffix(getSearchBarSuffix(spanMatches.length)); + return; + } + + // get prev + setFocusedSpanIdForSearch(spanMatches[prevMatchedIndex - 1]); + setSearchBarSuffix(getSearchBarSuffix(prevMatchedIndex)); + }; + + const getSearchBarSuffix = (index: number): string => { + if (spanFindMatches?.size && spanFindMatches?.size > 0) { + return index + ' of ' + spanFindMatches?.size; + } + return ''; + }; + return (
', () => { const span = transformTraceData(traceGenerator.trace({ numberOfSpans: 1 })).spans[0]; const detailState = new DetailState().toggleLogs().toggleProcess().toggleReferences().toggleTags(); const traceStartTime = 5; + const topOfExploreViewRef = jest.fn(); const props = { detailState, span, traceStartTime, + topOfExploreViewRef, logItemToggle: jest.fn(), logsToggle: jest.fn(), processToggle: jest.fn(), @@ -46,6 +48,7 @@ describe('', () => { warningsToggle: jest.fn(), referencesToggle: jest.fn(), createFocusSpanLink: jest.fn(), + topOfViewRefType: 'Explore', }; span.logs = [ { diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx index 7f445a19f08..0eb1a82a745 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx @@ -26,6 +26,7 @@ import LabeledList from '../../common/LabeledList'; import { SpanLinkFunc, TNil } from '../../types'; import { TraceKeyValuePair, TraceLink, TraceLog, TraceSpan, TraceSpanReference } from '../../types/trace'; import { uAlignIcon, ubM0, ubMb1, ubMy1, ubTxRightAlign } from '../../uberUtilityStyles'; +import { TopOfViewRefType } from '../VirtualizedTraceView'; import { formatDuration } from '../utils'; import AccordianKeyValues from './AccordianKeyValues'; @@ -119,6 +120,7 @@ type SpanDetailProps = { createSpanLink?: SpanLinkFunc; focusedSpanId?: string; createFocusSpanLink: (traceId: string, spanId: string) => LinkModel; + topOfViewRefType?: TopOfViewRefType; }; export default function SpanDetail(props: SpanDetailProps) { @@ -138,6 +140,7 @@ export default function SpanDetail(props: SpanDetailProps) { focusSpan, createSpanLink, createFocusSpanLink, + topOfViewRefType, } = props; const { isTagsOpen, @@ -281,27 +284,29 @@ export default function SpanDetail(props: SpanDetailProps) { focusSpan={focusSpan} /> )} - - { - // click handling logic copied from react router: - // https://github.com/remix-run/react-router/blob/997b4d67e506d39ac6571cb369d6d2d6b3dda557/packages/react-router-dom/index.tsx#L392-L394s - if ( - focusSpanLink.onClick && - e.button === 0 && // Ignore everything but left clicks - (!e.currentTarget.target || e.currentTarget.target === '_self') && // Let browser handle "target=_blank" etc. - !(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) // Ignore clicks with modifier keys - ) { - e.preventDefault(); - focusSpanLink.onClick(e); - } - }} - > - - - {spanID} - + {topOfViewRefType === TopOfViewRefType.Explore && ( + + { + // click handling logic copied from react router: + // https://github.com/remix-run/react-router/blob/997b4d67e506d39ac6571cb369d6d2d6b3dda557/packages/react-router-dom/index.tsx#L392-L394s + if ( + focusSpanLink.onClick && + e.button === 0 && // Ignore everything but left clicks + (!e.currentTarget.target || e.currentTarget.target === '_self') && // Let browser handle "target=_blank" etc. + !(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) // Ignore clicks with modifier keys + ) { + e.preventDefault(); + focusSpanLink.onClick(e); + } + }} + > + + + {spanID} + + )}
); diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetailRow.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetailRow.tsx index 8414405044a..7f5905ba6d6 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetailRow.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetailRow.tsx @@ -26,6 +26,7 @@ import SpanDetail from './SpanDetail'; import DetailState from './SpanDetail/DetailState'; import SpanTreeOffset from './SpanTreeOffset'; import TimelineRow from './TimelineRow'; +import { TopOfViewRefType } from './VirtualizedTraceView'; const getStyles = stylesFactory((theme: GrafanaTheme2) => { return { @@ -93,6 +94,7 @@ type SpanDetailRowProps = { createSpanLink?: SpanLinkFunc; focusedSpanId?: string; createFocusSpanLink: (traceId: string, spanId: string) => LinkModel; + topOfViewRefType?: TopOfViewRefType; }; export class UnthemedSpanDetailRow extends React.PureComponent { @@ -128,6 +130,7 @@ export class UnthemedSpanDetailRow extends React.PureComponent diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.test.js b/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.test.js index ee5186ac162..eede823f396 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.test.js +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.test.js @@ -31,6 +31,7 @@ describe('', () => { let instance; const trace = transformTraceData(traceGenerator.trace({ numberOfSpans: 10 })); + const topOfExploreViewRef = jest.fn(); const props = { childrenHiddenIDs: new Set(), childrenToggle: jest.fn(), @@ -51,6 +52,7 @@ describe('', () => { spanNameColumnWidth: 0.5, trace, uiFind: 'uiFind', + topOfExploreViewRef, }; function expandRow(rowIndex) { @@ -109,6 +111,10 @@ describe('', () => { expect(wrapper.find(ListView)).toBeDefined(); }); + it('renders scrollToTopButton', () => { + expect(wrapper.find({ title: 'Scroll to top' }).exists()).toBeTruthy(); + }); + it('sets the trace for global state.traceTimeline', () => { expect(props.setTrace.mock.calls).toEqual([[trace, props.uiFind]]); props.setTrace.mockReset(); diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx index 4fa87f1628f..bf3411015f8 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx @@ -45,7 +45,10 @@ type TExtractUiFindFromStateReturn = { uiFind: string | undefined; }; -const getStyles = stylesFactory(() => { +const getStyles = stylesFactory((props: TVirtualizedTraceViewOwnProps) => { + const { topOfViewRefType } = props; + const position = topOfViewRefType === TopOfViewRefType.Explore ? 'fixed' : 'absolute'; + return { rowsWrapper: css` width: 100%; @@ -60,7 +63,7 @@ const getStyles = stylesFactory(() => { align-items: center; width: 40px; height: 40px; - position: fixed; + position: ${position}; bottom: 30px; right: 30px; z-index: 1; @@ -74,6 +77,11 @@ type RowState = { spanIndex: number; }; +export enum TopOfViewRefType { + Explore = 'Explore', + Panel = 'Panel', +} + type TVirtualizedTraceViewOwnProps = { currentViewRangeTime: [number, number]; findMatchesIDs: Set | TNil; @@ -104,7 +112,8 @@ type TVirtualizedTraceViewOwnProps = { focusedSpanId?: string; focusedSpanIdForSearch: string; createFocusSpanLink: (traceId: string, spanId: string) => LinkModel; - topOfExploreViewRef?: RefObject; + topOfViewRef?: RefObject; + topOfViewRefType?: TopOfViewRefType; }; type VirtualizedTraceViewProps = TVirtualizedTraceViewOwnProps & TExtractUiFindFromStateReturn & TTraceTimeline; @@ -425,7 +434,7 @@ export class UnthemedVirtualizedTraceView extends React.Component ); } scrollToTop = () => { - const { topOfExploreViewRef } = this.props; - topOfExploreViewRef?.current?.scrollIntoView({ behavior: 'smooth' }); + const { topOfViewRef } = this.props; + topOfViewRef?.current?.scrollIntoView({ behavior: 'smooth' }); }; render() { - const styles = getStyles(); + const styles = getStyles(this.props); const { scrollElement } = this.props; return ( <> @@ -541,7 +552,6 @@ export class UnthemedVirtualizedTraceView extends React.Component - LinkModel; - topOfExploreViewRef?: RefObject; + topOfViewRef?: RefObject; + topOfViewRefType?: TopOfViewRefType; }; type State = { @@ -165,7 +166,7 @@ export class UnthemedTraceTimelineViewer extends React.PureComponent diff --git a/pkg/plugins/manager/manager_integration_test.go b/pkg/plugins/manager/manager_integration_test.go index f92059fbdfc..f698ac2ba96 100644 --- a/pkg/plugins/manager/manager_integration_test.go +++ b/pkg/plugins/manager/manager_integration_test.go @@ -126,6 +126,7 @@ func verifyCorePluginCatalogue(t *testing.T, pm *PluginManager) { "candlestick": {}, "news": {}, "nodeGraph": {}, + "traces": {}, "piechart": {}, "stat": {}, "state-timeline": {}, diff --git a/pkg/tests/api/plugins/data/expectedListResp.json b/pkg/tests/api/plugins/data/expectedListResp.json index 4df0785185c..0669cb89090 100644 --- a/pkg/tests/api/plugins/data/expectedListResp.json +++ b/pkg/tests/api/plugins/data/expectedListResp.json @@ -1 +1 @@ -[{"name":"Alert list","type":"panel","id":"alertlist","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Shows list of alerts and their current status","links":null,"logos":{"small":"public/app/plugins/panel/alertlist/img/icn-singlestat-panel.svg","large":"public/app/plugins/panel/alertlist/img/icn-singlestat-panel.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/alertlist/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Annotations list","type":"panel","id":"annolist","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"List annotations","links":null,"logos":{"small":"public/app/plugins/panel/annolist/img/icn-annolist-panel.svg","large":"public/app/plugins/panel/annolist/img/icn-annolist-panel.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/annolist/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Azure Monitor","type":"datasource","id":"grafana-azure-monitor-datasource","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Data source for Microsoft Azure Monitor \u0026 Application Insights","links":[{"name":"Learn more","url":"https://grafana.com/docs/grafana/latest/datasources/azuremonitor/"},{"name":"Apache License","url":"https://github.com/grafana/azure-monitor-datasource/blob/master/LICENSE"}],"logos":{"small":"public/app/plugins/datasource/grafana-azure-monitor-datasource/img/logo.jpg","large":"public/app/plugins/datasource/grafana-azure-monitor-datasource/img/logo.jpg"},"build":{},"screenshots":[{"name":"Azure Contoso Loans","path":"public/app/plugins/datasource/grafana-azure-monitor-datasource/img/contoso_loans_grafana_dashboard.png"},{"name":"Azure Monitor Network","path":"public/app/plugins/datasource/grafana-azure-monitor-datasource/img/azure_monitor_network.png"},{"name":"Azure Monitor CPU","path":"public/app/plugins/datasource/grafana-azure-monitor-datasource/img/azure_monitor_cpu.png"}],"version":"1.0.0","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"5.2.x","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/grafana-azure-monitor-datasource/","category":"cloud","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Bar chart","type":"panel","id":"barchart","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Categorical charts with group support","links":null,"logos":{"small":"public/app/plugins/panel/barchart/img/barchart.svg","large":"public/app/plugins/panel/barchart/img/barchart.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/barchart/","category":"","state":"beta","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Bar gauge","type":"panel","id":"bargauge","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Horizontal and vertical gauges","links":null,"logos":{"small":"public/app/plugins/panel/bargauge/img/icon_bar_gauge.svg","large":"public/app/plugins/panel/bargauge/img/icon_bar_gauge.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/bargauge/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Candlestick","type":"panel","id":"candlestick","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"","links":null,"logos":{"small":"public/app/plugins/panel/candlestick/img/candlestick.svg","large":"public/app/plugins/panel/candlestick/img/candlestick.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/candlestick/","category":"","state":"beta","signature":"internal","signatureType":"","signatureOrg":""},{"name":"CloudWatch","type":"datasource","id":"cloudwatch","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Data source for Amazon AWS monitoring service","links":null,"logos":{"small":"public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png","large":"public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/cloudwatch/","category":"cloud","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Dashboard list","type":"panel","id":"dashlist","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"List of dynamic links to other dashboards","links":null,"logos":{"small":"public/app/plugins/panel/dashlist/img/icn-dashlist-panel.svg","large":"public/app/plugins/panel/dashlist/img/icn-dashlist-panel.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/dashlist/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Elasticsearch","type":"datasource","id":"elasticsearch","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Open source logging \u0026 analytics database","links":[{"name":"Learn more","url":"https://grafana.com/docs/features/datasources/elasticsearch/"}],"logos":{"small":"public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg","large":"public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/elasticsearch/","category":"logging","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Gauge","type":"panel","id":"gauge","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Standard gauge visualization","links":null,"logos":{"small":"public/app/plugins/panel/gauge/img/icon_gauge.svg","large":"public/app/plugins/panel/gauge/img/icon_gauge.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/gauge/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Geomap","type":"panel","id":"geomap","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Geomap panel","links":null,"logos":{"small":"public/app/plugins/panel/geomap/img/icn-geomap.svg","large":"public/app/plugins/panel/geomap/img/icn-geomap.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/geomap/","category":"","state":"beta","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Getting Started","type":"panel","id":"gettingstarted","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"","links":null,"logos":{"small":"public/app/plugins/panel/gettingstarted/img/icn-dashlist-panel.svg","large":"public/app/plugins/panel/gettingstarted/img/icn-dashlist-panel.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/gettingstarted/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Google Cloud Monitoring","type":"datasource","id":"stackdriver","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Data source for Google's monitoring service (formerly named Stackdriver)","links":null,"logos":{"small":"public/app/plugins/datasource/cloud-monitoring/img/cloud_monitoring_logo.svg","large":"public/app/plugins/datasource/cloud-monitoring/img/cloud_monitoring_logo.svg"},"build":{},"screenshots":null,"version":"1.0.0","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/stackdriver/","category":"cloud","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Graph (old)","type":"panel","id":"graph","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"The old default graph panel","links":null,"logos":{"small":"public/app/plugins/panel/graph/img/icn-graph-panel.svg","large":"public/app/plugins/panel/graph/img/icn-graph-panel.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/graph/","category":"","state":"deprecated","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Graphite","type":"datasource","id":"graphite","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Open source time series database","links":[{"name":"Learn more","url":"https://graphiteapp.org/"},{"name":"Graphite 1.1 Release","url":"https://grafana.com/blog/2018/01/11/graphite-1.1-teaching-an-old-dog-new-tricks/"}],"logos":{"small":"public/app/plugins/datasource/graphite/img/graphite_logo.png","large":"public/app/plugins/datasource/graphite/img/graphite_logo.png"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/graphite/","category":"tsdb","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Heatmap","type":"panel","id":"heatmap","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Like a histogram over time","links":[{"name":"Brendan Gregg - Heatmaps","url":"http://www.brendangregg.com/heatmaps.html"},{"name":"Brendan Gregg - Latency Heatmaps","url":" http://www.brendangregg.com/HeatMaps/latency.html"}],"logos":{"small":"public/app/plugins/panel/heatmap/img/icn-heatmap-panel.svg","large":"public/app/plugins/panel/heatmap/img/icn-heatmap-panel.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/heatmap/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Histogram","type":"panel","id":"histogram","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"","links":null,"logos":{"small":"public/app/plugins/panel/histogram/img/histogram.svg","large":"public/app/plugins/panel/histogram/img/histogram.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/histogram/","category":"","state":"beta","signature":"internal","signatureType":"","signatureOrg":""},{"name":"InfluxDB","type":"datasource","id":"influxdb","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Open source time series database","links":null,"logos":{"small":"public/app/plugins/datasource/influxdb/img/influxdb_logo.svg","large":"public/app/plugins/datasource/influxdb/img/influxdb_logo.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/influxdb/","category":"tsdb","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Jaeger","type":"datasource","id":"jaeger","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Open source, end-to-end distributed tracing","links":[{"name":"Learn more","url":"https://www.jaegertracing.io"},{"name":"GitHub Project","url":"https://github.com/jaegertracing/jaeger"}],"logos":{"small":"public/app/plugins/datasource/jaeger/img/jaeger_logo.svg","large":"public/app/plugins/datasource/jaeger/img/jaeger_logo.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/jaeger/","category":"tracing","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Logs","type":"panel","id":"logs","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"","links":null,"logos":{"small":"public/app/plugins/panel/logs/img/icn-logs-panel.svg","large":"public/app/plugins/panel/logs/img/icn-logs-panel.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/logs/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Loki","type":"datasource","id":"loki","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Like Prometheus but for logs. OSS logging solution from Grafana Labs","links":[{"name":"Learn more","url":"https://grafana.com/loki"},{"name":"GitHub Project","url":"https://github.com/grafana/loki"}],"logos":{"small":"public/app/plugins/datasource/loki/img/loki_icon.svg","large":"public/app/plugins/datasource/loki/img/loki_icon.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/loki/","category":"logging","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Microsoft SQL Server","type":"datasource","id":"mssql","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Data source for Microsoft SQL Server compatible databases","links":null,"logos":{"small":"public/app/plugins/datasource/mssql/img/sql_server_logo.svg","large":"public/app/plugins/datasource/mssql/img/sql_server_logo.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/mssql/","category":"sql","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"MySQL","type":"datasource","id":"mysql","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Data source for MySQL databases","links":null,"logos":{"small":"public/app/plugins/datasource/mysql/img/mysql_logo.svg","large":"public/app/plugins/datasource/mysql/img/mysql_logo.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/mysql/","category":"sql","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"News","type":"panel","id":"news","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"RSS feed reader","links":null,"logos":{"small":"public/app/plugins/panel/news/img/news.svg","large":"public/app/plugins/panel/news/img/news.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/news/","category":"","state":"beta","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Node Graph","type":"panel","id":"nodeGraph","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"","links":null,"logos":{"small":"public/app/plugins/panel/nodeGraph/img/icn-node-graph.svg","large":"public/app/plugins/panel/nodeGraph/img/icn-node-graph.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/nodeGraph/","category":"","state":"beta","signature":"internal","signatureType":"","signatureOrg":""},{"name":"OpenTSDB","type":"datasource","id":"opentsdb","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Open source time series database","links":null,"logos":{"small":"public/app/plugins/datasource/opentsdb/img/opentsdb_logo.png","large":"public/app/plugins/datasource/opentsdb/img/opentsdb_logo.png"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/opentsdb/","category":"tsdb","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Pie chart","type":"panel","id":"piechart","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"The new core pie chart visualization","links":null,"logos":{"small":"public/app/plugins/panel/piechart/img/icon_piechart.svg","large":"public/app/plugins/panel/piechart/img/icon_piechart.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/piechart/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"PostgreSQL","type":"datasource","id":"postgres","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Data source for PostgreSQL and compatible databases","links":null,"logos":{"small":"public/app/plugins/datasource/postgres/img/postgresql_logo.svg","large":"public/app/plugins/datasource/postgres/img/postgresql_logo.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/postgres/","category":"sql","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Prometheus","type":"datasource","id":"prometheus","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Open source time series database \u0026 alerting","links":[{"name":"Learn more","url":"https://prometheus.io/"}],"logos":{"small":"public/app/plugins/datasource/prometheus/img/prometheus_logo.svg","large":"public/app/plugins/datasource/prometheus/img/prometheus_logo.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/prometheus/","category":"tsdb","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Stat","type":"panel","id":"stat","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Big stat values \u0026 sparklines","links":null,"logos":{"small":"public/app/plugins/panel/stat/img/icn-singlestat-panel.svg","large":"public/app/plugins/panel/stat/img/icn-singlestat-panel.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/stat/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"State timeline","type":"panel","id":"state-timeline","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"State changes and durations","links":null,"logos":{"small":"public/app/plugins/panel/state-timeline/img/timeline.svg","large":"public/app/plugins/panel/state-timeline/img/timeline.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/state-timeline/","category":"","state":"beta","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Status history","type":"panel","id":"status-history","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Periodic status history","links":null,"logos":{"small":"public/app/plugins/panel/status-history/img/status.svg","large":"public/app/plugins/panel/status-history/img/status.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/status-history/","category":"","state":"beta","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Table","type":"panel","id":"table","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Supports many column styles","links":null,"logos":{"small":"public/app/plugins/panel/table/img/icn-table-panel.svg","large":"public/app/plugins/panel/table/img/icn-table-panel.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/table/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Table (old)","type":"panel","id":"table-old","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Table Panel for Grafana","links":null,"logos":{"small":"public/app/plugins/panel/table-old/img/icn-table-panel.svg","large":"public/app/plugins/panel/table-old/img/icn-table-panel.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/table-old/","category":"","state":"deprecated","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Tempo","type":"datasource","id":"tempo","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"High volume, minimal dependency trace storage. OSS tracing solution from Grafana Labs.","links":[{"name":"GitHub Project","url":"https://github.com/grafana/tempo"}],"logos":{"small":"public/app/plugins/datasource/tempo/img/tempo_logo.svg","large":"public/app/plugins/datasource/tempo/img/tempo_logo.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/tempo/","category":"tracing","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"TestData DB","type":"datasource","id":"testdata","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Generates test data in different forms","links":null,"logos":{"small":"public/app/plugins/datasource/testdata/img/testdata.svg","large":"public/app/plugins/datasource/testdata/img/testdata.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/testdata/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Text","type":"panel","id":"text","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Supports markdown and html content","links":null,"logos":{"small":"public/app/plugins/panel/text/img/icn-text-panel.svg","large":"public/app/plugins/panel/text/img/icn-text-panel.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/text/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Time series","type":"panel","id":"timeseries","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Time based line, area and bar charts","links":null,"logos":{"small":"public/app/plugins/panel/timeseries/img/icn-timeseries-panel.svg","large":"public/app/plugins/panel/timeseries/img/icn-timeseries-panel.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/timeseries/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Welcome","type":"panel","id":"welcome","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"","links":null,"logos":{"small":"public/app/plugins/panel/welcome/img/icn-dashlist-panel.svg","large":"public/app/plugins/panel/welcome/img/icn-dashlist-panel.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/welcome/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Zipkin","type":"datasource","id":"zipkin","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Placeholder for the distributed tracing system.","links":[{"name":"Learn more","url":"https://zipkin.io"}],"logos":{"small":"public/app/plugins/datasource/zipkin/img/zipkin-logo.svg","large":"public/app/plugins/datasource/zipkin/img/zipkin-logo.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/zipkin/","category":"tracing","state":"","signature":"internal","signatureType":"","signatureOrg":""}] \ No newline at end of file +[{"name":"Alert list","type":"panel","id":"alertlist","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Shows list of alerts and their current status","links":null,"logos":{"small":"public/app/plugins/panel/alertlist/img/icn-singlestat-panel.svg","large":"public/app/plugins/panel/alertlist/img/icn-singlestat-panel.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/alertlist/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Annotations list","type":"panel","id":"annolist","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"List annotations","links":null,"logos":{"small":"public/app/plugins/panel/annolist/img/icn-annolist-panel.svg","large":"public/app/plugins/panel/annolist/img/icn-annolist-panel.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/annolist/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Azure Monitor","type":"datasource","id":"grafana-azure-monitor-datasource","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Data source for Microsoft Azure Monitor \u0026 Application Insights","links":[{"name":"Learn more","url":"https://grafana.com/docs/grafana/latest/datasources/azuremonitor/"},{"name":"Apache License","url":"https://github.com/grafana/azure-monitor-datasource/blob/master/LICENSE"}],"logos":{"small":"public/app/plugins/datasource/grafana-azure-monitor-datasource/img/logo.jpg","large":"public/app/plugins/datasource/grafana-azure-monitor-datasource/img/logo.jpg"},"build":{},"screenshots":[{"name":"Azure Contoso Loans","path":"public/app/plugins/datasource/grafana-azure-monitor-datasource/img/contoso_loans_grafana_dashboard.png"},{"name":"Azure Monitor Network","path":"public/app/plugins/datasource/grafana-azure-monitor-datasource/img/azure_monitor_network.png"},{"name":"Azure Monitor CPU","path":"public/app/plugins/datasource/grafana-azure-monitor-datasource/img/azure_monitor_cpu.png"}],"version":"1.0.0","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"5.2.x","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/grafana-azure-monitor-datasource/","category":"cloud","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Bar chart","type":"panel","id":"barchart","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Categorical charts with group support","links":null,"logos":{"small":"public/app/plugins/panel/barchart/img/barchart.svg","large":"public/app/plugins/panel/barchart/img/barchart.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/barchart/","category":"","state":"beta","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Bar gauge","type":"panel","id":"bargauge","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Horizontal and vertical gauges","links":null,"logos":{"small":"public/app/plugins/panel/bargauge/img/icon_bar_gauge.svg","large":"public/app/plugins/panel/bargauge/img/icon_bar_gauge.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/bargauge/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Candlestick","type":"panel","id":"candlestick","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"","links":null,"logos":{"small":"public/app/plugins/panel/candlestick/img/candlestick.svg","large":"public/app/plugins/panel/candlestick/img/candlestick.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/candlestick/","category":"","state":"beta","signature":"internal","signatureType":"","signatureOrg":""},{"name":"CloudWatch","type":"datasource","id":"cloudwatch","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Data source for Amazon AWS monitoring service","links":null,"logos":{"small":"public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png","large":"public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/cloudwatch/","category":"cloud","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Dashboard list","type":"panel","id":"dashlist","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"List of dynamic links to other dashboards","links":null,"logos":{"small":"public/app/plugins/panel/dashlist/img/icn-dashlist-panel.svg","large":"public/app/plugins/panel/dashlist/img/icn-dashlist-panel.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/dashlist/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Elasticsearch","type":"datasource","id":"elasticsearch","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Open source logging \u0026 analytics database","links":[{"name":"Learn more","url":"https://grafana.com/docs/features/datasources/elasticsearch/"}],"logos":{"small":"public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg","large":"public/app/plugins/datasource/elasticsearch/img/elasticsearch.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/elasticsearch/","category":"logging","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Gauge","type":"panel","id":"gauge","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Standard gauge visualization","links":null,"logos":{"small":"public/app/plugins/panel/gauge/img/icon_gauge.svg","large":"public/app/plugins/panel/gauge/img/icon_gauge.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/gauge/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Geomap","type":"panel","id":"geomap","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Geomap panel","links":null,"logos":{"small":"public/app/plugins/panel/geomap/img/icn-geomap.svg","large":"public/app/plugins/panel/geomap/img/icn-geomap.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/geomap/","category":"","state":"beta","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Getting Started","type":"panel","id":"gettingstarted","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"","links":null,"logos":{"small":"public/app/plugins/panel/gettingstarted/img/icn-dashlist-panel.svg","large":"public/app/plugins/panel/gettingstarted/img/icn-dashlist-panel.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/gettingstarted/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Google Cloud Monitoring","type":"datasource","id":"stackdriver","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Data source for Google's monitoring service (formerly named Stackdriver)","links":null,"logos":{"small":"public/app/plugins/datasource/cloud-monitoring/img/cloud_monitoring_logo.svg","large":"public/app/plugins/datasource/cloud-monitoring/img/cloud_monitoring_logo.svg"},"build":{},"screenshots":null,"version":"1.0.0","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/stackdriver/","category":"cloud","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Graph (old)","type":"panel","id":"graph","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"The old default graph panel","links":null,"logos":{"small":"public/app/plugins/panel/graph/img/icn-graph-panel.svg","large":"public/app/plugins/panel/graph/img/icn-graph-panel.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/graph/","category":"","state":"deprecated","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Graphite","type":"datasource","id":"graphite","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Open source time series database","links":[{"name":"Learn more","url":"https://graphiteapp.org/"},{"name":"Graphite 1.1 Release","url":"https://grafana.com/blog/2018/01/11/graphite-1.1-teaching-an-old-dog-new-tricks/"}],"logos":{"small":"public/app/plugins/datasource/graphite/img/graphite_logo.png","large":"public/app/plugins/datasource/graphite/img/graphite_logo.png"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/graphite/","category":"tsdb","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Heatmap","type":"panel","id":"heatmap","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Like a histogram over time","links":[{"name":"Brendan Gregg - Heatmaps","url":"http://www.brendangregg.com/heatmaps.html"},{"name":"Brendan Gregg - Latency Heatmaps","url":" http://www.brendangregg.com/HeatMaps/latency.html"}],"logos":{"small":"public/app/plugins/panel/heatmap/img/icn-heatmap-panel.svg","large":"public/app/plugins/panel/heatmap/img/icn-heatmap-panel.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/heatmap/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Histogram","type":"panel","id":"histogram","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"","links":null,"logos":{"small":"public/app/plugins/panel/histogram/img/histogram.svg","large":"public/app/plugins/panel/histogram/img/histogram.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/histogram/","category":"","state":"beta","signature":"internal","signatureType":"","signatureOrg":""},{"name":"InfluxDB","type":"datasource","id":"influxdb","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Open source time series database","links":null,"logos":{"small":"public/app/plugins/datasource/influxdb/img/influxdb_logo.svg","large":"public/app/plugins/datasource/influxdb/img/influxdb_logo.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/influxdb/","category":"tsdb","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Jaeger","type":"datasource","id":"jaeger","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Open source, end-to-end distributed tracing","links":[{"name":"Learn more","url":"https://www.jaegertracing.io"},{"name":"GitHub Project","url":"https://github.com/jaegertracing/jaeger"}],"logos":{"small":"public/app/plugins/datasource/jaeger/img/jaeger_logo.svg","large":"public/app/plugins/datasource/jaeger/img/jaeger_logo.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/jaeger/","category":"tracing","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Logs","type":"panel","id":"logs","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"","links":null,"logos":{"small":"public/app/plugins/panel/logs/img/icn-logs-panel.svg","large":"public/app/plugins/panel/logs/img/icn-logs-panel.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/logs/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Loki","type":"datasource","id":"loki","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Like Prometheus but for logs. OSS logging solution from Grafana Labs","links":[{"name":"Learn more","url":"https://grafana.com/loki"},{"name":"GitHub Project","url":"https://github.com/grafana/loki"}],"logos":{"small":"public/app/plugins/datasource/loki/img/loki_icon.svg","large":"public/app/plugins/datasource/loki/img/loki_icon.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/loki/","category":"logging","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Microsoft SQL Server","type":"datasource","id":"mssql","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Data source for Microsoft SQL Server compatible databases","links":null,"logos":{"small":"public/app/plugins/datasource/mssql/img/sql_server_logo.svg","large":"public/app/plugins/datasource/mssql/img/sql_server_logo.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/mssql/","category":"sql","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"MySQL","type":"datasource","id":"mysql","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Data source for MySQL databases","links":null,"logos":{"small":"public/app/plugins/datasource/mysql/img/mysql_logo.svg","large":"public/app/plugins/datasource/mysql/img/mysql_logo.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/mysql/","category":"sql","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"News","type":"panel","id":"news","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"RSS feed reader","links":null,"logos":{"small":"public/app/plugins/panel/news/img/news.svg","large":"public/app/plugins/panel/news/img/news.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/news/","category":"","state":"beta","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Node Graph","type":"panel","id":"nodeGraph","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"","links":null,"logos":{"small":"public/app/plugins/panel/nodeGraph/img/icn-node-graph.svg","large":"public/app/plugins/panel/nodeGraph/img/icn-node-graph.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/nodeGraph/","category":"","state":"beta","signature":"internal","signatureType":"","signatureOrg":""},{"name":"OpenTSDB","type":"datasource","id":"opentsdb","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Open source time series database","links":null,"logos":{"small":"public/app/plugins/datasource/opentsdb/img/opentsdb_logo.png","large":"public/app/plugins/datasource/opentsdb/img/opentsdb_logo.png"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/opentsdb/","category":"tsdb","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Pie chart","type":"panel","id":"piechart","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"The new core pie chart visualization","links":null,"logos":{"small":"public/app/plugins/panel/piechart/img/icon_piechart.svg","large":"public/app/plugins/panel/piechart/img/icon_piechart.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/piechart/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"PostgreSQL","type":"datasource","id":"postgres","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Data source for PostgreSQL and compatible databases","links":null,"logos":{"small":"public/app/plugins/datasource/postgres/img/postgresql_logo.svg","large":"public/app/plugins/datasource/postgres/img/postgresql_logo.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/postgres/","category":"sql","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Prometheus","type":"datasource","id":"prometheus","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Open source time series database \u0026 alerting","links":[{"name":"Learn more","url":"https://prometheus.io/"}],"logos":{"small":"public/app/plugins/datasource/prometheus/img/prometheus_logo.svg","large":"public/app/plugins/datasource/prometheus/img/prometheus_logo.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/prometheus/","category":"tsdb","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Stat","type":"panel","id":"stat","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Big stat values \u0026 sparklines","links":null,"logos":{"small":"public/app/plugins/panel/stat/img/icn-singlestat-panel.svg","large":"public/app/plugins/panel/stat/img/icn-singlestat-panel.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/stat/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"State timeline","type":"panel","id":"state-timeline","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"State changes and durations","links":null,"logos":{"small":"public/app/plugins/panel/state-timeline/img/timeline.svg","large":"public/app/plugins/panel/state-timeline/img/timeline.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/state-timeline/","category":"","state":"beta","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Status history","type":"panel","id":"status-history","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Periodic status history","links":null,"logos":{"small":"public/app/plugins/panel/status-history/img/status.svg","large":"public/app/plugins/panel/status-history/img/status.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/status-history/","category":"","state":"beta","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Table","type":"panel","id":"table","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Supports many column styles","links":null,"logos":{"small":"public/app/plugins/panel/table/img/icn-table-panel.svg","large":"public/app/plugins/panel/table/img/icn-table-panel.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/table/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Table (old)","type":"panel","id":"table-old","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Table Panel for Grafana","links":null,"logos":{"small":"public/app/plugins/panel/table-old/img/icn-table-panel.svg","large":"public/app/plugins/panel/table-old/img/icn-table-panel.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/table-old/","category":"","state":"deprecated","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Tempo","type":"datasource","id":"tempo","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"High volume, minimal dependency trace storage. OSS tracing solution from Grafana Labs.","links":[{"name":"GitHub Project","url":"https://github.com/grafana/tempo"}],"logos":{"small":"public/app/plugins/datasource/tempo/img/tempo_logo.svg","large":"public/app/plugins/datasource/tempo/img/tempo_logo.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/tempo/","category":"tracing","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"TestData DB","type":"datasource","id":"testdata","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Generates test data in different forms","links":null,"logos":{"small":"public/app/plugins/datasource/testdata/img/testdata.svg","large":"public/app/plugins/datasource/testdata/img/testdata.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/testdata/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Text","type":"panel","id":"text","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Supports markdown and html content","links":null,"logos":{"small":"public/app/plugins/panel/text/img/icn-text-panel.svg","large":"public/app/plugins/panel/text/img/icn-text-panel.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/text/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Time series","type":"panel","id":"timeseries","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Time based line, area and bar charts","links":null,"logos":{"small":"public/app/plugins/panel/timeseries/img/icn-timeseries-panel.svg","large":"public/app/plugins/panel/timeseries/img/icn-timeseries-panel.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/timeseries/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Traces","type":"panel","id":"traces","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"","links":null,"logos":{"small":"public/app/plugins/panel/traces/img/traces-panel.svg","large":"public/app/plugins/panel/traces/img/traces-panel.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/traces/","category":"","state":"beta","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Welcome","type":"panel","id":"welcome","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"","links":null,"logos":{"small":"public/app/plugins/panel/welcome/img/icn-dashlist-panel.svg","large":"public/app/plugins/panel/welcome/img/icn-dashlist-panel.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/welcome/","category":"","state":"","signature":"internal","signatureType":"","signatureOrg":""},{"name":"Zipkin","type":"datasource","id":"zipkin","enabled":true,"pinned":false,"info":{"author":{"name":"Grafana Labs","url":"https://grafana.com"},"description":"Placeholder for the distributed tracing system.","links":[{"name":"Learn more","url":"https://zipkin.io"}],"logos":{"small":"public/app/plugins/datasource/zipkin/img/zipkin-logo.svg","large":"public/app/plugins/datasource/zipkin/img/zipkin-logo.svg"},"build":{},"screenshots":null,"version":"","updated":""},"dependencies":{"grafanaDependency":"","grafanaVersion":"*","plugins":[]},"latestVersion":"","hasUpdate":false,"defaultNavUrl":"/plugins/zipkin/","category":"tracing","state":"","signature":"internal","signatureType":"","signatureOrg":""}] diff --git a/public/app/features/explore/AddToDashboard/addToDashboard.test.ts b/public/app/features/explore/AddToDashboard/addToDashboard.test.ts index cc6f3c054b2..67c302b99c3 100644 --- a/public/app/features/explore/AddToDashboard/addToDashboard.test.ts +++ b/public/app/features/explore/AddToDashboard/addToDashboard.test.ts @@ -90,12 +90,6 @@ describe('addPanelToDashboard', () => { [{ refId: 'A', hide: true }], { ...createEmptyQueryResponse(), logsFrames: [new MutableDataFrame({ refId: 'A', fields: [] })] }, ], - [ - // trace view is not supported in dashboards, we expect to fallback to table panel - 'If there are trace frames', - [{ refId: 'A' }], - { ...createEmptyQueryResponse(), traceFrames: [new MutableDataFrame({ refId: 'A', fields: [] })] }, - ], ]; it.each(cases)('%s', async (_, queries, queryResponse) => { @@ -115,15 +109,12 @@ describe('addPanelToDashboard', () => { framesType: string; expectedPanel: string; }; - // Note: traceFrames test is "duplicated" in "Defaults to table" tests. - // This is intentional as a way to enforce explicit tests for that case whenever in the future we'll - // add support for creating traceview panels it.each` framesType | expectedPanel ${'logsFrames'} | ${'logs'} ${'graphFrames'} | ${'timeseries'} ${'nodeGraphFrames'} | ${'nodeGraph'} - ${'traceFrames'} | ${'table'} + ${'traceFrames'} | ${'traces'} `( 'Sets visualization to $expectedPanel if there are $frameType frames', async ({ framesType, expectedPanel }: TestArgs) => { diff --git a/public/app/features/explore/AddToDashboard/addToDashboard.ts b/public/app/features/explore/AddToDashboard/addToDashboard.ts index 6128f221de8..17762d7823d 100644 --- a/public/app/features/explore/AddToDashboard/addToDashboard.ts +++ b/public/app/features/explore/AddToDashboard/addToDashboard.ts @@ -74,6 +74,9 @@ function getPanelType(queries: DataQuery[], queryResponse: ExplorePanelData) { if (queryResponse.nodeGraphFrames.some(hasQueryRefId)) { return 'nodeGraph'; } + if (queryResponse.traceFrames.some(hasQueryRefId)) { + return 'traces'; + } } // falling back to table diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 26f65b7eaf8..8993e49d98c 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -104,7 +104,7 @@ export type Props = ExploreProps & ConnectedProps; export class Explore extends React.PureComponent { scrollElement: HTMLDivElement | undefined; absoluteTimeUnsubsciber: Unsubscribable | undefined; - topOfExploreViewRef = createRef(); + topOfViewRef = createRef(); constructor(props: Props) { super(props); @@ -313,8 +313,8 @@ export class Explore extends React.PureComponent { dataFrames={dataFrames} splitOpenFn={splitOpen} scrollElement={this.scrollElement} - topOfExploreViewRef={this.topOfExploreViewRef} queryResponse={queryResponse} + topOfViewRef={this.topOfViewRef} /> ) ); @@ -357,11 +357,7 @@ export class Explore extends React.PureComponent { autoHeightMin={'100%'} scrollRefCallback={(scrollElement) => (this.scrollElement = scrollElement || undefined)} > - + {datasourceMissing ? this.renderEmptyState() : null} {datasourceInstance && (
diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index afa39a549ad..f4e7b0468b3 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -36,7 +36,7 @@ const AddToDashboard = lazy(() => interface OwnProps { exploreId: ExploreId; onChangeTime: (range: RawTimeRange, changedByScanner?: boolean) => void; - topOfExploreViewRef?: RefObject; + topOfViewRef: RefObject; } type Props = OwnProps & ConnectedProps; @@ -114,14 +114,14 @@ class UnConnectedExploreToolbar extends PureComponent { containerWidth, onChangeTimeZone, onChangeFiscalYearStartMonth, - topOfExploreViewRef, + topOfViewRef, } = this.props; const showSmallDataSourcePicker = (splitted ? containerWidth < 700 : containerWidth < 800) || false; const showSmallTimePicker = splitted || containerWidth < 1210; return ( -
+
(); const traceView = ( @@ -30,13 +32,10 @@ function getTraceView(frames: DataFrame[]) { traceProp={transformDataFrames(frames[0])!} search="" focusedSpanIdForSearch="" - expandOne={() => {}} - expandAll={() => {}} - collapseOne={() => {}} - collapseAll={() => {}} - childrenToggle={() => {}} - childrenHiddenIDs={new Set()} queryResponse={mockPanelData} + datasource={undefined} + topOfViewRef={topOfViewRef} + topOfViewRefType={TopOfViewRefType.Explore} /> ); @@ -85,10 +84,10 @@ describe('TraceView', () => { expect(prettyDOM(baseElement)).toEqual(prettyDOM(baseElementOld)); }); - it('does not render anything on missing trace', () => { + it('only renders noDataMsg on missing trace', () => { // Simulating Explore's access to empty response data const { container } = renderTraceView([]); - expect(container.hasChildNodes()).toBeFalsy(); + expect(container.childNodes.length === 1).toBeTruthy(); }); it('toggles detailState', async () => { diff --git a/public/app/features/explore/TraceView/TraceView.tsx b/public/app/features/explore/TraceView/TraceView.tsx index ff2e2bb5dee..ae36bb1b8d9 100644 --- a/public/app/features/explore/TraceView/TraceView.tsx +++ b/public/app/features/explore/TraceView/TraceView.tsx @@ -1,25 +1,24 @@ -import React, { RefObject, useCallback, useEffect, useMemo, useState } from 'react'; +import { css } from '@emotion/css'; +import { TopOfViewRefType } from '@jaegertracing/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView'; +import React, { RefObject, useCallback, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { DataFrame, DataLink, + DataQuery, DataSourceApi, + DataSourceJsonData, Field, + GrafanaTheme2, LinkModel, - LoadingState, mapInternalLinkToExplore, PanelData, SplitOpen, } from '@grafana/data'; import { getTemplateSrv } from '@grafana/runtime'; -import { - Trace, - TracePageHeader, - TraceSpan, - TraceTimelineViewer, - TTraceTimeline, -} from '@jaegertracing/jaeger-ui-components'; +import { useStyles2 } from '@grafana/ui'; +import { Trace, TracePageHeader, TraceTimelineViewer, TTraceTimeline } from '@jaegertracing/jaeger-ui-components'; import { TraceToLogsData } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { getTimeZone } from 'app/features/profile/state/selectors'; @@ -29,44 +28,43 @@ import { ExploreId } from 'app/types/explore'; import { changePanelState } from '../state/explorePane'; import { createSpanLinkFactory } from './createSpanLink'; +import { useChildrenState } from './useChildrenState'; import { useDetailState } from './useDetailState'; import { useHoverIndentGuide } from './useHoverIndentGuide'; import { useViewRange } from './useViewRange'; +const getStyles = (theme: GrafanaTheme2) => ({ + noDataMsg: css` + height: 100%; + width: 100%; + display: grid; + place-items: center; + font-size: ${theme.typography.h4.fontSize}; + color: ${theme.colors.text.secondary}; + `, +}); + function noop(): {} { return {}; } type Props = { dataFrames: DataFrame[]; - splitOpenFn: SplitOpen; - exploreId: ExploreId; + splitOpenFn?: SplitOpen; + exploreId?: ExploreId; scrollElement?: Element; - topOfExploreViewRef?: RefObject; traceProp: Trace; spanFindMatches?: Set; search: string; focusedSpanIdForSearch: string; - expandOne: (spans: TraceSpan[]) => void; - expandAll: () => void; - collapseOne: (spans: TraceSpan[]) => void; - collapseAll: (spans: TraceSpan[]) => void; - childrenToggle: (spanId: string) => void; - childrenHiddenIDs: Set; queryResponse: PanelData; + datasource: DataSourceApi | undefined; + topOfViewRef: RefObject; + topOfViewRefType: TopOfViewRefType; }; export function TraceView(props: Props) { - const { - expandOne, - expandAll, - collapseOne, - collapseAll, - childrenToggle, - childrenHiddenIDs, - spanFindMatches, - traceProp, - } = props; + const { spanFindMatches, traceProp, datasource, topOfViewRef, topOfViewRefType } = props; const { detailStates, @@ -83,6 +81,9 @@ export function TraceView(props: Props) { const { removeHoverIndentGuideId, addHoverIndentGuideId, hoverIndentGuideIds } = useHoverIndentGuide(); const { viewRange, updateViewRangeTime, updateNextViewRangeTime } = useViewRange(); + const { expandOne, collapseOne, childrenToggle, collapseAll, childrenHiddenIDs, expandAll } = useChildrenState(); + + const styles = useStyles2(getStyles); /** * Keeps state of resizable name column width @@ -93,13 +94,9 @@ export function TraceView(props: Props) { */ const [slim, setSlim] = useState(false); - const datasource = useSelector( - (state: StoreState) => state.explore[props.exploreId]?.datasourceInstance ?? undefined - ); - const [focusedSpanId, createFocusSpanLink] = useFocusSpanLink({ refId: props.dataFrames[0]?.refId, - exploreId: props.exploreId, + exploreId: props.exploreId!, datasource, }); @@ -120,79 +117,77 @@ export function TraceView(props: Props) { [childrenHiddenIDs, detailStates, hoverIndentGuideIds, spanNameColumnWidth, props.traceProp?.traceID] ); - useEffect(() => { - if (props.queryResponse.state === LoadingState.Done) { - props.topOfExploreViewRef?.current?.scrollIntoView(); - } - }, [props.queryResponse, props.topOfExploreViewRef]); - const traceToLogsOptions = (getDatasourceSrv().getInstanceSettings(datasource?.name)?.jsonData as TraceToLogsData) ?.tracesToLogs; const createSpanLink = useMemo( - () => createSpanLinkFactory({ splitOpenFn: props.splitOpenFn, traceToLogsOptions, dataFrame: props.dataFrames[0] }), + () => + createSpanLinkFactory({ splitOpenFn: props.splitOpenFn!, traceToLogsOptions, dataFrame: props.dataFrames[0] }), [props.splitOpenFn, traceToLogsOptions, props.dataFrames] ); const onSlimViewClicked = useCallback(() => setSlim(!slim), [slim]); const timeZone = useSelector((state: StoreState) => getTimeZone(state.user)); - if (!props.dataFrames?.length || !traceProp) { - return null; - } - return ( <> - - + {props.dataFrames?.length && props.dataFrames[0]?.meta?.preferredVisualisationType === 'trace' && traceProp ? ( + <> + + + + ) : ( +
No data
+ )} ); } diff --git a/public/app/features/explore/TraceView/TraceViewContainer.test.tsx b/public/app/features/explore/TraceView/TraceViewContainer.test.tsx index 89ee235f4c8..959420ad7c2 100644 --- a/public/app/features/explore/TraceView/TraceViewContainer.test.tsx +++ b/public/app/features/explore/TraceView/TraceViewContainer.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import React from 'react'; +import React, { createRef } from 'react'; import { Provider } from 'react-redux'; import { getDefaultTimeRange, LoadingState } from '@grafana/data'; @@ -18,6 +18,7 @@ function renderTraceViewContainer(frames = [frameOld]) { series: [], timeRange: getDefaultTimeRange(), }; + const topOfViewRef = createRef(); const { container, baseElement } = render( @@ -26,6 +27,7 @@ function renderTraceViewContainer(frames = [frameOld]) { dataFrames={frames} splitOpenFn={() => {}} queryResponse={mockPanelData} + topOfViewRef={topOfViewRef} /> ); diff --git a/public/app/features/explore/TraceView/TraceViewContainer.tsx b/public/app/features/explore/TraceView/TraceViewContainer.tsx index e5bd747f68b..db2a3654356 100644 --- a/public/app/features/explore/TraceView/TraceViewContainer.tsx +++ b/public/app/features/explore/TraceView/TraceViewContainer.tsx @@ -1,12 +1,14 @@ import TracePageSearchBar from '@jaegertracing/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar'; +import { TopOfViewRefType } from '@jaegertracing/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView'; import React, { RefObject, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; import { DataFrame, SplitOpen, PanelData } from '@grafana/data'; import { Collapse } from '@grafana/ui'; +import { StoreState } from 'app/types'; import { ExploreId } from 'app/types/explore'; import { TraceView } from './TraceView'; -import { useChildrenState } from './useChildrenState'; import { useSearch } from './useSearch'; import { transformDataFrames } from './utils/transform'; interface Props { @@ -14,71 +16,20 @@ interface Props { splitOpenFn: SplitOpen; exploreId: ExploreId; scrollElement?: Element; - topOfExploreViewRef?: RefObject; queryResponse: PanelData; + topOfViewRef: RefObject; } export function TraceViewContainer(props: Props) { // At this point we only show single trace const frame = props.dataFrames[0]; - - const { dataFrames, splitOpenFn, exploreId, scrollElement, topOfExploreViewRef, queryResponse } = props; + const { dataFrames, splitOpenFn, exploreId, scrollElement, topOfViewRef, queryResponse } = props; const traceProp = useMemo(() => transformDataFrames(frame), [frame]); const { search, setSearch, spanFindMatches } = useSearch(traceProp?.spans); - const { expandOne, collapseOne, childrenToggle, collapseAll, childrenHiddenIDs, expandAll } = useChildrenState(); - const [focusedSpanIdForSearch, setFocusedSpanIdForSearch] = useState(''); const [searchBarSuffix, setSearchBarSuffix] = useState(''); - - const setTraceSearch = (value: string) => { - setFocusedSpanIdForSearch(''); - setSearchBarSuffix(''); - setSearch(value); - }; - - const nextResult = () => { - expandAll(); - const spanMatches = Array.from(spanFindMatches!); - const prevMatchedIndex = spanMatches.indexOf(focusedSpanIdForSearch) - ? spanMatches.indexOf(focusedSpanIdForSearch) - : 0; - - // new query || at end, go to start - if (prevMatchedIndex === -1 || prevMatchedIndex === spanMatches.length - 1) { - setFocusedSpanIdForSearch(spanMatches[0]); - setSearchBarSuffix(getSearchBarSuffix(1)); - return; - } - - // get next - setFocusedSpanIdForSearch(spanMatches[prevMatchedIndex + 1]); - setSearchBarSuffix(getSearchBarSuffix(prevMatchedIndex + 2)); - }; - - const prevResult = () => { - expandAll(); - const spanMatches = Array.from(spanFindMatches!); - const prevMatchedIndex = spanMatches.indexOf(focusedSpanIdForSearch) - ? spanMatches.indexOf(focusedSpanIdForSearch) - : 0; - - // new query || at start, go to end - if (prevMatchedIndex === -1 || prevMatchedIndex === 0) { - setFocusedSpanIdForSearch(spanMatches[spanMatches.length - 1]); - setSearchBarSuffix(getSearchBarSuffix(spanMatches.length)); - return; - } - - // get prev - setFocusedSpanIdForSearch(spanMatches[prevMatchedIndex - 1]); - setSearchBarSuffix(getSearchBarSuffix(prevMatchedIndex)); - }; - - const getSearchBarSuffix = (index: number): string => { - if (spanFindMatches?.size && spanFindMatches?.size > 0) { - return index + ' of ' + spanFindMatches?.size; - } - return ''; - }; + const datasource = useSelector( + (state: StoreState) => state.explore[props.exploreId!]?.datasourceInstance ?? undefined + ); if (!traceProp) { return null; @@ -87,12 +38,14 @@ export function TraceViewContainer(props: Props) { return ( <> @@ -101,18 +54,14 @@ export function TraceViewContainer(props: Props) { dataFrames={dataFrames} splitOpenFn={splitOpenFn} scrollElement={scrollElement} - topOfExploreViewRef={topOfExploreViewRef} traceProp={traceProp} spanFindMatches={spanFindMatches} search={search} focusedSpanIdForSearch={focusedSpanIdForSearch} - expandOne={expandOne} - collapseOne={collapseOne} - collapseAll={collapseAll} - expandAll={expandAll} - childrenToggle={childrenToggle} - childrenHiddenIDs={childrenHiddenIDs} queryResponse={queryResponse} + datasource={datasource} + topOfViewRef={topOfViewRef} + topOfViewRefType={TopOfViewRefType.Explore} /> diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts index 9bd37e1a13f..6202e566bb6 100644 --- a/public/app/features/plugins/built_in_plugins.ts +++ b/public/app/features/plugins/built_in_plugins.ts @@ -63,6 +63,7 @@ import * as statusHistoryPanel from 'app/plugins/panel/status-history/module'; import * as tablePanel from 'app/plugins/panel/table/module'; import * as textPanel from 'app/plugins/panel/text/module'; import * as timeseriesPanel from 'app/plugins/panel/timeseries/module'; +import * as tracesPanel from 'app/plugins/panel/traces/module'; import * as welcomeBanner from 'app/plugins/panel/welcome/module'; import * as xyChartPanel from 'app/plugins/panel/xychart/module'; @@ -125,6 +126,7 @@ const builtInPlugins: any = { 'app/plugins/panel/bargauge/module': barGaugePanel, 'app/plugins/panel/barchart/module': barChartPanel, 'app/plugins/panel/logs/module': logsPanel, + 'app/plugins/panel/traces/module': tracesPanel, 'app/plugins/panel/welcome/module': welcomeBanner, 'app/plugins/panel/nodeGraph/module': nodeGraph, 'app/plugins/panel/histogram/module': histogramPanel, diff --git a/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.tsx b/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.tsx index 994f2af75c8..e05d2a310ba 100644 --- a/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.tsx +++ b/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.tsx @@ -5,6 +5,7 @@ import React, { useCallback, useState, useEffect, useMemo } from 'react'; import { Node } from 'slate'; import { GrafanaTheme2, isValidGoDuration, SelectableValue } from '@grafana/data'; +import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import { InlineFieldRow, InlineField, @@ -152,6 +153,8 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props } }; + const templateSrv: TemplateSrv = getTemplateSrv(); + return ( <>
@@ -235,7 +238,8 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props value={query.minDuration || ''} placeholder={durationPlaceholder} onBlur={() => { - if (query.minDuration && !isValidGoDuration(query.minDuration)) { + const templatedMinDuration = templateSrv.replace(query.minDuration ?? ''); + if (query.minDuration && !isValidGoDuration(templatedMinDuration)) { setInputErrors({ ...inputErrors, minDuration: true }); } else { setInputErrors({ ...inputErrors, minDuration: false }); @@ -258,7 +262,8 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props value={query.maxDuration || ''} placeholder={durationPlaceholder} onBlur={() => { - if (query.maxDuration && !isValidGoDuration(query.maxDuration)) { + const templatedMaxDuration = templateSrv.replace(query.maxDuration ?? ''); + if (query.maxDuration && !isValidGoDuration(templatedMaxDuration)) { setInputErrors({ ...inputErrors, maxDuration: true }); } else { setInputErrors({ ...inputErrors, maxDuration: false }); diff --git a/public/app/plugins/datasource/tempo/datasource.test.ts b/public/app/plugins/datasource/tempo/datasource.test.ts index 9798915f6a7..36d5d2c8531 100644 --- a/public/app/plugins/datasource/tempo/datasource.test.ts +++ b/public/app/plugins/datasource/tempo/datasource.test.ts @@ -32,6 +32,57 @@ describe('Tempo data source', () => { expect(response).toBe('empty'); }); + describe('Variables should be interpolated correctly', () => { + function getQuery(): TempoQuery { + return { + refId: 'x', + queryType: 'traceId', + linkedQuery: { + refId: 'linked', + expr: '{instance="$interpolationVar"}', + }, + query: '$interpolationVar', + search: '$interpolationVar', + minDuration: '$interpolationVar', + maxDuration: '$interpolationVar', + }; + } + + it('when traceId query for dashboard->explore', async () => { + const templateSrv: any = { replace: jest.fn() }; + const ds = new TempoDatasource(defaultSettings, templateSrv); + const text = 'interpolationText'; + templateSrv.replace.mockReturnValue(text); + + const queries = ds.interpolateVariablesInQueries([getQuery()], { + interpolationVar: { text: text, value: text }, + }); + expect(templateSrv.replace).toBeCalledTimes(5); + expect(queries[0].linkedQuery?.expr).toBe(text); + expect(queries[0].query).toBe(text); + expect(queries[0].search).toBe(text); + expect(queries[0].minDuration).toBe(text); + expect(queries[0].maxDuration).toBe(text); + }); + + it('when traceId query for template variable', async () => { + const templateSrv: any = { replace: jest.fn() }; + const ds = new TempoDatasource(defaultSettings, templateSrv); + const text = 'interpolationText'; + templateSrv.replace.mockReturnValue(text); + + const resp = ds.applyTemplateVariables(getQuery(), { + interpolationVar: { text: text, value: text }, + }); + expect(templateSrv.replace).toBeCalledTimes(5); + expect(resp.linkedQuery?.expr).toBe(text); + expect(resp.query).toBe(text); + expect(resp.search).toBe(text); + expect(resp.minDuration).toBe(text); + expect(resp.maxDuration).toBe(text); + }); + }); + it('parses json fields from backend', async () => { setupBackendSrv( new MutableDataFrame({ @@ -49,7 +100,8 @@ describe('Tempo data source', () => { ], }) ); - const ds = new TempoDatasource(defaultSettings); + const templateSrv: any = { replace: jest.fn() }; + const ds = new TempoDatasource(defaultSettings, templateSrv); const response = await lastValueFrom(ds.query({ targets: [{ refId: 'refid1', query: '12345' }] } as any)); expect( @@ -152,7 +204,10 @@ describe('Tempo data source', () => { }); it('should build search query correctly', () => { - const ds = new TempoDatasource(defaultSettings); + const templateSrv: any = { replace: jest.fn() }; + const ds = new TempoDatasource(defaultSettings, templateSrv); + const duration = '10ms'; + templateSrv.replace.mockReturnValue(duration); const tempoQuery: TempoQuery = { queryType: 'search', refId: 'A', @@ -160,15 +215,15 @@ describe('Tempo data source', () => { serviceName: 'frontend', spanName: '/config', search: 'root.http.status_code=500', - minDuration: '1ms', - maxDuration: '100s', + minDuration: '$interpolationVar', + maxDuration: '$interpolationVar', limit: 10, }; const builtQuery = ds.buildSearchQuery(tempoQuery); expect(builtQuery).toStrictEqual({ tags: 'root.http.status_code=500 service.name="frontend" name="/config"', - minDuration: '1ms', - maxDuration: '100s', + minDuration: duration, + maxDuration: duration, limit: 10, }); }); diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index c83dfbf9e4d..d6b1413a09e 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -11,8 +11,16 @@ import { DataSourceJsonData, isValidGoDuration, LoadingState, + ScopedVars, } from '@grafana/data'; -import { config, BackendSrvRequest, DataSourceWithBackend, getBackendSrv } from '@grafana/runtime'; +import { + config, + BackendSrvRequest, + DataSourceWithBackend, + getBackendSrv, + TemplateSrv, + getTemplateSrv, +} from '@grafana/runtime'; import { NodeGraphOptions } from 'app/core/components/NodeGraphSettings'; import { TraceToLogsOptions } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; import { serializeParams } from 'app/core/utils/fetch'; @@ -92,7 +100,10 @@ export class TempoDatasource extends DataSourceWithBackend) { + constructor( + private instanceSettings: DataSourceInstanceSettings, + private readonly templateSrv: TemplateSrv = getTemplateSrv() + ) { super(instanceSettings); this.tracesToLogs = instanceSettings.jsonData.tracesToLogs; this.serviceMap = instanceSettings.jsonData.serviceMap; @@ -151,7 +162,8 @@ export class TempoDatasource extends DataSourceWithBackend { @@ -193,6 +205,43 @@ export class TempoDatasource extends DataSourceWithBackend { + return this.applyVariables(query, scopedVars); + } + + interpolateVariablesInQueries(queries: TempoQuery[], scopedVars: ScopedVars): TempoQuery[] { + if (!queries || queries.length === 0) { + return []; + } + + return queries.map((query) => { + return { + ...query, + datasource: this.getRef(), + ...this.applyVariables(query, scopedVars), + }; + }); + } + + applyVariables(query: TempoQuery, scopedVars: ScopedVars) { + const expandedQuery = { ...query }; + + if (query.linkedQuery) { + expandedQuery.linkedQuery = { + ...query.linkedQuery, + expr: this.templateSrv.replace(query.linkedQuery?.expr ?? '', scopedVars), + }; + } + + return { + ...expandedQuery, + query: this.templateSrv.replace(query.query ?? '', scopedVars), + search: this.templateSrv.replace(query.search ?? '', scopedVars), + minDuration: this.templateSrv.replace(query.minDuration ?? '', scopedVars), + maxDuration: this.templateSrv.replace(query.maxDuration ?? '', scopedVars), + }; + } + /** * Handles the simplest of the queries where we have just a trace id and return trace data for it. * @param options @@ -278,12 +327,14 @@ export class TempoDatasource extends DataSourceWithBackend { + it('shows no data message when no data supplied', async () => { + const props = { + data: { + error: undefined, + series: [], + state: LoadingState.Done, + }, + } as unknown as PanelProps; + + render(); + + await screen.findByText('No data found in response'); + }); +}); diff --git a/public/app/plugins/panel/traces/TracesPanel.tsx b/public/app/plugins/panel/traces/TracesPanel.tsx new file mode 100644 index 00000000000..5643354cd85 --- /dev/null +++ b/public/app/plugins/panel/traces/TracesPanel.tsx @@ -0,0 +1,69 @@ +import { css } from '@emotion/css'; +import TracePageSearchBar from '@jaegertracing/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar'; +import { TopOfViewRefType } from '@jaegertracing/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView'; +import React, { useMemo, useState, createRef } from 'react'; +import { useAsync } from 'react-use'; + +import { PanelProps } from '@grafana/data'; +import { getDataSourceSrv } from '@grafana/runtime'; +import { TraceView } from 'app/features/explore/TraceView/TraceView'; +import { useSearch } from 'app/features/explore/TraceView/useSearch'; +import { transformDataFrames } from 'app/features/explore/TraceView/utils/transform'; + +const styles = { + wrapper: css` + height: 100%; + overflow: scroll; + `, +}; + +export const TracesPanel: React.FunctionComponent = ({ data }) => { + const topOfViewRef = createRef(); + const traceProp = useMemo(() => transformDataFrames(data.series[0]), [data.series]); + const { search, setSearch, spanFindMatches } = useSearch(traceProp?.spans); + const [focusedSpanIdForSearch, setFocusedSpanIdForSearch] = useState(''); + const [searchBarSuffix, setSearchBarSuffix] = useState(''); + const dataSource = useAsync(async () => { + return await getDataSourceSrv().get(data.request?.targets[0].datasource?.uid); + }); + const scrollElement = document.getElementsByClassName(styles.wrapper)[0]; + + if (!data || !data.series.length || !traceProp) { + return ( +
+

No data found in response

+
+ ); + } + + return ( +
+
+ {data.series[0]?.meta?.preferredVisualisationType === 'trace' ? ( + + ) : null} + + +
+ ); +}; diff --git a/public/app/plugins/panel/traces/img/traces-panel.svg b/public/app/plugins/panel/traces/img/traces-panel.svg new file mode 100644 index 00000000000..4a475bc6897 --- /dev/null +++ b/public/app/plugins/panel/traces/img/traces-panel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/app/plugins/panel/traces/module.tsx b/public/app/plugins/panel/traces/module.tsx new file mode 100644 index 00000000000..06b2f4fee43 --- /dev/null +++ b/public/app/plugins/panel/traces/module.tsx @@ -0,0 +1,5 @@ +import { PanelPlugin } from '@grafana/data'; + +import { TracesPanel } from './TracesPanel'; + +export const plugin = new PanelPlugin(TracesPanel); diff --git a/public/app/plugins/panel/traces/plugin.json b/public/app/plugins/panel/traces/plugin.json new file mode 100644 index 00000000000..335c4b41372 --- /dev/null +++ b/public/app/plugins/panel/traces/plugin.json @@ -0,0 +1,17 @@ +{ + "type": "panel", + "name": "Traces", + "id": "traces", + "state": "beta", + + "info": { + "author": { + "name": "Grafana Labs", + "url": "https://grafana.com" + }, + "logos": { + "small": "img/traces-panel.svg", + "large": "img/traces-panel.svg" + } + } +} From 0b5ffcfcf052aa91b761f168286614e0e1028413 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Tue, 3 May 2022 15:03:41 -0500 Subject: [PATCH 008/440] Geomap: Legend style update (#48641) Legend style update --- .../geomap/layers/data/MarkersLegend.tsx | 54 ++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/public/app/plugins/panel/geomap/layers/data/MarkersLegend.tsx b/public/app/plugins/panel/geomap/layers/data/MarkersLegend.tsx index b1e982b68f4..a5240a6cbe9 100644 --- a/public/app/plugins/panel/geomap/layers/data/MarkersLegend.tsx +++ b/public/app/plugins/panel/geomap/layers/data/MarkersLegend.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from 'react'; -import { Label, stylesFactory, useTheme2, VizLegendItem } from '@grafana/ui'; +import { useStyles2, VizLegendItem } from '@grafana/ui'; import { DataFrame, formattedValueToString, getFieldColorModeForField, GrafanaTheme2 } from '@grafana/data'; -import { css } from '@emotion/css'; +import { css, cx } from '@emotion/css'; import { config } from 'app/core/config'; import { DimensionSupplier } from 'app/features/dimensions'; import { getThresholdItems } from 'app/plugins/panel/state-timeline/utils'; @@ -23,8 +23,7 @@ export interface MarkersLegendProps { export function MarkersLegend(props: MarkersLegendProps) { const { layerName, styleConfig, layer } = props; - const theme = useTheme2(); - const style = getStyles(theme); + const style = useStyles2(getStyles); const hoverEvent = useObservable(((layer as any)?.__state as MapLayerState)?.mouseEvents ?? of(undefined)); @@ -55,14 +54,14 @@ export function MarkersLegend(props: MarkersLegendProps) { if (color && symbol && !colorField) { return (
-
+
{layerName}
+
- {layerName}
) @@ -90,15 +89,12 @@ export function MarkersLegend(props: MarkersLegendProps) { const display = colorField.display ? (v: number) => formattedValueToString(colorField.display!(v)) : (v: number) => `${v}`; return ( - <> -
- - -
-
+
+
{layerName}
+
- +
); } @@ -110,7 +106,8 @@ export function MarkersLegend(props: MarkersLegendProps) { const items = getThresholdItems(colorField!.config, config.theme2); return (
-
+
{layerName}
+
{items.map((item: VizLegendItem, idx: number) => (
@@ -122,17 +119,28 @@ export function MarkersLegend(props: MarkersLegendProps) { ); } -const getStyles = stylesFactory((theme: GrafanaTheme2) => ({ +const getStyles = (theme: GrafanaTheme2) => ({ infoWrap: css` + display: flex; + flex-direction: column; background: ${theme.colors.background.secondary}; - border-radius: 2px; + border-radius: 1px; padding: ${theme.spacing(1)}; + border-bottom: 2px solid ${theme.colors.border.strong}; + min-width: 150px; + `, + layerName: css` + font-size: ${theme.typography.body.fontSize}; + `, + layerBody: css` + padding-left: 10px; `, legend: css` line-height: 18px; display: flex; flex-direction: column; font-size: ${theme.typography.bodySmall.fontSize}; + padding: 5px 10px 0; i { width: 18px; @@ -148,20 +156,16 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => ({ fixedColorContainer: css` min-width: 80px; font-size: ${theme.typography.bodySmall.fontSize}; + padding-top: 5px; `, legendSymbol: css` - height: 10px; - width: 10px; + height: 15px; + width: 15px; margin: auto; - margin-right: 4px; `, colorScaleWrapper: css` min-width: 200px; font-size: ${theme.typography.bodySmall.fontSize}; - padding: ${theme.spacing(0, 0.5)}; + padding-top: 10px; `, - labelsWrapper: css` - display: flex; - justify-content: space-between; - ` -})); +}); From b88644cb8328b20baf2f144426221550614e38e7 Mon Sep 17 00:00:00 2001 From: Jeff Levin Date: Tue, 3 May 2022 15:08:40 -0800 Subject: [PATCH 009/440] public dashboards: add public dashboard table (#48470) * add public dashboard table migration --- .../migrations/dashboard_public_config_mig.go | 26 +++++++++++++++++++ .../sqlstore/migrations/migrations.go | 2 ++ 2 files changed, 28 insertions(+) create mode 100644 pkg/services/sqlstore/migrations/dashboard_public_config_mig.go diff --git a/pkg/services/sqlstore/migrations/dashboard_public_config_mig.go b/pkg/services/sqlstore/migrations/dashboard_public_config_mig.go new file mode 100644 index 00000000000..97c86e5bab0 --- /dev/null +++ b/pkg/services/sqlstore/migrations/dashboard_public_config_mig.go @@ -0,0 +1,26 @@ +package migrations + +import ( + . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" +) + +func addPublicDashboardMigration(mg *Migrator) { + var dashboardPublicCfgV1 = Table{ + Name: "dashboard_public_config", + Columns: []*Column{ + {Name: "uid", Type: DB_BigInt, IsPrimaryKey: true}, + {Name: "dashboard_uid", Type: DB_NVarchar, Length: 40, Nullable: false}, + {Name: "org_id", Type: DB_BigInt, Nullable: false}, + {Name: "refresh_rate", Type: DB_Int, Nullable: false, Default: "30"}, + {Name: "template_variables", Type: DB_MediumText, Nullable: true}, + {Name: "time_variables", Type: DB_Text, Nullable: false}, + }, + Indices: []*Index{ + {Cols: []string{"uid"}, Type: UniqueIndex}, + {Cols: []string{"org_id", "dashboard_uid"}}, + }, + } + + mg.AddMigration("create dashboard public config v1", NewAddTableMigration(dashboardPublicCfgV1)) + addTableIndicesMigrations(mg, "v1", dashboardPublicCfgV1) +} diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 4df98b3f495..32f287d8296 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -89,6 +89,8 @@ func (*OSSMigrations) AddMigration(mg *Migrator) { } addEntityEventsTableMigration(mg) + + addPublicDashboardMigration(mg) } func addMigrationLogMigrations(mg *Migrator) { From 38fc0c68e4ed4825eebaba182741e65f9c26a3b7 Mon Sep 17 00:00:00 2001 From: Jeff Levin Date: Tue, 3 May 2022 15:10:59 -0800 Subject: [PATCH 010/440] Update documentation to explicitly state we should not be putting migrations behind feature flags (#48663) --- contribute/architecture/backend/database.md | 2 ++ pkg/services/sqlstore/migrations/migrations.go | 3 +++ 2 files changed, 5 insertions(+) diff --git a/contribute/architecture/backend/database.md b/contribute/architecture/backend/database.md index 75f7a63704b..4d2700015bf 100644 --- a/contribute/architecture/backend/database.md +++ b/contribute/architecture/backend/database.md @@ -99,6 +99,8 @@ To add a migration: - In the `AddMigrations` function, find the `addXxxMigration` function for the service you want to create a migration for. - At the end of the `addXxxMigration` function, register your migration: +> **NOTE:** Putting migrations behind feature flags is no longer recommended as it may cause the migration skip integration testing. + [Example](https://github.com/grafana/grafana/blob/00d0640b6e778ddaca021670fe851fe00982acf2/pkg/services/sqlstore/migrations/migrations.go#L55-L70) ### Implement `DatabaseMigrator` diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 32f287d8296..760c0172604 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -13,6 +13,9 @@ import ( // 1. Never change a migration that is committed and pushed to main // 2. Always add new migrations (to change or undo previous migrations) // 3. Some migrations are not yet written (rename column, table, drop table, index etc) +// 4. Putting migrations behind feature flags is no longer recommended as broken +// migrations may not be caught by integration tests unless feature flags are +// specifically added type OSSMigrations struct { } From 66d7105b34e9f3c7d4cf5e432e51d58561252996 Mon Sep 17 00:00:00 2001 From: Nathan Marrs Date: Tue, 3 May 2022 19:51:01 -0700 Subject: [PATCH 011/440] Canvas: Group constraint support (#48563) --- .../app/features/canvas/runtime/element.tsx | 21 ++++- public/app/features/canvas/runtime/group.tsx | 84 ++++++++++--------- public/app/features/canvas/runtime/root.tsx | 14 ++++ public/app/features/canvas/runtime/scene.tsx | 50 ++++++++++- .../canvas/editor/LayerElementListEditor.tsx | 9 +- .../panel/canvas/editor/layerEditor.tsx | 1 + 6 files changed, 129 insertions(+), 50 deletions(-) diff --git a/public/app/features/canvas/runtime/element.tsx b/public/app/features/canvas/runtime/element.tsx index 37000ed02bd..dd41cf89d99 100644 --- a/public/app/features/canvas/runtime/element.tsx +++ b/public/app/features/canvas/runtime/element.tsx @@ -14,6 +14,7 @@ import { DimensionContext } from 'app/features/dimensions'; import { HorizontalConstraint, Placement, VerticalConstraint } from '../types'; import { GroupState } from './group'; +import { RootElement } from './root'; import { Scene } from './scene'; let counter = 0; @@ -68,6 +69,11 @@ export class ElementState implements LayerElement { /** Use the configured options to update CSS style properties directly on the wrapper div **/ applyLayoutStylesToDiv() { + if (this.isRoot()) { + // Root supersedes layout engine and is always 100% width + height of panel + return; + } + const { constraint } = this.options; const { vertical, horizontal } = constraint ?? {}; const placement = this.options.placement ?? ({} as Placement); @@ -170,12 +176,16 @@ export class ElementState implements LayerElement { } } - setPlacementFromConstraint() { + setPlacementFromConstraint(elementContainer?: DOMRect, parentContainer?: DOMRect) { const { constraint } = this.options; const { vertical, horizontal } = constraint ?? {}; - const elementContainer = this.div && this.div.getBoundingClientRect(); - const parentContainer = this.div && this.div.parentElement?.getBoundingClientRect(); + if (!elementContainer) { + elementContainer = this.div && this.div.getBoundingClientRect(); + } + if (!parentContainer) { + parentContainer = this.div && this.div.parentElement?.getBoundingClientRect(); + } const relativeTop = elementContainer && parentContainer ? Math.abs(Math.round(elementContainer.top - parentContainer.top)) : 0; @@ -305,6 +315,11 @@ export class ElementState implements LayerElement { } this.dataStyle = css; + this.applyLayoutStylesToDiv(); + } + + isRoot(): this is RootElement { + return false; } /** Recursively visit all nodes */ diff --git a/public/app/features/canvas/runtime/group.tsx b/public/app/features/canvas/runtime/group.tsx index 35d38155a90..809dfc12fa4 100644 --- a/public/app/features/canvas/runtime/group.tsx +++ b/public/app/features/canvas/runtime/group.tsx @@ -82,7 +82,7 @@ export class GroupState extends ElementState { // ??? or should this be on the element directly? // are actions scoped to layers? - doAction = (action: LayerActionID, element: ElementState, updateName = true) => { + doAction = (action: LayerActionID, element: ElementState, updateName = true, shiftItemsOnDuplicate = true) => { switch (action) { case LayerActionID.Delete: this.elements = this.elements.filter((e) => e !== element); @@ -97,48 +97,50 @@ export class GroupState extends ElementState { } const opts = cloneDeep(element.options); - const { constraint, placement: oldPlacement } = element.options; - const { vertical, horizontal } = constraint ?? {}; - const placement = oldPlacement ?? ({} as Placement); + if (shiftItemsOnDuplicate) { + const { constraint, placement: oldPlacement } = element.options; + const { vertical, horizontal } = constraint ?? {}; + const placement = oldPlacement ?? ({} as Placement); - switch (vertical) { - case VerticalConstraint.Top: - case VerticalConstraint.TopBottom: - if (placement.top == null) { - placement.top = 25; - } else { - placement.top += 10; - } - break; - case VerticalConstraint.Bottom: - if (placement.bottom == null) { - placement.bottom = 100; - } else { - placement.bottom -= 10; - } - break; + switch (vertical) { + case VerticalConstraint.Top: + case VerticalConstraint.TopBottom: + if (placement.top == null) { + placement.top = 25; + } else { + placement.top += 10; + } + break; + case VerticalConstraint.Bottom: + if (placement.bottom == null) { + placement.bottom = 100; + } else { + placement.bottom -= 10; + } + break; + } + + switch (horizontal) { + case HorizontalConstraint.Left: + case HorizontalConstraint.LeftRight: + if (placement.left == null) { + placement.left = 50; + } else { + placement.left += 10; + } + break; + case HorizontalConstraint.Right: + if (placement.right == null) { + placement.right = 50; + } else { + placement.right -= 10; + } + break; + } + + opts.placement = placement; } - switch (horizontal) { - case HorizontalConstraint.Left: - case HorizontalConstraint.LeftRight: - if (placement.left == null) { - placement.left = 50; - } else { - placement.left += 10; - } - break; - case HorizontalConstraint.Right: - if (placement.right == null) { - placement.right = 50; - } else { - placement.right -= 10; - } - break; - } - - opts.placement = placement; - const copy = new ElementState(element.item, opts, this); copy.updateData(this.scene.context); if (updateName) { @@ -157,7 +159,7 @@ export class GroupState extends ElementState { render() { return ( -
+
{this.elements.map((v) => v.render())}
); diff --git a/public/app/features/canvas/runtime/root.tsx b/public/app/features/canvas/runtime/root.tsx index 8eebe0c01aa..47b1339356d 100644 --- a/public/app/features/canvas/runtime/root.tsx +++ b/public/app/features/canvas/runtime/root.tsx @@ -1,3 +1,5 @@ +import React from 'react'; + import { CanvasGroupOptions, CanvasElementOptions } from 'app/features/canvas'; import { GroupState } from './group'; @@ -32,4 +34,16 @@ export class RootElement extends GroupState { elements: this.elements.map((v) => v.getSaveModel()), }; } + + setRootRef = (target: HTMLDivElement) => { + this.div = target; + }; + + render() { + return ( +
+ {this.elements.map((v) => v.render())} +
+ ); + } } diff --git a/public/app/features/canvas/runtime/scene.tsx b/public/app/features/canvas/runtime/scene.tsx index 41db45e95a6..edfcb5689f2 100644 --- a/public/app/features/canvas/runtime/scene.tsx +++ b/public/app/features/canvas/runtime/scene.tsx @@ -26,6 +26,8 @@ import { } from 'app/features/dimensions/utils'; import { LayerActionID } from 'app/plugins/panel/canvas/types'; +import { Placement } from '../types'; + import { ElementState } from './element'; import { GroupState } from './group'; import { RootElement } from './root'; @@ -138,11 +140,19 @@ export class Scene { currentSelectedElements[0].parent ); + const groupPlacement = this.generateGroupContainer(currentSelectedElements); + + newLayer.options.placement = groupPlacement; + currentSelectedElements.forEach((element: ElementState) => { + const elementContainer = element.div?.getBoundingClientRect(); + element.setPlacementFromConstraint(elementContainer, groupPlacement as DOMRect); currentLayer.doAction(LayerActionID.Delete, element); - newLayer.doAction(LayerActionID.Duplicate, element, false); + newLayer.doAction(LayerActionID.Duplicate, element, false, false); }); + newLayer.setPlacementFromConstraint(groupPlacement as DOMRect, currentLayer.div?.getBoundingClientRect()); + currentLayer.elements.push(newLayer); this.byName.set(newLayer.getName(), newLayer); @@ -151,6 +161,44 @@ export class Scene { }); } + private generateGroupContainer = (elements: ElementState[]): Placement => { + let minTop = Infinity; + let minLeft = Infinity; + let maxRight = 0; + let maxBottom = 0; + + elements.forEach((element: ElementState) => { + const elementContainer = element.div?.getBoundingClientRect(); + + if (!elementContainer) { + return; + } + + if (minTop > elementContainer.top) { + minTop = elementContainer.top; + } + + if (minLeft > elementContainer.left) { + minLeft = elementContainer.left; + } + + if (maxRight < elementContainer.right) { + maxRight = elementContainer.right; + } + + if (maxBottom < elementContainer.bottom) { + maxBottom = elementContainer.bottom; + } + }); + + return { + top: minTop, + left: minLeft, + width: maxRight - minLeft, + height: maxBottom - minTop, + }; + }; + clearCurrentSelection() { let event: MouseEvent = new MouseEvent('click'); this.selecto?.clickTarget(event, this.div); diff --git a/public/app/plugins/panel/canvas/editor/LayerElementListEditor.tsx b/public/app/plugins/panel/canvas/editor/LayerElementListEditor.tsx index 75d36be7784..809495898b5 100644 --- a/public/app/plugins/panel/canvas/editor/LayerElementListEditor.tsx +++ b/public/app/plugins/panel/canvas/editor/LayerElementListEditor.tsx @@ -55,10 +55,7 @@ export class LayerElementListEditor extends PureComponent { let selection: SelectionParams = { targets: [] }; if (item instanceof GroupState) { const targetElements: HTMLDivElement[] = []; - item.elements.forEach((element: ElementState) => { - targetElements.push(element.div!); - }); - + targetElements.push(item?.div!); selection.targets = targetElements; selection.group = item; settings.scene.select(selection); @@ -129,7 +126,9 @@ export class LayerElementListEditor extends PureComponent { this.deleteGroup(); layer.elements.forEach((element: ElementState) => { - layer.parent?.doAction(LayerActionID.Duplicate, element, false); + const elementContainer = element.div?.getBoundingClientRect(); + element.setPlacementFromConstraint(elementContainer, layer.parent?.div?.getBoundingClientRect()); + layer.parent?.doAction(LayerActionID.Duplicate, element, false, false); }); }; diff --git a/public/app/plugins/panel/canvas/editor/layerEditor.tsx b/public/app/plugins/panel/canvas/editor/layerEditor.tsx index 9c07fd425c1..dcf2ba29abe 100644 --- a/public/app/plugins/panel/canvas/editor/layerEditor.tsx +++ b/public/app/plugins/panel/canvas/editor/layerEditor.tsx @@ -57,6 +57,7 @@ export function getLayerEditor(opts: InstanceState): NestedPanelOptions Date: Wed, 4 May 2022 07:36:18 +0200 Subject: [PATCH 012/440] CloudWatch: Display dynamic label field in case feature is enabled (#48614) * move metrics editor related files to a separate folder * cleanup * add tests * remove snapshot test * nit * remove unsued import * remove snapshot --- .../{ => MetricsQueryEditor}/Alias.tsx | 5 +- .../MetricsQueryEditor.test.tsx | 61 +++++++++++++++---- .../MetricsQueryEditor.tsx | 55 +++++++++++------ .../MetricsQueryHeader.test.tsx | 4 +- .../MetricsQueryHeader.tsx | 4 +- .../usePreparedMetricsQuery.test.ts | 2 +- .../usePreparedMetricsQuery.ts | 4 +- .../components/PanelQueryEditor.tsx | 2 +- .../cloudwatch/components/QueryHeader.tsx | 2 +- .../MetricsQueryEditor.test.tsx.snap | 3 - .../datasource/cloudwatch/components/index.ts | 1 - 11 files changed, 98 insertions(+), 45 deletions(-) rename public/app/plugins/datasource/cloudwatch/components/{ => MetricsQueryEditor}/Alias.tsx (78%) rename public/app/plugins/datasource/cloudwatch/components/{ => MetricsQueryEditor}/MetricsQueryEditor.test.tsx (76%) rename public/app/plugins/datasource/cloudwatch/components/{ => MetricsQueryEditor}/MetricsQueryEditor.tsx (73%) rename public/app/plugins/datasource/cloudwatch/components/{ => MetricsQueryEditor}/MetricsQueryHeader.test.tsx (97%) rename public/app/plugins/datasource/cloudwatch/components/{ => MetricsQueryEditor}/MetricsQueryHeader.tsx (96%) rename public/app/plugins/datasource/cloudwatch/components/{ => MetricsQueryEditor}/usePreparedMetricsQuery.test.ts (99%) rename public/app/plugins/datasource/cloudwatch/components/{ => MetricsQueryEditor}/usePreparedMetricsQuery.ts (93%) delete mode 100644 public/app/plugins/datasource/cloudwatch/components/__snapshots__/MetricsQueryEditor.test.tsx.snap diff --git a/public/app/plugins/datasource/cloudwatch/components/Alias.tsx b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/Alias.tsx similarity index 78% rename from public/app/plugins/datasource/cloudwatch/components/Alias.tsx rename to public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/Alias.tsx index 2f3ddd8d8b5..044f1416af0 100644 --- a/public/app/plugins/datasource/cloudwatch/components/Alias.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/Alias.tsx @@ -6,9 +6,10 @@ import { Input } from '@grafana/ui'; export interface Props { onChange: (alias: any) => void; value: string; + id?: string; } -export const Alias: FunctionComponent = ({ value = '', onChange }) => { +export const Alias: FunctionComponent = ({ value = '', onChange, id }) => { const [alias, setAlias] = useState(value); const propagateOnChange = debounce(onChange, 1500); @@ -18,5 +19,5 @@ export const Alias: FunctionComponent = ({ value = '', onChange }) => { propagateOnChange(e.target.value); }; - return ; + return ; }; diff --git a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx similarity index 76% rename from public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor.test.tsx rename to public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx index f74ddbf21d8..92349bb29e6 100644 --- a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx @@ -1,14 +1,14 @@ import { render, screen, act } from '@testing-library/react'; import React from 'react'; import selectEvent from 'react-select-event'; -import renderer from 'react-test-renderer'; import { DataSourceInstanceSettings } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { TemplateSrv } from 'app/features/templating/template_srv'; -import { CustomVariableModel, initialVariableModelState } from '../../../../features/variables/types'; -import { CloudWatchDatasource } from '../datasource'; -import { CloudWatchJsonData, MetricEditorMode, MetricQueryType } from '../types'; +import { CustomVariableModel, initialVariableModelState } from '../../../../../features/variables/types'; +import { CloudWatchDatasource } from '../../datasource'; +import { CloudWatchJsonData, MetricEditorMode, MetricQueryType } from '../../types'; import { MetricsQueryEditor, Props } from './MetricsQueryEditor'; @@ -70,15 +70,6 @@ const setup = () => { }; describe('QueryEditor', () => { - it('should render component', async () => { - const { act } = renderer; - await act(async () => { - const props = setup(); - const tree = renderer.create().toJSON(); - expect(tree).toMatchSnapshot(); - }); - }); - describe('should handle editor modes correctly', () => { it('when metric query type is metric search and editor mode is builder', async () => { await act(async () => { @@ -165,4 +156,48 @@ describe('QueryEditor', () => { expect(await screen.findByText('*')).toBeInTheDocument(); }); }); + + describe('when dynamic labels feature toggle is enabled', () => { + it('shoud render label field', async () => { + await act(async () => { + const props = setup(); + const originalValue = config.featureToggles.cloudWatchDynamicLabels; + config.featureToggles.cloudWatchDynamicLabels = true; + + render( + + ); + + expect(screen.getByText('Label')).toBeInTheDocument(); + expect(screen.queryByText('Alias')).toBeNull(); + expect(screen.getByLabelText('Label - optional')).toHaveValue( + "Period: ${PROP('Period')} InstanceId: ${PROP('Dim.InstanceId')}" + ); + + config.featureToggles.cloudWatchDynamicLabels = originalValue; + }); + }); + }); + + describe('when dynamic labels feature toggle is disabled', () => { + it('shoud render alias field', async () => { + await act(async () => { + const props = setup(); + const originalValue = config.featureToggles.cloudWatchDynamicLabels; + config.featureToggles.cloudWatchDynamicLabels = false; + + const expected = 'Period: {{period}} InstanceId: {{InstanceId}}'; + render(); + + expect(await screen.getByText('Alias')).toBeInTheDocument(); + expect(screen.queryByText('Label')).toBeNull(); + expect(screen.getByLabelText('Alias - optional')).toHaveValue(expected); + + config.featureToggles.cloudWatchDynamicLabels = originalValue; + }); + }); + }); }); diff --git a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.tsx similarity index 73% rename from public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor.tsx rename to public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.tsx index c484dec6cbc..3b65e28c7ca 100644 --- a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.tsx @@ -2,10 +2,12 @@ import React, { ChangeEvent, useState } from 'react'; import { QueryEditorProps } from '@grafana/data'; import { EditorField, EditorRow, Space } from '@grafana/experimental'; +import { config } from '@grafana/runtime'; import { Input } from '@grafana/ui'; -import { CloudWatchDatasource } from '../datasource'; -import { isCloudWatchMetricsQuery } from '../guards'; +import { MathExpressionQueryField, MetricStatEditor, SQLBuilderEditor, SQLCodeEditor } from '../'; +import { CloudWatchDatasource } from '../../datasource'; +import { isCloudWatchMetricsQuery } from '../../guards'; import { CloudWatchJsonData, CloudWatchMetricsQuery, @@ -13,13 +15,12 @@ import { MetricEditorMode, MetricQueryType, MetricStat, -} from '../types'; +} from '../../types'; +import QueryHeader from '../QueryHeader'; -import QueryHeader from './QueryHeader'; +import { Alias } from './Alias'; import usePreparedMetricsQuery from './usePreparedMetricsQuery'; -import { Alias, MathExpressionQueryField, MetricStatEditor, SQLBuilderEditor, SQLCodeEditor } from './'; - export interface Props extends QueryEditorProps { query: CloudWatchMetricsQuery; } @@ -130,17 +131,37 @@ export const MetricsQueryEditor = (props: Props) => { /> - - onChange({ ...preparedQuery, alias: value })} - /> - + {config.featureToggles.cloudWatchDynamicLabels ? ( + + ) => + onChange({ ...preparedQuery, label: event.target.value }) + } + /> + + ) : ( + + onChange({ ...preparedQuery, alias: value })} + /> + + )} ); diff --git a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryHeader.test.tsx b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryHeader.test.tsx similarity index 97% rename from public/app/plugins/datasource/cloudwatch/components/MetricsQueryHeader.test.tsx rename to public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryHeader.test.tsx index eaf5d1d616f..54951d12547 100644 --- a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryHeader.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryHeader.test.tsx @@ -2,8 +2,8 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { act } from 'react-dom/test-utils'; -import { setupMockedDataSource } from '../__mocks__/CloudWatchDataSource'; -import { CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType } from '../types'; +import { setupMockedDataSource } from '../../__mocks__/CloudWatchDataSource'; +import { CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType } from '../../types'; import MetricsQueryHeader from './MetricsQueryHeader'; diff --git a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryHeader.tsx b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryHeader.tsx similarity index 96% rename from public/app/plugins/datasource/cloudwatch/components/MetricsQueryHeader.tsx rename to public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryHeader.tsx index 29c08845ce5..dadb2424ff0 100644 --- a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryHeader.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryHeader.tsx @@ -4,8 +4,8 @@ import { SelectableValue } from '@grafana/data'; import { InlineSelect, FlexItem } from '@grafana/experimental'; import { Button, ConfirmModal, RadioButtonGroup } from '@grafana/ui'; -import { CloudWatchDatasource } from '../datasource'; -import { CloudWatchMetricsQuery, CloudWatchQuery, MetricEditorMode, MetricQueryType } from '../types'; +import { CloudWatchDatasource } from '../../datasource'; +import { CloudWatchMetricsQuery, CloudWatchQuery, MetricEditorMode, MetricQueryType } from '../../types'; interface MetricsQueryHeaderProps { query: CloudWatchMetricsQuery; diff --git a/public/app/plugins/datasource/cloudwatch/components/usePreparedMetricsQuery.test.ts b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/usePreparedMetricsQuery.test.ts similarity index 99% rename from public/app/plugins/datasource/cloudwatch/components/usePreparedMetricsQuery.test.ts rename to public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/usePreparedMetricsQuery.test.ts index 8f179549d0a..45bfb80d0e8 100644 --- a/public/app/plugins/datasource/cloudwatch/components/usePreparedMetricsQuery.test.ts +++ b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/usePreparedMetricsQuery.test.ts @@ -1,6 +1,6 @@ import { renderHook } from '@testing-library/react-hooks'; -import { CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType } from '../types'; +import { CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType } from '../../types'; import usePreparedMetricsQuery, { DEFAULT_QUERY } from './usePreparedMetricsQuery'; diff --git a/public/app/plugins/datasource/cloudwatch/components/usePreparedMetricsQuery.ts b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/usePreparedMetricsQuery.ts similarity index 93% rename from public/app/plugins/datasource/cloudwatch/components/usePreparedMetricsQuery.ts rename to public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/usePreparedMetricsQuery.ts index 15375c58664..0e0f618fcc1 100644 --- a/public/app/plugins/datasource/cloudwatch/components/usePreparedMetricsQuery.ts +++ b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/usePreparedMetricsQuery.ts @@ -1,8 +1,8 @@ import deepEqual from 'fast-deep-equal'; import { useEffect, useMemo } from 'react'; -import { migrateMetricQuery } from '../migrations/metricQueryMigrations'; -import { CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType } from '../types'; +import { migrateMetricQuery } from '../../migrations/metricQueryMigrations'; +import { CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType } from '../../types'; export const DEFAULT_QUERY: Omit = { queryMode: 'Metrics', diff --git a/public/app/plugins/datasource/cloudwatch/components/PanelQueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/PanelQueryEditor.tsx index 079fdae5671..a901584d643 100644 --- a/public/app/plugins/datasource/cloudwatch/components/PanelQueryEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/PanelQueryEditor.tsx @@ -6,8 +6,8 @@ import { CloudWatchDatasource } from '../datasource'; import { isCloudWatchMetricsQuery } from '../guards'; import { CloudWatchJsonData, CloudWatchQuery } from '../types'; +import { MetricsQueryEditor } from '././MetricsQueryEditor/MetricsQueryEditor'; import LogsQueryEditor from './LogsQueryEditor'; -import { MetricsQueryEditor } from './MetricsQueryEditor'; export type Props = QueryEditorProps; diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx index f90298afd75..3444c3feef9 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx @@ -8,7 +8,7 @@ import { CloudWatchDatasource } from '../datasource'; import { useRegions } from '../hooks'; import { CloudWatchQuery, CloudWatchQueryMode } from '../types'; -import MetricsQueryHeader from './MetricsQueryHeader'; +import MetricsQueryHeader from './MetricsQueryEditor/MetricsQueryHeader'; interface QueryHeaderProps { query: CloudWatchQuery; diff --git a/public/app/plugins/datasource/cloudwatch/components/__snapshots__/MetricsQueryEditor.test.tsx.snap b/public/app/plugins/datasource/cloudwatch/components/__snapshots__/MetricsQueryEditor.test.tsx.snap deleted file mode 100644 index d977a61df6d..00000000000 --- a/public/app/plugins/datasource/cloudwatch/components/__snapshots__/MetricsQueryEditor.test.tsx.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`QueryEditor should render component 1`] = `null`; diff --git a/public/app/plugins/datasource/cloudwatch/components/index.ts b/public/app/plugins/datasource/cloudwatch/components/index.ts index 0362b31b34f..3c3d07f66f0 100644 --- a/public/app/plugins/datasource/cloudwatch/components/index.ts +++ b/public/app/plugins/datasource/cloudwatch/components/index.ts @@ -1,6 +1,5 @@ export { Dimensions } from './Dimensions/Dimensions'; export { QueryInlineField, QueryField } from './Forms'; -export { Alias } from './Alias'; export { PanelQueryEditor } from './PanelQueryEditor'; export { CloudWatchLogsQueryEditor } from './LogsQueryEditor'; export { MetricStatEditor } from './MetricStatEditor'; From ff38f24044ec3462e014d96ebb3b0ba64f300bac Mon Sep 17 00:00:00 2001 From: Nathan Marrs Date: Tue, 3 May 2022 22:58:00 -0700 Subject: [PATCH 013/440] Canvas: Refactor group to frame (#48671) --- .../components/Layers/LayerDragDropList.tsx | 6 +- public/app/features/canvas/frame.ts | 6 ++ public/app/features/canvas/group.ts | 7 --- public/app/features/canvas/index.ts | 2 +- .../app/features/canvas/runtime/element.tsx | 4 +- .../canvas/runtime/{group.tsx => frame.tsx} | 26 ++++---- public/app/features/canvas/runtime/root.tsx | 12 ++-- public/app/features/canvas/runtime/scene.tsx | 44 +++++++------- .../app/plugins/panel/canvas/CanvasPanel.tsx | 4 +- .../canvas/editor/LayerElementListEditor.tsx | 60 +++++++++---------- .../panel/canvas/editor/layerEditor.tsx | 8 +-- public/app/plugins/panel/canvas/models.gen.ts | 6 +- public/app/plugins/panel/canvas/module.tsx | 4 +- 13 files changed, 94 insertions(+), 95 deletions(-) create mode 100644 public/app/features/canvas/frame.ts delete mode 100644 public/app/features/canvas/group.ts rename public/app/features/canvas/runtime/{group.tsx => frame.tsx} (88%) diff --git a/public/app/core/components/Layers/LayerDragDropList.tsx b/public/app/core/components/Layers/LayerDragDropList.tsx index d4ceae1dc90..e3ee75bf669 100644 --- a/public/app/core/components/Layers/LayerDragDropList.tsx +++ b/public/app/core/components/Layers/LayerDragDropList.tsx @@ -16,7 +16,7 @@ type LayerDragDropListProps = { onSelect: (element: T) => any; onDelete: (element: T) => any; onDuplicate?: (element: T) => any; - isGroup?: (element: T) => boolean; + isFrame?: (element: T) => boolean; selection?: string[]; // list of unique ids (names) excludeBaseLayer?: boolean; onNameChange: (element: T, newName: string) => any; @@ -30,7 +30,7 @@ export const LayerDragDropList = ({ onSelect, onDelete, onDuplicate, - isGroup, + isFrame, selection, excludeBaseLayer, onNameChange, @@ -74,7 +74,7 @@ export const LayerDragDropList = ({ />
  {getLayerInfo(element)}
- {!isGroup!(element) && ( + {!isFrame!(element) && ( <> {onDuplicate ? ( ({ config: {}, @@ -24,16 +24,16 @@ export const groupItemDummy: CanvasElementItem = { // eslint-disable-next-line react/display-name display: () => { - return
GROUP!
; + return
FRAME!
; }, }; -export class GroupState extends ElementState { +export class FrameState extends ElementState { elements: ElementState[] = []; scene: Scene; - constructor(public options: CanvasGroupOptions, scene: Scene, public parent?: GroupState) { - super(groupItemDummy, options, parent); + constructor(public options: CanvasFrameOptions, scene: Scene, public parent?: FrameState) { + super(frameItemDummy, options, parent); this.scene = scene; @@ -44,8 +44,8 @@ export class GroupState extends ElementState { } for (const c of elements) { - if (c.type === 'group') { - this.elements.push(new GroupState(c as CanvasGroupOptions, scene, this)); + if (c.type === 'frame') { + this.elements.push(new FrameState(c as CanvasFrameOptions, scene, this)); } else { const item = canvasElementRegistry.getIfExists(c.type) ?? notFoundItem; this.elements.push(new ElementState(item, c, this)); @@ -91,8 +91,8 @@ export class GroupState extends ElementState { this.reinitializeMoveable(); break; case LayerActionID.Duplicate: - if (element.item.id === 'group') { - console.log('Can not duplicate groups (yet)', action, element); + if (element.item.id === 'frame') { + console.log('Can not duplicate frames (yet)', action, element); return; } const opts = cloneDeep(element.options); diff --git a/public/app/features/canvas/runtime/root.tsx b/public/app/features/canvas/runtime/root.tsx index 47b1339356d..1ac9acbc9e9 100644 --- a/public/app/features/canvas/runtime/root.tsx +++ b/public/app/features/canvas/runtime/root.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import { CanvasGroupOptions, CanvasElementOptions } from 'app/features/canvas'; +import { CanvasFrameOptions, CanvasElementOptions } from 'app/features/canvas'; -import { GroupState } from './group'; +import { FrameState } from './frame'; import { Scene } from './scene'; -export class RootElement extends GroupState { - constructor(public options: CanvasGroupOptions, public scene: Scene, private changeCallback: () => void) { +export class RootElement extends FrameState { + constructor(public options: CanvasFrameOptions, public scene: Scene, private changeCallback: () => void) { super(options, scene); this.sizeStyle = { @@ -22,11 +22,11 @@ export class RootElement extends GroupState { // root type can not change onChange(options: CanvasElementOptions) { this.revId++; - this.options = { ...options } as CanvasGroupOptions; + this.options = { ...options } as CanvasFrameOptions; this.changeCallback(); } - getSaveModel(): CanvasGroupOptions { + getSaveModel(): CanvasFrameOptions { const { placement, constraint, ...rest } = this.options; return { diff --git a/public/app/features/canvas/runtime/scene.tsx b/public/app/features/canvas/runtime/scene.tsx index edfcb5689f2..0b951cb2bc6 100644 --- a/public/app/features/canvas/runtime/scene.tsx +++ b/public/app/features/canvas/runtime/scene.tsx @@ -8,7 +8,7 @@ import Selecto from 'selecto'; import { GrafanaTheme2, PanelData } from '@grafana/data'; import { stylesFactory } from '@grafana/ui'; import { config } from 'app/core/config'; -import { CanvasGroupOptions, DEFAULT_CANVAS_ELEMENT_CONFIG } from 'app/features/canvas'; +import { CanvasFrameOptions, DEFAULT_CANVAS_ELEMENT_CONFIG } from 'app/features/canvas'; import { ColorDimensionConfig, ResourceDimensionConfig, @@ -29,12 +29,12 @@ import { LayerActionID } from 'app/plugins/panel/canvas/types'; import { Placement } from '../types'; import { ElementState } from './element'; -import { GroupState } from './group'; +import { FrameState } from './frame'; import { RootElement } from './root'; export interface SelectionParams { targets: Array; - group?: GroupState; + frame?: FrameState; } export class Scene { @@ -53,15 +53,15 @@ export class Scene { selecto?: Selecto; moveable?: Moveable; div?: HTMLDivElement; - currentLayer?: GroupState; + currentLayer?: FrameState; isEditingEnabled?: boolean; - constructor(cfg: CanvasGroupOptions, enableEditing: boolean, public onSave: (cfg: CanvasGroupOptions) => void) { + constructor(cfg: CanvasFrameOptions, enableEditing: boolean, public onSave: (cfg: CanvasFrameOptions) => void) { this.root = this.load(cfg, enableEditing); } - getNextElementName = (isGroup = false) => { - const label = isGroup ? 'Group' : 'Element'; + getNextElementName = (isFrame = false) => { + const label = isFrame ? 'Frame' : 'Element'; let idx = this.byName.size + 1; const max = idx + 100; @@ -79,10 +79,10 @@ export class Scene { return !this.byName.has(v); }; - load(cfg: CanvasGroupOptions, enableEditing: boolean) { + load(cfg: CanvasFrameOptions, enableEditing: boolean) { this.root = new RootElement( cfg ?? { - type: 'group', + type: 'frame', elements: [DEFAULT_CANVAS_ELEMENT_CONFIG], }, this, @@ -126,13 +126,13 @@ export class Scene { } } - groupSelection() { + frameSelection() { this.selection.pipe(first()).subscribe((currentSelectedElements) => { const currentLayer = currentSelectedElements[0].parent!; - const newLayer = new GroupState( + const newLayer = new FrameState( { - type: 'group', + type: 'frame', name: this.getNextElementName(true), elements: [], }, @@ -140,18 +140,18 @@ export class Scene { currentSelectedElements[0].parent ); - const groupPlacement = this.generateGroupContainer(currentSelectedElements); + const framePlacement = this.generateFrameContainer(currentSelectedElements); - newLayer.options.placement = groupPlacement; + newLayer.options.placement = framePlacement; currentSelectedElements.forEach((element: ElementState) => { const elementContainer = element.div?.getBoundingClientRect(); - element.setPlacementFromConstraint(elementContainer, groupPlacement as DOMRect); + element.setPlacementFromConstraint(elementContainer, framePlacement as DOMRect); currentLayer.doAction(LayerActionID.Delete, element); newLayer.doAction(LayerActionID.Duplicate, element, false, false); }); - newLayer.setPlacementFromConstraint(groupPlacement as DOMRect, currentLayer.div?.getBoundingClientRect()); + newLayer.setPlacementFromConstraint(framePlacement as DOMRect, currentLayer.div?.getBoundingClientRect()); currentLayer.elements.push(newLayer); @@ -161,7 +161,7 @@ export class Scene { }); } - private generateGroupContainer = (elements: ElementState[]): Placement => { + private generateFrameContainer = (elements: ElementState[]): Placement => { let minTop = Infinity; let minLeft = Infinity; let maxRight = 0; @@ -204,7 +204,7 @@ export class Scene { this.selecto?.clickTarget(event, this.div); } - updateCurrentLayer(newLayer: GroupState) { + updateCurrentLayer(newLayer: FrameState) { this.currentLayer = newLayer; this.clearCurrentSelection(); this.save(); @@ -233,7 +233,7 @@ export class Scene { return currentElement; } - const nestedElements = currentElement instanceof GroupState ? currentElement.elements : []; + const nestedElements = currentElement instanceof FrameState ? currentElement.elements : []; for (const nestedElement of nestedElements) { stack.unshift(nestedElement); } @@ -256,8 +256,8 @@ export class Scene { private updateSelection = (selection: SelectionParams) => { this.moveable!.target = selection.targets; - if (selection.group) { - this.selection.next([selection.group]); + if (selection.frame) { + this.selection.next([selection.frame]); } else { const s = selection.targets.map((t) => this.findElementByTarget(t)!); this.selection.next(s); @@ -275,7 +275,7 @@ export class Scene { targetElements.push(currentElement.div); } - const nestedElements = currentElement instanceof GroupState ? currentElement.elements : []; + const nestedElements = currentElement instanceof FrameState ? currentElement.elements : []; for (const nestedElement of nestedElements) { stack.unshift(nestedElement); } diff --git a/public/app/plugins/panel/canvas/CanvasPanel.tsx b/public/app/plugins/panel/canvas/CanvasPanel.tsx index 9e3b741d681..6b1c52468e9 100644 --- a/public/app/plugins/panel/canvas/CanvasPanel.tsx +++ b/public/app/plugins/panel/canvas/CanvasPanel.tsx @@ -3,7 +3,7 @@ import { Subscription } from 'rxjs'; import { PanelProps } from '@grafana/data'; import { PanelContext, PanelContextRoot } from '@grafana/ui'; -import { CanvasGroupOptions } from 'app/features/canvas'; +import { CanvasFrameOptions } from 'app/features/canvas'; import { ElementState } from 'app/features/canvas/runtime/element'; import { Scene } from 'app/features/canvas/runtime/scene'; import { PanelEditEnteredEvent, PanelEditExitedEvent } from 'app/types/events'; @@ -85,7 +85,7 @@ export class CanvasPanel extends Component { // NOTE, all changes to the scene flow through this function // even the editor gets current state from the same scene instance! - onUpdateScene = (root: CanvasGroupOptions) => { + onUpdateScene = (root: CanvasFrameOptions) => { const { onOptionsChange, options } = this.props; onOptionsChange({ ...options, diff --git a/public/app/plugins/panel/canvas/editor/LayerElementListEditor.tsx b/public/app/plugins/panel/canvas/editor/LayerElementListEditor.tsx index 809495898b5..36ed1cc2943 100644 --- a/public/app/plugins/panel/canvas/editor/LayerElementListEditor.tsx +++ b/public/app/plugins/panel/canvas/editor/LayerElementListEditor.tsx @@ -9,7 +9,7 @@ import { LayerDragDropList } from 'app/core/components/Layers/LayerDragDropList' import { CanvasElementOptions, canvasElementRegistry } from 'app/features/canvas'; import { notFoundItem } from 'app/features/canvas/elements/notFound'; import { ElementState } from 'app/features/canvas/runtime/element'; -import { GroupState } from 'app/features/canvas/runtime/group'; +import { FrameState } from 'app/features/canvas/runtime/frame'; import { SelectionParams } from 'app/features/canvas/runtime/scene'; import { ShowConfirmModalEvent } from 'app/types/events'; @@ -53,11 +53,11 @@ export class LayerElementListEditor extends PureComponent { if (settings?.scene) { try { let selection: SelectionParams = { targets: [] }; - if (item instanceof GroupState) { + if (item instanceof FrameState) { const targetElements: HTMLDivElement[] = []; targetElements.push(item?.div!); selection.targets = targetElements; - selection.group = item; + selection.frame = item; settings.scene.select(selection); } else if (item instanceof ElementState) { const targetElement = [item?.div!]; @@ -115,7 +115,7 @@ export class LayerElementListEditor extends PureComponent { } }; - private decoupleGroup = () => { + private decoupleFrame = () => { const settings = this.props.item.settings; if (!settings?.layer) { @@ -124,7 +124,7 @@ export class LayerElementListEditor extends PureComponent { const { layer } = settings; - this.deleteGroup(); + this.deleteFrame(); layer.elements.forEach((element: ElementState) => { const elementContainer = element.div?.getBoundingClientRect(); element.setPlacementFromConstraint(elementContainer, layer.parent?.div?.getBoundingClientRect()); @@ -132,22 +132,22 @@ export class LayerElementListEditor extends PureComponent { }); }; - private onDecoupleGroup = () => { + private onDecoupleFrame = () => { appEvents.publish( new ShowConfirmModalEvent({ - title: 'Decouple group', - text: `Are you sure you want to decouple this group?`, - text2: 'This will remove the group and push nested elements in the next level up.', + title: 'Decouple frame', + text: `Are you sure you want to decouple this frame?`, + text2: 'This will remove the frame and push nested elements in the next level up.', confirmText: 'Yes', yesText: 'Decouple', onConfirm: async () => { - this.decoupleGroup(); + this.decoupleFrame(); }, }) ); }; - private deleteGroup = () => { + private deleteFrame = () => { const settings = this.props.item.settings; if (!settings?.layer) { @@ -164,26 +164,26 @@ export class LayerElementListEditor extends PureComponent { this.goUpLayer(); }; - private onGroupSelection = () => { + private onFrameSelection = () => { const scene = this.getScene(); if (scene) { - scene.groupSelection(); + scene.frameSelection(); } else { console.warn('no scene!'); } }; - private onDeleteGroup = () => { + private onDeleteFrame = () => { appEvents.publish( new ShowConfirmModalEvent({ - title: 'Delete group', - text: `Are you sure you want to delete this group?`, - text2: 'This will delete the group and all nested elements.', + title: 'Delete frame', + text: `Are you sure you want to delete this frame?`, + text2: 'This will delete the frame and all nested elements.', icon: 'trash-alt', confirmText: 'Delete', yesText: 'Delete', onConfirm: async () => { - this.deleteGroup(); + this.deleteFrame(); }, }) ); @@ -215,8 +215,8 @@ export class LayerElementListEditor extends PureComponent { element.onChange({ ...element.options, name }); }; - const isGroup = (element: ElementState) => { - return element instanceof GroupState; + const isFrame = (element: ElementState) => { + return element instanceof FrameState; }; const verifyLayerNameUniqueness = (nameToVerify: string) => { @@ -231,16 +231,16 @@ export class LayerElementListEditor extends PureComponent { {!layer.isRoot() && ( <> - - )} @@ -252,7 +252,7 @@ export class LayerElementListEditor extends PureComponent { getLayerInfo={getLayerInfo} onNameChange={onNameChange} verifyLayerNameUniqueness={verifyLayerNameUniqueness} - isGroup={isGroup} + isFrame={isFrame} layers={layer.elements} selection={selection} /> @@ -266,12 +266,12 @@ export class LayerElementListEditor extends PureComponent { /> {selection.length > 0 && ( )} {selection.length > 1 && ( - )} diff --git a/public/app/plugins/panel/canvas/editor/layerEditor.tsx b/public/app/plugins/panel/canvas/editor/layerEditor.tsx index dcf2ba29abe..cb5891f2fd3 100644 --- a/public/app/plugins/panel/canvas/editor/layerEditor.tsx +++ b/public/app/plugins/panel/canvas/editor/layerEditor.tsx @@ -2,7 +2,7 @@ import { get as lodashGet } from 'lodash'; import { NestedPanelOptions, NestedValueAccess } from '@grafana/data/src/utils/OptionsUIBuilders'; import { ElementState } from 'app/features/canvas/runtime/element'; -import { GroupState } from 'app/features/canvas/runtime/group'; +import { FrameState } from 'app/features/canvas/runtime/frame'; import { Scene } from 'app/features/canvas/runtime/scene'; import { setOptionImmutably } from 'app/features/dashboard/components/PanelEditor/utils'; @@ -14,7 +14,7 @@ import { optionBuilder } from './options'; export interface LayerEditorProps { scene: Scene; - layer: GroupState; + layer: FrameState; selected: ElementState[]; } @@ -22,12 +22,12 @@ export function getLayerEditor(opts: InstanceState): NestedPanelOptions(CanvasPanel) const selection = state.selected; if (selection?.length === 1) { const element = selection[0]; - if (!(element instanceof GroupState)) { + if (!(element instanceof FrameState)) { builder.addNestedOptions( getElementEditor({ category: [`Selected element (${element.options.name})`], From 18f089d1bddfc8a2c8de29c11c9083f40e8b775c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 4 May 2022 09:32:18 +0200 Subject: [PATCH 014/440] Update dependency @babel/preset-env to v7.17.10 (#48638) Co-authored-by: Renovate Bot --- package.json | 2 +- packages/grafana-e2e/package.json | 2 +- yarn.lock | 342 +++++++++++++++++++++++++++++- 3 files changed, 340 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index d56cdbe99d8..8526e32b6d4 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "@babel/plugin-transform-react-constant-elements": "7.17.6", "@babel/plugin-transform-runtime": "7.17.0", "@babel/plugin-transform-typescript": "7.16.8", - "@babel/preset-env": "7.16.11", + "@babel/preset-env": "7.17.10", "@babel/preset-react": "7.16.7", "@babel/preset-typescript": "7.16.7", "@betterer/betterer": "5.3.0", diff --git a/packages/grafana-e2e/package.json b/packages/grafana-e2e/package.json index 05056bb0f8f..2df6a77c89f 100644 --- a/packages/grafana-e2e/package.json +++ b/packages/grafana-e2e/package.json @@ -46,7 +46,7 @@ "types": "src/index.ts", "dependencies": { "@babel/core": "7.17.8", - "@babel/preset-env": "7.16.11", + "@babel/preset-env": "7.17.10", "@cypress/webpack-preprocessor": "5.11.1", "@grafana/e2e-selectors": "9.0.0-pre", "@grafana/tsconfig": "^1.2.0-rc1", diff --git a/yarn.lock b/yarn.lock index 38ba14a2547..e7af82e445c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -71,6 +71,13 @@ __metadata: languageName: node linkType: hard +"@babel/compat-data@npm:^7.17.10": + version: 7.17.10 + resolution: "@babel/compat-data@npm:7.17.10" + checksum: e85051087cd4690de5061909a2dd2d7f8b6434a3c2e30be6c119758db2027ae1845bcd75a81127423dd568b706ac6994a1a3d7d701069a23bf5cfe900728290b + languageName: node + linkType: hard + "@babel/compat-data@npm:^7.17.7": version: 7.17.7 resolution: "@babel/compat-data@npm:7.17.7" @@ -397,6 +404,20 @@ __metadata: languageName: node linkType: hard +"@babel/helper-compilation-targets@npm:^7.17.10": + version: 7.17.10 + resolution: "@babel/helper-compilation-targets@npm:7.17.10" + dependencies: + "@babel/compat-data": ^7.17.10 + "@babel/helper-validator-option": ^7.16.7 + browserslist: ^4.20.2 + semver: ^6.3.0 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 5f547c7ebd372e90fa72c2aaea867e7193166e9f469dec5acde4f0e18a78b80bdca8e02a0f641f3e998be984fb5b802c729a9034faaee8b1a9ef6670cb76f120 + languageName: node + linkType: hard + "@babel/helper-compilation-targets@npm:^7.17.7": version: 7.17.7 resolution: "@babel/helper-compilation-targets@npm:7.17.7" @@ -478,6 +499,23 @@ __metadata: languageName: node linkType: hard +"@babel/helper-create-class-features-plugin@npm:^7.17.6": + version: 7.17.9 + resolution: "@babel/helper-create-class-features-plugin@npm:7.17.9" + dependencies: + "@babel/helper-annotate-as-pure": ^7.16.7 + "@babel/helper-environment-visitor": ^7.16.7 + "@babel/helper-function-name": ^7.17.9 + "@babel/helper-member-expression-to-functions": ^7.17.7 + "@babel/helper-optimise-call-expression": ^7.16.7 + "@babel/helper-replace-supers": ^7.16.7 + "@babel/helper-split-export-declaration": ^7.16.7 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: db7be8852096084883dbbd096f925976695e5b34919a888fded9fd359d75d9994960e459f4eeb51ff6700109f83be6c1359e57809deb3fe36fc589b2a208b6d7 + languageName: node + linkType: hard + "@babel/helper-create-regexp-features-plugin@npm:^7.14.5": version: 7.14.5 resolution: "@babel/helper-create-regexp-features-plugin@npm:7.14.5" @@ -502,6 +540,18 @@ __metadata: languageName: node linkType: hard +"@babel/helper-create-regexp-features-plugin@npm:^7.17.0": + version: 7.17.0 + resolution: "@babel/helper-create-regexp-features-plugin@npm:7.17.0" + dependencies: + "@babel/helper-annotate-as-pure": ^7.16.7 + regexpu-core: ^5.0.1 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: eb66d9241544c705e9ce96d2d122b595ef52d926e6e031653e09af8a01050bd9d7e7fee168bf33a863342774d7d6a8cc7e8e9e5a45b955e9c01121c7a2d51708 + languageName: node + linkType: hard + "@babel/helper-define-polyfill-provider@npm:^0.1.5": version: 0.1.5 resolution: "@babel/helper-define-polyfill-provider@npm:0.1.5" @@ -625,6 +675,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-function-name@npm:^7.17.9": + version: 7.17.9 + resolution: "@babel/helper-function-name@npm:7.17.9" + dependencies: + "@babel/template": ^7.16.7 + "@babel/types": ^7.17.0 + checksum: a59b2e5af56d8f43b9b0019939a43774754beb7cb01a211809ca8031c71890999d07739e955343135ec566c4d8ff725435f1f60fb0af3bb546837c1f9f84f496 + languageName: node + linkType: hard + "@babel/helper-get-function-arity@npm:^7.15.4": version: 7.15.4 resolution: "@babel/helper-get-function-arity@npm:7.15.4" @@ -697,6 +757,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-member-expression-to-functions@npm:^7.17.7": + version: 7.17.7 + resolution: "@babel/helper-member-expression-to-functions@npm:7.17.7" + dependencies: + "@babel/types": ^7.17.0 + checksum: 70f361bab627396c714c3938e94a569cb0da522179328477cdbc4318e4003c2666387ad4931d6bd5de103338c667c9e4bbe3e917fc8c527b3f3eb6175b888b7d + languageName: node + linkType: hard + "@babel/helper-module-imports@npm:^7.0.0, @babel/helper-module-imports@npm:^7.12.13, @babel/helper-module-imports@npm:^7.14.5, @babel/helper-module-imports@npm:^7.15.4": version: 7.15.4 resolution: "@babel/helper-module-imports@npm:7.15.4" @@ -1283,6 +1352,19 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-proposal-class-static-block@npm:^7.17.6": + version: 7.17.6 + resolution: "@babel/plugin-proposal-class-static-block@npm:7.17.6" + dependencies: + "@babel/helper-create-class-features-plugin": ^7.17.6 + "@babel/helper-plugin-utils": ^7.16.7 + "@babel/plugin-syntax-class-static-block": ^7.14.5 + peerDependencies: + "@babel/core": ^7.12.0 + checksum: 0ef00d73b4a7667059f71614669fb5ec989a0a6d5fe58118310c892507f2556a6f3ae66f0c547cd06e50bdf3ff528ef486e611079d41ef321300c967d2c26e1d + languageName: node + linkType: hard + "@babel/plugin-proposal-decorators@npm:^7.12.12": version: 7.17.2 resolution: "@babel/plugin-proposal-decorators@npm:7.17.2" @@ -1467,7 +1549,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-proposal-object-rest-spread@npm:7.17.3, @babel/plugin-proposal-object-rest-spread@npm:^7.12.1": +"@babel/plugin-proposal-object-rest-spread@npm:7.17.3, @babel/plugin-proposal-object-rest-spread@npm:^7.12.1, @babel/plugin-proposal-object-rest-spread@npm:^7.17.3": version: 7.17.3 resolution: "@babel/plugin-proposal-object-rest-spread@npm:7.17.3" dependencies: @@ -2081,6 +2163,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-destructuring@npm:^7.17.7": + version: 7.17.7 + resolution: "@babel/plugin-transform-destructuring@npm:7.17.7" + dependencies: + "@babel/helper-plugin-utils": ^7.16.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 767ecf6640fea9a06a4859f0c34daa30ac7d146a96476caa1f77081d5b6e43699f45e14acd52682078f2b7c230ff0814312b41f61b21ca2b5f9c5a2cc93c2b58 + languageName: node + linkType: hard + "@babel/plugin-transform-dotall-regex@npm:^7.12.13, @babel/plugin-transform-dotall-regex@npm:^7.4.4": version: 7.14.5 resolution: "@babel/plugin-transform-dotall-regex@npm:7.14.5" @@ -2308,6 +2401,20 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-modules-commonjs@npm:^7.17.9": + version: 7.17.9 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.17.9" + dependencies: + "@babel/helper-module-transforms": ^7.17.7 + "@babel/helper-plugin-utils": ^7.16.7 + "@babel/helper-simple-access": ^7.17.7 + babel-plugin-dynamic-import-node: ^2.3.3 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 23f248a28b43978c7ee187a91392510f665db32f2cc869007da4922e5a83da47f27ecd5da37c8f66fe6b89e4b324f1a978a4493ae59edf2b3129387d844fde1b + languageName: node + linkType: hard + "@babel/plugin-transform-modules-systemjs@npm:^7.13.8": version: 7.15.4 resolution: "@babel/plugin-transform-modules-systemjs@npm:7.15.4" @@ -2338,6 +2445,21 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-modules-systemjs@npm:^7.17.8": + version: 7.17.8 + resolution: "@babel/plugin-transform-modules-systemjs@npm:7.17.8" + dependencies: + "@babel/helper-hoist-variables": ^7.16.7 + "@babel/helper-module-transforms": ^7.17.7 + "@babel/helper-plugin-utils": ^7.16.7 + "@babel/helper-validator-identifier": ^7.16.7 + babel-plugin-dynamic-import-node: ^2.3.3 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 058c0e7987aab64c4019bc9eab3f80c5dd05bec737e230e5c60e9222dfb3d01b2dfa3aa1db6cbb75a4095c40af3bba2e3a60170b1570a158d3e781376569ce49 + languageName: node + linkType: hard + "@babel/plugin-transform-modules-umd@npm:^7.13.0": version: 7.14.5 resolution: "@babel/plugin-transform-modules-umd@npm:7.14.5" @@ -2384,6 +2506,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-named-capturing-groups-regex@npm:^7.17.10": + version: 7.17.10 + resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.17.10" + dependencies: + "@babel/helper-create-regexp-features-plugin": ^7.17.0 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: a2be5f9f23d4dd49106e1c84a1cb625a56d6c7e5cb466602151f9e05aa0a70f68b52206f034447d37e6790914ea953ebf2ab705f377ee7ef00e14453ba3c3d6a + languageName: node + linkType: hard + "@babel/plugin-transform-new-target@npm:^7.12.13": version: 7.14.5 resolution: "@babel/plugin-transform-new-target@npm:7.14.5" @@ -2571,6 +2704,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-regenerator@npm:^7.17.9": + version: 7.17.9 + resolution: "@babel/plugin-transform-regenerator@npm:7.17.9" + dependencies: + regenerator-transform: ^0.15.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: bf92f7228397615f12fa62d1decbe854ee9065d44e55036f99bf312783d51b082981bab38ba61de9858f7e20513484a043bfa958c0ce4a0d4d1710710df029a9 + languageName: node + linkType: hard + "@babel/plugin-transform-reserved-words@npm:^7.12.13": version: 7.14.5 resolution: "@babel/plugin-transform-reserved-words@npm:7.14.5" @@ -2872,7 +3016,91 @@ __metadata: languageName: node linkType: hard -"@babel/preset-env@npm:7.16.11, @babel/preset-env@npm:^7.12.11": +"@babel/preset-env@npm:7.17.10": + version: 7.17.10 + resolution: "@babel/preset-env@npm:7.17.10" + dependencies: + "@babel/compat-data": ^7.17.10 + "@babel/helper-compilation-targets": ^7.17.10 + "@babel/helper-plugin-utils": ^7.16.7 + "@babel/helper-validator-option": ^7.16.7 + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ^7.16.7 + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ^7.16.7 + "@babel/plugin-proposal-async-generator-functions": ^7.16.8 + "@babel/plugin-proposal-class-properties": ^7.16.7 + "@babel/plugin-proposal-class-static-block": ^7.17.6 + "@babel/plugin-proposal-dynamic-import": ^7.16.7 + "@babel/plugin-proposal-export-namespace-from": ^7.16.7 + "@babel/plugin-proposal-json-strings": ^7.16.7 + "@babel/plugin-proposal-logical-assignment-operators": ^7.16.7 + "@babel/plugin-proposal-nullish-coalescing-operator": ^7.16.7 + "@babel/plugin-proposal-numeric-separator": ^7.16.7 + "@babel/plugin-proposal-object-rest-spread": ^7.17.3 + "@babel/plugin-proposal-optional-catch-binding": ^7.16.7 + "@babel/plugin-proposal-optional-chaining": ^7.16.7 + "@babel/plugin-proposal-private-methods": ^7.16.11 + "@babel/plugin-proposal-private-property-in-object": ^7.16.7 + "@babel/plugin-proposal-unicode-property-regex": ^7.16.7 + "@babel/plugin-syntax-async-generators": ^7.8.4 + "@babel/plugin-syntax-class-properties": ^7.12.13 + "@babel/plugin-syntax-class-static-block": ^7.14.5 + "@babel/plugin-syntax-dynamic-import": ^7.8.3 + "@babel/plugin-syntax-export-namespace-from": ^7.8.3 + "@babel/plugin-syntax-json-strings": ^7.8.3 + "@babel/plugin-syntax-logical-assignment-operators": ^7.10.4 + "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3 + "@babel/plugin-syntax-numeric-separator": ^7.10.4 + "@babel/plugin-syntax-object-rest-spread": ^7.8.3 + "@babel/plugin-syntax-optional-catch-binding": ^7.8.3 + "@babel/plugin-syntax-optional-chaining": ^7.8.3 + "@babel/plugin-syntax-private-property-in-object": ^7.14.5 + "@babel/plugin-syntax-top-level-await": ^7.14.5 + "@babel/plugin-transform-arrow-functions": ^7.16.7 + "@babel/plugin-transform-async-to-generator": ^7.16.8 + "@babel/plugin-transform-block-scoped-functions": ^7.16.7 + "@babel/plugin-transform-block-scoping": ^7.16.7 + "@babel/plugin-transform-classes": ^7.16.7 + "@babel/plugin-transform-computed-properties": ^7.16.7 + "@babel/plugin-transform-destructuring": ^7.17.7 + "@babel/plugin-transform-dotall-regex": ^7.16.7 + "@babel/plugin-transform-duplicate-keys": ^7.16.7 + "@babel/plugin-transform-exponentiation-operator": ^7.16.7 + "@babel/plugin-transform-for-of": ^7.16.7 + "@babel/plugin-transform-function-name": ^7.16.7 + "@babel/plugin-transform-literals": ^7.16.7 + "@babel/plugin-transform-member-expression-literals": ^7.16.7 + "@babel/plugin-transform-modules-amd": ^7.16.7 + "@babel/plugin-transform-modules-commonjs": ^7.17.9 + "@babel/plugin-transform-modules-systemjs": ^7.17.8 + "@babel/plugin-transform-modules-umd": ^7.16.7 + "@babel/plugin-transform-named-capturing-groups-regex": ^7.17.10 + "@babel/plugin-transform-new-target": ^7.16.7 + "@babel/plugin-transform-object-super": ^7.16.7 + "@babel/plugin-transform-parameters": ^7.16.7 + "@babel/plugin-transform-property-literals": ^7.16.7 + "@babel/plugin-transform-regenerator": ^7.17.9 + "@babel/plugin-transform-reserved-words": ^7.16.7 + "@babel/plugin-transform-shorthand-properties": ^7.16.7 + "@babel/plugin-transform-spread": ^7.16.7 + "@babel/plugin-transform-sticky-regex": ^7.16.7 + "@babel/plugin-transform-template-literals": ^7.16.7 + "@babel/plugin-transform-typeof-symbol": ^7.16.7 + "@babel/plugin-transform-unicode-escapes": ^7.16.7 + "@babel/plugin-transform-unicode-regex": ^7.16.7 + "@babel/preset-modules": ^0.1.5 + "@babel/types": ^7.17.10 + babel-plugin-polyfill-corejs2: ^0.3.0 + babel-plugin-polyfill-corejs3: ^0.5.0 + babel-plugin-polyfill-regenerator: ^0.3.0 + core-js-compat: ^3.22.1 + semver: ^6.3.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: d81a11a0866e9a90eaa799211a609f3f3838eebcaaa717c63438cbb9c90e99ed336822717614bf974b90438e1f5c6157c9b43a0bfceece5b2c9188d67cbbae92 + languageName: node + linkType: hard + +"@babel/preset-env@npm:^7.12.11": version: 7.16.11 resolution: "@babel/preset-env@npm:7.16.11" dependencies: @@ -3225,6 +3453,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.17.10": + version: 7.17.10 + resolution: "@babel/types@npm:7.17.10" + dependencies: + "@babel/helper-validator-identifier": ^7.16.7 + to-fast-properties: ^2.0.0 + checksum: 40cfc3f43a3ab7374df8ee6844793f804c65e7bea0fd1b090886b425106ba26e16e8fa698ae4b2caf2746083fe3e62f03f12997a5982e0d131700f17cbdcfca1 + languageName: node + linkType: hard + "@base2/pretty-print-object@npm:1.0.1": version: 1.0.1 resolution: "@base2/pretty-print-object@npm:1.0.1" @@ -4062,7 +4300,7 @@ __metadata: resolution: "@grafana/e2e@workspace:packages/grafana-e2e" dependencies: "@babel/core": 7.17.8 - "@babel/preset-env": 7.16.11 + "@babel/preset-env": 7.17.10 "@cypress/webpack-preprocessor": 5.11.1 "@grafana/e2e-selectors": 9.0.0-pre "@grafana/tsconfig": ^1.2.0-rc1 @@ -14054,6 +14292,21 @@ __metadata: languageName: node linkType: hard +"browserslist@npm:^4.20.3": + version: 4.20.3 + resolution: "browserslist@npm:4.20.3" + dependencies: + caniuse-lite: ^1.0.30001332 + electron-to-chromium: ^1.4.118 + escalade: ^3.1.1 + node-releases: ^2.0.3 + picocolors: ^1.0.0 + bin: + browserslist: cli.js + checksum: 1e4b719ac2ca0fe235218a606e8b8ef16b8809e0973b924158c39fbc435a0b0fe43437ea52dd6ef5ad2efcb83fcb07431244e472270177814217f7c563651f7d + languageName: node + linkType: hard + "bs-logger@npm:0.x": version: 0.2.6 resolution: "bs-logger@npm:0.2.6" @@ -14454,6 +14707,13 @@ __metadata: languageName: node linkType: hard +"caniuse-lite@npm:^1.0.30001332": + version: 1.0.30001335 + resolution: "caniuse-lite@npm:1.0.30001335" + checksum: fe08b49ec6cb76cc69958ff001cf89d0a8ef9f35e0c8028b65981585046384f76e007d64dea372a34ca56d91caa83cc614c00779fe2b4d378aa0e68696374f67 + languageName: node + linkType: hard + "capture-exit@npm:^2.0.0": version: 2.0.0 resolution: "capture-exit@npm:2.0.0" @@ -15669,6 +15929,16 @@ __metadata: languageName: node linkType: hard +"core-js-compat@npm:^3.22.1": + version: 3.22.4 + resolution: "core-js-compat@npm:3.22.4" + dependencies: + browserslist: ^4.20.3 + semver: 7.0.0 + checksum: b58111ba60091ad99be7246ecbb806ff89f504a80f74d1ddd0f219fd51a8b9460db6043bd7fe046acd8bd1b4370c595cfadf70b18fca8520ad8fed52b1f837b5 + languageName: node + linkType: hard + "core-js-compat@npm:^3.8.1, core-js-compat@npm:^3.9.0": version: 3.19.0 resolution: "core-js-compat@npm:3.19.0" @@ -17704,6 +17974,13 @@ __metadata: languageName: node linkType: hard +"electron-to-chromium@npm:^1.4.118": + version: 1.4.131 + resolution: "electron-to-chromium@npm:1.4.131" + checksum: adf3159d22dd8ae3e46e86fe89e91ad39f622855ece36a0f73e53171792dead8e6876c3246e8b54b259b3a7d68ef093e020a12d55adf34692ed38d4c172b0376 + languageName: node + linkType: hard + "electron-to-chromium@npm:^1.4.17": version: 1.4.37 resolution: "electron-to-chromium@npm:1.4.37" @@ -20552,7 +20829,7 @@ __metadata: "@babel/plugin-transform-react-constant-elements": 7.17.6 "@babel/plugin-transform-runtime": 7.17.0 "@babel/plugin-transform-typescript": 7.16.8 - "@babel/preset-env": 7.16.11 + "@babel/preset-env": 7.17.10 "@babel/preset-react": 7.16.7 "@babel/preset-typescript": 7.16.7 "@betterer/betterer": 5.3.0 @@ -26996,6 +27273,13 @@ __metadata: languageName: node linkType: hard +"node-releases@npm:^2.0.3": + version: 2.0.4 + resolution: "node-releases@npm:2.0.4" + checksum: b32d6c2032c7b169ae3938b416fc50f123f5bd577d54a79b2ae201febf27b22846b01c803dd35ac8689afe840f8ba4e5f7154723db629b80f359836b6707b92f + languageName: node + linkType: hard + "nodemon@npm:2.0.15": version: 2.0.15 resolution: "nodemon@npm:2.0.15" @@ -31911,6 +32195,15 @@ __metadata: languageName: node linkType: hard +"regenerate-unicode-properties@npm:^10.0.1": + version: 10.0.1 + resolution: "regenerate-unicode-properties@npm:10.0.1" + dependencies: + regenerate: ^1.4.2 + checksum: 1b638b7087d8143e5be3e20e2cda197ea0440fa0bc2cc49646b2f50c5a2b1acdc54b21e4215805a5a2dd487c686b2291accd5ad00619534098d2667e76247754 + languageName: node + linkType: hard + "regenerate-unicode-properties@npm:^9.0.0": version: 9.0.0 resolution: "regenerate-unicode-properties@npm:9.0.0" @@ -31950,6 +32243,15 @@ __metadata: languageName: node linkType: hard +"regenerator-transform@npm:^0.15.0": + version: 0.15.0 + resolution: "regenerator-transform@npm:0.15.0" + dependencies: + "@babel/runtime": ^7.8.4 + checksum: 86e54849ab1167618d28bb56d214c52a983daf29b0d115c976d79840511420049b6b42c9ebdf187defa8e7129bdd74b6dd266420d0d3868c9fa7f793b5d15d49 + languageName: node + linkType: hard + "regex-not@npm:^1.0.0, regex-not@npm:^1.0.2": version: 1.0.2 resolution: "regex-not@npm:1.0.2" @@ -31991,6 +32293,20 @@ __metadata: languageName: node linkType: hard +"regexpu-core@npm:^5.0.1": + version: 5.0.1 + resolution: "regexpu-core@npm:5.0.1" + dependencies: + regenerate: ^1.4.2 + regenerate-unicode-properties: ^10.0.1 + regjsgen: ^0.6.0 + regjsparser: ^0.8.2 + unicode-match-property-ecmascript: ^2.0.0 + unicode-match-property-value-ecmascript: ^2.0.0 + checksum: 6151a9700dad512fadb5564ad23246d54c880eb9417efa5e5c3658b910c1ff894d622dfd159af2ed527ffd44751bfe98682ae06c717155c254d8e2b4bab62785 + languageName: node + linkType: hard + "regextras@npm:^0.8.0": version: 0.8.0 resolution: "regextras@npm:0.8.0" @@ -32023,6 +32339,13 @@ __metadata: languageName: node linkType: hard +"regjsgen@npm:^0.6.0": + version: 0.6.0 + resolution: "regjsgen@npm:0.6.0" + checksum: c5158ebd735e75074e41292ade1ff05d85566d205426cc61501e360c450a63baced8512ee3ae238e5c0a0e42969563c7875b08fa69d6f0402daf36bcb3e4d348 + languageName: node + linkType: hard + "regjsparser@npm:^0.7.0": version: 0.7.0 resolution: "regjsparser@npm:0.7.0" @@ -32034,6 +32357,17 @@ __metadata: languageName: node linkType: hard +"regjsparser@npm:^0.8.2": + version: 0.8.4 + resolution: "regjsparser@npm:0.8.4" + dependencies: + jsesc: ~0.5.0 + bin: + regjsparser: bin/parser + checksum: d069b932491761cda127ce11f6bd2729c3b1b394a35200ec33f1199e937423db28ceb86cf33f0a97c76ecd7c0f8db996476579eaf0d80a1f74c1934f4ca8b27a + languageName: node + linkType: hard + "relateurl@npm:0.2.x, relateurl@npm:^0.2.7": version: 0.2.7 resolution: "relateurl@npm:0.2.7" From c41397a6e76e3a2b6ca3fd3db193febe835bc76d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20D=C4=85browski?= Date: Wed, 4 May 2022 09:35:10 +0200 Subject: [PATCH 015/440] LDAP: validate organization role during parsing (#37188) * LDAP: validate organization role during parsing * Trigger a new build * Check if grafana_admin is present --- pkg/models/org_user.go | 14 +++---- pkg/services/ldap/settings.go | 4 ++ .../features/admin/ldap/LdapUserGroups.tsx | 40 +++++++------------ 3 files changed, 24 insertions(+), 34 deletions(-) diff --git a/pkg/models/org_user.go b/pkg/models/org_user.go index f91c28f0b58..e08a9352ee3 100644 --- a/pkg/models/org_user.go +++ b/pkg/models/org_user.go @@ -1,9 +1,9 @@ package models import ( - "encoding/json" "errors" "fmt" + "strings" "time" ) @@ -61,18 +61,14 @@ func (r RoleType) Parents() []RoleType { } } -func (r *RoleType) UnmarshalJSON(data []byte) error { - var str string - err := json.Unmarshal(data, &str) - if err != nil { - return err - } +func (r *RoleType) UnmarshalText(data []byte) error { + // make sure "viewer" and "Viewer" are both correct + str := strings.Title(string(data)) *r = RoleType(str) - if !r.IsValid() { if (*r) != "" { - return fmt.Errorf("JSON validation error: invalid role value: %s", *r) + return fmt.Errorf("invalid role value: %s", *r) } *r = ROLE_VIEWER diff --git a/pkg/services/ldap/settings.go b/pkg/services/ldap/settings.go index 9ed021519ab..7ba504021dd 100644 --- a/pkg/services/ldap/settings.go +++ b/pkg/services/ldap/settings.go @@ -153,6 +153,10 @@ func readConfig(configFile string) (*Config, error) { } for _, groupMap := range server.Groups { + if groupMap.OrgRole == "" && groupMap.IsGrafanaAdmin == nil { + return nil, fmt.Errorf("LDAP group mapping: organization role or grafana admin status is required") + } + if groupMap.OrgId == 0 { groupMap.OrgId = 1 } diff --git a/public/app/features/admin/ldap/LdapUserGroups.tsx b/public/app/features/admin/ldap/LdapUserGroups.tsx index 0fa361acbb5..d2d0d80b08f 100644 --- a/public/app/features/admin/ldap/LdapUserGroups.tsx +++ b/public/app/features/admin/ldap/LdapUserGroups.tsx @@ -33,31 +33,21 @@ export const LdapUserGroups: FC = ({ groups, showAttributeMapping }) => { {items.map((group, index) => { return ( - {showAttributeMapping && ( - <> - {group.groupDN} - {!group.orgRole && ( - <> - - - - No match - - - - - - - - - )} - - )} - {group.orgName && ( - <> - {group.orgName} - {group.orgRole} - + {showAttributeMapping && {group.groupDN}} + {group.orgName && group.orgRole ? {group.orgName} : } + {group.orgRole ? ( + {group.orgRole} + ) : ( + + + No match + + + + + + + )} ); From 713e624790e1cee7a829b102dd654ad290eec726 Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Wed, 4 May 2022 09:39:41 +0100 Subject: [PATCH 016/440] DashboardModel: Tidy up some of the older code (#48355) --- .../dashboard/state/DashboardMigrator.test.ts | 8 +- .../dashboard/state/DashboardMigrator.ts | 9 +- .../dashboard/state/DashboardModel.test.ts | 2 +- .../dashboard/state/DashboardModel.ts | 462 +++++++----------- .../features/dashboard/state/PanelModel.ts | 2 +- .../features/dashboard/state/utils.test.ts | 8 +- 6 files changed, 184 insertions(+), 307 deletions(-) diff --git a/public/app/features/dashboard/state/DashboardMigrator.test.ts b/public/app/features/dashboard/state/DashboardMigrator.test.ts index b53a4088208..a2269ab56df 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.test.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.test.ts @@ -1449,7 +1449,7 @@ describe('DashboardModel', () => { }); it('should ignore fieldConfig.defaults', () => { - expect(model.panels[0].panels[0].fieldConfig.defaults).toEqual(undefined); + expect(model.panels[0].panels?.[0].fieldConfig.defaults).toEqual(undefined); }); }); @@ -1749,8 +1749,8 @@ describe('DashboardModel', () => { }, ], }); - panel1Targets = nestedModel.panels[0].panels[0].targets; - panel2Targets = nestedModel.panels[0].panels[1].targets; + panel1Targets = nestedModel.panels[0].panels?.[0].targets; + panel2Targets = nestedModel.panels[0].panels?.[1].targets; }); it('multiple stats query should have been split into one query per stat', () => { @@ -1851,7 +1851,7 @@ describe('DashboardModel', () => { }); it('should update datasources in panels collapsed rows', () => { - expect(model.panels[3].panels[0].datasource).toEqual({ type: 'prometheus', uid: 'prom-uid' }); + expect(model.panels[3].panels?.[0].datasource).toEqual({ type: 'prometheus', uid: 'prom-uid' }); }); }); diff --git a/public/app/features/dashboard/state/DashboardMigrator.ts b/public/app/features/dashboard/state/DashboardMigrator.ts index 57279169b29..79b747773e9 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.ts @@ -784,9 +784,10 @@ export class DashboardMigrator { for (j = 0; j < this.dashboard.panels.length; j++) { for (k = 0; k < panelUpgrades.length; k++) { this.dashboard.panels[j] = panelUpgrades[k].call(this, this.dashboard.panels[j]); - if (this.dashboard.panels[j].panels) { - for (n = 0; n < this.dashboard.panels[j].panels.length; n++) { - this.dashboard.panels[j].panels[n] = panelUpgrades[k].call(this, this.dashboard.panels[j].panels[n]); + const rowPanels = this.dashboard.panels[j].panels; + if (rowPanels) { + for (n = 0; n < rowPanels.length; n++) { + rowPanels[n] = panelUpgrades[k].call(this, rowPanels[n]); } } } @@ -895,7 +896,7 @@ export class DashboardMigrator { delete panel.span; if (rowPanelModel && rowPanel.collapsed) { - rowPanelModel.panels.push(panel); + rowPanelModel.panels?.push(panel); } else { this.dashboard.panels.push(new PanelModel(panel)); } diff --git a/public/app/features/dashboard/state/DashboardModel.test.ts b/public/app/features/dashboard/state/DashboardModel.test.ts index f5d494a780c..b14fc23d754 100644 --- a/public/app/features/dashboard/state/DashboardModel.test.ts +++ b/public/app/features/dashboard/state/DashboardModel.test.ts @@ -370,7 +370,7 @@ describe('DashboardModel', () => { it('should remove panels and put them inside collapsed row', () => { expect(dashboard.panels.length).toBe(3); - expect(dashboard.panels[1].panels.length).toBe(2); + expect(dashboard.panels[1].panels?.length).toBe(2); }); describe('and when removing row and its panels', () => { diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index 1c99bb5a33a..7398d9cd803 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -1,17 +1,4 @@ -import { - cloneDeep, - defaults as _defaults, - each, - filter, - find, - findIndex, - indexOf, - isEqual, - map, - maxBy, - pull, - some, -} from 'lodash'; +import { cloneDeep, defaults as _defaults, filter, indexOf, isEqual, map, maxBy, pull } from 'lodash'; import { Subscription } from 'rxjs'; import { @@ -194,14 +181,7 @@ export class DashboardModel implements TimeModel { } addBuiltInAnnotationQuery() { - let found = false; - for (const item of this.annotations.list) { - if (item.builtIn === 1) { - found = true; - break; - } - } - + const found = this.annotations.list.some((item) => item.builtIn === 1); if (found) { return; } @@ -267,9 +247,7 @@ export class DashboardModel implements TimeModel { // sort by keys copy = sortedDeepCloneWithoutNulls(copy); - copy.getVariables = () => { - return copy.templating.list; - }; + copy.getVariables = () => copy.templating.list; return copy; } @@ -296,24 +274,11 @@ export class DashboardModel implements TimeModel { private getPanelSaveModels() { return this.panels - .filter((panel: PanelModel) => { - if (this.isSnapshotTruthy()) { - return true; - } - if (panel.type === 'add-panel') { - return false; - } - // skip repeated panels in the saved model - if (panel.repeatPanelId) { - return false; - } - // skip repeated rows in the saved model - if (panel.repeatedByRow) { - return false; - } - return true; - }) - .map((panel: PanelModel) => { + .filter( + (panel) => + this.isSnapshotTruthy() || !(panel.type === 'add-panel' || panel.repeatPanelId || panel.repeatedByRow) + ) + .map((panel) => { // If we save while editing we should include the panel in edit mode instead of the // unmodified source panel if (this.panelInEdit && this.panelInEdit.id === panel.id) { @@ -358,18 +323,19 @@ export class DashboardModel implements TimeModel { }; if (!defaults.saveVariables) { - for (let i = 0; i < copy.templating.list.length; i++) { - const current = copy.templating.list[i]; - const original: any = find(originalVariables, { name: current.name, type: current.type }); + for (const current of copy.templating.list) { + const original = originalVariables.find( + ({ name, type }: any) => name === current.name && type === current.type + ); if (!original) { continue; } if (current.type === 'adhoc') { - copy.templating.list[i].filters = original.filters; + current.filters = original.filters; } else { - copy.templating.list[i].current = original.current; + current.current = original.current; } } } @@ -384,18 +350,14 @@ export class DashboardModel implements TimeModel { this.events.publish(new RefreshEvent()); this.lastRefresh = Date.now(); - if (this.panelInEdit) { - if (event.refreshAll || event.panelIds.includes(this.panelInEdit.id)) { - this.panelInEdit.refresh(); - return; - } + if (this.panelInEdit && (event.refreshAll || event.panelIds.includes(this.panelInEdit.id))) { + this.panelInEdit.refresh(); + return; } for (const panel of this.panels) { - if (!this.otherPanelInFullscreen(panel)) { - if (event.refreshAll || event.panelIds.includes(panel.id)) { - panel.refresh(); - } + if (!this.otherPanelInFullscreen(panel) && (event.refreshAll || event.panelIds.includes(panel.id))) { + panel.refresh(); } } } @@ -453,51 +415,40 @@ export class DashboardModel implements TimeModel { } private ensurePanelsHaveIds() { - for (const panel of this.panels) { - if (!panel.id) { - panel.id = this.getNextPanelId(); - } - - if (panel.panels) { - for (const rowPanel of panel.panels) { - if (!rowPanel.id) { - rowPanel.id = this.getNextPanelId(); - } - } - } + let nextPanelId = this.getNextPanelId(); + for (const panel of this.panelIterator()) { + panel.id ??= nextPanelId++; } } - private ensureListExist(data: any) { - if (!data) { - data = {}; - } - if (!data.list) { - data.list = []; - } + private ensureListExist(data: any = {}) { + data.list ??= []; return data; } getNextPanelId() { let max = 0; - for (const panel of this.panels) { + for (const panel of this.panelIterator()) { if (panel.id > max) { max = panel.id; } - - if (panel.collapsed) { - for (const rowPanel of panel.panels) { - if (rowPanel.id > max) { - max = rowPanel.id; - } - } - } } return max + 1; } + *panelIterator() { + for (const panel of this.panels) { + yield panel; + + const rowPanels = panel.panels ?? []; + for (const rowPanel of rowPanels) { + yield rowPanel as PanelModel; + } + } + } + forEachPanel(callback: (panel: PanelModel, index: number) => void) { for (let i = 0; i < this.panels.length; i++) { callback(this.panels[i], i); @@ -509,13 +460,7 @@ export class DashboardModel implements TimeModel { return this.panelInEdit; } - for (const panel of this.panels) { - if (panel.id === id) { - return panel; - } - } - - return null; + return this.panels.find((p) => p.id === id) ?? null; } canEditPanel(panel?: PanelModel | null): boolean | undefined | null { @@ -553,13 +498,12 @@ export class DashboardModel implements TimeModel { } hasUnsavedChanges() { - for (const panel of this.panels) { - if (panel.hasChanged) { - console.log('Panel has changed', panel); - return true; - } + const changedPanel = this.panels.find((p) => p.hasChanged); + if (changedPanel) { + console.log('Panel has changed', changedPanel); } - return false; + + return Boolean(changedPanel); } cleanUpRepeats() { @@ -568,17 +512,12 @@ export class DashboardModel implements TimeModel { } this.iteration = (this.iteration || new Date().getTime()) + 1; - const panelsToRemove = []; - // cleanup scopedVars deleteScopeVars(this.panels); - for (let i = 0; i < this.panels.length; i++) { - const panel = this.panels[i]; - if ((!panel.repeat || panel.repeatedByRow) && panel.repeatPanelId && panel.repeatIteration !== this.iteration) { - panelsToRemove.push(panel); - } - } + const panelsToRemove = this.panels.filter( + (p) => (!p.repeat || p.repeatedByRow) && p.repeatPanelId && p.repeatIteration !== this.iteration + ); // remove panels pull(this.panels, ...panelsToRemove); @@ -607,13 +546,8 @@ export class DashboardModel implements TimeModel { } cleanUpRowRepeats(rowPanels: PanelModel[]) { - const panelsToRemove = []; - for (let i = 0; i < rowPanels.length; i++) { - const panel = rowPanels[i]; - if (!panel.repeat && panel.repeatPanelId) { - panelsToRemove.push(panel); - } - } + const panelsToRemove = rowPanels.filter((p) => !p.repeat && p.repeatPanelId); + pull(rowPanels, ...panelsToRemove); pull(this.panels, ...panelsToRemove); } @@ -623,18 +557,17 @@ export class DashboardModel implements TimeModel { return; } - let rowPanels = row.panels; + let rowPanels = row.panels ?? []; if (!row.collapsed) { - const rowPanelIndex = findIndex(this.panels, (p: PanelModel) => p.id === row.id); + const rowPanelIndex = this.panels.findIndex((p) => p.id === row.id); rowPanels = this.getRowPanels(rowPanelIndex); } this.cleanUpRowRepeats(rowPanels); - for (let i = 0; i < rowPanels.length; i++) { - const panel = rowPanels[i]; + for (const panel of rowPanels) { if (panel.repeat) { - const panelIndex = findIndex(this.panels, (p: PanelModel) => p.id === panel.id); + const panelIndex = this.panels.findIndex((p) => p.id === panel.id); this.repeatPanel(panel, panelIndex); } } @@ -679,13 +612,13 @@ export class DashboardModel implements TimeModel { // for row clones we need to figure out panels under row to clone and where to insert clone let rowPanels: PanelModel[], insertPos: number; if (sourceRowPanel.collapsed) { - rowPanels = cloneDeep(sourceRowPanel.panels); + rowPanels = cloneDeep(sourceRowPanel.panels) ?? []; clone.panels = rowPanels; // insert copied row after preceding row insertPos = sourcePanelIndex + valueIndex; } else { rowPanels = this.getRowPanels(sourcePanelIndex); - clone.panels = map(rowPanels, (panel: PanelModel) => panel.getSaveModel()); + clone.panels = rowPanels.map((panel) => panel.getSaveModel()); // insert copied row after preceding row's panels insertPos = sourcePanelIndex + (rowPanels.length + 1) * valueIndex; } @@ -696,7 +629,7 @@ export class DashboardModel implements TimeModel { } repeatPanel(panel: PanelModel, panelIndex: number) { - const variable: any = this.getPanelRepeatVariable(panel); + const variable = this.getPanelRepeatVariable(panel); if (!variable) { return; } @@ -717,7 +650,7 @@ export class DashboardModel implements TimeModel { let copy; copy = this.getPanelRepeatClone(panel, index, panelIndex); - copy.scopedVars = copy.scopedVars || {}; + copy.scopedVars ??= {}; copy.scopedVars[variable.name] = option; if (panel.repeatDirection === REPEAT_DIR_VERTICAL) { @@ -746,12 +679,12 @@ export class DashboardModel implements TimeModel { const yOffset = yPos - panel.gridPos.y; if (yOffset > 0) { const panelBelowIndex = panelIndex + selectedOptions.length; - for (let i = panelBelowIndex; i < this.panels.length; i++) { - if (isOnTheSameGridRow(panel, this.panels[i])) { + for (const curPanel of this.panels.slice(panelBelowIndex)) { + if (isOnTheSameGridRow(panel, curPanel)) { continue; } - this.panels[i].gridPos.y += yOffset; + curPanel.gridPos.y += yOffset; } } } @@ -761,7 +694,7 @@ export class DashboardModel implements TimeModel { let yPos = panel.gridPos.y; function setScopedVars(panel: PanelModel, variableOption: any) { - panel.scopedVars = panel.scopedVars || {}; + panel.scopedVars ??= {}; panel.scopedVars[variable.name] = variableOption; } @@ -776,19 +709,19 @@ export class DashboardModel implements TimeModel { if (panel.collapsed) { // For collapsed row just copy its panels and set scoped vars and proper IDs - each(rowPanels, (rowPanel: PanelModel, i: number) => { + for (const rowPanel of rowPanels) { setScopedVars(rowPanel, option); if (optionIndex > 0) { this.updateRepeatedPanelIds(rowPanel, true); } - }); + } rowCopy.gridPos.y += optionIndex; yPos += optionIndex; panelBelowIndex = panelIndex + optionIndex + 1; } else { // insert after 'row' panel const insertPos = panelIndex + (rowPanels.length + 1) * optionIndex + 1; - each(rowPanels, (rowPanel: PanelModel, i: number) => { + rowPanels.forEach((rowPanel: PanelModel, i: number) => { setScopedVars(rowPanel, option); if (optionIndex > 0) { const cloneRowPanel = new PanelModel(rowPanel); @@ -806,8 +739,8 @@ export class DashboardModel implements TimeModel { // Update gridPos for panels below if we inserted more than 1 repeated row panel if (selectedOptions.length > 1) { - for (let i = panelBelowIndex; i < this.panels.length; i++) { - this.panels[i].gridPos.y += yPos; + for (const panel of this.panels.slice(panelBelowIndex)) { + panel.gridPos.y += yPos; } } } @@ -839,11 +772,10 @@ export class DashboardModel implements TimeModel { if (!rowPanel.panels || rowPanel.panels.length === 0) { return 0; } + const rowYPos = rowPanel.gridPos.y; const positions = map(rowPanel.panels, 'gridPos'); - const maxPos = maxBy(positions, (pos: GridPos) => { - return pos.y + pos.h; - }); + const maxPos = maxBy(positions, (pos: GridPos) => pos.y + pos.h); return maxPos!.y + maxPos!.h - rowYPos; } @@ -853,9 +785,9 @@ export class DashboardModel implements TimeModel { } removeRow(row: PanelModel, removePanels: boolean) { - const needToogle = (!removePanels && row.collapsed) || (removePanels && !row.collapsed); + const needToggle = (!removePanels && row.collapsed) || (removePanels && !row.collapsed); - if (needToogle) { + if (needToggle) { this.toggleRow(row); } @@ -863,60 +795,30 @@ export class DashboardModel implements TimeModel { } expandRows() { - for (let i = 0; i < this.panels.length; i++) { - const panel = this.panels[i]; - - if (panel.type !== 'row') { - continue; - } - - if (panel.collapsed) { - this.toggleRow(panel); - } + const collapsedRows = this.panels.filter((p) => p.type === 'row' && p.collapsed); + for (const row of collapsedRows) { + this.toggleRow(row); } } collapseRows() { - for (let i = 0; i < this.panels.length; i++) { - const panel = this.panels[i]; - - if (panel.type !== 'row') { - continue; - } - - if (!panel.collapsed) { - this.toggleRow(panel); - } + const collapsedRows = this.panels.filter((p) => p.type === 'row' && !p.collapsed); + for (const row of collapsedRows) { + this.toggleRow(row); } } isSubMenuVisible() { - if (this.links.length > 0) { - return true; - } - - if (this.getVariables().find((variable) => variable.hide !== 2)) { - return true; - } - - if (this.annotations.list.find((annotation) => annotation.hide !== true)) { - return true; - } - - return false; + return ( + this.links.length > 0 || + this.getVariables().some((variable) => variable.hide !== 2) || + this.annotations.list.some((annotation) => !annotation.hide) + ); } getPanelInfoById(panelId: number) { - for (let i = 0; i < this.panels.length; i++) { - if (this.panels[i].id === panelId) { - return { - panel: this.panels[i], - index: i, - }; - } - } - - return null; + const panelIndex = this.panels.findIndex((p) => p.id === panelId); + return panelIndex >= 0 ? { panel: this.panels[panelIndex], index: panelIndex } : null; } duplicatePanel(panel: PanelModel) { @@ -962,64 +864,64 @@ export class DashboardModel implements TimeModel { toggleRow(row: PanelModel) { const rowIndex = indexOf(this.panels, row); - if (row.collapsed) { - row.collapsed = false; - const hasRepeat = some(row.panels as PanelModel[], (p: PanelModel) => p.repeat); + if (!row.collapsed) { + const rowPanels = this.getRowPanels(rowIndex); - if (row.panels.length > 0) { - // Use first panel to figure out if it was moved or pushed - // If the panel doesn't have gridPos.y, use the row gridPos.y instead. - // This can happen for some generated dashboards. - const firstPanelYPos = row.panels[0].gridPos.y ?? row.gridPos.y; - const yDiff = firstPanelYPos - (row.gridPos.y + row.gridPos.h); - - // start inserting after row - let insertPos = rowIndex + 1; - // y max will represent the bottom y pos after all panels have been added - // needed to know home much panels below should be pushed down - let yMax = row.gridPos.y; - - for (const panel of row.panels) { - // set the y gridPos if it wasn't already set - panel.gridPos.y ?? (panel.gridPos.y = row.gridPos.y); // (Safari 13.1 lacks ??= support) - // make sure y is adjusted (in case row moved while collapsed) - panel.gridPos.y -= yDiff; - // insert after row - this.panels.splice(insertPos, 0, new PanelModel(panel)); - // update insert post and y max - insertPos += 1; - yMax = Math.max(yMax, panel.gridPos.y + panel.gridPos.h); - } - - const pushDownAmount = yMax - row.gridPos.y - 1; - - // push panels below down - for (let panelIndex = insertPos; panelIndex < this.panels.length; panelIndex++) { - this.panels[panelIndex].gridPos.y += pushDownAmount; - } - - row.panels = []; - - if (hasRepeat) { - this.processRowRepeats(row); - } - } - - // sort panels - this.sortPanelsByGridPos(); + // remove panels + pull(this.panels, ...rowPanels); + // save panel models inside row panel + row.panels = rowPanels.map((panel: PanelModel) => panel.getSaveModel()); + row.collapsed = true; // emit change event this.events.publish(new DashboardPanelsChangedEvent()); return; } - const rowPanels = this.getRowPanels(rowIndex); + row.collapsed = false; + const rowPanels = row.panels ?? []; + const hasRepeat = rowPanels.some((p: PanelModel) => p.repeat); + if (rowPanels.length > 0) { + // Use first panel to figure out if it was moved or pushed + // If the panel doesn't have gridPos.y, use the row gridPos.y instead. + // This can happen for some generated dashboards. + const firstPanelYPos = rowPanels[0].gridPos.y ?? row.gridPos.y; + const yDiff = firstPanelYPos - (row.gridPos.y + row.gridPos.h); - // remove panels - pull(this.panels, ...rowPanels); - // save panel models inside row panel - row.panels = map(rowPanels, (panel: PanelModel) => panel.getSaveModel()); - row.collapsed = true; + // start inserting after row + let insertPos = rowIndex + 1; + // y max will represent the bottom y pos after all panels have been added + // needed to know home much panels below should be pushed down + let yMax = row.gridPos.y; + + for (const panel of rowPanels) { + // set the y gridPos if it wasn't already set + panel.gridPos.y ?? (panel.gridPos.y = row.gridPos.y); // (Safari 13.1 lacks ??= support) + // make sure y is adjusted (in case row moved while collapsed) + panel.gridPos.y -= yDiff; + // insert after row + this.panels.splice(insertPos, 0, new PanelModel(panel)); + // update insert post and y max + insertPos += 1; + yMax = Math.max(yMax, panel.gridPos.y + panel.gridPos.h); + } + + const pushDownAmount = yMax - row.gridPos.y - 1; + + // push panels below down + for (const panel of this.panels.slice(insertPos)) { + panel.gridPos.y += pushDownAmount; + } + + row.panels = []; + + if (hasRepeat) { + this.processRowRepeats(row); + } + } + + // sort panels + this.sortPanelsByGridPos(); // emit change event this.events.publish(new DashboardPanelsChangedEvent()); @@ -1029,19 +931,11 @@ export class DashboardModel implements TimeModel { * Will return all panels after rowIndex until it encounters another row */ getRowPanels(rowIndex: number): PanelModel[] { - const rowPanels = []; + const panelsBelowRow = this.panels.slice(rowIndex + 1); + const nextRowIndex = panelsBelowRow.findIndex((p) => p.type === 'row'); - for (let index = rowIndex + 1; index < this.panels.length; index++) { - const panel = this.panels[index]; - - // break when encountering another row - if (panel.type === 'row') { - break; - } - - // this panel must belong to row - rowPanels.push(panel); - } + // Take all panels up to next row, or all panels if there are no other rows + const rowPanels = panelsBelowRow.slice(0, nextRowIndex >= 0 ? nextRowIndex : this.panels.length); return rowPanels; } @@ -1095,14 +989,12 @@ export class DashboardModel implements TimeModel { hasTimeChanged() { const { time, originalTime } = this; - if (isEqual(time, originalTime)) { - return false; - } - // Compare momemt values vs strings values + // Compare moment values vs strings values return !( - isEqual(dateTime(time?.from), dateTime(originalTime?.from)) && - isEqual(dateTime(time?.to), dateTime(originalTime?.to)) + isEqual(time, originalTime) || + (isEqual(dateTime(time?.from), dateTime(originalTime?.from)) && + isEqual(dateTime(time?.to), dateTime(originalTime?.to))) ); } @@ -1120,11 +1012,7 @@ export class DashboardModel implements TimeModel { } autoFitPanels(viewHeight: number, kioskMode?: UrlQueryValue) { - const currentGridHeight = Math.max( - ...this.panels.map((panel) => { - return panel.gridPos.h + panel.gridPos.y; - }) - ); + const currentGridHeight = Math.max(...this.panels.map((panel) => panel.gridPos.h + panel.gridPos.y)); const navbarHeight = 55; const margin = 20; @@ -1145,10 +1033,10 @@ export class DashboardModel implements TimeModel { const visibleGridHeight = Math.floor(visibleHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN)); const scaleFactor = currentGridHeight / visibleGridHeight; - this.panels.forEach((panel, i) => { + for (const panel of this.panels) { panel.gridPos.y = Math.round(panel.gridPos.y / scaleFactor) || 1; panel.gridPos.h = Math.round(panel.gridPos.h / scaleFactor) || 1; - }); + } } templateVariableValueUpdated() { @@ -1160,39 +1048,32 @@ export class DashboardModel implements TimeModel { const panelId = parseInt(panelUrlId ?? '0', 10); // First try to find it in a collapsed row and exand it - for (const panel of this.panels) { - if (panel.collapsed) { - for (const rowPanel of panel.panels) { - if (rowPanel.id === panelId) { - this.toggleRow(panel); - break; - } - } - } + const collapsedPanels = this.panels.filter((p) => p.collapsed); + for (const panel of collapsedPanels) { + const hasPanel = panel.panels?.some((rp: any) => rp.id === panelId); + hasPanel && this.toggleRow(panel); } return this.getPanelById(panelId); } toggleLegendsForAll() { - const panelsWithLegends = this.panels.filter((panel) => { - return panel.legend !== undefined && panel.legend !== null; - }); + const panelsWithLegends = this.panels.filter(isPanelWithLegend); // determine if more panels are displaying legends or not - const onCount = panelsWithLegends.filter((panel) => panel.legend!.show).length; + const onCount = panelsWithLegends.filter((panel) => panel.legend.show).length; const offCount = panelsWithLegends.length - onCount; const panelLegendsOn = onCount >= offCount; for (const panel of panelsWithLegends) { - panel.legend!.show = !panelLegendsOn; + panel.legend.show = !panelLegendsOn; panel.render(); } } - getVariables = () => { + getVariables() { return this.getVariablesFromState(this.uid); - }; + } canEditAnnotations(dashboardId: number) { let canEdit = true; @@ -1209,13 +1090,8 @@ export class DashboardModel implements TimeModel { } canAddAnnotations() { - let canAdd = true; - - // if RBAC is enabled there are additional conditions to check - if (contextSrv.accessControlEnabled()) { - canAdd = !!this.meta.annotationsPermissions?.dashboard.canAdd; - } - + // If RBAC is enabled there are additional conditions to check + const canAdd = !contextSrv.accessControlEnabled() || this.meta.annotationsPermissions?.dashboard.canAdd; return this.canEditDashboard() && canAdd; } @@ -1247,27 +1123,23 @@ export class DashboardModel implements TimeModel { return false; } - const updated = map(currentVariables, (variable: any) => { - return { - name: variable.name, - type: variable.type, - current: cloneDeep(variable.current), - filters: cloneDeep(variable.filters), - }; - }); + const updated = currentVariables.map((variable: any) => ({ + name: variable.name, + type: variable.type, + current: cloneDeep(variable.current), + filters: cloneDeep(variable.filters), + })); return !isEqual(updated, originalVariables); } private cloneVariablesFrom(variables: any[]): any[] { - return variables.map((variable) => { - return { - name: variable.name, - type: variable.type, - current: cloneDeep(variable.current), - filters: cloneDeep(variable.filters), - }; - }); + return variables.map((variable) => ({ + name: variable.name, + type: variable.type, + current: cloneDeep(variable.current), + filters: cloneDeep(variable.filters), + })); } private variablesTimeRangeProcessDoneHandler(event: VariablesTimeRangeProcessDone) { @@ -1299,3 +1171,7 @@ export class DashboardModel implements TimeModel { this.startRefresh(event.payload); } } + +function isPanelWithLegend(panel: PanelModel): panel is PanelModel & Pick, 'legend'> { + return Boolean(panel.legend); +} diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index 81723a24d62..b4a64914493 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -139,7 +139,7 @@ export class PanelModel implements DataConfigSource, IPanelModel { maxPerRow?: number; collapsed?: boolean; - panels?: any; + panels?: PanelModel[]; declare targets: DataQuery[]; transformations?: DataTransformerConfig[]; datasource: DataSourceRef | null = null; diff --git a/public/app/features/dashboard/state/utils.test.ts b/public/app/features/dashboard/state/utils.test.ts index 5c63afdf2ee..1e0f10bb20f 100644 --- a/public/app/features/dashboard/state/utils.test.ts +++ b/public/app/features/dashboard/state/utils.test.ts @@ -50,14 +50,14 @@ describe('deleteScopeVars', () => { }); expect(panel1.scopedVars).toBeDefined(); - expect(panel1.panels[0].scopedVars).toBeDefined(); - expect(panel1.panels[1].scopedVars).toBeDefined(); + expect(panel1.panels?.[0].scopedVars).toBeDefined(); + expect(panel1.panels?.[1].scopedVars).toBeDefined(); deleteScopeVars([panel1]); expect(panel1.scopedVars).toBeUndefined(); - expect(panel1.panels[0].scopedVars).toBeUndefined(); - expect(panel1.panels[1].scopedVars).toBeUndefined(); + expect(panel1.panels?.[0].scopedVars).toBeUndefined(); + expect(panel1.panels?.[1].scopedVars).toBeUndefined(); }); }); }); From b420179be43e098b287009095fd392b3bfbb3816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Bedi?= Date: Wed, 4 May 2022 10:41:03 +0200 Subject: [PATCH 017/440] GraphNG: Fix thresholds by color not following data update (#48571) * GraphNG: Fix thresholds by color not following data update * Refactor dynamicSeriesColor to time series * avoid exposing frames on builder rely on seriesIdx & cached alignedFrame to grab field handle dynamic fill recoloring only recolor when not in a special gradient mode * bail when opacity = 0 Co-authored-by: Leon Sorokin --- .../src/components/GraphNG/utils.test.ts | 16 +++++++++++ .../src/components/TimeSeries/utils.ts | 17 +++++++++++- .../uPlot/config/UPlotSeriesBuilder.ts | 27 ++++++++++++++++++- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/packages/grafana-ui/src/components/GraphNG/utils.test.ts b/packages/grafana-ui/src/components/GraphNG/utils.test.ts index 23efa7a7726..71b4f7a673e 100644 --- a/packages/grafana-ui/src/components/GraphNG/utils.test.ts +++ b/packages/grafana-ui/src/components/GraphNG/utils.test.ts @@ -5,6 +5,7 @@ import { DataFrame, DefaultTimeZone, EventBusSrv, + FieldColorModeId, FieldConfig, FieldMatcherID, fieldMatchers, @@ -38,6 +39,9 @@ function mockDataFrame() { const f1Config: FieldConfig = { displayName: 'Metric 1', + color: { + mode: FieldColorModeId.Fixed, + }, decimals: 2, custom: { drawStyle: GraphDrawStyle.Line, @@ -62,6 +66,9 @@ function mockDataFrame() { const f2Config: FieldConfig = { displayName: 'Metric 2', + color: { + mode: FieldColorModeId.Fixed, + }, decimals: 2, custom: { drawStyle: GraphDrawStyle.Bars, @@ -87,6 +94,9 @@ function mockDataFrame() { const f3Config: FieldConfig = { displayName: 'Metric 3', decimals: 2, + color: { + mode: FieldColorModeId.Fixed, + }, custom: { drawStyle: GraphDrawStyle.Line, gradientMode: GraphGradientMode.Opacity, @@ -110,6 +120,9 @@ function mockDataFrame() { const f4Config: FieldConfig = { displayName: 'Metric 4', decimals: 2, + color: { + mode: FieldColorModeId.Fixed, + }, custom: { drawStyle: GraphDrawStyle.Bars, gradientMode: GraphGradientMode.Hue, @@ -133,6 +146,9 @@ function mockDataFrame() { const f5Config: FieldConfig = { displayName: 'Metric 4', decimals: 2, + color: { + mode: FieldColorModeId.Fixed, + }, custom: { drawStyle: GraphDrawStyle.Bars, gradientMode: GraphGradientMode.Hue, diff --git a/packages/grafana-ui/src/components/TimeSeries/utils.ts b/packages/grafana-ui/src/components/TimeSeries/utils.ts index 062378b5124..fe587a7b64f 100644 --- a/packages/grafana-ui/src/components/TimeSeries/utils.ts +++ b/packages/grafana-ui/src/components/TimeSeries/utils.ts @@ -14,6 +14,7 @@ import { getFieldSeriesColor, getFieldDisplayName, getDisplayProcessor, + FieldColorModeId, } from '@grafana/data'; import { AxisPlacement, @@ -54,7 +55,14 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ }) => { const builder = new UPlotConfigBuilder(timeZone); - builder.setPrepData((frames) => preparePlotData2(frames[0], builder.getStackingGroups())); + let alignedFrame: DataFrame; + + builder.setPrepData((frames) => { + // cache alignedFrame + alignedFrame = frames[0]; + + return preparePlotData2(frames[0], builder.getStackingGroups()); + }); // X is the first field in the aligned frame const xField = frame.fields[0]; @@ -278,6 +286,12 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ } } + let dynamicSeriesColor: ((seriesIdx: number) => string | undefined) | undefined = undefined; + + if (colorMode.id === FieldColorModeId.Thresholds) { + dynamicSeriesColor = (seriesIdx) => getFieldSeriesColor(alignedFrame.fields[seriesIdx], theme).color; + } + builder.addSeries({ pathBuilder, pointsBuilder, @@ -287,6 +301,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ colorMode, fillOpacity, theme, + dynamicSeriesColor, drawStyle: customConfig.drawStyle!, lineColor: customConfig.lineColor ?? seriesColor, lineWidth: customConfig.lineWidth, diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts index fe0c7f081f3..cddb4918c57 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts @@ -29,6 +29,7 @@ export interface SeriesProps extends LineConfig, BarConfig, FillConfig, PointsCo scaleKey: string; pxAlign?: boolean; gradientMode?: GraphGradientMode; + dynamicSeriesColor?: (seriesIdx: number) => string | undefined; facets?: uPlot.Series.Facet[]; @@ -150,7 +151,22 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder { } private getLineColor(): Series.Stroke { - const { lineColor, gradientMode, colorMode, thresholds, theme, hardMin, hardMax, softMin, softMax } = this.props; + const { + lineColor, + gradientMode, + colorMode, + thresholds, + theme, + hardMin, + hardMax, + softMin, + softMax, + dynamicSeriesColor, + } = this.props; + + if (gradientMode === GraphGradientMode.None && dynamicSeriesColor) { + return (plot: uPlot, seriesIdx: number) => dynamicSeriesColor(seriesIdx) ?? lineColor ?? FALLBACK_COLOR; + } if (gradientMode === GraphGradientMode.Scheme && colorMode?.id !== FieldColorModeId.Fixed) { return getScaleGradientFn(1, theme, colorMode, thresholds, hardMin, hardMax, softMin, softMax); @@ -172,6 +188,7 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder { hardMax, softMin, softMax, + dynamicSeriesColor, } = this.props; if (fillColor) { @@ -181,6 +198,14 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder { const mode = gradientMode ?? GraphGradientMode.None; const opacityPercent = (fillOpacity ?? 0) / 100; + if (mode === GraphGradientMode.None && dynamicSeriesColor && opacityPercent > 0) { + return (u: uPlot, seriesIdx: number) => { + // @ts-ignore + let lineColor = u.series[seriesIdx]._stroke; // cache + return colorManipulator.alpha(lineColor ?? '', opacityPercent); + }; + } + switch (mode) { case GraphGradientMode.Opacity: return getOpacityGradientFn((fillColor ?? lineColor)!, opacityPercent); From bb7e556efcfd625af64f03cada17707a621e8516 Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Wed, 4 May 2022 10:04:15 +0100 Subject: [PATCH 018/440] Templating: Prefix variable picker element IDs (#48405) --- e2e/dashboards-suite/new-text-box-variable.spec.ts | 2 +- .../variables/pickers/OptionsPicker/OptionsPicker.tsx | 4 ++-- public/app/features/variables/pickers/PickerRenderer.tsx | 5 +++-- .../app/features/variables/textbox/TextBoxVariablePicker.tsx | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/e2e/dashboards-suite/new-text-box-variable.spec.ts b/e2e/dashboards-suite/new-text-box-variable.spec.ts index 0f5dddb8a04..7882bc4d128 100644 --- a/e2e/dashboards-suite/new-text-box-variable.spec.ts +++ b/e2e/dashboards-suite/new-text-box-variable.spec.ts @@ -20,7 +20,7 @@ describe('Variables - Text box', () => { // Navigate back to the homepage and change the selected variable value e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); e2e.components.BackButton.backArrow().click({ force: true }); - e2e().get('#VariableUnderTest').clear().type('dog-cat').blur(); + e2e().get('#var-VariableUnderTest').clear().type('dog-cat').blur(); // Assert it was rendered e2e().get('.markdown-html').should('include.text', 'VariableUnderTest: dog-cat'); diff --git a/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx b/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx index aad1ab98e0f..6d264cd06ef 100644 --- a/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx +++ b/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx @@ -125,7 +125,7 @@ export const optionPickerFactory = ): ReactElement | nul return null; } + const elementId = `var-${variable.id}`; if (variable.description) { return ( @@ -52,7 +53,7 @@ function PickerLabel({ variable }: PropsWithChildren): ReactElement | nul diff --git a/public/app/features/variables/textbox/TextBoxVariablePicker.tsx b/public/app/features/variables/textbox/TextBoxVariablePicker.tsx index 14d9251d9a7..e52d4dcdde2 100644 --- a/public/app/features/variables/textbox/TextBoxVariablePicker.tsx +++ b/public/app/features/variables/textbox/TextBoxVariablePicker.tsx @@ -70,7 +70,7 @@ export function TextBoxVariablePicker({ variable, onVariableChange }: Props): Re onBlur={onBlur} onKeyDown={onKeyDown} placeholder="Enter variable value" - id={variable.id} + id={`var-${variable.id}`} /> ); } From e04d8fca7b2d5fcd9bc4fb28888ac4f72553cb58 Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Wed, 4 May 2022 11:35:08 +0200 Subject: [PATCH 019/440] Alerting: correctly show all alerts in a folder (#48684) --- .../alerting/unified/AlertsFolderView.test.tsx | 17 ++++++++++++++--- .../alerting/unified/AlertsFolderView.tsx | 4 ++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/public/app/features/alerting/unified/AlertsFolderView.test.tsx b/public/app/features/alerting/unified/AlertsFolderView.test.tsx index bb28d3984ec..e588e306178 100644 --- a/public/app/features/alerting/unified/AlertsFolderView.test.tsx +++ b/public/app/features/alerting/unified/AlertsFolderView.test.tsx @@ -55,13 +55,21 @@ describe('AlertsFolderView tests', () => { rulesSource: GRAFANA_RULES_SOURCE_NAME, groups: [ { - name: 'default', + name: 'group1', rules: [ mockCombinedRule({ name: 'Test Alert 1' }), mockCombinedRule({ name: 'Test Alert 2' }), mockCombinedRule({ name: 'Test Alert 3' }), ], }, + { + name: 'group2', + rules: [ + mockCombinedRule({ name: 'Test Alert 4' }), + mockCombinedRule({ name: 'Test Alert 5' }), + mockCombinedRule({ name: 'Test Alert 6' }), + ], + }, ], }; @@ -78,13 +86,16 @@ describe('AlertsFolderView tests', () => { // Assert const alertRows = ui.ruleList.row.queryAll(); - expect(alertRows).toHaveLength(3); + expect(alertRows).toHaveLength(6); expect(alertRows[0]).toHaveTextContent('Test Alert 1'); expect(alertRows[1]).toHaveTextContent('Test Alert 2'); expect(alertRows[2]).toHaveTextContent('Test Alert 3'); + expect(alertRows[3]).toHaveTextContent('Test Alert 4'); + expect(alertRows[4]).toHaveTextContent('Test Alert 5'); + expect(alertRows[5]).toHaveTextContent('Test Alert 6'); }); - it('Shold not display alert rules when the namespace name does not match the folder name', () => { + it('Should not display alert rules when the namespace name does not match the folder name', () => { // Arrange const store = configureStore(); const folder = mockFolder(); diff --git a/public/app/features/alerting/unified/AlertsFolderView.tsx b/public/app/features/alerting/unified/AlertsFolderView.tsx index 77124c6a01b..8e94caa63e1 100644 --- a/public/app/features/alerting/unified/AlertsFolderView.tsx +++ b/public/app/features/alerting/unified/AlertsFolderView.tsx @@ -56,7 +56,7 @@ export const AlertsFolderView = ({ folder }: Props) => { useAlertsFolderViewParams(); const matchingNamespace = combinedNamespaces.find((namespace) => namespace.name === folder.title); - const alertRules = matchingNamespace?.groups[0]?.rules ?? []; + const alertRules = matchingNamespace?.groups.flatMap((group) => group.rules) ?? []; const filteredRules = filterAndSortRules(alertRules, nameFilter, labelFilter, sortOrder ?? SortOrder.Ascending); @@ -177,7 +177,7 @@ function filterAndSortRules( (rule) => rule.name.toLowerCase().includes(nameFilter.toLowerCase()) && labelsMatchMatchers(rule.labels, matchers) ); - return orderBy(rules, (x) => x.name, [sortOrder === SortOrder.Ascending ? 'asc' : 'desc']); + return orderBy(rules, (x) => x.name.toLowerCase(), [sortOrder === SortOrder.Ascending ? 'asc' : 'desc']); } export const getStyles = (theme: GrafanaTheme2) => ({ From f45dc224d9d98bac67505a1ecdf0a83e569a34bc Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 4 May 2022 05:35:22 -0400 Subject: [PATCH 020/440] ReleaseNotes: Updated changelog and release notes for 8.5.2 (#48681) --- CHANGELOG.md | 18 ++++++++++++++++ docs/sources/release-notes/_index.md | 1 + .../release-notes/release-notes-8-5-2.md | 21 +++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 docs/sources/release-notes/release-notes-8-5-2.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 4df03eab80d..4c97f5c75f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ + + +# 8.5.2 (2022-05-03) + +### Features and enhancements + +- **Alerting:** Add safeguard for migrations that might cause dataloss. [#48526](https://github.com/grafana/grafana/pull/48526), [@JohnnyQQQQ](https://github.com/JohnnyQQQQ) +- **AzureMonitor:** Add support for not equals and startsWith operators when creating Azure Metrics dimension filters. [#48077](https://github.com/grafana/grafana/pull/48077), [@aangelisc](https://github.com/aangelisc) +- **Elasticsearch:** Add deprecation notice for < 7.10 versions. [#48506](https://github.com/grafana/grafana/pull/48506), [@ivanahuckova](https://github.com/ivanahuckova) +- **Traces:** Filter by service/span name and operation in Tempo and Jaeger. [#48209](https://github.com/grafana/grafana/pull/48209), [@joey-grafana](https://github.com/joey-grafana) + +### Bug fixes + +- **AzureAd Oauth:** Fix strictMode to reject users without an assigned role. [#48474](https://github.com/grafana/grafana/pull/48474), [@kyschouv](https://github.com/kyschouv) +- **CloudWatch:** Fix variable query tag migration. [#48587](https://github.com/grafana/grafana/pull/48587), [@iwysiu](https://github.com/iwysiu) +- **Plugins:** Ensure catching all appropriate 4xx api/ds/query scenarios. [#47565](https://github.com/grafana/grafana/pull/47565), [@wbrowne](https://github.com/wbrowne) + + # 8.5.1 (2022-04-27) diff --git a/docs/sources/release-notes/_index.md b/docs/sources/release-notes/_index.md index 7296f801e6d..aacc38ff83b 100644 --- a/docs/sources/release-notes/_index.md +++ b/docs/sources/release-notes/_index.md @@ -8,6 +8,7 @@ weight = 10000 Here you can find detailed release notes that list everything that is included in every release as well as notices about deprecations, breaking changes as well as changes that relate to plugin development. +- [Release notes for 8.5.2]({{< relref "release-notes-8-5-2" >}}) - [Release notes for 8.5.1]({{< relref "release-notes-8-5-1" >}}) - [Release notes for 8.5.0]({{< relref "release-notes-8-5-0" >}}) - [Release notes for 8.5.0-beta1]({{< relref "release-notes-8-5-0-beta1" >}}) diff --git a/docs/sources/release-notes/release-notes-8-5-2.md b/docs/sources/release-notes/release-notes-8-5-2.md new file mode 100644 index 00000000000..5fcacd30a29 --- /dev/null +++ b/docs/sources/release-notes/release-notes-8-5-2.md @@ -0,0 +1,21 @@ ++++ +title = "Release notes for Grafana 8.5.2" +hide_menu = true ++++ + + + +# Release notes for Grafana 8.5.2 + +### Features and enhancements + +- **Alerting:** Add safeguard for migrations that might cause dataloss. [#48526](https://github.com/grafana/grafana/pull/48526), [@JohnnyQQQQ](https://github.com/JohnnyQQQQ) +- **AzureMonitor:** Add support for not equals and startsWith operators when creating Azure Metrics dimension filters. [#48077](https://github.com/grafana/grafana/pull/48077), [@aangelisc](https://github.com/aangelisc) +- **Elasticsearch:** Add deprecation notice for < 7.10 versions. [#48506](https://github.com/grafana/grafana/pull/48506), [@ivanahuckova](https://github.com/ivanahuckova) +- **Traces:** Filter by service/span name and operation in Tempo and Jaeger. [#48209](https://github.com/grafana/grafana/pull/48209), [@joey-grafana](https://github.com/joey-grafana) + +### Bug fixes + +- **AzureAd Oauth:** Fix strictMode to reject users without an assigned role. [#48474](https://github.com/grafana/grafana/pull/48474), [@kyschouv](https://github.com/kyschouv) +- **CloudWatch:** Fix variable query tag migration. [#48587](https://github.com/grafana/grafana/pull/48587), [@iwysiu](https://github.com/iwysiu) +- **Plugins:** Ensure catching all appropriate 4xx api/ds/query scenarios. [#47565](https://github.com/grafana/grafana/pull/47565), [@wbrowne](https://github.com/wbrowne) From b71aa912c6420d248177cf9027cc0dd2bdb894ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 4 May 2022 11:36:15 +0200 Subject: [PATCH 021/440] TimeRange: Fixes updating time range from url and browser history (#48657) --- public/app/features/dashboard/services/TimeSrv.ts | 9 ++------- .../dashboard/utils/getRefreshFromUrl.test.ts | 8 +++----- .../features/dashboard/utils/getRefreshFromUrl.ts | 12 ++++++------ 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/public/app/features/dashboard/services/TimeSrv.ts b/public/app/features/dashboard/services/TimeSrv.ts index 7966a83387b..cf3b7e7f6e1 100644 --- a/public/app/features/dashboard/services/TimeSrv.ts +++ b/public/app/features/dashboard/services/TimeSrv.ts @@ -169,14 +169,9 @@ export class TimeSrv { } } - let paramsJSON: Record = {}; - params.forEach(function (value, key) { - paramsJSON[key] = value; - }); - // but if refresh explicitly set then use that this.refresh = getRefreshFromUrl({ - params: paramsJSON, + urlRefresh: params.get('refresh'), currentRefresh: this.refresh, refreshIntervals: Array.isArray(this.timeModel?.timepicker?.refresh_intervals) ? this.timeModel?.timepicker?.refresh_intervals @@ -203,7 +198,7 @@ export class TimeSrv { if (from !== urlRange.from || to !== urlRange.to) { // issue update this.initTimeFromUrl(); - this.setTime(this.time, true); + this.setTime(this.time, false); } } else if (this.timeHasChangedSinceLoad()) { this.setTime(this.timeAtLoad, true); diff --git a/public/app/features/dashboard/utils/getRefreshFromUrl.test.ts b/public/app/features/dashboard/utils/getRefreshFromUrl.test.ts index c63ab1b64c9..1d8da07d8a7 100644 --- a/public/app/features/dashboard/utils/getRefreshFromUrl.test.ts +++ b/public/app/features/dashboard/utils/getRefreshFromUrl.test.ts @@ -3,13 +3,12 @@ import { getRefreshFromUrl } from './getRefreshFromUrl'; describe('getRefreshFromUrl', () => { describe('when refresh is not part of params', () => { it('then it should return current refresh value', () => { - const params = {}; const currentRefresh = false; const minRefreshInterval = '5s'; const isAllowedIntervalFn = () => false; const actual = getRefreshFromUrl({ - params, + urlRefresh: null, currentRefresh, minRefreshInterval, isAllowedIntervalFn, @@ -22,14 +21,13 @@ describe('getRefreshFromUrl', () => { describe('when refresh is part of params', () => { describe('and refresh is an existing and valid interval', () => { it('then it should return the refresh value', () => { - const params = { refresh: '10s' }; const currentRefresh = ''; const minRefreshInterval = '5s'; const isAllowedIntervalFn = () => true; const refreshIntervals = ['5s', '10s', '30s']; const actual = getRefreshFromUrl({ - params, + urlRefresh: '10s', currentRefresh, minRefreshInterval, isAllowedIntervalFn, @@ -61,7 +59,7 @@ describe('getRefreshFromUrl', () => { 'when called with refresh:{$refresh}, isAllowedInterval:{$isAllowedInterval}, minRefreshInterval:{$minRefreshInterval}, refreshIntervals:{$refreshIntervals} then it should return: $expected', ({ refresh, isAllowedInterval, minRefreshInterval, refreshIntervals, expected }) => { const actual = getRefreshFromUrl({ - params: { refresh }, + urlRefresh: refresh, currentRefresh: 'currentRefresh', minRefreshInterval, isAllowedIntervalFn: () => isAllowedInterval, diff --git a/public/app/features/dashboard/utils/getRefreshFromUrl.ts b/public/app/features/dashboard/utils/getRefreshFromUrl.ts index a838313b662..44ce399041a 100644 --- a/public/app/features/dashboard/utils/getRefreshFromUrl.ts +++ b/public/app/features/dashboard/utils/getRefreshFromUrl.ts @@ -1,7 +1,7 @@ import { defaultIntervals } from '@grafana/ui'; interface Args { - params: Record; + urlRefresh: string | null; currentRefresh: string | boolean | undefined; isAllowedIntervalFn: (interval: string) => boolean; minRefreshInterval: string; @@ -13,18 +13,18 @@ interface Args { // try to find the first refresh interval that matches the minRefreshInterval (min_refresh_interval in ini) // or just take the first interval. export function getRefreshFromUrl({ - params, + urlRefresh, currentRefresh, isAllowedIntervalFn, minRefreshInterval, refreshIntervals = defaultIntervals, }: Args): string | boolean | undefined { - if (!params.refresh) { + if (!urlRefresh) { return currentRefresh; } - const isAllowedInterval = isAllowedIntervalFn(params.refresh); - const isExistingInterval = refreshIntervals.find((interval) => interval === params.refresh); + const isAllowedInterval = isAllowedIntervalFn(urlRefresh); + const isExistingInterval = refreshIntervals.find((interval) => interval === urlRefresh); if (!isAllowedInterval || !isExistingInterval) { const minRefreshIntervalInIntervals = minRefreshInterval @@ -35,5 +35,5 @@ export function getRefreshFromUrl({ return minRefreshIntervalInIntervals ?? lowestRefreshInterval ?? currentRefresh; } - return params.refresh || currentRefresh; + return urlRefresh || currentRefresh; } From b0f41b9772345f79ba97857cf383ce3736ecf28f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Wed, 4 May 2022 12:10:54 +0200 Subject: [PATCH 022/440] update latest.json to 8.5.2 (#48690) --- latest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/latest.json b/latest.json index 09576b0a005..b8355dff5ef 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "stable": "8.5.1", - "testing": "8.5.1" + "stable": "8.5.2", + "testing": "8.5.2" } From f00ffb190c758654adb46a157047823ddfaaa228 Mon Sep 17 00:00:00 2001 From: Alexander Kubyshkin Date: Wed, 4 May 2022 13:49:04 +0300 Subject: [PATCH 023/440] Escape backslashes in regexps in Loki label browser (#45809, #47039). (#47412) * Escape backslashes in regexps in Loki label browser (#45809, #47039). * Escape values in Loki Query Builder. * Escape more values in Loki Query Builder. --- .../components/Typeahead/TypeaheadItem.tsx | 7 +++- .../loki/components/LokiLabelBrowser.tsx | 5 +-- .../loki/components/LokiQueryField.tsx | 15 ++++++-- .../datasource/loki/datasource.test.ts | 22 +++++++++++- .../app/plugins/datasource/loki/datasource.ts | 11 +++--- .../plugins/datasource/loki/language_utils.ts | 34 +++++++++++++++++++ .../components/LokiQueryBuilder.tsx | 13 ++++--- 7 files changed, 89 insertions(+), 18 deletions(-) diff --git a/packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx b/packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx index d75685d866e..660af9683c0 100644 --- a/packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx +++ b/packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx @@ -92,7 +92,12 @@ export const TypeaheadItem: React.FC = (props: Props) => { highlightParts={item.highlightParts} > ) : ( - + )} ); diff --git a/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx b/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx index d5ff9246194..0357e78cc87 100644 --- a/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx +++ b/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx @@ -18,6 +18,7 @@ import { import PromQlLanguageProvider from '../../prometheus/language_provider'; import LokiLanguageProvider from '../language_provider'; +import { escapeLabelValueInExactSelector, escapeLabelValueInRegexSelector } from '../language_utils'; // Hard limit on labels to render const MAX_LABEL_COUNT = 1000; @@ -67,9 +68,9 @@ export function buildSelector(labels: SelectableLabel[]): string { if (label.selected && label.values && label.values.length > 0) { const selectedValues = label.values.filter((value) => value.selected).map((value) => value.name); if (selectedValues.length > 1) { - selectedLabels.push(`${label.name}=~"${selectedValues.join('|')}"`); + selectedLabels.push(`${label.name}=~"${selectedValues.map(escapeLabelValueInRegexSelector).join('|')}"`); } else if (selectedValues.length === 1) { - selectedLabels.push(`${label.name}="${selectedValues[0]}"`); + selectedLabels.push(`${label.name}="${escapeLabelValueInExactSelector(selectedValues[0])}"`); } } } diff --git a/public/app/plugins/datasource/loki/components/LokiQueryField.tsx b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx index 1be894153a0..c72286df18b 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryField.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx @@ -17,7 +17,7 @@ import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValue import { LokiDatasource } from '../datasource'; import LokiLanguageProvider from '../language_provider'; -import { shouldRefreshLabels } from '../language_utils'; +import { escapeLabelValueInSelector, shouldRefreshLabels } from '../language_utils'; import { LokiQuery, LokiOptions } from '../types'; import { LokiLabelBrowser } from './LokiLabelBrowser'; @@ -47,17 +47,26 @@ function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadTe case 'context-label-values': { // Always add quotes and remove existing ones instead + let suggestionModified = ''; + if (!typeaheadText.match(/^(!?=~?"|")/)) { - suggestion = `"${suggestion}`; + suggestionModified = '"'; } + + suggestionModified += escapeLabelValueInSelector(suggestion, typeaheadText); + if (DOMUtil.getNextCharacter() !== '"') { - suggestion = `${suggestion}"`; + suggestionModified += '"'; } + + suggestion = suggestionModified; + break; } default: } + return suggestion; } diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index e47abeecf95..b790bd86f42 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -453,7 +453,7 @@ describe('LokiDatasource', () => { ]); await lastValueFrom(ds.query(options as any)); expect(ds.runRangeQuery).toBeCalledWith( - { expr: 'rate({bar="baz",job="foo",k1=~"v.*",k2=~"v\\\\\'.*"} |= "bar" [5m])' }, + { expr: 'rate({bar="baz",job="foo",k1=~"v\\\\.\\\\*",k2=~"v\'\\\\.\\\\*"} |= "bar" [5m])' }, expect.anything() ); }); @@ -767,6 +767,16 @@ describe('LokiDatasource', () => { expect(result.expr).toEqual('{bar="baz",job="grafana"}'); }); + it('then the correctly escaped label should be added for logs query', () => { + const query: LokiQuery = { refId: 'A', expr: '{bar="baz"}' }; + const action = { key: 'job', value: '\\test', type: 'ADD_FILTER' }; + const ds = createLokiDSForTests(); + const result = ds.modifyQuery(query, action); + + expect(result.refId).toEqual('A'); + expect(result.expr).toEqual('{bar="baz",job="\\\\test"}'); + }); + it('then the correct label should be added for metrics query', () => { const query: LokiQuery = { refId: 'A', expr: 'rate({bar="baz"}[5m])' }; const action = { key: 'job', value: 'grafana', type: 'ADD_FILTER' }; @@ -811,6 +821,16 @@ describe('LokiDatasource', () => { expect(result.expr).toEqual('{bar="baz",job!="grafana"}'); }); + it('then the correctly escaped label should be added for logs query', () => { + const query: LokiQuery = { refId: 'A', expr: '{bar="baz"}' }; + const action = { key: 'job', value: '"test', type: 'ADD_FILTER_OUT' }; + const ds = createLokiDSForTests(); + const result = ds.modifyQuery(query, action); + + expect(result.refId).toEqual('A'); + expect(result.expr).toEqual('{bar="baz",job!="\\"test"}'); + }); + it('then the correct label should be added for metrics query', () => { const query: LokiQuery = { refId: 'A', expr: 'rate({bar="baz"}[5m])' }; const action = { key: 'job', value: 'grafana', type: 'ADD_FILTER_OUT' }; diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index cc0ec9cafb6..70f2b40d074 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -49,6 +49,7 @@ import { addLabelToQuery } from './add_label_to_query'; import { transformBackendResult } from './backendResultTransformer'; import { DEFAULT_RESOLUTION } from './components/LokiOptionFields'; import LanguageProvider from './language_provider'; +import { escapeLabelValueInSelector } from './language_utils'; import { LiveStreams, LokiLiveTarget } from './live_streams'; import { addParsedLabelToQuery, getNormalizedLokiQuery, queryHasPipeParser } from './query_utils'; import { lokiResultsToTableModel, lokiStreamsToDataFrames, processRangeQueryResponse } from './result_transformer'; @@ -825,10 +826,6 @@ export class LokiDatasource expr = adhocFilters.reduce((acc: string, filter: { key?: any; operator?: any; value?: any }) => { const { key, operator } = filter; let { value } = filter; - if (operator === '=~' || operator === '!~') { - value = lokiRegularEscape(value); - } - return this.addLabelToQuery(acc, key, value, operator, true); }, expr); @@ -843,11 +840,13 @@ export class LokiDatasource // Override to make sure that we use label as actual label and not parsed label notParsedLabelOverride?: boolean ) { + let escapedValue = escapeLabelValueInSelector(value.toString(), operator); + if (queryHasPipeParser(queryExpr) && !isMetricsQuery(queryExpr) && !notParsedLabelOverride) { // If query has parser, we treat all labels as parsed and use | key="value" syntax - return addParsedLabelToQuery(queryExpr, key, value, operator); + return addParsedLabelToQuery(queryExpr, key, escapedValue, operator); } else { - return addLabelToQuery(queryExpr, key, value, operator, true); + return addLabelToQuery(queryExpr, key, escapedValue, operator, true); } } diff --git a/public/app/plugins/datasource/loki/language_utils.ts b/public/app/plugins/datasource/loki/language_utils.ts index c7c140022fa..65bd5cde569 100644 --- a/public/app/plugins/datasource/loki/language_utils.ts +++ b/public/app/plugins/datasource/loki/language_utils.ts @@ -17,3 +17,37 @@ export function shouldRefreshLabels(range?: TimeRange, prevRange?: TimeRange): b } return false; } + +// Loki regular-expressions use the RE2 syntax (https://github.com/google/re2/wiki/Syntax), +// so every character that matches something in that list has to be escaped. +// the list of meta characters is: *+?()|\.[]{}^$ +// we make a javascript regular expression that matches those characters: +const RE2_METACHARACTERS = /[*+?()|\\.\[\]{}^$]/g; +function escapeLokiRegexp(value: string): string { + return value.replace(RE2_METACHARACTERS, '\\$&'); +} + +// based on the openmetrics-documentation, the 3 symbols we have to handle are: +// - \n ... the newline character +// - \ ... the backslash character +// - " ... the double-quote character +export function escapeLabelValueInExactSelector(labelValue: string): string { + return labelValue.replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/"/g, '\\"'); +} + +export function escapeLabelValueInRegexSelector(labelValue: string): string { + return escapeLabelValueInExactSelector(escapeLokiRegexp(labelValue)); +} + +export function escapeLabelValueInSelector(labelValue: string, selector?: string): string { + return isRegexSelector(selector) + ? escapeLabelValueInRegexSelector(labelValue) + : escapeLabelValueInExactSelector(labelValue); +} + +export function isRegexSelector(selector?: string) { + if (selector && (selector.includes('=~') || selector.includes('!~'))) { + return true; + } + return false; +} diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx index 4ea05365853..13fff38993b 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx @@ -8,6 +8,7 @@ import { OperationsEditorRow } from 'app/plugins/datasource/prometheus/querybuil import { QueryBuilderLabelFilter } from 'app/plugins/datasource/prometheus/querybuilder/shared/types'; import { LokiDatasource } from '../../datasource'; +import { escapeLabelValueInSelector } from '../../language_utils'; import { lokiQueryModeller } from '../LokiQueryModeller'; import { LokiOperationId, LokiVisualQuery } from '../types'; @@ -49,15 +50,17 @@ export const LokiQueryBuilder = React.memo(({ datasource, query, nested, return []; } + let values; const labelsToConsider = query.labels.filter((x) => x !== forLabel); if (labelsToConsider.length === 0) { - return await datasource.languageProvider.fetchLabelValues(forLabel.label); + values = await datasource.languageProvider.fetchLabelValues(forLabel.label); + } else { + const expr = lokiQueryModeller.renderLabels(labelsToConsider); + const result = await datasource.languageProvider.fetchSeriesLabels(expr); + values = result[datasource.interpolateString(forLabel.label)]; } - const expr = lokiQueryModeller.renderLabels(labelsToConsider); - const result = await datasource.languageProvider.fetchSeriesLabels(expr); - const forLabelInterpolated = datasource.interpolateString(forLabel.label); - return result[forLabelInterpolated] ?? []; + return values ? values.map((v) => escapeLabelValueInSelector(v, forLabel.op)) : []; // Escape values in return }; const labelFilterError: string | undefined = useMemo(() => { From 082cfbdb06e87cd5605e7621e239dfd2b23f62f6 Mon Sep 17 00:00:00 2001 From: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> Date: Wed, 4 May 2022 08:22:11 -0400 Subject: [PATCH 024/440] Docs: Refactor alerting documentation (part 1) (#48664) * Initial commit * Moved files, ad fixed broken relrefs. * Fixed other broken relrefs * More changes. * Fixing broken relrefs * More changes. * Fixed last of the broken links * More re-org. * Added aliases and some weight adjustments * More aliases. * Fix fundamentals topic. * Fixed remaining metadata issues * Ran prettier --- docs/sources/administration/configuration.md | 2 +- .../set-up-for-high-availability.md | 6 +-- docs/sources/alerting/_index.md | 41 ++++++++++--------- .../{unified-alerting => }/alert-groups.md | 1 + docs/sources/alerting/alerting-limitations.md | 10 +++++ .../alerting-rules/_index.md | 2 +- .../alerting-rules/alert-annotation-label.md | 1 + .../create-grafana-managed-rule.md | 5 ++- ...reate-mimir-loki-managed-recording-rule.md | 2 + .../create-mimir-loki-managed-rule.md | 2 +- .../edit-mimir-loki-namespace-group.md | 1 + .../alerting-rules/rule-list.md | 1 + .../{unified-alerting => }/contact-points.md | 5 ++- .../difference-old-new.md | 3 +- .../fundamentals/_index.md | 4 +- .../fundamentals/alertmanager.md | 4 +- .../fundamentals/evaluate-grafana-alerts.md | 12 ++++-- .../fundamentals/state-and-health.md | 0 .../high-availability/_index.md | 3 +- .../high-availability/enable-alerting-ha.md | 0 .../message-templating/_index.md | 2 +- .../message-templating/template-data.md | 1 + .../message-templating/template-functions.md | 1 + .../opt-in.md => migrating-legacy-alerts.md} | 36 ++++------------ .../notifications/_index.md | 3 +- .../notifications/mute-timings.md | 3 +- docs/sources/alerting/opt-in.md | 41 +++++++++++++++++++ .../{unified-alerting => }/silences.md | 5 ++- .../alerting/unified-alerting/_index.md | 28 ------------- docs/sources/basics/timeseries-dimensions.md | 2 +- .../developers/plugins/backend/_index.md | 4 +- .../enterprise/access-control/about-rbac.md | 4 +- .../rbac-fixed-basic-role-definitions.md | 2 +- docs/sources/http_api/alerting.md | 4 +- .../alerting_notification_channels.md | 2 +- docs/sources/http_api/folder.md | 2 +- docs/sources/image-rendering/_index.md | 2 +- docs/sources/introduction/oss-details.md | 4 +- .../{alerting => }/old-alerting/_index.md | 1 + .../old-alerting/add-notification-template.md | 1 + .../old-alerting/create-alerts.md | 1 + .../old-alerting/notifications.md | 1 + .../old-alerting/pause-an-alert-rule.md | 1 + .../old-alerting/troubleshoot-alerts.md | 1 + .../old-alerting/view-alerts.md | 1 + .../about-expressions.md | 2 +- .../navigate-panel-editor.md | 2 +- docs/sources/whatsnew/whats-new-in-v7-4.md | 2 +- docs/sources/whatsnew/whats-new-in-v8-0.md | 4 +- 49 files changed, 148 insertions(+), 120 deletions(-) rename docs/sources/alerting/{unified-alerting => }/alert-groups.md (95%) create mode 100644 docs/sources/alerting/alerting-limitations.md rename docs/sources/alerting/{unified-alerting => }/alerting-rules/_index.md (91%) rename docs/sources/alerting/{unified-alerting => }/alerting-rules/alert-annotation-label.md (97%) rename docs/sources/alerting/{unified-alerting => }/alerting-rules/create-grafana-managed-rule.md (94%) rename docs/sources/alerting/{unified-alerting => }/alerting-rules/create-mimir-loki-managed-recording-rule.md (93%) rename docs/sources/alerting/{unified-alerting => }/alerting-rules/create-mimir-loki-managed-rule.md (96%) rename docs/sources/alerting/{unified-alerting => }/alerting-rules/edit-mimir-loki-namespace-group.md (91%) rename docs/sources/alerting/{unified-alerting => }/alerting-rules/rule-list.md (97%) rename docs/sources/alerting/{unified-alerting => }/contact-points.md (98%) rename docs/sources/alerting/{unified-alerting => }/difference-old-new.md (96%) rename docs/sources/alerting/{unified-alerting => }/fundamentals/_index.md (73%) rename docs/sources/alerting/{unified-alerting => }/fundamentals/alertmanager.md (88%) rename docs/sources/alerting/{unified-alerting => }/fundamentals/evaluate-grafana-alerts.md (89%) rename docs/sources/alerting/{unified-alerting => }/fundamentals/state-and-health.md (100%) rename docs/sources/alerting/{unified-alerting => }/high-availability/_index.md (97%) rename docs/sources/alerting/{unified-alerting => }/high-availability/enable-alerting-ha.md (100%) rename docs/sources/alerting/{unified-alerting => }/message-templating/_index.md (97%) rename docs/sources/alerting/{unified-alerting => }/message-templating/template-data.md (98%) rename docs/sources/alerting/{unified-alerting => }/message-templating/template-functions.md (98%) rename docs/sources/alerting/{unified-alerting/opt-in.md => migrating-legacy-alerts.md} (52%) rename docs/sources/alerting/{unified-alerting => }/notifications/_index.md (97%) rename docs/sources/alerting/{unified-alerting => }/notifications/mute-timings.md (94%) create mode 100644 docs/sources/alerting/opt-in.md rename docs/sources/alerting/{unified-alerting => }/silences.md (91%) delete mode 100644 docs/sources/alerting/unified-alerting/_index.md rename docs/sources/{alerting => }/old-alerting/_index.md (98%) rename docs/sources/{alerting => }/old-alerting/add-notification-template.md (99%) rename docs/sources/{alerting => }/old-alerting/create-alerts.md (99%) rename docs/sources/{alerting => }/old-alerting/notifications.md (99%) rename docs/sources/{alerting => }/old-alerting/pause-an-alert-rule.md (98%) rename docs/sources/{alerting => }/old-alerting/troubleshoot-alerts.md (99%) rename docs/sources/{alerting => }/old-alerting/view-alerts.md (98%) diff --git a/docs/sources/administration/configuration.md b/docs/sources/administration/configuration.md index c2466a03b4c..49beb2a70a4 100644 --- a/docs/sources/administration/configuration.md +++ b/docs/sources/administration/configuration.md @@ -1181,7 +1181,7 @@ Sets a global limit on number of alert rules that can be created. Default is -1 ## [unified_alerting] -For more information about the Grafana alerts, refer to [Unified Alerting]({{< relref "../alerting/unified-alerting/_index.md" >}}). +For more information about the Grafana alerts, refer to [About Grafana alerting]({{< relref "../alerting/_index.md" >}}). ### enabled diff --git a/docs/sources/administration/set-up-for-high-availability.md b/docs/sources/administration/set-up-for-high-availability.md index 9dd11efcc41..25232458a75 100644 --- a/docs/sources/administration/set-up-for-high-availability.md +++ b/docs/sources/administration/set-up-for-high-availability.md @@ -22,13 +22,13 @@ Grafana will now persist all long term data in the database. How to configure th ## Alerting high availability -Grafana alerting provides a new [highly-available model]({{< relref "../alerting/unified-alerting/high-availability/_index.md" >}}). It also preserves the semantics of legacy dashboard alerting by executing all alerts on every server and by sending notifications only once per alert. Load distribution between servers is not supported at this time. +Grafana alerting provides a new [highly-available model]({{< relref "../alerting/high-availability/_index.md" >}}). It also preserves the semantics of legacy dashboard alerting by executing all alerts on every server and by sending notifications only once per alert. Load distribution between servers is not supported at this time. -For instructions on setting up alerting high availability, see [enable alerting high availability]({{< relref "../alerting/unified-alerting/high-availability/enable-alerting-ha.md" >}}). +For instructions on setting up alerting high availability, see [enable alerting high availability](https://grafana.com/docs/grafana/next/alerting/old-alerting/notifications/). **Legacy dashboard alerts** -Legacy Grafana alerting supports a limited form of high availability. In this model, [alert notifications]({{< relref "../alerting/old-alerting/notifications.md" >}}) are deduplicated when running multiple servers. This means all alerts are executed on every server, but alert notifications are only sent once per alert. Grafana does not support load distribution between servers. +Legacy Grafana alerting supports a limited form of high availability. In this model, [alert notifications](https://grafana.com/docs/grafana/next/alerting/old-alerting/notifications/) are deduplicated when running multiple servers. This means all alerts are executed on every server, but alert notifications are only sent once per alert. Grafana does not support load distribution between servers. ## Grafana Live diff --git a/docs/sources/alerting/_index.md b/docs/sources/alerting/_index.md index 54c7bfb19c2..5a9a396a5ca 100644 --- a/docs/sources/alerting/_index.md +++ b/docs/sources/alerting/_index.md @@ -1,35 +1,36 @@ +++ title = "Alerts" -weight = 110 +weight = 455 +aliases = ["/docs/grafana/latest/alerting/"] +++ # Grafana alerts -Alerts allow you to learn about problems in your systems moments after they occur. Robust and actionable alerts help you identify and resolve issues quickly, minimizing disruption to your services. - -Grafana 8.0 introduced new and improved alerting that centralizes alerting information in a single, searchable view. It allows you to: +Grafana alerts allow you to learn about problems in your systems moments after they occur. Robust and actionable alerts help you identify and resolve issues quickly, minimizing disruption to your services. It centralizes alerting information in a single, searchable view that allows you to: - Create and manage Grafana alerts - Create and manage Grafana Mimir and Loki managed alerts - View alerting information from Prometheus and Alertmanager compatible data sources -Grafana alerting is enabled by default for new OSS installations. For older installations, it is still an [opt-in]({{< relref "./unified-alerting/opt-in.md" >}}) feature. +For new installations or existing installs without alerting configured, Grafana alerting is enabled by default. -| Release | Cloud | Enterprise | OSS | -| ------------------------ | ------------- | ------------- | -------------------------------- | -| Grafana 8.2 | On by default | Opt-in | Opt-in | -| Grafana 8.3 | On by default | Opt-in | On by default for new installs\* | -| Grafana 9.0 (unreleased) | On by default | On by default | On by default | +| Release | Cloud | Enterprise | OSS | +| ----------- | ------------- | ------------- | ------------- | +| Grafana 9.0 | On by default | On by default | On by default | -> **Note:** New installs include existing installs which do not have any alerts configured. +- For existing OSS installations with legacy dashboard alerting, you can [opt-in]({{< relref "./opt-in.md" >}}) to Grafana alerting. +- For Grafana Cloud instances using legacy cloud alerting, contact customer support to migrate to Grafana alerting. -Grafana alerting has four key components: +Before you begin, we recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "./fundamentals/_index.md" >}}) of Grafana alerting. Refer to [Fine-grained access control]({{< relref "../enterprise/access-control/_index.md" >}}) in Grafana Enterprise to learn more about controlling access to alerts using fine-grained permissions. -- Alerting rule - Evaluation criteria that determine whether an alert will fire. It consists of one or more queries and expressions, a condition, the frequency of evaluation, and optionally, the duration over which the condition is met. -- Contact point - Channel for sending notifications when the conditions of an alerting rule are met. -- Notification policy - Set of matching and grouping criteria used to determine where and how frequently to send notifications. -- Silences - Date and matching criteria used to silence notifications. - -To learn more, see [What's New with Grafana alerting]({{< relref "../alerting/unified-alerting/difference-old-new.md" >}}). - -For information on how to create and manage Grafana alerts and notifications, refer to [Overview of Grafana alerts]({{< relref "../alerting/unified-alerting/_index.md" >}}) and [Create and manage Grafana alerting rules]({{< relref "./unified-alerting/alerting-rules/_index.md" >}}). +- [What's new in Grafana alerting]({{< relref "./difference-old-new.md" >}}) +- [Enable Grafana alerting in OSS]({{< relref "./opt-in.md" >}}) +- [Migrating legacy alerts]({{< relref "./migrating-legacy-alerts.md" >}}) +- [Create Grafana managed alerting rules]({{< relref "alerting-rules/create-grafana-managed-rule.md" >}}) +- [Create Grafana Mimir or Loki managed alerting rules]({{< relref "alerting-rules/create-mimir-loki-managed-rule.md" >}}) +- [View existing alerting rules and manage their current state]({{< relref "alerting-rules/rule-list.md" >}}) +- [View the state and health of alerting rules]({{< relref "./fundamentals/state-and-health.md" >}}) +- [View alert groupings]({{< relref "./alert-groups.md" >}}) +- [Add or edit an alert contact point]({{< relref "./contact-points.md" >}}) +- [Add or edit notification policies]({{< relref "./notifications/_index.md" >}}) +- [Add or edit silences]({{< relref "./silences.md" >}}) diff --git a/docs/sources/alerting/unified-alerting/alert-groups.md b/docs/sources/alerting/alert-groups.md similarity index 95% rename from docs/sources/alerting/unified-alerting/alert-groups.md rename to docs/sources/alerting/alert-groups.md index 8160174c3d1..d20c064ed1b 100644 --- a/docs/sources/alerting/unified-alerting/alert-groups.md +++ b/docs/sources/alerting/alert-groups.md @@ -3,6 +3,7 @@ title = "Alert groups" description = "Alert groups" keywords = ["grafana", "alerting", "alerts", "groups"] weight = 400 +aliases = ["/docs/grafana/latest/alerting/unified-alerting/alert-groups/"] +++ # Alert groups diff --git a/docs/sources/alerting/alerting-limitations.md b/docs/sources/alerting/alerting-limitations.md new file mode 100644 index 00000000000..21571f01137 --- /dev/null +++ b/docs/sources/alerting/alerting-limitations.md @@ -0,0 +1,10 @@ ++++ +title = "Limitations" +weight = 552 +aliases = ["/docs/grafana/latest/alerting/alerting-limitations/"] ++++ + +# Limitations + +- Grafana alerting system can retrieve rules from all available Prometheus, Loki, and Alertmanager data sources. It might not be able to fetch alerting rules from all other supported data sources at this time. +- We aim to support the latest two minor versions of both Prometheus and Alertmanager. We cannot guarantee that older versions will work. As an example, if the current Prometheus version is `2.31.1`, we support >= `2.29.0`. diff --git a/docs/sources/alerting/unified-alerting/alerting-rules/_index.md b/docs/sources/alerting/alerting-rules/_index.md similarity index 91% rename from docs/sources/alerting/unified-alerting/alerting-rules/_index.md rename to docs/sources/alerting/alerting-rules/_index.md index 00dcf726d7b..f8906778823 100644 --- a/docs/sources/alerting/unified-alerting/alerting-rules/_index.md +++ b/docs/sources/alerting/alerting-rules/_index.md @@ -1,6 +1,6 @@ +++ title = "Create and manage rules" -aliases = ["/docs/grafana/latest/alerting/rules/"] +aliases = ["/docs/grafana/latest/alerting/rules/", "/docs/grafana/latest/alerting/unified-alerting/alerting-rules/"] weight = 130 +++ diff --git a/docs/sources/alerting/unified-alerting/alerting-rules/alert-annotation-label.md b/docs/sources/alerting/alerting-rules/alert-annotation-label.md similarity index 97% rename from docs/sources/alerting/unified-alerting/alerting-rules/alert-annotation-label.md rename to docs/sources/alerting/alerting-rules/alert-annotation-label.md index 0f421c3df99..19e72834f3d 100644 --- a/docs/sources/alerting/unified-alerting/alerting-rules/alert-annotation-label.md +++ b/docs/sources/alerting/alerting-rules/alert-annotation-label.md @@ -3,6 +3,7 @@ title = "Annotations and labels for alerting rules" description = "Annotations and labels for alerting" keywords = ["grafana", "alerting", "guide", "rules", "create"] weight = 401 +aliases = ["/docs/grafana/latest/alerting/unified-alerting/alerting-rules/alert-annotation-label/"] +++ # Annotations and labels for alerting rules diff --git a/docs/sources/alerting/unified-alerting/alerting-rules/create-grafana-managed-rule.md b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md similarity index 94% rename from docs/sources/alerting/unified-alerting/alerting-rules/create-grafana-managed-rule.md rename to docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md index 56fb61fc26e..7d9b21e1e5b 100644 --- a/docs/sources/alerting/unified-alerting/alerting-rules/create-grafana-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md @@ -3,6 +3,7 @@ title = "Create Grafana managed alert rule" description = "Create Grafana managed alert rule" keywords = ["grafana", "alerting", "guide", "rules", "create"] weight = 400 +aliases = ["/docs/grafana/latest/alerting/unified-alerting/alerting-rules/create-grafana-managed-rule/"] +++ # Create a Grafana managed alerting rule @@ -20,7 +21,7 @@ Grafana allows you to create alerting rules that query one or more data sources, 1. In Step 2, add queries and expressions to evaluate. - Keep the default name or hover over and click the edit icon to change the name. - For queries, select a data source from the drop-down. - - Add one or more [queries]({{< relref "../../../panels/query-a-data-source/add-a-query.md" >}}) or [expressions]({{< relref "../../../panels/query-a-data-source/use-expressions-to-manipulate-data/about-expressions.md" >}}). + - Add one or more [queries]({{< relref "../../panels/query-a-data-source/add-a-query.md" >}}) or [expressions]({{< relref "../../panels/query-a-data-source/use-expressions-to-manipulate-data/about-expressions.md" >}}). - For each expression, select either **Classic condition** to create a single alert rule, or choose from **Math**, **Reduce**, **Resample** options to generate separate alert for each series. For details on these options, see [Single and multi dimensional rule](#single-and-multi-dimensional-rule). - Click **Run queries** to verify that the query is successful. 1. In Step 3, add conditions. @@ -57,7 +58,7 @@ To generate a separate alert for each series, create a multi-dimensional rule. U #### Rule with classic condition -For more information, see [expressions documentation]({{< relref "../../../panels/query-a-data-source/use-expressions-to-manipulate-data/about-expressions.md" >}}). +For more information, see [expressions documentation]({{< relref "../../panels/query-a-data-source/use-expressions-to-manipulate-data/about-expressions.md" >}}). ### No data and error handling diff --git a/docs/sources/alerting/unified-alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md similarity index 93% rename from docs/sources/alerting/unified-alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md rename to docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md index d56fb1a41cb..d0ca1870555 100644 --- a/docs/sources/alerting/unified-alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md +++ b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md @@ -3,6 +3,8 @@ title = "Create Grafana Mimir or Loki managed recording rule" description = "Create Grafana Mimir or Loki managed recording rule" keywords = ["grafana", "alerting", "guide", "rules", "recording rules", "create"] weight = 400 +aliases = ["/docs/grafana/latest/alerting/unified-alerting/alerting-rules/create-mimir-loki-managed-recording-rule/", "/docs/grafana/latest/alerting/unified-alerting/alerting-rules/create-cortex-loki-managed-recording-rule/"] + +++ # Create a Grafana Mimir or Loki managed recording rule diff --git a/docs/sources/alerting/unified-alerting/alerting-rules/create-mimir-loki-managed-rule.md b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md similarity index 96% rename from docs/sources/alerting/unified-alerting/alerting-rules/create-mimir-loki-managed-rule.md rename to docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md index 416d4b8b3ea..2e1974e3fa2 100644 --- a/docs/sources/alerting/unified-alerting/alerting-rules/create-mimir-loki-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md @@ -2,7 +2,7 @@ title = "Create Grafana Mimir or Loki managed alert rule" description = "Create Grafana Mimir or Loki managed alerting rule" keywords = ["grafana", "alerting", "guide", "rules", "create"] -aliases = ["/docs/grafana/latest/alerting/unified-alerting/alerting-rules/create-cortex-loki-managed-recording-rule/"] +aliases = ["/docs/grafana/latest/alerting/unified-alerting/alerting-rules/create-cortex-loki-managed-recording-rule/", "/docs/grafana/latest/alerting/unified-alerting/alerting-rules/create-mimir-loki-managed-recording-rule/"] weight = 400 +++ diff --git a/docs/sources/alerting/unified-alerting/alerting-rules/edit-mimir-loki-namespace-group.md b/docs/sources/alerting/alerting-rules/edit-mimir-loki-namespace-group.md similarity index 91% rename from docs/sources/alerting/unified-alerting/alerting-rules/edit-mimir-loki-namespace-group.md rename to docs/sources/alerting/alerting-rules/edit-mimir-loki-namespace-group.md index 992d58f2d73..f4ce26ca0b8 100644 --- a/docs/sources/alerting/unified-alerting/alerting-rules/edit-mimir-loki-namespace-group.md +++ b/docs/sources/alerting/alerting-rules/edit-mimir-loki-namespace-group.md @@ -3,6 +3,7 @@ title = "Grafana Mimir or Loki rule groups and namespaces" description = "Edit Grafana Mimir or Loki rule groups and namespaces" keywords = ["grafana", "alerting", "guide", "group", "namespace", "grafana mimir", "loki"] weight = 405 +aliases = ["/docs/grafana/latest/alerting/unified-alerting/alerting-rules/edit-mimir-loki-namespace-group/", "/docs/grafana/latest/alerting/unified-alerting/alerting-rules/edit-cortex-loki-namespace-group/"] +++ # Grafana Mimir or Loki rule groups and namespaces diff --git a/docs/sources/alerting/unified-alerting/alerting-rules/rule-list.md b/docs/sources/alerting/alerting-rules/rule-list.md similarity index 97% rename from docs/sources/alerting/unified-alerting/alerting-rules/rule-list.md rename to docs/sources/alerting/alerting-rules/rule-list.md index 3a4213c8298..e18858600b3 100644 --- a/docs/sources/alerting/unified-alerting/alerting-rules/rule-list.md +++ b/docs/sources/alerting/alerting-rules/rule-list.md @@ -3,6 +3,7 @@ title = "Manage alerting rules" description = "Manage alerting rules" keywords = ["grafana", "alerting", "guide", "rules", "view"] weight = 402 +aliases = ["/docs/grafana/latest/alerting/unified-alerting/alerting-rules/rule-list/"] +++ # Manage alerting rules diff --git a/docs/sources/alerting/unified-alerting/contact-points.md b/docs/sources/alerting/contact-points.md similarity index 98% rename from docs/sources/alerting/unified-alerting/contact-points.md rename to docs/sources/alerting/contact-points.md index d1b53e98807..f955e8860ed 100644 --- a/docs/sources/alerting/unified-alerting/contact-points.md +++ b/docs/sources/alerting/contact-points.md @@ -3,13 +3,14 @@ title = "Contact points" description = "Create or edit contact point" keywords = ["grafana", "alerting", "guide", "contact point", "notification channel", "create"] weight = 430 +aliases = ["/docs/grafana/latest/alerting/unified-alerting/contact-points/"] +++ # Contact points Use contact points to define how your contacts are notified when an alert fires. A contact point can have one or more contact point types, for example, email, slack, webhook, and so on. When an alert fires, a notification is sent to all contact point types listed for a contact point. Optionally, use [message templates]({{< relref "./message-templating/_index.md" >}}) to customize notification messages for the contact point types. -You can configure Grafana managed contact points as well as contact points for an [external Alertmanager data source]({{< relref "../../datasources/alertmanager.md" >}}). For more information, see [Alertmanager]({{< relref "./fundamentals/alertmanager.md" >}}). +You can configure Grafana managed contact points as well as contact points for an [external Alertmanager data source]({{< relref "../datasources/alertmanager.md" >}}). For more information, see [Alertmanager]({{< relref "./fundamentals/alertmanager.md" >}}). ## Add a contact point @@ -60,7 +61,7 @@ To edit global configuration options for an external Alertmanager, like SMTP ser 1. Add global configuration settings. 1. Click **Save global config** to save your changes. -> **Note** This option is available only for external Alertmanagers. You can configure some global options for Grafana contact types, like email settings, via [Grafana configuration]({{< relref "../../administration/configuration.md" >}}). +> **Note** This option is available only for external Alertmanagers. You can configure some global options for Grafana contact types, like email settings, via [Grafana configuration]({{< relref "../administration/configuration.md" >}}). ## List of notifiers supported by Grafana diff --git a/docs/sources/alerting/unified-alerting/difference-old-new.md b/docs/sources/alerting/difference-old-new.md similarity index 96% rename from docs/sources/alerting/unified-alerting/difference-old-new.md rename to docs/sources/alerting/difference-old-new.md index 05b00763ff0..3018f2e5df0 100644 --- a/docs/sources/alerting/unified-alerting/difference-old-new.md +++ b/docs/sources/alerting/difference-old-new.md @@ -2,7 +2,8 @@ title = "What's new in Grafana alerting" description = "What's New with Grafana alerts" keywords = ["grafana", "alerting", "guide"] -weight = 114 +aliases = ["/docs/grafana/latest/alerting/unified-alerting/difference-old-new/"] +weight = 108 +++ # What's new in Grafana alerting diff --git a/docs/sources/alerting/unified-alerting/fundamentals/_index.md b/docs/sources/alerting/fundamentals/_index.md similarity index 73% rename from docs/sources/alerting/unified-alerting/fundamentals/_index.md rename to docs/sources/alerting/fundamentals/_index.md index 4b1cddeb243..f46be80b976 100644 --- a/docs/sources/alerting/unified-alerting/fundamentals/_index.md +++ b/docs/sources/alerting/fundamentals/_index.md @@ -1,7 +1,7 @@ +++ title = "Alerting fundamentals" -aliases = ["/docs/grafana/latest/alerting/metrics/"] -weight = 120 +weight = 110 +aliases = ["/docs/grafana/latest/alerting/metrics/", "/docs/grafana/latest/alerting/unified-alerting/fundamentals/"] +++ # Alerting fundamentals diff --git a/docs/sources/alerting/unified-alerting/fundamentals/alertmanager.md b/docs/sources/alerting/fundamentals/alertmanager.md similarity index 88% rename from docs/sources/alerting/unified-alerting/fundamentals/alertmanager.md rename to docs/sources/alerting/fundamentals/alertmanager.md index 1b8169c086c..8b7d1a21e3d 100644 --- a/docs/sources/alerting/unified-alerting/fundamentals/alertmanager.md +++ b/docs/sources/alerting/fundamentals/alertmanager.md @@ -1,6 +1,6 @@ +++ title = "Alertmanager" -aliases = ["/docs/grafana/latest/alerting/metrics/"] +aliases = ["/docs/grafana/latest/alerting/metrics/", "/docs/grafana/latest/alerting/unified-alerting/fundamentals/alertmanager/"] weight = 116 +++ @@ -12,7 +12,7 @@ Grafana includes built-in support for Prometheus Alertmanager. By default, notif > **Note:** Before v8.2, the configuration of the embedded Alertmanager was shared across organizations. If you are on an older Grafana version, we recommend that you use Grafana alerts only if you have one organization. Otherwise, your contact points are visible to all organizations. -Grafana alerting added support for external Alertmanager configuration. When you add an [Alertmanager data source]({{< relref "../../../datasources/alertmanager.md" >}}), the Alertmanager drop-down shows a list of available external Alertmanager data sources. Select a data source to create and manage alerting for standalone Grafana Mimir or Loki data sources. +Grafana alerting added support for external Alertmanager configuration. When you add an [Alertmanager data source]({{< relref "../../datasources/alertmanager.md" >}}), the Alertmanager drop-down shows a list of available external Alertmanager data sources. Select a data source to create and manage alerting for standalone Grafana Mimir or Loki data sources. {{< figure max-width="40%" src="/static/img/docs/alerting/unified/contact-points-select-am-8-0.gif" max-width="250px" caption="Select Alertmanager" >}} diff --git a/docs/sources/alerting/unified-alerting/fundamentals/evaluate-grafana-alerts.md b/docs/sources/alerting/fundamentals/evaluate-grafana-alerts.md similarity index 89% rename from docs/sources/alerting/unified-alerting/fundamentals/evaluate-grafana-alerts.md rename to docs/sources/alerting/fundamentals/evaluate-grafana-alerts.md index 8509319f4d5..772260ed567 100644 --- a/docs/sources/alerting/unified-alerting/fundamentals/evaluate-grafana-alerts.md +++ b/docs/sources/alerting/fundamentals/evaluate-grafana-alerts.md @@ -1,6 +1,6 @@ +++ title = "Alerting on numeric data" -aliases = ["/docs/grafana/latest/alerting/metrics/"] +aliases = ["/docs/grafana/latest/alerting/metrics/", "/docs/grafana/latest/alerting/unified-alerting/fundamentals/evaluate-grafana-alerts/"] weight = 116 +++ @@ -8,8 +8,12 @@ weight = 116 This topic describes how Grafana managed alerts are evaluated by the backend engine as well as how Grafana handles alerting on numeric rather than time series data. -- [Alert evaluation](#alert-evaluation) - [Alerting on numeric data](#alerting-on-numeric-data) + - [Alert evaluation](#alert-evaluation) + - [Metrics from the alerting engine](#metrics-from-the-alerting-engine) + - [Alerting on numeric data](#alerting-on-numeric-data-1) + - [Tabular Data](#tabular-data) + - [Example](#example) ## Alert evaluation @@ -17,11 +21,11 @@ Grafana managed alerts query the following backend data sources that have alerti - built-in data sources or those developed and maintained by Grafana: `Graphite`, `Prometheus`, `Loki`, `InfluxDB`, `Elasticsearch`, `Google Cloud Monitoring`, `Cloudwatch`, `Azure Monitor`, `MySQL`, `PostgreSQL`, `MSSQL`, `OpenTSDB`, `Oracle`, and `Azure Monitor` -- community developed backend data sources with alerting enabled (`backend` and `alerting` properties are set in the [plugin.json]({{< relref "../../../developers/plugins/metadata.md" >}})) +- community developed backend data sources with alerting enabled (`backend` and `alerting` properties are set in the [plugin.json]({{< relref "../../developers/plugins/metadata.md" >}})) ### Metrics from the alerting engine -The alerting engine publishes some internal metrics about itself. You can read more about how Grafana publishes [internal metrics]({{< relref "../../../administration/view-server/internal-metrics.md" >}}). See also, [View alert rules and their current state]({{< relref "../alerting-rules/rule-list.md" >}}). +The alerting engine publishes some internal metrics about itself. You can read more about how Grafana publishes [internal metrics]({{< relref "../../administration/view-server/internal-metrics.md" >}}). See also, [View alert rules and their current state]({{< relref "../alerting-rules/rule-list.md" >}}). | Metric Name | Type | Description | | ------------------------------------------------- | --------- | ---------------------------------------------------------------------------------------- | diff --git a/docs/sources/alerting/unified-alerting/fundamentals/state-and-health.md b/docs/sources/alerting/fundamentals/state-and-health.md similarity index 100% rename from docs/sources/alerting/unified-alerting/fundamentals/state-and-health.md rename to docs/sources/alerting/fundamentals/state-and-health.md diff --git a/docs/sources/alerting/unified-alerting/high-availability/_index.md b/docs/sources/alerting/high-availability/_index.md similarity index 97% rename from docs/sources/alerting/unified-alerting/high-availability/_index.md rename to docs/sources/alerting/high-availability/_index.md index fc1a07a33ea..c0f7a667c7f 100644 --- a/docs/sources/alerting/unified-alerting/high-availability/_index.md +++ b/docs/sources/alerting/high-availability/_index.md @@ -1,8 +1,9 @@ +++ -title = " About alerting high availability" +title = " Alerting high availability" description = "High availability" keywords = ["grafana", "alerting", "tutorials", "ha", "high availability"] aliases = ["/docs/grafana/latest/alerting/unified-alerting/high-availability/"] + weight = 450 +++ diff --git a/docs/sources/alerting/unified-alerting/high-availability/enable-alerting-ha.md b/docs/sources/alerting/high-availability/enable-alerting-ha.md similarity index 100% rename from docs/sources/alerting/unified-alerting/high-availability/enable-alerting-ha.md rename to docs/sources/alerting/high-availability/enable-alerting-ha.md diff --git a/docs/sources/alerting/unified-alerting/message-templating/_index.md b/docs/sources/alerting/message-templating/_index.md similarity index 97% rename from docs/sources/alerting/unified-alerting/message-templating/_index.md rename to docs/sources/alerting/message-templating/_index.md index 10a97c30480..92f3a4da3b1 100644 --- a/docs/sources/alerting/unified-alerting/message-templating/_index.md +++ b/docs/sources/alerting/message-templating/_index.md @@ -1,8 +1,8 @@ +++ title = "Message templating" description = "Message templating" -aliases = ["/docs/grafana/latest/alerting/message-templating/"] keywords = ["grafana", "alerting", "guide", "contact point", "templating"] +aliases = ["/docs/grafana/latest/alerting/message-templating/", "/docs/grafana/latest/alerting/unified-alerting/message-templating/"] weight = 440 +++ diff --git a/docs/sources/alerting/unified-alerting/message-templating/template-data.md b/docs/sources/alerting/message-templating/template-data.md similarity index 98% rename from docs/sources/alerting/unified-alerting/message-templating/template-data.md rename to docs/sources/alerting/message-templating/template-data.md index 22423f59090..08cd2ed9517 100644 --- a/docs/sources/alerting/unified-alerting/message-templating/template-data.md +++ b/docs/sources/alerting/message-templating/template-data.md @@ -1,6 +1,7 @@ +++ title = "Template data" keywords = ["grafana", "alerting", "guide", "contact point", "templating"] +aliases = ["/docs/grafana/latest/alerting/unified-alerting/message-templating/template-data/"] +++ # Template data diff --git a/docs/sources/alerting/unified-alerting/message-templating/template-functions.md b/docs/sources/alerting/message-templating/template-functions.md similarity index 98% rename from docs/sources/alerting/unified-alerting/message-templating/template-functions.md rename to docs/sources/alerting/message-templating/template-functions.md index b018418b5fd..1b8d773d8a7 100644 --- a/docs/sources/alerting/unified-alerting/message-templating/template-functions.md +++ b/docs/sources/alerting/message-templating/template-functions.md @@ -1,6 +1,7 @@ +++ title = "Template functions" keywords = ["grafana", "alerting", "guide", "contact point", "templating"] +aliases = ["/docs/grafana/latest/alerting/unified-alerting/message-templating/template-functions/"] +++ # Template Functions diff --git a/docs/sources/alerting/unified-alerting/opt-in.md b/docs/sources/alerting/migrating-legacy-alerts.md similarity index 52% rename from docs/sources/alerting/unified-alerting/opt-in.md rename to docs/sources/alerting/migrating-legacy-alerts.md index 1618d8cdeb6..b33e8c58d77 100644 --- a/docs/sources/alerting/unified-alerting/opt-in.md +++ b/docs/sources/alerting/migrating-legacy-alerts.md @@ -1,35 +1,13 @@ +++ -title = "Opt-in to Grafana alerting" +title = "Migrating legacy alerts" description = "Enable Grafana alerts" -weight = 115 +weight = 114 +aliases = ["/docs/grafana/latest/alerting/unified-alerting/opt-in/"] +++ -# Opt-in to Grafana alerting +# Migrating legacy alerts to Grafana alerting -Grafana alerting is enabled by default for new Cloud and OSS installations. - -**Note:** If you are an existing Grafana Cloud user and want to explore unified alerting, contact Grafana Support. They will enable unified alerting for your Cloud stack. - -For older OSS installations that use legacy dashboard alerts, unified alerting is still an opt-in feature. This topic describes how to opt-in to Grafana alerting if you have an existing Grafana installation and the rules and restrictions that govern the migration of existing dashboard alerts to the new alerting system. You can [disable Grafana alerts]({{< relref "./opt-in.md#disable-grafana-alerts" >}}) and use the legacy dashboard alerting if needed. - -Before you begin, we recommend that you backup Grafana's database. If you are using PostgreSQL as the backend database, then the minimum required version is 9.5. - -## Enable Grafana alerting - -To enable Grafana alerts: - -1. In your custom configuration file ($WORKING_DIR/conf/custom.ini), go to the [unified alerts]({{< relref "../../administration/configuration.md#unified_alerting" >}}) section. -1. Set the `enabled` property to `true`. -1. Next, for [legacy dashboard alerting]({{< relref "../../administration/configuration.md#alerting" >}}), set the `enabled` flag to `false`. -1. Restart Grafana for the configuration changes to take effect. - -> **Note:** The `ngalert` toggle previously used to enable or disable Grafana alerting is no longer available. - -Before v8.2, notification logs and silences were stored on a disk. If you did not use persistent disks, you would have lost any configured silences and logs on a restart, resulting in unwanted or duplicate notifications. We no longer require the use of a persistent disk. Instead, the notification logs and silences are stored regularly (every 15 minutes). If you used the file-based approach, Grafana reads the existing file and persists it eventually. - -## Migrating legacy alerts to Grafana alerting system - -When Grafana alerting is enabled or Grafana is upgraded to version 8.3, existing legacy dashboard alerts migrate in a format compatible with the Grafana alerting. In the Alerting page of your Grafana instance, you can view the migrated alerts alongside new alerts. +When Grafana alerting is enabled or Grafana is upgraded to the latest version, existing legacy dashboard alerts migrate in a format compatible with the Grafana alerting. In the Alerting page of your Grafana instance, you can view the migrated alerts alongside new alerts. Read and write access to legacy dashboard alerts and Grafana alerts are governed by the permissions of the folders storing them. During migration, legacy dashboard alert permissions are matched to the new rules permissions as follows: @@ -52,9 +30,9 @@ Grafana alerting system can retrieve rules from all available Prometheus, Loki, To disable Grafana alerts and enable legacy dashboard alerts: -1. In your custom configuration file ($WORKING_DIR/conf/custom.ini), go to the [Grafana alerting]({{< relref "../../administration/configuration.md#unified_alerting" >}}) section. +1. In your custom configuration file ($WORKING_DIR/conf/custom.ini), go to the [Grafana alerting]({{< relref "../administration/configuration.md#unified_alerting" >}}) section. 1. Set the `enabled` property to `false`. -1. For [legacy dashboard alerting]({{< relref "../../administration/configuration.md#alerting" >}}), set the `enabled` flag to `true`. +1. For [legacy dashboard alerting]({{< relref "../administration/configuration.md#alerting" >}}), set the `enabled` flag to `true`. 1. Restart Grafana for the configuration changes to take effect. > **Note:** Switching from one flavor of alerting to another can result in data loss. This is applicable to the fresh installation as well as upgraded setups. diff --git a/docs/sources/alerting/unified-alerting/notifications/_index.md b/docs/sources/alerting/notifications/_index.md similarity index 97% rename from docs/sources/alerting/unified-alerting/notifications/_index.md rename to docs/sources/alerting/notifications/_index.md index 3a263fb8143..90ad217b90f 100644 --- a/docs/sources/alerting/unified-alerting/notifications/_index.md +++ b/docs/sources/alerting/notifications/_index.md @@ -3,13 +3,14 @@ title = "Notification policies" description = "Notification policies" keywords = ["grafana", "alerting", "guide", "notification policies", "routes"] weight = 450 +aliases = ["/docs/grafana/latest/alerting/unified-alerting/notifications/"] +++ # Notification policies Notification policies determine how alerts are routed to contact points. Policies have a tree structure, where each policy can have one or more child policies. Each policy, except for the root policy, can also match specific alert labels. Each alert is evaluated by the root policy and subsequently by each child policy. If you enable the `Continue matching subsequent sibling nodes` option is enabled for a specific policy, then evaluation continues even after one or more matches. A parent policy’s configuration settings and contact point information govern the behavior of an alert that does not match any of the child policies. A root policy governs any alert that does not match a specific policy. -You can configure Grafana managed notification policies as well as notification policies for an [external Alertmanager data source]({{< relref "../../../datasources/alertmanager.md" >}}). For more information, see [Alertmanager]({{< relref "../fundamentals/alertmanager.md" >}}). +You can configure Grafana managed notification policies as well as notification policies for an [external Alertmanager data source]({{< relref "../../datasources/alertmanager.md" >}}). For more information, see [Alertmanager]({{< relref "../fundamentals/alertmanager.md" >}}). ## Grouping diff --git a/docs/sources/alerting/unified-alerting/notifications/mute-timings.md b/docs/sources/alerting/notifications/mute-timings.md similarity index 94% rename from docs/sources/alerting/unified-alerting/notifications/mute-timings.md rename to docs/sources/alerting/notifications/mute-timings.md index 411f9fdb479..73f4a5e9581 100644 --- a/docs/sources/alerting/unified-alerting/notifications/mute-timings.md +++ b/docs/sources/alerting/notifications/mute-timings.md @@ -3,6 +3,7 @@ title = "Mute timings" description = "Mute timings" keywords = ["grafana", "alerting", "guide", "mute", "mute timings", "mute time interval"] weight = 450 +aliases = ["/docs/grafana/latest/alerting/unified-alerting/notifications/mute-timings/"] +++ # Mute timings @@ -11,7 +12,7 @@ A mute timing is a recurring interval of time when no new notifications for a po Similar to silences, mute timings do not prevent alert rules from being evaluated, nor do they stop alert instances from being shown in the user interface. They only prevent notifications from being created. -You can configure Grafana managed mute timings as well as mute timings for an [external Alertmanager data source]({{< relref "../../../datasources/alertmanager.md" >}}). For more information, see [Alertmanager]({{< relref "../fundamentals/alertmanager.md" >}}). +You can configure Grafana managed mute timings as well as mute timings for an [external Alertmanager data source]({{< relref "../../datasources/alertmanager.md" >}}). For more information, see [Alertmanager documentation]({{< relref "../fundamentals/alertmanager.md" >}}). ## Mute timings vs silences diff --git a/docs/sources/alerting/opt-in.md b/docs/sources/alerting/opt-in.md new file mode 100644 index 00000000000..e7484552a1d --- /dev/null +++ b/docs/sources/alerting/opt-in.md @@ -0,0 +1,41 @@ ++++ +title = "Opt-in to Grafana alerting" +description = "Enable Grafana alerts" +weight = 113 +aliases = ["/docs/grafana/latest/alerting/unified-alerting/opt-in/"] ++++ + +# Opt-in to Grafana alerting in OSS + +Grafana alerting is enabled by default for new Cloud and OSS installations. + +- For existing OSS installations that use legacy dashboard alerts, unified alerting is still an opt-in feature. +- For existing Grafana Cloud users, contact customer support to enable Grafana alerting for your Cloud stack. + +## Before you begin + +We recommend that you backup Grafana's database. If you are using PostgreSQL as the backend database, then the minimum required version is 9.5. + +## Enable Grafana alerting + +To enable Grafana alerts: + +1. In your custom configuration file ($WORKING_DIR/conf/custom.ini), go to the [unified alerts]({{< relref "../administration/configuration.md#unified_alerting" >}}) section. +2. Set the `enabled` property to `true`. +3. Next, for [legacy dashboard alerting]({{< relref "../administration/configuration.md#alerting" >}}), set the `enabled` flag to `false`. +4. Restart Grafana for the configuration changes to take effect. + +> **Note:** The `ngalert` toggle previously used to enable or disable Grafana alerting is no longer available. + +Before v8.2, notification logs and silences were stored on a disk. If you did not use persistent disks, you would have lost any configured silences and logs on a restart, resulting in unwanted or duplicate notifications. We no longer require the use of a persistent disk. Instead, the notification logs and silences are stored regularly (every 15 minutes). If you used the file-based approach, Grafana reads the existing file and persists it eventually. + +## Disable Grafana alerts + +To disable Grafana alerts and roll back to legacy dashboard alerting: + +1. In your custom configuration file ($WORKING_DIR/conf/custom.ini), go to the [Grafana alerting]({{< relref "../administration/configuration.md#unified_alerting" >}}) section. +1. Set the `enabled` property to `false`. +1. For [legacy dashboard alerting]({{< relref "../administration/configuration.md#alerting" >}}), set the `enabled` flag to `true`. +1. Restart Grafana for the configuration changes to take effect. + +> **Note:** Switching from one flavor of alerting to another can result in data loss. This is applicable to the fresh installation as well as upgraded setups. diff --git a/docs/sources/alerting/unified-alerting/silences.md b/docs/sources/alerting/silences.md similarity index 91% rename from docs/sources/alerting/unified-alerting/silences.md rename to docs/sources/alerting/silences.md index 6804cd6fddc..fc090985204 100644 --- a/docs/sources/alerting/unified-alerting/silences.md +++ b/docs/sources/alerting/silences.md @@ -3,6 +3,7 @@ title = "Silences" description = "Silences alert notifications" keywords = ["grafana", "alerting", "silence", "mute"] weight = 400 +aliases = ["/docs/grafana/latest/alerting/unified-alerting/silences/"] +++ # Silences @@ -11,7 +12,7 @@ Use silences to stop notifications from one or more alerting rules. Silences do Silences do not prevent alert rules from being evaluated. They also do not stop alert instances being shown in the user interface. Silences only prevent notifications from being created. -You can configure Grafana managed silences as well as silences for an [external Alertmanager data source]({{< relref "../../datasources/alertmanager.md" >}}). For more information, see [Alertmanager]({{< relref "./fundamentals/alertmanager.md" >}}). +You can configure Grafana managed silences as well as silences for an [external Alertmanager data source]({{< relref "../datasources/alertmanager.md" >}}). For more information, see [Alertmanager]({{< relref "./fundamentals/alertmanager.md" >}}). > **Note:** Before Grafana v8.2, the configuration of the embedded Alertmanager was shared across organisations. Users of Grafana 8.0 and 8.1 are advised to use the new Grafana 8 Alerts only if they have one organisation. Otherwise, silences for the Grafana managed alerts will be visible by all organizations. @@ -61,4 +62,4 @@ When linking to a silence form, provide the default matching labels and comment For example, to link to silence form with matching labels `severity=critical` & `cluster!~europe-.*` and comment `Silence critical EU alerts`, create a URL `https://mygrafana/alerting/silence/new?matchers=severity%3Dcritical%2Ccluster!~europe-*&comment=Silence%20critical%20EU%20alert`. -To link to a new silence page for an [external Alertmanager]({{< relref "../../datasources/alertmanager.md" >}}), add a `alertmanager` query parameter with the Alertmanager data source name. +To link to a new silence page for an [external Alertmanager]({{< relref "../datasources/alertmanager.md" >}}), add a `alertmanager` query parameter with the Alertmanager data source name. diff --git a/docs/sources/alerting/unified-alerting/_index.md b/docs/sources/alerting/unified-alerting/_index.md deleted file mode 100644 index 0dc5778ef3d..00000000000 --- a/docs/sources/alerting/unified-alerting/_index.md +++ /dev/null @@ -1,28 +0,0 @@ -+++ -title = "Grafana alerts" -aliases = ["/docs/grafana/latest/alerting/metrics/"] -weight = 113 -+++ - -# Overview of Grafana alerting - -Grafana 8.0 has new and improved alerting that centralizes alerting information in a single, searchable view. It is enabled by default for all new OSS instances, and is an [opt-in]({{< relref "./opt-in.md" >}}) feature for older installations that still use legacy dashboard alerting. We encourage you to create issues in the Grafana GitHub repository for bugs found while testing Grafana alerting. See also, [What's New with Grafana alerting]({{< relref "./difference-old-new.md" >}}). - -> Refer to [Fine-grained access control]({{< relref "../../enterprise/access-control/_index.md" >}}) in Grafana Enterprise to learn more about controlling access to alerts using fine-grained permissions. - -When Grafana alerting is enabled, you can: - -- [Create Grafana managed alerting rules]({{< relref "alerting-rules/create-grafana-managed-rule.md" >}}) -- [Create Grafana Mimir or Loki managed alerting rules]({{< relref "alerting-rules/create-mimir-loki-managed-rule.md" >}}) -- [View existing alerting rules and manage their current state]({{< relref "alerting-rules/rule-list.md" >}}) -- [View the state and health of alerting rules]({{< relref "./fundamentals/state-and-health.md" >}}) -- [Add or edit an alert contact point]({{< relref "./contact-points.md" >}}) -- [Add or edit notification policies]({{< relref "./notifications/_index.md" >}}) -- [Add or edit silences]({{< relref "./silences.md" >}}) - -Before you begin using Grafana alerting, we recommend that you familiarize yourself with some [basic concepts]({{< relref "./fundamentals/_index.md" >}}) of Grafana alerting. - -## Limitations - -- The Grafana alerting system can retrieve rules from all available Prometheus, Loki, and Alertmanager data sources. It might not be able to fetch rules from other supported data sources. -- We aim to support the latest two minor versions of both Prometheus and Alertmanager. We cannot guarantee that older versions will work. As an example, if the current Prometheus version is `2.31.1`, we support >= `2.29.0`. diff --git a/docs/sources/basics/timeseries-dimensions.md b/docs/sources/basics/timeseries-dimensions.md index 1194915d04c..c1422122391 100644 --- a/docs/sources/basics/timeseries-dimensions.md +++ b/docs/sources/basics/timeseries-dimensions.md @@ -76,7 +76,7 @@ In this case the labels that represent the dimensions will have two keys based o > **Note:** More than one dimension is currently only supported in the Logs queries within the Azure Monitor service as of version 7.1. -> **Note:** Multiple dimensions are not supported in a way that maps to multiple alerts in Grafana, but rather they are treated as multiple conditions to a single alert. See the documentation on [creating alerts with multiple series]({{< relref "../alerting/old-alerting/create-alerts.md#multiple-series" >}}). +> **Note:** Multiple dimensions are not supported in a way that maps to multiple alerts in Grafana, but rather they are treated as multiple conditions to a single alert. For more information, see See the documentation on [creating alerts with multiple series]({{< relref "../alerting/alerting-rules/create-grafana-managed-rule.md#single-and-multi-dimensional-rule" >}}). ### Multiple values diff --git a/docs/sources/developers/plugins/backend/_index.md b/docs/sources/developers/plugins/backend/_index.md index 392f0047371..ee0bbed1e14 100644 --- a/docs/sources/developers/plugins/backend/_index.md +++ b/docs/sources/developers/plugins/backend/_index.md @@ -12,13 +12,13 @@ However, one limitation with these plugins are that they execute on the client-s We use the term _backend plugin_ to denote that a plugin has a backend component. Still, normally a backend plugin requires frontend components as well. This is for example true for backend data source plugins which normally need configuration and query editor components implemented for the frontend. -Data source plugins can be extended with a backend component. In the future we plan to support additional types and possibly new kinds of plugins, such as [notifiers for Grafana Alerting]({{< relref "../../../alerting/old-alerting/notifications.md" >}}) and custom authentication to name a few. +Data source plugins can be extended with a backend component. In the future we plan to support additional types and possibly new kinds of plugins, such as [notifiers for Grafana alerting]({{< relref "../../../alerting/notifications/_index.md" >}}) and custom authentication to name a few. ## Use cases for implementing a backend plugin The following examples gives you an idea of why you'd consider implementing a backend plugin: -- Enable [Grafana Alerting]({{< relref "../../../alerting" >}}) for data sources. +- Enable [Grafana alerting]({{< relref "../../../alerting" >}}) for data sources. - Connect to non-HTTP services that normally can't be connected to from a web browser, e.g. SQL database servers. - Keep state between users, e.g. query caching for data sources. - Use custom authentication methods and/or authorization checks that aren't supported in Grafana. diff --git a/docs/sources/enterprise/access-control/about-rbac.md b/docs/sources/enterprise/access-control/about-rbac.md index 46303b978b8..fec7532ee1a 100644 --- a/docs/sources/enterprise/access-control/about-rbac.md +++ b/docs/sources/enterprise/access-control/about-rbac.md @@ -55,13 +55,13 @@ Grafana Enterprise includes the ability for you to assign discrete fixed roles t Assign fixed roles when the basic roles do not meet your permission requirements. For example, you might want a user with the basic viewer role to also edit dashboards. Or, you might want anyone with the editor role to also add and manage users. Fixed roles provide users more granular access to create, view, and update the following Grafana resources: -- [Alerting]({{< relref "../../alerting/unified-alerting/_index.md">}}) +- [Alerting]({{< relref "../../alerting/_index.md">}}) - [Annotations]({{< relref "../../dashboards/annotations.md" >}}) - [API keys]({{< relref "../../administration/api-keys/_index.md" >}}) - [Dashboards and folders]({{< relref "../../dashboards/_index.md" >}}) - [Data sources]({{< relref "../../datasources/_index.md" >}}) - [Explore]({{< relref "../../explore/_index.md" >}}) -- [Folders]({{< relref "../../dashboards/dashboard_folders.md" >}}) +- [Folders]({{< relref "../../dashboards/dashboard-folders.md" >}}) - [LDAP]({{< relref "../../auth/ldap/_index.md" >}}) - [Licenses]({{< relref "../license/_index.md" >}}) - [Organizations]({{< relref "../../administration/manage-organizations/_index.md" >}}) diff --git a/docs/sources/enterprise/access-control/rbac-fixed-basic-role-definitions.md b/docs/sources/enterprise/access-control/rbac-fixed-basic-role-definitions.md index 3ff07b56894..4e16ec04569 100644 --- a/docs/sources/enterprise/access-control/rbac-fixed-basic-role-definitions.md +++ b/docs/sources/enterprise/access-control/rbac-fixed-basic-role-definitions.md @@ -76,7 +76,7 @@ The following tables list permissions associated with basic and fixed roles. ### Alerting roles -If you [enable]({{< relref "../../alerting/unified-alerting/opt-in.md" >}}) Grafana Alerting, you can use predefined roles to manage user access to alert rules, alert instances, and alert notification settings and create custom roles to limit user access to alert rules in a folder. +If alerting is [enabled]({{< relref "../../alerting/opt-in.md" >}}), you can use predefined roles to manage user access to alert rules, alert instances, and alert notification settings and create custom roles to limit user access to alert rules in a folder. Access to Grafana alert rules is an intersection of many permissions: diff --git a/docs/sources/http_api/alerting.md b/docs/sources/http_api/alerting.md index 7cf58c95638..1ed7ae36663 100644 --- a/docs/sources/http_api/alerting.md +++ b/docs/sources/http_api/alerting.md @@ -7,9 +7,9 @@ aliases = ["/docs/grafana/latest/http_api/alerting/"] # Alerting API -> **Note:** This topic is relevant for the [legacy dashboard alerts]({{< relref "../alerting/old-alerting/_index.md" >}}) only. +> **Note:** This topic is relevant for the [legacy dashboard alerts](https://grafana.com/docs/grafana/latest/alerting/old-alerting/) only. -You can find Grafana 8 alerts API specification details [here](https://editor.swagger.io/?url=https://raw.githubusercontent.com/grafana/grafana/main/pkg/services/ngalert/api/tooling/post.json). Also, refer to [Grafana 8 alerts documentation]({{< relref "../alerting/unified-alerting/_index.md" >}}) for details on how to create and manage new alerts. +You can find Grafana alerting API specification details [here](https://editor.swagger.io/?url=https://raw.githubusercontent.com/grafana/grafana/main/pkg/services/ngalert/api/tooling/post.json). Also, refer to [Grafana alerting alerts documentation]({{< relref "../alerting/_index.md" >}}) for details on how to create and manage new alerts. You can use the Alerting API to get information about legacy dashboard alerts and their states but this API cannot be used to modify the alert. To create new alerts or modify them you need to update the dashboard JSON that contains the alerts. diff --git a/docs/sources/http_api/alerting_notification_channels.md b/docs/sources/http_api/alerting_notification_channels.md index 77edc08808c..380c60292cd 100644 --- a/docs/sources/http_api/alerting_notification_channels.md +++ b/docs/sources/http_api/alerting_notification_channels.md @@ -176,7 +176,7 @@ Content-Type: application/json ## Create notification channel -You can find the full list of [supported notifiers]({{< relref "../alerting/old-alerting/notifications/#list-of-supported-notifiers" >}}) on the alert notifiers page. +You can find the full list of [supported notifiers](https://grafana.com/docs/grafana/latest/alerting/old-alerting/notifications/) on the alert notifiers page. `POST /api/alert-notifications` diff --git a/docs/sources/http_api/folder.md b/docs/sources/http_api/folder.md index f6b5411030c..fc3071f1f9e 100644 --- a/docs/sources/http_api/folder.md +++ b/docs/sources/http_api/folder.md @@ -239,7 +239,7 @@ Content-Length: 97 Deletes an existing folder identified by UID along with all dashboards (and their alerts) stored in the folder. This operation cannot be reverted. -If [Grafana 8 Alerts]({{< relref "../alerting/unified-alerting/_index.md" >}}) are enabled, you can set an optional query parameter `forceDeleteRules=false` so that requests will fail with 400 (Bad Request) error if the folder contains any Grafana 8 Alerts. However, if this parameter is set to `true` then it will delete any Grafana 8 Alerts under this folder. +If [Grafana alerting]({{< relref "../alerting/_index.md" >}}) is enabled, you can set an optional query parameter `forceDeleteRules=false` so that requests will fail with 400 (Bad Request) error if the folder contains any Grafana alerts. However, if this parameter is set to `true` then it will delete any Grafana alerts under this folder. **Example Request**: diff --git a/docs/sources/image-rendering/_index.md b/docs/sources/image-rendering/_index.md index 93efc8bccde..43bf48a095c 100644 --- a/docs/sources/image-rendering/_index.md +++ b/docs/sources/image-rendering/_index.md @@ -8,7 +8,7 @@ weight = 55 # Image rendering -Grafana supports automatic rendering of panels as PNG images. This allows Grafana to automatically generate images of your panels to include in [alert notifications]({{< relref "../alerting/old-alerting/notifications.md" >}}), [PDF export]({{< relref "../enterprise/export-pdf.md" >}}), and [Reporting]({{< relref "../enterprise/reporting.md" >}}). PDF Export and Reporting are available only in [Grafana Enterprise]({{< relref "../enterprise/" >}}). +Grafana supports automatic rendering of panels as PNG images. This allows Grafana to automatically generate images of your panels to include in [alert notifications]({{< relref "../alerting/notifications/_index.md" >}}), [PDF export]({{< relref "../enterprise/export-pdf.md" >}}), and [Reporting]({{< relref "../enterprise/reporting.md" >}}). PDF Export and Reporting are available only in [Grafana Enterprise]({{< relref "../enterprise/" >}}). > **Note:** Image rendering of dashboards is not supported at this time. diff --git a/docs/sources/introduction/oss-details.md b/docs/sources/introduction/oss-details.md index fcea8cb3bc1..6e24d132225 100644 --- a/docs/sources/introduction/oss-details.md +++ b/docs/sources/introduction/oss-details.md @@ -18,9 +18,9 @@ Explore your data through ad-hoc queries and dynamic drilldown. Split view and c ## Alerts -If you're using Grafana alerting, then you can have alerts sent through a number of different [alert notifiers]({{< relref "../alerting/old-alerting/notifications.md" >}}), including PagerDuty, SMS, email, VictorOps, OpsGenie, or Slack. +If you're using Grafana alerting, then you can have alerts sent through a number of different [alert notifiers]({{< relref "../alerting/contact-points.md#list-of-notifiers-supported-by-grafana" >}}), including PagerDuty, SMS, email, VictorOps, OpsGenie, or Slack. -Alert hooks allow you to create different notifiers with a bit of code if you prefer some other channels of communication. Visually define [alert rules]({{< relref "../alerting/_index.md" >}}) for your most important metrics. +Alert hooks allow you to create different notifiers with a bit of code if you prefer some other channels of communication. Visually define [alert rules]({{< relref "../alerting/alerting-rules/_index.md" >}}) for your most important metrics. ## Annotations diff --git a/docs/sources/alerting/old-alerting/_index.md b/docs/sources/old-alerting/_index.md similarity index 98% rename from docs/sources/alerting/old-alerting/_index.md rename to docs/sources/old-alerting/_index.md index 8d0cd673290..9a73cc89e57 100644 --- a/docs/sources/alerting/old-alerting/_index.md +++ b/docs/sources/old-alerting/_index.md @@ -1,6 +1,7 @@ +++ title = "Legacy Grafana Alerts" weight = 114 +draft = true +++ # Legacy Grafana alerts diff --git a/docs/sources/alerting/old-alerting/add-notification-template.md b/docs/sources/old-alerting/add-notification-template.md similarity index 99% rename from docs/sources/alerting/old-alerting/add-notification-template.md rename to docs/sources/old-alerting/add-notification-template.md index bafab9a4f9e..79d74e08fbf 100644 --- a/docs/sources/alerting/old-alerting/add-notification-template.md +++ b/docs/sources/old-alerting/add-notification-template.md @@ -3,6 +3,7 @@ title = "Alert notification templating" keywords = ["grafana", "documentation", "alerting", "alerts", "notification", "templating"] weight = 110 aliases = ["/docs/grafana/latest/alerting/add-notification-template/"] +draft = true +++ # Alert notification templating diff --git a/docs/sources/alerting/old-alerting/create-alerts.md b/docs/sources/old-alerting/create-alerts.md similarity index 99% rename from docs/sources/alerting/old-alerting/create-alerts.md rename to docs/sources/old-alerting/create-alerts.md index 49e9cfa4424..472ce16526c 100644 --- a/docs/sources/alerting/old-alerting/create-alerts.md +++ b/docs/sources/old-alerting/create-alerts.md @@ -4,6 +4,7 @@ description = "Configure alert rules" keywords = ["grafana", "alerting", "guide", "rules"] weight = 200 aliases = ["/docs/grafana/latest/alerting/create-alerts/"] +draft = true +++ # Create alerts diff --git a/docs/sources/alerting/old-alerting/notifications.md b/docs/sources/old-alerting/notifications.md similarity index 99% rename from docs/sources/alerting/old-alerting/notifications.md rename to docs/sources/old-alerting/notifications.md index 9bcf5bf025e..c4e95ee79ce 100644 --- a/docs/sources/alerting/old-alerting/notifications.md +++ b/docs/sources/old-alerting/notifications.md @@ -4,6 +4,7 @@ description = "Alerting notifications guide" keywords = ["Grafana", "alerting", "guide", "notifications"] weight = 100 aliases = ["/docs/grafana/latest/alerting/notifications/"] +draft = true +++ # Alert notifications diff --git a/docs/sources/alerting/old-alerting/pause-an-alert-rule.md b/docs/sources/old-alerting/pause-an-alert-rule.md similarity index 98% rename from docs/sources/alerting/old-alerting/pause-an-alert-rule.md rename to docs/sources/old-alerting/pause-an-alert-rule.md index b55c02bbab9..29e171606c2 100644 --- a/docs/sources/alerting/old-alerting/pause-an-alert-rule.md +++ b/docs/sources/old-alerting/pause-an-alert-rule.md @@ -4,6 +4,7 @@ description = "Pause an existing alert rule" keywords = ["grafana", "alerting", "guide", "rules", "view"] weight = 400 aliases = ["/docs/grafana/latest/alerting/pause-an-alert-rule/"] +draft = true +++ # Pause an alert rule diff --git a/docs/sources/alerting/old-alerting/troubleshoot-alerts.md b/docs/sources/old-alerting/troubleshoot-alerts.md similarity index 99% rename from docs/sources/alerting/old-alerting/troubleshoot-alerts.md rename to docs/sources/old-alerting/troubleshoot-alerts.md index ae15da619a1..d037faea0d7 100644 --- a/docs/sources/alerting/old-alerting/troubleshoot-alerts.md +++ b/docs/sources/old-alerting/troubleshoot-alerts.md @@ -4,6 +4,7 @@ description = "Troubleshoot alert rules" keywords = ["grafana", "alerting", "guide", "rules", "troubleshoot"] weight = 500 aliases = ["/docs/grafana/latest/alerting/troubleshoot-alerts/"] +draft = true +++ # Troubleshoot alerts diff --git a/docs/sources/alerting/old-alerting/view-alerts.md b/docs/sources/old-alerting/view-alerts.md similarity index 98% rename from docs/sources/alerting/old-alerting/view-alerts.md rename to docs/sources/old-alerting/view-alerts.md index 132b60b9110..af468d4b7be 100644 --- a/docs/sources/alerting/old-alerting/view-alerts.md +++ b/docs/sources/old-alerting/view-alerts.md @@ -4,6 +4,7 @@ description = "View existing alert rules" keywords = ["grafana", "alerting", "guide", "rules", "view"] weight = 400 aliases = ["/docs/grafana/latest/alerting/view-alerts/"] +draft = true +++ # View existing alert rules diff --git a/docs/sources/panels/query-a-data-source/use-expressions-to-manipulate-data/about-expressions.md b/docs/sources/panels/query-a-data-source/use-expressions-to-manipulate-data/about-expressions.md index 9085d2a400e..91efea43f66 100644 --- a/docs/sources/panels/query-a-data-source/use-expressions-to-manipulate-data/about-expressions.md +++ b/docs/sources/panels/query-a-data-source/use-expressions-to-manipulate-data/about-expressions.md @@ -12,7 +12,7 @@ Server-side expressions allow you to manipulate data returned from queries with ## Using expressions -Expressions are primarily used by the new [Grafana 8 alerts]({{< relref "../../../alerting/unified-alerting/_index.md" >}}). The processing is done server-side, so expressions can operate without a browser session. However, expressions can also be used with backend data sources and visualization. +Expressions are primarily used by [Grafana alerting]({{< relref "../../../alerting/_index.md" >}}). The processing is done server-side, so expressions can operate without a browser session. However, expressions can also be used with backend data sources and visualization. > **Note:** Expressions do not work with legacy dashboard alerts. diff --git a/docs/sources/panels/working-with-panels/navigate-panel-editor.md b/docs/sources/panels/working-with-panels/navigate-panel-editor.md index b39d212d71d..31ff4472764 100644 --- a/docs/sources/panels/working-with-panels/navigate-panel-editor.md +++ b/docs/sources/panels/working-with-panels/navigate-panel-editor.md @@ -27,7 +27,7 @@ This page describes the parts of the Grafana panel editor. - **Query tab -** Select your data source and enter queries here. For more information, refer to [Add a query]({{< relref "../query-a-data-source/add-a-query.md" >}}). - **Transform tab -** Apply data transformations. For more information, refer to [Transform data]({{< relref "../transform-data/_index.md" >}}). - - **Alert tab -** Write alert rules. For more information, refer to [Overview of Grafana 8 alerting]({{< relref "../../alerting/unified-alerting/_index.md" >}}). + - **Alert tab -** Write alert rules. For more information, refer to [Overview of Grafana 8 alerting]({{< relref "../../alerting/_index.md" >}}). 4. Panel display options: The display options section contains tabs where you configure almost every aspect of your data visualization, including: diff --git a/docs/sources/whatsnew/whats-new-in-v7-4.md b/docs/sources/whatsnew/whats-new-in-v7-4.md index 267efa72fd4..2eecdc29f45 100644 --- a/docs/sources/whatsnew/whats-new-in-v7-4.md +++ b/docs/sources/whatsnew/whats-new-in-v7-4.md @@ -107,7 +107,7 @@ You can now provide detailed information to alert notification recipients by inj {{< figure src="/static/img/docs/alerting/alert-notification-template-7-4.png" max-width="700px" caption="Variable support in alert notifications" >}} -For more information, refer to the [alert notification docs]({{< relref "../alerting/old-alerting/notifications.md#notification-templating" >}}). +For more information, refer to the [alert notification docs](https://grafana.com/docs/grafana/latest/alerting/old-alerting/add-notification-template/). ### Content security policy support diff --git a/docs/sources/whatsnew/whats-new-in-v8-0.md b/docs/sources/whatsnew/whats-new-in-v8-0.md index 568699193a0..97486fb05f6 100644 --- a/docs/sources/whatsnew/whats-new-in-v8-0.md +++ b/docs/sources/whatsnew/whats-new-in-v8-0.md @@ -18,13 +18,13 @@ These features are included in the Grafana open source edition. ### Grafana v8.0 alerts -The new alerts in Grafana 8.0 are an opt-in feature that centralizes alerting information for Grafana managed alerts and alerts from Prometheus-compatible data sources in one UI and API. You can create and edit alerting rules for Grafana managed alerts, Cortex alerts, and Loki alerts as well as see alerting information from prometheus-compatible data sources in a single, searchable view. For more information, on how to create and edit alerts and notifications, refer to [Overview of Grafana 8.0 alerts]({{< relref "../alerting/unified-alerting/_index.md" >}}). +The new alerts in Grafana 8.0 are an opt-in feature that centralizes alerting information for Grafana managed alerts and alerts from Prometheus-compatible data sources in one UI and API. You can create and edit alerting rules for Grafana managed alerts, Cortex alerts, and Loki alerts as well as see alerting information from prometheus-compatible data sources in a single, searchable view. For more information, on how to create and edit alerts and notifications, refer to [Overview of Grafana 8.0 alerts]({{< relref "../alerting/_index.md" >}}). As part of the new alert changes, we have introduced a new data source, Alertmanager, which includes built-in support for Prometheus Alertmanager. It is presently in alpha and it not accessible unless alpha plugins are enabled in Grafana settings. For more information, refer to [Alertmanager data source]({{< relref "../datasources/alertmanager.md" >}}). > **Note:** Out of the box, Grafana still supports old Grafana alerts. They are legacy alerts at this time, and will be deprecated in a future release. -To learn more about the differences between new alerts and the legacy alerts, refer to [What's New with Grafana 8 Alerts]({{< relref "../alerting/unified-alerting/difference-old-new.md" >}}). +To learn more about the differences between new alerts and the legacy alerts, refer to [What's New with Grafana 8 Alerts]({{< relref "../alerting/difference-old-new.md" >}}). ### Library panels From 51c3d9d664fbaf51157c0b90c20b195a555194b9 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Wed, 4 May 2022 13:46:04 +0100 Subject: [PATCH 025/440] I18n: Developer documentation (#48415) * Docs: Add developer documentation for localisation * touch up docs * more assertive * add example for variable names * Add naming conventions * Rewrite 'what lingui does' section --- contribute/localisation.md | 194 +++++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 contribute/localisation.md diff --git a/contribute/localisation.md b/contribute/localisation.md new file mode 100644 index 00000000000..97f4ed8d284 --- /dev/null +++ b/contribute/localisation.md @@ -0,0 +1,194 @@ +# Localisation + +Grafana uses the [LinguiJS](https://github.com/lingui/js-lingui) framework for managing translating phrases in the Grafana frontend. + +## tl;dr + +- Use `Go to {panel.title}` in code to add a translatable phrase +- Translations are stored in .po files in `public/locales/{locale}/messages.po` +- If a particular phrase is not available in the a language then it will fall back to English + +## How to add a new translation phrase + +1. Use one of `@lingui/macro`'s React components with the `id`, ensuring it conforms to the guidelines below, with the default english translation. e.g. + +```jsx +import { Trans } from @lingui/macro + +const SearchTitle = ({term}) => ( + + Results for {term} + +); +``` + +Prefer using the JSX components (compared to the plain javascript functions, see below) where possible for phrases. Many props can (and probably should) be changed to accept the `React.ReactNode` instead of `string` for phrases put into the DOM. + +Note that Lingui must be able to statically analyse the code to extract the phrase, so the `id` can not be dynamic. e.g. the following will not work: + +```jsx +const ErrorMessage = ({ id, message }) => There was an error: {message}; +``` + +2. Upon reload, the default English phrase will appear on the page. + +3. Before submitting your PR, run the `yarn i18n:extract` command to extract the messages you added into the `messages.po` file and make them available for translation. + +## How translations work in Grafana + +Grafana uses the [LinguiJS](https://github.com/lingui/js-lingui) framework for managing translating phrases in the Grafana frontend. It: + +- Marks up phrases within our code for extraction +- Extracts phrases into messages catalogues for translating in external systems +- "Compiles" the catalogues to a format that can be used in the website +- Manages the user's locale and putting the translated phrases in the UI + +### Phrase ID naming convention + +We set explicit IDs for phrases to make it easier to identify phrases out of context, and to track where they're used. IDs follow a naming scheme that includes _where_ the phrase is used. The exception is the rare case of single reoccuring words like "Cancel", but default to using a feature/phrase specific phrase. + +Message IDs are made of _up to_ three segments in the format `feature.area.phrase`. For example: + +- `dashboard.header.refresh-label` +- `explore.toolbar.share-tooltip` + +For components used all over the site, use just two segments: + +- `footer.update` +- `navigation.home` + +### Top-level provider + +In [AppWrapper.tsx](/public/app/AppWrapper.tsx) the app is wrapped with `I18nProvider` from `public/app/core/localisation.tsx` where the Lingui instance is created with the user's preferred locale. This sets the appropriate context and allows any component from `@lingui/macro` to use the translations for the user's preferred locale. + +### Message format + +Lingui uses the [ICU MessageFormat](https://unicode-org.github.io/icu/userguide/format_parse/messages/) for the phrases in the .po catalogues. ICU has special syntax especially for describing plurals across multiple languages. For more details see the [Lingui docs](https://lingui.js.org/ref/message-format.html). + +### Plain JS usage + +See [Lingui Docs](https://lingui.js.org/ref/macro.html#t) for more details. + +Sometimes you may need to translate a string cannot be represented in JSX, such as `placeholder` props. Use the `t` macro for this. + +```jsx +import { t } from "@lingui/macro" + +const placeholder = t({ + id: 'form.username-placeholder', + message: `Username` +}); + +return +``` + +While the `t` macro can technically be used outside of React functions (e.g, in actions/reducers), aim to keep all UI phrases within the React UI functions. + +## Examples + +See the [Lingui docs](https://lingui.js.org/ref/macro.html#usage) for more details. + +### Basic usage + +For fixed phrases: + +```jsx +import { Trans } from '@lingui/macro'; + +Hello user!; +``` + +You can include variables, just like regular JSX. Prefer using "simple" variables to make the extracted phrase easier to read for translators + +```jsx +import { Trans } from '@lingui/macro'; + +// Bad - translators will see: Hello {0} +Hello {user.name}!; + +// Good - translators will see: Hello {userName} +const userName = user.name; +Hello {userName}!; +``` + +Variables must be strings (or, must support calling `.toString()`, which we almost never want). + +```jsx +import { Trans } from '@lingui/macro'; + +// This will not work +const userName = user.name; +Hello {userName}!; + +// Instead, put the JSX inside the phrase directly +const userName = user.name; + + Hello {userName}! +; +``` + +### React components and HTML tags + +Both HTML tags and React components can be included in a phase. The Lingui macro will replace them with placeholder tags for the translators + +```js +import { Trans } from "@lingui/macro" + +const randomVariable = "variable" + + + Click to learn more. + + +// ↓ is transformed by macros into ↓ +, + + ]} +/> + +// ↓ is in the messages.po file like ↓ +msgid "page.explainer" +msgstr "Click <0>here to <1>learn more" +``` + +### Plurals + +See the [Lingui docs](https://lingui.js.org/ref/macro.html#id1) for more details. + +Plurals require special handling to make sure they can be translating according to the rules of each locale (which may be more complex that you think!). Use the `` component and specify the plural forms for the default language (English). The message will be extracted into a form where translators can extend it with rules for other locales. + +```js +import { Plural } from "@lingui/macro" + + + +// ↓ is transformed by macros into ↓ + + + +// sharedCount = 0 -> Not shared with anyone +// sharedCount = 1 -> Shared with one person +// sharedCount = 3 -> Shared with # people +``` + +### Date and time + +[Lingui has functions](https://lingui.js.org/ref/core.html#I18n.date) to format dates and times according to the convention to the user's preferred locale, based on the browser [Intl.DateTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat) API. However, as displaying dates and times is fundamental to Grafana, guidelines have not been established for this yet. + +## Documentation + +[Grafana's documentation](https://grafana.com/docs/grafana/latest/) is not yet open for translation and should be authored in English only. From 8fcae1ef3c55f8d72407e7e7d112100dcfca1a81 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 4 May 2022 14:24:10 +0100 Subject: [PATCH 026/440] Navigation: change `Search Dashboards` back to sentence case (`Search dashboards`) (#48272) * revert search dashboards to sentence case * Saved Items -> Saved items --- pkg/api/index.go | 2 +- public/app/core/components/NavBar/Next/NavBarNext.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/api/index.go b/pkg/api/index.go index e0f0707ea26..7df72ac4b67 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -179,7 +179,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool, prefs * } navTree = append(navTree, &dtos.NavLink{ - Text: "Saved Items", + Text: "Saved items", Id: "saved-items", Icon: "bookmark", SortWeight: dtos.WeightSavedItems, diff --git a/public/app/core/components/NavBar/Next/NavBarNext.tsx b/public/app/core/components/NavBar/Next/NavBarNext.tsx index aaf0bd73699..526cea4fd00 100644 --- a/public/app/core/components/NavBar/Next/NavBarNext.tsx +++ b/public/app/core/components/NavBar/Next/NavBarNext.tsx @@ -30,7 +30,7 @@ const onOpenSearch = () => { const searchItem: NavModelItem = { id: SEARCH_ITEM_ID, onClick: onOpenSearch, - text: 'Search Dashboards', + text: 'Search dashboards', icon: 'search', }; From 2e5f7976668d13218be45c7c25a626ae96340be8 Mon Sep 17 00:00:00 2001 From: Joao Silva <100691367+JoaoSilvaGrafana@users.noreply.github.com> Date: Wed, 4 May 2022 14:27:22 +0100 Subject: [PATCH 027/440] Variables: Fixes issue with null variables breaking the dropdown (#48644) --- public/app/features/variables/state/sharedReducer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/variables/state/sharedReducer.ts b/public/app/features/variables/state/sharedReducer.ts index 53c29487412..572f8f1485f 100644 --- a/public/app/features/variables/state/sharedReducer.ts +++ b/public/app/features/variables/state/sharedReducer.ts @@ -130,6 +130,7 @@ const sharedReducerSlice = createSlice({ instanceState.current = current; instanceState.options = instanceState.options.map((option) => { option.value = ensureStringValues(option.value); + option.text = ensureStringValues(option.text); let selected = false; if (Array.isArray(current.value)) { for (let index = 0; index < current.value.length; index++) { From 7b4bc3eda66a281e4cbdd7e28093a83894e7d06a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 4 May 2022 14:28:06 +0100 Subject: [PATCH 028/440] Update dependency @react-aria/utils to v3.12.0 (#48674) Co-authored-by: Renovate Bot --- package.json | 2 +- yarn.lock | 28 ++++++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 8526e32b6d4..b9cd0a9e647 100644 --- a/package.json +++ b/package.json @@ -273,7 +273,7 @@ "@react-aria/interactions": "3.8.3", "@react-aria/menu": "3.4.3", "@react-aria/overlays": "3.8.1", - "@react-aria/utils": "3.11.3", + "@react-aria/utils": "3.12.0", "@react-stately/collections": "3.3.7", "@react-stately/menu": "3.2.6", "@react-stately/tree": "3.2.3", diff --git a/yarn.lock b/yarn.lock index e7af82e445c..17a1a028cef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7309,7 +7309,22 @@ __metadata: languageName: node linkType: hard -"@react-aria/utils@npm:3.11.3, @react-aria/utils@npm:^3.11.3": +"@react-aria/utils@npm:3.12.0": + version: 3.12.0 + resolution: "@react-aria/utils@npm:3.12.0" + dependencies: + "@babel/runtime": ^7.6.2 + "@react-aria/ssr": ^3.1.2 + "@react-stately/utils": ^3.4.1 + "@react-types/shared": ^3.12.0 + clsx: ^1.1.1 + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 + checksum: 8dbcecbc84828e6a76f2509405e9cc500c51a9b25ac795b1f723fde99a0de3a08f8733468d6c5f667f65d94adf9b05c7d345f7fd754057d09481907b2073fdbf + languageName: node + linkType: hard + +"@react-aria/utils@npm:^3.11.3": version: 3.11.3 resolution: "@react-aria/utils@npm:3.11.3" dependencies: @@ -7498,6 +7513,15 @@ __metadata: languageName: node linkType: hard +"@react-types/shared@npm:^3.12.0": + version: 3.12.0 + resolution: "@react-types/shared@npm:3.12.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 + checksum: 400590e560154506919debb3bdf3c54760176e7d6b521c94a45730e7388366b874f0167be0f6710c2a9bd35f9edbf895b0147881689ac0242880c532ababe369 + languageName: node + linkType: hard + "@reduxjs/toolkit@npm:1.8.0": version: 1.8.0 resolution: "@reduxjs/toolkit@npm:1.8.0" @@ -20873,7 +20897,7 @@ __metadata: "@react-aria/interactions": 3.8.3 "@react-aria/menu": 3.4.3 "@react-aria/overlays": 3.8.1 - "@react-aria/utils": 3.11.3 + "@react-aria/utils": 3.12.0 "@react-stately/collections": 3.3.7 "@react-stately/menu": 3.2.6 "@react-stately/tree": 3.2.3 From f85e758972450e987a0a481c23eefc14e36ba8bf Mon Sep 17 00:00:00 2001 From: Yuriy Tseretyan Date: Wed, 4 May 2022 09:31:05 -0400 Subject: [PATCH 029/440] unhide alert rule's data sources during migraiton (#48559) --- pkg/services/sqlstore/migrations/ualert/alert_rule.go | 2 ++ pkg/services/sqlstore/migrations/ualert/alert_rule_test.go | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/pkg/services/sqlstore/migrations/ualert/alert_rule.go b/pkg/services/sqlstore/migrations/ualert/alert_rule.go index 9846d6630d7..cefa976130a 100644 --- a/pkg/services/sqlstore/migrations/ualert/alert_rule.go +++ b/pkg/services/sqlstore/migrations/ualert/alert_rule.go @@ -160,6 +160,8 @@ func migrateAlertRuleQueries(data []alertQuery) ([]alertQuery, error) { if err != nil { return nil, err } + // remove hidden tag from the query (if exists) + delete(fixedData, "hide") fixedData = fixGraphiteReferencedSubQueries(fixedData) updatedModel, err := json.Marshal(fixedData) if err != nil { diff --git a/pkg/services/sqlstore/migrations/ualert/alert_rule_test.go b/pkg/services/sqlstore/migrations/ualert/alert_rule_test.go index 533dad039fa..b3d16922a1e 100644 --- a/pkg/services/sqlstore/migrations/ualert/alert_rule_test.go +++ b/pkg/services/sqlstore/migrations/ualert/alert_rule_test.go @@ -25,6 +25,11 @@ func TestMigrateAlertRuleQueries(t *testing.T) { input: simplejson.NewFromAny(map[string]interface{}{"target": "ahalfquery"}), expected: `{"target":"ahalfquery"}`, }, + { + name: "when query was hidden, it removes the flag", + input: simplejson.NewFromAny(map[string]interface{}{"hide": true}), + expected: `{}`, + }, } for _, tt := range tc { From b2644de6c8a08181cbe0a9f545393bd42753477c Mon Sep 17 00:00:00 2001 From: Adam Simpson Date: Wed, 4 May 2022 09:54:09 -0400 Subject: [PATCH 030/440] AzureMonitor: add feature toggle azureMonitorExperimentalUI for migrating to experimental UI (#48658) * feat: add feature toggle azureMonitorExperimentalUI Add QueryHeader which adds an experimental header to AzureMonitor. This work is documented in #44432. --- .../src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 7 +++ pkg/services/featuremgmt/toggles_gen.go | 4 ++ .../QueryEditor/QueryEditor.test.tsx | 18 ++++++++ .../components/QueryEditor/QueryEditor.tsx | 6 ++- .../components/QueryHeader.tsx | 45 +++++++++++++++++++ 6 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryHeader.tsx diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index c00c802c03c..53216524eb1 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -59,4 +59,5 @@ export interface FeatureToggles { savedItems?: boolean; cloudWatchDynamicLabels?: boolean; datasourceQueryMultiStatus?: boolean; + azureMonitorExperimentalUI?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 04386a7940b..5080805e73a 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -241,5 +241,12 @@ var ( Description: "Introduce HTTP 207 Multi Status for api/ds/query", State: FeatureStateAlpha, }, + { + Name: "azureMonitorExperimentalUI", + Description: "Use grafana-experimental UI in Azure Monitor", + State: FeatureStateAlpha, + RequiresDevMode: true, + FrontendOnly: true, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 7309dce29ef..ded1ce2e065 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -178,4 +178,8 @@ const ( // FlagDatasourceQueryMultiStatus // Introduce HTTP 207 Multi Status for api/ds/query FlagDatasourceQueryMultiStatus = "datasourceQueryMultiStatus" + + // FlagAzureMonitorExperimentalUI + // Use grafana-experimental UI in Azure Monitor + FlagAzureMonitorExperimentalUI = "azureMonitorExperimentalUI" ) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor.test.tsx index d23ec33fcd3..9b602d04215 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor.test.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor.test.tsx @@ -90,4 +90,22 @@ describe('Azure Monitor QueryEditor', () => { // reset config to not impact future tests config.featureToggles.azureMonitorResourcePickerForMetrics = originalConfigValue; }); + + it('should render the experimental QueryHeader when feature toggle is enabled', async () => { + const originalConfigValue = config.featureToggles.azureMonitorExperimentalUI; + + config.featureToggles.azureMonitorExperimentalUI = true; + + const mockDatasource = createMockDatasource(); + const mockQuery = { + ...createMockQuery(), + queryType: AzureQueryType.AzureMonitor, + }; + + render( {}} onRunQuery={() => {}} />); + + await waitFor(() => expect(screen.getByTestId('azure-monitor-experimental-header')).toBeInTheDocument()); + + config.featureToggles.azureMonitorExperimentalUI = originalConfigValue; + }); }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor.tsx index 5efe979923b..bd797cd6334 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor.tsx @@ -18,6 +18,7 @@ import ArgQueryEditor from '../ArgQueryEditor'; import LogsQueryEditor from '../LogsQueryEditor'; import MetricsQueryEditor from '../MetricsQueryEditor'; import NewMetricsQueryEditor from '../NewMetricsQueryEditor/MetricsQueryEditor'; +import { QueryHeader } from '../QueryHeader'; import { Space } from '../Space'; import QueryTypeField from './QueryTypeField'; @@ -57,7 +58,10 @@ const QueryEditor: React.FC = ({ return (
- + {config.featureToggles.azureMonitorExperimentalUI && } + {!config.featureToggles.azureMonitorExperimentalUI && ( + + )} void; +} + +export const QueryHeader: React.FC = ({ query, onQueryChange }) => { + const queryTypes: Array<{ value: AzureQueryType; label: string }> = [ + { value: AzureQueryType.AzureMonitor, label: 'Metrics' }, + { value: AzureQueryType.LogAnalytics, label: 'Logs' }, + { value: AzureQueryType.AzureResourceGraph, label: 'Azure Resource Graph' }, + ]; + + const handleChange = useCallback( + (change: SelectableValue) => { + change.value && + onQueryChange({ + ...query, + queryType: change.value, + }); + }, + [onQueryChange, query] + ); + + return ( + + + + + + ); +}; From 00dbea91ea62179ccd80aaf8ab8a9dbced7b4c58 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 4 May 2022 15:07:27 +0100 Subject: [PATCH 031/440] Update dependency lru-cache to v7.9.0 (#47480) * Update dependency lru-cache to v7.9.0 * update snapshot Co-authored-by: Renovate Bot Co-authored-by: Ashley Harrison --- package.json | 2 +- .../__snapshots__/LokiExploreQueryEditor.test.tsx.snap | 2 ++ yarn.lock | 10 +++++----- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index b9cd0a9e647..d72d0542880 100644 --- a/package.json +++ b/package.json @@ -324,7 +324,7 @@ "lezer-promql": "0.22.0", "lodash": "4.17.21", "logfmt": "^1.3.2", - "lru-cache": "7.7.1", + "lru-cache": "7.9.0", "memoize-one": "6.0.0", "minisearch": "5.0.0-beta1", "moment": "2.29.2", diff --git a/public/app/plugins/datasource/loki/components/__snapshots__/LokiExploreQueryEditor.test.tsx.snap b/public/app/plugins/datasource/loki/components/__snapshots__/LokiExploreQueryEditor.test.tsx.snap index 05befddc9ef..ac3ed818ed2 100644 --- a/public/app/plugins/datasource/loki/components/__snapshots__/LokiExploreQueryEditor.test.tsx.snap +++ b/public/app/plugins/datasource/loki/components/__snapshots__/LokiExploreQueryEditor.test.tsx.snap @@ -136,6 +136,7 @@ exports[`LokiExploreQueryEditor should render component 1`] = ` "ttlAutopurge": false, "ttlResolution": 1, "updateAgeOnGet": false, + "updateAgeOnHas": false, "valList": Array [ null, null, @@ -221,6 +222,7 @@ exports[`LokiExploreQueryEditor should render component 1`] = ` "ttlAutopurge": false, "ttlResolution": 1, "updateAgeOnGet": false, + "updateAgeOnHas": false, "valList": Array [ null, null, diff --git a/yarn.lock b/yarn.lock index 17a1a028cef..0b374e8de56 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21054,7 +21054,7 @@ __metadata: lint-staged: 12.3.7 lodash: 4.17.21 logfmt: ^1.3.2 - lru-cache: 7.7.1 + lru-cache: 7.9.0 memoize-one: 6.0.0 mini-css-extract-plugin: 2.6.0 minisearch: 5.0.0-beta1 @@ -25794,10 +25794,10 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:7.7.1": - version: 7.7.1 - resolution: "lru-cache@npm:7.7.1" - checksum: f362c5a2cfa8ad6fe557ec43dc1b7a9695cce84a5652a43ff813609f782f5da576631e7dfad41878bf19a7a69438f38375178635ee80de269aa314280ca2f59e +"lru-cache@npm:7.9.0": + version: 7.9.0 + resolution: "lru-cache@npm:7.9.0" + checksum: c91a293a103d11ea4f07de4122ba4f73d8203d0de51852fb612b1764296aebf623a3e11dddef1b3aefdc8d71af97d52b222dad5459dcb967713bbab9a19fed7d languageName: node linkType: hard From 2738d1c5570271ace05978e4dba43ba5f989bca8 Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Wed, 4 May 2022 16:12:09 +0200 Subject: [PATCH 032/440] Access Control: Move dashboard actions and create scope provider (#48618) * Move dashboard actions and create scope provider --- pkg/api/accesscontrol.go | 26 +++---- pkg/api/annotations.go | 3 +- pkg/api/api.go | 22 +++--- pkg/api/index.go | 6 +- pkg/services/accesscontrol/models.go | 11 --- .../ossaccesscontrol/permissions_services.go | 8 +- pkg/services/dashboardimport/api/api.go | 3 +- pkg/services/dashboards/accesscontrol.go | 13 +++- .../dashboards/manager/dashboard_service.go | 8 +- .../guardian/accesscontrol_guardian.go | 46 +++++------ .../guardian/accesscontrol_guardian_test.go | 78 +++++++++---------- pkg/services/sqlstore/annotation_test.go | 11 +-- .../accesscontrol/dashboard_permissions.go | 22 +++--- .../sqlstore/permissions/dashboard.go | 6 +- .../sqlstore/permissions/dashboard_test.go | 14 ++-- 15 files changed, 135 insertions(+), 142 deletions(-) diff --git a/pkg/api/accesscontrol.go b/pkg/api/accesscontrol.go index 9f948866810..4d2f33a9516 100644 --- a/pkg/api/accesscontrol.go +++ b/pkg/api/accesscontrol.go @@ -328,7 +328,7 @@ func (hs *HTTPServer) declareFixedRoles() error { Group: "Dashboards", Permissions: []ac.Permission{ {Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)}, - {Action: ac.ActionDashboardsCreate, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)}, + {Action: dashboards.ActionDashboardsCreate, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)}, }, }, Grants: []string{"Editor"}, @@ -342,7 +342,7 @@ func (hs *HTTPServer) declareFixedRoles() error { Description: "Read all dashboards.", Group: "Dashboards", Permissions: []ac.Permission{ - {Action: ac.ActionDashboardsRead, Scope: ac.ScopeDashboardsAll}, + {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeDashboardsAll}, }, }, Grants: []string{"Admin"}, @@ -356,11 +356,11 @@ func (hs *HTTPServer) declareFixedRoles() error { Group: "Dashboards", Description: "Create, read, write or delete all dashboards and their permissions.", Permissions: ac.ConcatPermissions(dashboardsReaderRole.Role.Permissions, []ac.Permission{ - {Action: ac.ActionDashboardsWrite, Scope: ac.ScopeDashboardsAll}, - {Action: ac.ActionDashboardsDelete, Scope: ac.ScopeDashboardsAll}, - {Action: ac.ActionDashboardsCreate, Scope: dashboards.ScopeFoldersAll}, - {Action: ac.ActionDashboardsPermissionsRead, Scope: ac.ScopeDashboardsAll}, - {Action: ac.ActionDashboardsPermissionsWrite, Scope: ac.ScopeDashboardsAll}, + {Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeDashboardsAll}, + {Action: dashboards.ActionDashboardsDelete, Scope: dashboards.ScopeDashboardsAll}, + {Action: dashboards.ActionDashboardsCreate, Scope: dashboards.ScopeFoldersAll}, + {Action: dashboards.ActionDashboardsPermissionsRead, Scope: dashboards.ScopeDashboardsAll}, + {Action: dashboards.ActionDashboardsPermissionsWrite, Scope: dashboards.ScopeDashboardsAll}, }), }, Grants: []string{"Admin"}, @@ -389,7 +389,7 @@ func (hs *HTTPServer) declareFixedRoles() error { Group: "Folders", Permissions: []ac.Permission{ {Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersAll}, - {Action: ac.ActionDashboardsRead, Scope: dashboards.ScopeFoldersAll}, + {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersAll}, }, }, Grants: []string{"Admin"}, @@ -408,11 +408,11 @@ func (hs *HTTPServer) declareFixedRoles() error { {Action: dashboards.ActionFoldersCreate}, {Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersAll}, {Action: dashboards.ActionFoldersDelete, Scope: dashboards.ScopeFoldersAll}, - {Action: ac.ActionDashboardsWrite, Scope: dashboards.ScopeFoldersAll}, - {Action: ac.ActionDashboardsDelete, Scope: dashboards.ScopeFoldersAll}, - {Action: ac.ActionDashboardsCreate, Scope: dashboards.ScopeFoldersAll}, - {Action: ac.ActionDashboardsPermissionsRead, Scope: dashboards.ScopeFoldersAll}, - {Action: ac.ActionDashboardsPermissionsWrite, Scope: dashboards.ScopeFoldersAll}, + {Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeFoldersAll}, + {Action: dashboards.ActionDashboardsDelete, Scope: dashboards.ScopeFoldersAll}, + {Action: dashboards.ActionDashboardsCreate, Scope: dashboards.ScopeFoldersAll}, + {Action: dashboards.ActionDashboardsPermissionsRead, Scope: dashboards.ScopeFoldersAll}, + {Action: dashboards.ActionDashboardsPermissionsWrite, Scope: dashboards.ScopeFoldersAll}, }), }, Grants: []string{"Admin"}, diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index 24a34e77e08..d59c6d97583 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/annotations" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" @@ -456,7 +457,7 @@ func AnnotationTypeScopeResolver() (string, accesscontrol.ScopeAttributeResolver OrgId: orgID, Permissions: map[int64]map[string][]string{ orgID: { - accesscontrol.ActionDashboardsRead: {accesscontrol.ScopeDashboardsAll}, + dashboards.ActionDashboardsRead: {dashboards.ScopeDashboardsAll}, accesscontrol.ActionAnnotationsRead: {accesscontrol.ScopeAnnotationsAll}, }, }, diff --git a/pkg/api/api.go b/pkg/api/api.go index 595a8faa6d4..3ebbb720dc8 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -354,12 +354,12 @@ func (hs *HTTPServer) registerRoutes() { // Dashboard apiRoute.Group("/dashboards", func(dashboardRoute routing.RouteRegister) { - dashboardRoute.Get("/uid/:uid", authorize(reqSignedIn, ac.EvalPermission(ac.ActionDashboardsRead)), routing.Wrap(hs.GetDashboard)) - dashboardRoute.Delete("/uid/:uid", authorize(reqSignedIn, ac.EvalPermission(ac.ActionDashboardsDelete)), routing.Wrap(hs.DeleteDashboardByUID)) + dashboardRoute.Get("/uid/:uid", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsRead)), routing.Wrap(hs.GetDashboard)) + dashboardRoute.Delete("/uid/:uid", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsDelete)), routing.Wrap(hs.DeleteDashboardByUID)) dashboardRoute.Group("/uid/:uid", func(dashUidRoute routing.RouteRegister) { dashUidRoute.Group("/permissions", func(dashboardPermissionRoute routing.RouteRegister) { - dashboardPermissionRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(ac.ActionDashboardsPermissionsRead)), routing.Wrap(hs.GetDashboardPermissionList)) - dashboardPermissionRoute.Post("/", authorize(reqSignedIn, ac.EvalPermission(ac.ActionDashboardsPermissionsWrite)), routing.Wrap(hs.UpdateDashboardPermissions)) + dashboardPermissionRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsPermissionsRead)), routing.Wrap(hs.GetDashboardPermissionList)) + dashboardPermissionRoute.Post("/", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsPermissionsWrite)), routing.Wrap(hs.UpdateDashboardPermissions)) }) }) @@ -372,22 +372,22 @@ func (hs *HTTPServer) registerRoutes() { } } - dashboardRoute.Post("/calculate-diff", authorize(reqSignedIn, ac.EvalPermission(ac.ActionDashboardsWrite)), routing.Wrap(hs.CalculateDashboardDiff)) + dashboardRoute.Post("/calculate-diff", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.CalculateDashboardDiff)) dashboardRoute.Post("/trim", routing.Wrap(hs.TrimDashboard)) - dashboardRoute.Post("/db", authorize(reqSignedIn, ac.EvalAny(ac.EvalPermission(ac.ActionDashboardsCreate), ac.EvalPermission(ac.ActionDashboardsWrite))), routing.Wrap(hs.PostDashboard)) + dashboardRoute.Post("/db", authorize(reqSignedIn, ac.EvalAny(ac.EvalPermission(dashboards.ActionDashboardsCreate), ac.EvalPermission(dashboards.ActionDashboardsWrite))), routing.Wrap(hs.PostDashboard)) dashboardRoute.Get("/home", routing.Wrap(hs.GetHomeDashboard)) dashboardRoute.Get("/tags", hs.GetDashboardTags) // Deprecated: use /uid/:uid API instead. dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute routing.RouteRegister) { - dashIdRoute.Get("/versions", authorize(reqSignedIn, ac.EvalPermission(ac.ActionDashboardsWrite)), routing.Wrap(hs.GetDashboardVersions)) - dashIdRoute.Get("/versions/:id", authorize(reqSignedIn, ac.EvalPermission(ac.ActionDashboardsWrite)), routing.Wrap(hs.GetDashboardVersion)) - dashIdRoute.Post("/restore", authorize(reqSignedIn, ac.EvalPermission(ac.ActionDashboardsWrite)), routing.Wrap(hs.RestoreDashboardVersion)) + dashIdRoute.Get("/versions", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.GetDashboardVersions)) + dashIdRoute.Get("/versions/:id", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.GetDashboardVersion)) + dashIdRoute.Post("/restore", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.RestoreDashboardVersion)) dashIdRoute.Group("/permissions", func(dashboardPermissionRoute routing.RouteRegister) { - dashboardPermissionRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(ac.ActionDashboardsPermissionsRead)), routing.Wrap(hs.GetDashboardPermissionList)) - dashboardPermissionRoute.Post("/", authorize(reqSignedIn, ac.EvalPermission(ac.ActionDashboardsPermissionsWrite)), routing.Wrap(hs.UpdateDashboardPermissions)) + dashboardPermissionRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsPermissionsRead)), routing.Wrap(hs.GetDashboardPermissionList)) + dashboardPermissionRoute.Post("/", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsPermissionsWrite)), routing.Wrap(hs.UpdateDashboardPermissions)) }) }) }) diff --git a/pkg/api/index.go b/pkg/api/index.go index 7df72ac4b67..1e3ad32d444 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -572,7 +572,7 @@ func (hs *HTTPServer) buildCreateNavLinks(c *models.ReqContext) []*dtos.NavLink hasAccess := ac.HasAccess(hs.AccessControl, c) var children []*dtos.NavLink - if hasAccess(ac.ReqSignedIn, ac.EvalPermission(ac.ActionDashboardsCreate)) { + if hasAccess(ac.ReqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsCreate)) { children = append(children, &dtos.NavLink{Text: "Dashboard", Icon: "apps", Url: hs.Cfg.AppSubURL + "/dashboard/new", Id: "create-dashboard"}) } @@ -583,7 +583,7 @@ func (hs *HTTPServer) buildCreateNavLinks(c *models.ReqContext) []*dtos.NavLink }) } - if hasAccess(ac.ReqSignedIn, ac.EvalPermission(ac.ActionDashboardsCreate)) { + if hasAccess(ac.ReqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsCreate)) { children = append(children, &dtos.NavLink{ Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "import", Url: hs.Cfg.AppSubURL + "/dashboard/import", @@ -651,7 +651,7 @@ func (hs *HTTPServer) editorInAnyFolder(c *models.ReqContext) bool { func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewData, error) { hasAccess := ac.HasAccess(hs.AccessControl, c) - hasEditPerm := hasAccess(hs.editorInAnyFolder, ac.EvalAny(ac.EvalPermission(ac.ActionDashboardsCreate), ac.EvalPermission(dashboards.ActionFoldersCreate))) + hasEditPerm := hasAccess(hs.editorInAnyFolder, ac.EvalAny(ac.EvalPermission(dashboards.ActionDashboardsCreate), ac.EvalPermission(dashboards.ActionFoldersCreate))) settings, err := hs.getFrontendSettingsMap(c) if err != nil { diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index 26222da0baa..2552a1eb17f 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -359,17 +359,6 @@ const ( ActionAnnotationsRead = "annotations:read" ActionAnnotationsWrite = "annotations:write" - // Dashboard actions - ActionDashboardsCreate = "dashboards:create" - ActionDashboardsRead = "dashboards:read" - ActionDashboardsWrite = "dashboards:write" - ActionDashboardsDelete = "dashboards:delete" - ActionDashboardsPermissionsRead = "dashboards.permissions:read" - ActionDashboardsPermissionsWrite = "dashboards.permissions:write" - - // Dashboard scopes - ScopeDashboardsAll = "dashboards:*" - // Alert scopes are divided into two groups. The internal (to Grafana) and the external ones. // For the Grafana ones, given we have ACID control we're able to provide better granularity by defining CRUD options. // For the external ones, we only have read and write permissions due to the lack of atomicity control of the external system. diff --git a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go index 217812cea9f..a1208cb90fc 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go @@ -138,9 +138,9 @@ func ProvideTeamPermissions( return resourcepermissions.New(options, cfg, router, ac, store, sql) } -var DashboardViewActions = []string{accesscontrol.ActionDashboardsRead} -var DashboardEditActions = append(DashboardViewActions, []string{accesscontrol.ActionDashboardsWrite, accesscontrol.ActionDashboardsDelete}...) -var DashboardAdminActions = append(DashboardEditActions, []string{accesscontrol.ActionDashboardsPermissionsRead, accesscontrol.ActionDashboardsPermissionsWrite}...) +var DashboardViewActions = []string{dashboards.ActionDashboardsRead} +var DashboardEditActions = append(DashboardViewActions, []string{dashboards.ActionDashboardsWrite, dashboards.ActionDashboardsDelete}...) +var DashboardAdminActions = append(DashboardEditActions, []string{dashboards.ActionDashboardsPermissionsRead, dashboards.ActionDashboardsPermissionsWrite}...) func ProvideDashboardPermissions( cfg *setting.Cfg, router routing.RouteRegister, sql *sqlstore.SQLStore, @@ -203,7 +203,7 @@ func ProvideDashboardPermissions( } var FolderViewActions = []string{dashboards.ActionFoldersRead} -var FolderEditActions = append(FolderViewActions, []string{dashboards.ActionFoldersWrite, dashboards.ActionFoldersDelete, accesscontrol.ActionDashboardsCreate}...) +var FolderEditActions = append(FolderViewActions, []string{dashboards.ActionFoldersWrite, dashboards.ActionFoldersDelete, dashboards.ActionDashboardsCreate}...) var FolderAdminActions = append(FolderEditActions, []string{dashboards.ActionFoldersPermissionsRead, dashboards.ActionFoldersPermissionsWrite}...) func ProvideFolderPermissions( diff --git a/pkg/services/dashboardimport/api/api.go b/pkg/services/dashboardimport/api/api.go index 9c64d2b512a..95b05e52ddc 100644 --- a/pkg/services/dashboardimport/api/api.go +++ b/pkg/services/dashboardimport/api/api.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboardimport" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/web" ) @@ -39,7 +40,7 @@ func (api *ImportDashboardAPI) RegisterAPIEndpoints(routeRegister routing.RouteR routeRegister.Group("/api/dashboards", func(route routing.RouteRegister) { route.Post( "/import", - authorize(middleware.ReqSignedIn, accesscontrol.EvalPermission(accesscontrol.ActionDashboardsCreate)), + authorize(middleware.ReqSignedIn, accesscontrol.EvalPermission(dashboards.ActionDashboardsCreate)), routing.Wrap(api.ImportDashboard), ) }, middleware.ReqSignedIn) diff --git a/pkg/services/dashboards/accesscontrol.go b/pkg/services/dashboards/accesscontrol.go index aa6f1db6971..ae2fb0cd4e2 100644 --- a/pkg/services/dashboards/accesscontrol.go +++ b/pkg/services/dashboards/accesscontrol.go @@ -21,11 +21,20 @@ const ( ScopeDashboardsRoot = "dashboards" ScopeDashboardsPrefix = "dashboards:uid:" + + ActionDashboardsCreate = "dashboards:create" + ActionDashboardsRead = "dashboards:read" + ActionDashboardsWrite = "dashboards:write" + ActionDashboardsDelete = "dashboards:delete" + ActionDashboardsPermissionsRead = "dashboards.permissions:read" + ActionDashboardsPermissionsWrite = "dashboards.permissions:write" ) var ( - ScopeFoldersAll = ac.GetResourceAllScope(ScopeFoldersRoot) - ScopeFoldersProvider = ac.NewScopeProvider(ScopeFoldersRoot) + ScopeFoldersProvider = ac.NewScopeProvider(ScopeFoldersRoot) + ScopeFoldersAll = ScopeFoldersProvider.GetResourceAllScope() + ScopeDashboardsProvider = ac.NewScopeProvider(ScopeDashboardsRoot) + ScopeDashboardsAll = ScopeDashboardsProvider.GetResourceAllScope() ) // NewFolderNameScopeResolver provides an ScopeAttributeResolver that is able to convert a scope prefixed with "folders:name:" into an uid based scope. diff --git a/pkg/services/dashboards/manager/dashboard_service.go b/pkg/services/dashboards/manager/dashboard_service.go index d23bf6fce8a..56c41c3568c 100644 --- a/pkg/services/dashboards/manager/dashboard_service.go +++ b/pkg/services/dashboards/manager/dashboard_service.go @@ -22,10 +22,10 @@ import ( var ( provisionerPermissions = map[string][]string{ - m.ActionFoldersCreate: {}, - m.ActionFoldersWrite: {m.ScopeFoldersAll}, - accesscontrol.ActionDashboardsCreate: {m.ScopeFoldersAll}, - accesscontrol.ActionDashboardsWrite: {m.ScopeFoldersAll}, + m.ActionFoldersCreate: {}, + m.ActionFoldersWrite: {m.ScopeFoldersAll}, + m.ActionDashboardsCreate: {m.ScopeFoldersAll}, + m.ActionDashboardsWrite: {m.ScopeFoldersAll}, } ) diff --git a/pkg/services/guardian/accesscontrol_guardian.go b/pkg/services/guardian/accesscontrol_guardian.go index 80f4d71e9de..5e3dd3b33ff 100644 --- a/pkg/services/guardian/accesscontrol_guardian.go +++ b/pkg/services/guardian/accesscontrol_guardian.go @@ -53,12 +53,12 @@ func (a *AccessControlDashboardGuardian) CanSave() (bool, error) { } if a.dashboard.IsFolder { - return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, folderScope(a.dashboard.Uid))) + return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.Uid))) } return a.evaluate(accesscontrol.EvalAny( - accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, dashboardScope(a.dashboard.Uid)), - accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, folderScope(a.parentFolderUID)), + accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.Uid)), + accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.parentFolderUID)), )) } @@ -71,12 +71,12 @@ func (a *AccessControlDashboardGuardian) CanEdit() (bool, error) { } if a.dashboard.IsFolder { - return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, folderScope(a.dashboard.Uid))) + return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.Uid))) } return a.evaluate(accesscontrol.EvalAny( - accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, dashboardScope(a.dashboard.Uid)), - accesscontrol.EvalPermission(accesscontrol.ActionDashboardsWrite, folderScope(a.parentFolderUID)), + accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.Uid)), + accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.parentFolderUID)), )) } @@ -86,12 +86,12 @@ func (a *AccessControlDashboardGuardian) CanView() (bool, error) { } if a.dashboard.IsFolder { - return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, folderScope(a.dashboard.Uid))) + return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.Uid))) } return a.evaluate(accesscontrol.EvalAny( - accesscontrol.EvalPermission(accesscontrol.ActionDashboardsRead, dashboardScope(a.dashboard.Uid)), - accesscontrol.EvalPermission(accesscontrol.ActionDashboardsRead, folderScope(a.parentFolderUID)), + accesscontrol.EvalPermission(dashboards.ActionDashboardsRead, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.Uid)), + accesscontrol.EvalPermission(dashboards.ActionDashboardsRead, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.parentFolderUID)), )) } @@ -102,19 +102,19 @@ func (a *AccessControlDashboardGuardian) CanAdmin() (bool, error) { if a.dashboard.IsFolder { return a.evaluate(accesscontrol.EvalAll( - accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsRead, folderScope(a.dashboard.Uid)), - accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsWrite, folderScope(a.dashboard.Uid)), + accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsRead, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.Uid)), + accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.Uid)), )) } return a.evaluate(accesscontrol.EvalAny( accesscontrol.EvalAll( - accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsRead, dashboardScope(a.dashboard.Uid)), - accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsWrite, dashboardScope(a.dashboard.Uid)), + accesscontrol.EvalPermission(dashboards.ActionDashboardsPermissionsRead, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.Uid)), + accesscontrol.EvalPermission(dashboards.ActionDashboardsPermissionsWrite, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.Uid)), ), accesscontrol.EvalAll( - accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsRead, folderScope(a.parentFolderUID)), - accesscontrol.EvalPermission(accesscontrol.ActionDashboardsPermissionsWrite, folderScope(a.parentFolderUID)), + accesscontrol.EvalPermission(dashboards.ActionDashboardsPermissionsRead, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.parentFolderUID)), + accesscontrol.EvalPermission(dashboards.ActionDashboardsPermissionsWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.parentFolderUID)), ), )) } @@ -125,12 +125,12 @@ func (a *AccessControlDashboardGuardian) CanDelete() (bool, error) { } if a.dashboard.IsFolder { - return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, folderScope(a.dashboard.Uid))) + return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.Uid))) } return a.evaluate(accesscontrol.EvalAny( - accesscontrol.EvalPermission(accesscontrol.ActionDashboardsDelete, dashboardScope(a.dashboard.Uid)), - accesscontrol.EvalPermission(accesscontrol.ActionDashboardsDelete, folderScope(a.parentFolderUID)), + accesscontrol.EvalPermission(dashboards.ActionDashboardsDelete, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.Uid)), + accesscontrol.EvalPermission(dashboards.ActionDashboardsDelete, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.parentFolderUID)), )) } @@ -142,7 +142,7 @@ func (a *AccessControlDashboardGuardian) CanCreate(folderID int64, isFolder bool if err != nil { return false, err } - return a.evaluate(accesscontrol.EvalPermission(accesscontrol.ActionDashboardsCreate, folderScope(folder.Uid))) + return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionDashboardsCreate, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.Uid))) } func (a *AccessControlDashboardGuardian) evaluate(evaluator accesscontrol.Evaluator) (bool, error) { @@ -283,11 +283,3 @@ func (a *AccessControlDashboardGuardian) loadParentFolder(folderID int64) (*mode } return folderQuery.Result, nil } - -func dashboardScope(uid string) string { - return accesscontrol.GetResourceScopeUID("dashboards", uid) -} - -func folderScope(uid string) string { - return dashboards.ScopeFoldersProvider.GetResourceScopeUID(uid) -} diff --git a/pkg/services/guardian/accesscontrol_guardian_test.go b/pkg/services/guardian/accesscontrol_guardian_test.go index b38672d8fcc..c780df1cd17 100644 --- a/pkg/services/guardian/accesscontrol_guardian_test.go +++ b/pkg/services/guardian/accesscontrol_guardian_test.go @@ -36,7 +36,7 @@ func TestAccessControlDashboardGuardian_CanSave(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsWrite, + Action: dashboards.ActionDashboardsWrite, Scope: "dashboards:*", }, }, @@ -47,7 +47,7 @@ func TestAccessControlDashboardGuardian_CanSave(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsWrite, + Action: dashboards.ActionDashboardsWrite, Scope: "folders:*", }, }, @@ -58,7 +58,7 @@ func TestAccessControlDashboardGuardian_CanSave(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsWrite, + Action: dashboards.ActionDashboardsWrite, Scope: "dashboards:uid:1", }, }, @@ -69,7 +69,7 @@ func TestAccessControlDashboardGuardian_CanSave(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsWrite, + Action: dashboards.ActionDashboardsWrite, Scope: "folders:uid:general", }, }, @@ -80,7 +80,7 @@ func TestAccessControlDashboardGuardian_CanSave(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsWrite, + Action: dashboards.ActionDashboardsWrite, Scope: "dashboards:uid:10", }, }, @@ -91,7 +91,7 @@ func TestAccessControlDashboardGuardian_CanSave(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsWrite, + Action: dashboards.ActionDashboardsWrite, Scope: "folders:uid:100", }, }, @@ -116,7 +116,7 @@ func TestAccessControlDashboardGuardian_CanEdit(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsWrite, + Action: dashboards.ActionDashboardsWrite, Scope: "dashboards:*", }, }, @@ -127,7 +127,7 @@ func TestAccessControlDashboardGuardian_CanEdit(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsWrite, + Action: dashboards.ActionDashboardsWrite, Scope: "folders:*", }, }, @@ -138,7 +138,7 @@ func TestAccessControlDashboardGuardian_CanEdit(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsWrite, + Action: dashboards.ActionDashboardsWrite, Scope: "dashboards:uid:1", }, }, @@ -149,7 +149,7 @@ func TestAccessControlDashboardGuardian_CanEdit(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsWrite, + Action: dashboards.ActionDashboardsWrite, Scope: "folders:uid:general", }, }, @@ -160,7 +160,7 @@ func TestAccessControlDashboardGuardian_CanEdit(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsWrite, + Action: dashboards.ActionDashboardsWrite, Scope: "dashboards:uid:10", }, }, @@ -171,7 +171,7 @@ func TestAccessControlDashboardGuardian_CanEdit(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsWrite, + Action: dashboards.ActionDashboardsWrite, Scope: "folders:uid:10", }, }, @@ -182,7 +182,7 @@ func TestAccessControlDashboardGuardian_CanEdit(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsRead, + Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:1", }, }, @@ -212,7 +212,7 @@ func TestAccessControlDashboardGuardian_CanView(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsRead, + Action: dashboards.ActionDashboardsRead, Scope: "dashboards:*", }, }, @@ -223,7 +223,7 @@ func TestAccessControlDashboardGuardian_CanView(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsRead, + Action: dashboards.ActionDashboardsRead, Scope: "folders:*", }, }, @@ -234,7 +234,7 @@ func TestAccessControlDashboardGuardian_CanView(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsRead, + Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:1", }, }, @@ -245,7 +245,7 @@ func TestAccessControlDashboardGuardian_CanView(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsRead, + Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:general", }, }, @@ -256,7 +256,7 @@ func TestAccessControlDashboardGuardian_CanView(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsRead, + Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:10", }, }, @@ -267,7 +267,7 @@ func TestAccessControlDashboardGuardian_CanView(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsRead, + Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:10", }, }, @@ -292,11 +292,11 @@ func TestAccessControlDashboardGuardian_CanAdmin(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsPermissionsRead, + Action: dashboards.ActionDashboardsPermissionsRead, Scope: "dashboards:*", }, { - Action: accesscontrol.ActionDashboardsPermissionsWrite, + Action: dashboards.ActionDashboardsPermissionsWrite, Scope: "dashboards:*", }, }, @@ -307,11 +307,11 @@ func TestAccessControlDashboardGuardian_CanAdmin(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsPermissionsRead, + Action: dashboards.ActionDashboardsPermissionsRead, Scope: "folders:*", }, { - Action: accesscontrol.ActionDashboardsPermissionsWrite, + Action: dashboards.ActionDashboardsPermissionsWrite, Scope: "folders:*", }, }, @@ -322,11 +322,11 @@ func TestAccessControlDashboardGuardian_CanAdmin(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsPermissionsRead, + Action: dashboards.ActionDashboardsPermissionsRead, Scope: "dashboards:uid:1", }, { - Action: accesscontrol.ActionDashboardsPermissionsWrite, + Action: dashboards.ActionDashboardsPermissionsWrite, Scope: "dashboards:uid:1", }, }, @@ -337,11 +337,11 @@ func TestAccessControlDashboardGuardian_CanAdmin(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsPermissionsRead, + Action: dashboards.ActionDashboardsPermissionsRead, Scope: "folders:uid:general", }, { - Action: accesscontrol.ActionDashboardsPermissionsWrite, + Action: dashboards.ActionDashboardsPermissionsWrite, Scope: "folders:uid:general", }, }, @@ -352,11 +352,11 @@ func TestAccessControlDashboardGuardian_CanAdmin(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsPermissionsRead, + Action: dashboards.ActionDashboardsPermissionsRead, Scope: "dashboards:uid:10", }, { - Action: accesscontrol.ActionDashboardsPermissionsWrite, + Action: dashboards.ActionDashboardsPermissionsWrite, Scope: "dashboards:uid:10", }, }, @@ -367,11 +367,11 @@ func TestAccessControlDashboardGuardian_CanAdmin(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsPermissionsRead, + Action: dashboards.ActionDashboardsPermissionsRead, Scope: "folders:uid:10", }, { - Action: accesscontrol.ActionDashboardsPermissionsWrite, + Action: dashboards.ActionDashboardsPermissionsWrite, Scope: "folders:uid:10", }, }, @@ -396,7 +396,7 @@ func TestAccessControlDashboardGuardian_CanDelete(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsDelete, + Action: dashboards.ActionDashboardsDelete, Scope: "dashboards:*", }, }, @@ -407,7 +407,7 @@ func TestAccessControlDashboardGuardian_CanDelete(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsDelete, + Action: dashboards.ActionDashboardsDelete, Scope: "folders:*", }, }, @@ -418,7 +418,7 @@ func TestAccessControlDashboardGuardian_CanDelete(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsDelete, + Action: dashboards.ActionDashboardsDelete, Scope: "dashboards:uid:1", }, }, @@ -429,7 +429,7 @@ func TestAccessControlDashboardGuardian_CanDelete(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsDelete, + Action: dashboards.ActionDashboardsDelete, Scope: "folders:uid:general", }, }, @@ -440,7 +440,7 @@ func TestAccessControlDashboardGuardian_CanDelete(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsDelete, + Action: dashboards.ActionDashboardsDelete, Scope: "dashboards:uid:10", }, }, @@ -451,7 +451,7 @@ func TestAccessControlDashboardGuardian_CanDelete(t *testing.T) { dashUID: "1", permissions: []*accesscontrol.Permission{ { - Action: accesscontrol.ActionDashboardsDelete, + Action: dashboards.ActionDashboardsDelete, Scope: "folders:uid:10", }, }, @@ -485,7 +485,7 @@ func TestAccessControlDashboardGuardian_CanCreate(t *testing.T) { isFolder: false, folderID: 0, permissions: []*accesscontrol.Permission{ - {Action: accesscontrol.ActionDashboardsCreate, Scope: "folders:uid:general"}, + {Action: dashboards.ActionDashboardsCreate, Scope: "folders:uid:general"}, }, expected: true, }, @@ -494,7 +494,7 @@ func TestAccessControlDashboardGuardian_CanCreate(t *testing.T) { isFolder: false, folderID: 0, permissions: []*accesscontrol.Permission{ - {Action: accesscontrol.ActionDashboardsCreate, Scope: "folders:*"}, + {Action: dashboards.ActionDashboardsCreate, Scope: "folders:*"}, }, expected: true, }, diff --git a/pkg/services/sqlstore/annotation_test.go b/pkg/services/sqlstore/annotation_test.go index e5362d49c06..52d57374a6d 100644 --- a/pkg/services/sqlstore/annotation_test.go +++ b/pkg/services/sqlstore/annotation_test.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/annotations" + "github.com/grafana/grafana/pkg/services/dashboards" dashboardstore "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -406,7 +407,7 @@ func TestAnnotationListingWithRBAC(t *testing.T) { description: "Should find all annotations when has permissions to list all annotations and read all dashboards", permissions: map[string][]string{ accesscontrol.ActionAnnotationsRead: {accesscontrol.ScopeAnnotationsAll}, - accesscontrol.ActionDashboardsRead: {accesscontrol.ScopeDashboardsAll}, + dashboards.ActionDashboardsRead: {dashboards.ScopeDashboardsAll}, }, expectedAnnotationIds: []int64{dash1Annotation.Id, dash2Annotation.Id, organizationAnnotation.Id}, }, @@ -414,7 +415,7 @@ func TestAnnotationListingWithRBAC(t *testing.T) { description: "Should find all dashboard annotations", permissions: map[string][]string{ accesscontrol.ActionAnnotationsRead: {accesscontrol.ScopeAnnotationsTypeDashboard}, - accesscontrol.ActionDashboardsRead: {accesscontrol.ScopeDashboardsAll}, + dashboards.ActionDashboardsRead: {dashboards.ScopeDashboardsAll}, }, expectedAnnotationIds: []int64{dash1Annotation.Id, dash2Annotation.Id}, }, @@ -422,7 +423,7 @@ func TestAnnotationListingWithRBAC(t *testing.T) { description: "Should find only annotations from dashboards that user can read", permissions: map[string][]string{ accesscontrol.ActionAnnotationsRead: {accesscontrol.ScopeAnnotationsTypeDashboard}, - accesscontrol.ActionDashboardsRead: {fmt.Sprintf("dashboards:uid:%s", dash1UID)}, + dashboards.ActionDashboardsRead: {fmt.Sprintf("dashboards:uid:%s", dash1UID)}, }, expectedAnnotationIds: []int64{dash1Annotation.Id}, }, @@ -437,14 +438,14 @@ func TestAnnotationListingWithRBAC(t *testing.T) { description: "Should find only organization annotations", permissions: map[string][]string{ accesscontrol.ActionAnnotationsRead: {accesscontrol.ScopeAnnotationsTypeOrganization}, - accesscontrol.ActionDashboardsRead: {accesscontrol.ScopeDashboardsAll}, + dashboards.ActionDashboardsRead: {dashboards.ScopeDashboardsAll}, }, expectedAnnotationIds: []int64{organizationAnnotation.Id}, }, { description: "Should error if user doesn't have annotation read permissions", permissions: map[string][]string{ - accesscontrol.ActionDashboardsRead: {accesscontrol.ScopeDashboardsAll}, + dashboards.ActionDashboardsRead: {dashboards.ScopeDashboardsAll}, }, expectedError: true, }, diff --git a/pkg/services/sqlstore/migrations/accesscontrol/dashboard_permissions.go b/pkg/services/sqlstore/migrations/accesscontrol/dashboard_permissions.go index ea76bb15232..409af398583 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/dashboard_permissions.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/dashboard_permissions.go @@ -16,20 +16,20 @@ import ( var dashboardPermissionTranslation = map[models.PermissionType][]string{ models.PERMISSION_VIEW: { - ac.ActionDashboardsRead, + dashboards.ActionDashboardsRead, }, models.PERMISSION_EDIT: { - ac.ActionDashboardsRead, - ac.ActionDashboardsWrite, - ac.ActionDashboardsDelete, + dashboards.ActionDashboardsRead, + dashboards.ActionDashboardsWrite, + dashboards.ActionDashboardsDelete, }, models.PERMISSION_ADMIN: { - ac.ActionDashboardsRead, - ac.ActionDashboardsWrite, - ac.ActionDashboardsCreate, - ac.ActionDashboardsDelete, - ac.ActionDashboardsPermissionsRead, - ac.ActionDashboardsPermissionsWrite, + dashboards.ActionDashboardsRead, + dashboards.ActionDashboardsWrite, + dashboards.ActionDashboardsCreate, + dashboards.ActionDashboardsDelete, + dashboards.ActionDashboardsPermissionsRead, + dashboards.ActionDashboardsPermissionsWrite, }, } @@ -38,7 +38,7 @@ var folderPermissionTranslation = map[models.PermissionType][]string{ dashboards.ActionFoldersRead, }...), models.PERMISSION_EDIT: append(dashboardPermissionTranslation[models.PERMISSION_EDIT], []string{ - ac.ActionDashboardsCreate, + dashboards.ActionDashboardsCreate, dashboards.ActionFoldersRead, dashboards.ActionFoldersWrite, dashboards.ActionFoldersCreate, diff --git a/pkg/services/sqlstore/permissions/dashboard.go b/pkg/services/sqlstore/permissions/dashboard.go index 21751538381..f67f22e9099 100644 --- a/pkg/services/sqlstore/permissions/dashboard.go +++ b/pkg/services/sqlstore/permissions/dashboard.go @@ -94,10 +94,10 @@ func NewAccessControlDashboardPermissionFilter(user *models.SignedInUser, permis folderActions = append(folderActions, accesscontrol.ActionAlertingRuleCreate) } } else { - dashboardActions = append(dashboardActions, accesscontrol.ActionDashboardsRead) + dashboardActions = append(dashboardActions, dashboards.ActionDashboardsRead) if needEdit { - folderActions = append(folderActions, accesscontrol.ActionDashboardsCreate) - dashboardActions = append(dashboardActions, accesscontrol.ActionDashboardsWrite) + folderActions = append(folderActions, dashboards.ActionDashboardsCreate) + dashboardActions = append(dashboardActions, dashboards.ActionDashboardsWrite) } } return AccessControlDashboardPermissionFilter{User: user, folderActions: folderActions, dashboardActions: dashboardActions} diff --git a/pkg/services/sqlstore/permissions/dashboard_test.go b/pkg/services/sqlstore/permissions/dashboard_test.go index 264954d2739..3645eb4a968 100644 --- a/pkg/services/sqlstore/permissions/dashboard_test.go +++ b/pkg/services/sqlstore/permissions/dashboard_test.go @@ -55,31 +55,31 @@ func TestNewAccessControlDashboardPermissionFilter(t *testing.T) { queryType: randomType, permission: models.PERMISSION_ADMIN, expectedDashboardActions: []string{ - accesscontrol.ActionDashboardsRead, - accesscontrol.ActionDashboardsWrite, + dashboards.ActionDashboardsRead, + dashboards.ActionDashboardsWrite, }, expectedFolderActions: []string{ dashboards.ActionFoldersRead, - accesscontrol.ActionDashboardsCreate, + dashboards.ActionDashboardsCreate, }, }, { queryType: randomType, permission: models.PERMISSION_EDIT, expectedDashboardActions: []string{ - accesscontrol.ActionDashboardsRead, - accesscontrol.ActionDashboardsWrite, + dashboards.ActionDashboardsRead, + dashboards.ActionDashboardsWrite, }, expectedFolderActions: []string{ dashboards.ActionFoldersRead, - accesscontrol.ActionDashboardsCreate, + dashboards.ActionDashboardsCreate, }, }, { queryType: randomType, permission: models.PERMISSION_VIEW, expectedDashboardActions: []string{ - accesscontrol.ActionDashboardsRead, + dashboards.ActionDashboardsRead, }, expectedFolderActions: []string{ dashboards.ActionFoldersRead, From 06d3c27bc11c0f83579550d9c6a3cd03453ed1e2 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 4 May 2022 15:12:59 +0100 Subject: [PATCH 033/440] Select: Portal menu by default (#48176) * Remove menuShouldPortal from all ({ > o.value === dataSourceConfig.access)[0] || DEFAULT_ACCESS_OPTION} diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerFooter.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerFooter.tsx index faead588eb3..d93fdbb6f80 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerFooter.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerFooter.tsx @@ -109,6 +109,7 @@ export const TimePickerFooter: FC = (props) => { {}} /> + } + render={({ field }) => { it('renders with the inputId of its children', () => { render( - {}} /> ); diff --git a/packages/grafana-ui/src/components/Forms/Legacy/Select/Select.story.internal.tsx b/packages/grafana-ui/src/components/Forms/Legacy/Select/Select.story.internal.tsx index 0c1dccb4102..6e3e4748118 100644 --- a/packages/grafana-ui/src/components/Forms/Legacy/Select/Select.story.internal.tsx +++ b/packages/grafana-ui/src/components/Forms/Legacy/Select/Select.story.internal.tsx @@ -72,7 +72,6 @@ export const Basic: Story = (args) => { {(value, updateValue) => { return ( ; + return ; } - return ; + return ; }); FieldNamesMatcherEditor.displayName = 'FieldNameMatcherEditor'; diff --git a/packages/grafana-ui/src/components/MatchersUI/FieldTypeMatcherEditor.tsx b/packages/grafana-ui/src/components/MatchersUI/FieldTypeMatcherEditor.tsx index a4f3a1de274..87c901bef3b 100644 --- a/packages/grafana-ui/src/components/MatchersUI/FieldTypeMatcherEditor.tsx +++ b/packages/grafana-ui/src/components/MatchersUI/FieldTypeMatcherEditor.tsx @@ -19,7 +19,7 @@ export const FieldTypeMatcherEditor = memo>((props) => { ); const selectedOption = selectOptions.find((v) => v.value === options); - return ; }); FieldTypeMatcherEditor.displayName = 'FieldTypeMatcherEditor'; diff --git a/packages/grafana-ui/src/components/MatchersUI/FieldsByFrameRefIdMatcher.tsx b/packages/grafana-ui/src/components/MatchersUI/FieldsByFrameRefIdMatcher.tsx index b50cda2db54..e452fcde5bb 100644 --- a/packages/grafana-ui/src/components/MatchersUI/FieldsByFrameRefIdMatcher.tsx +++ b/packages/grafana-ui/src/components/MatchersUI/FieldsByFrameRefIdMatcher.tsx @@ -26,7 +26,7 @@ export const FieldsByFrameRefIdMatcher = memo>((props) => ); const selectedOption = selectOptions.find((v) => v.value === options); - return ; }); FieldsByFrameRefIdMatcher.displayName = 'FieldsByFrameRefIdMatcher'; diff --git a/packages/grafana-ui/src/components/OptionsUI/fieldColor.tsx b/packages/grafana-ui/src/components/OptionsUI/fieldColor.tsx index 41e6d0fdb61..fa665bc4987 100644 --- a/packages/grafana-ui/src/components/OptionsUI/fieldColor.tsx +++ b/packages/grafana-ui/src/components/OptionsUI/fieldColor.tsx @@ -79,7 +79,6 @@ export const FieldColorEditor: React.FC + ; }; interface ModeProps { diff --git a/packages/grafana-ui/src/components/OptionsUI/multiSelect.tsx b/packages/grafana-ui/src/components/OptionsUI/multiSelect.tsx index 5970d7cddfe..e158b28405d 100644 --- a/packages/grafana-ui/src/components/OptionsUI/multiSelect.tsx +++ b/packages/grafana-ui/src/components/OptionsUI/multiSelect.tsx @@ -61,7 +61,6 @@ export class MultiSelectValueEditor extends React.PureComponent, Sta const { settings } = item; return ( - menuShouldPortal isLoading={isLoading} value={value} defaultValue={value} diff --git a/packages/grafana-ui/src/components/OptionsUI/select.tsx b/packages/grafana-ui/src/components/OptionsUI/select.tsx index 2737963faf9..43506e183e1 100644 --- a/packages/grafana-ui/src/components/OptionsUI/select.tsx +++ b/packages/grafana-ui/src/components/OptionsUI/select.tsx @@ -64,7 +64,6 @@ export class SelectValueEditor extends React.PureComponent, State } return ( - menuShouldPortal isLoading={isLoading} value={current} defaultValue={value} diff --git a/packages/grafana-ui/src/components/Segment/SegmentSelect.tsx b/packages/grafana-ui/src/components/Segment/SegmentSelect.tsx index 6d6a1fed373..2857c07bc62 100644 --- a/packages/grafana-ui/src/components/Segment/SegmentSelect.tsx +++ b/packages/grafana-ui/src/components/Segment/SegmentSelect.tsx @@ -60,7 +60,6 @@ export function SegmentSelect({ return (
= (args) => { return ( <> { @@ -148,7 +146,6 @@ export const SelectWithOptionDescriptions: Story = (args) => { return ( <> { @@ -305,7 +296,6 @@ export const WidthAuto: Story = (args) => { <>
{ diff --git a/packages/grafana-ui/src/components/Select/SelectBase.test.tsx b/packages/grafana-ui/src/components/Select/SelectBase.test.tsx index 26b8b20cb4a..7d9567a4aa9 100644 --- a/packages/grafana-ui/src/components/Select/SelectBase.test.tsx +++ b/packages/grafana-ui/src/components/Select/SelectBase.test.tsx @@ -21,11 +21,11 @@ describe('SelectBase', () => { ]; it('renders without error', () => { - render(); + render(); }); it('renders empty options information', async () => { - render(); + render(); await userEvent.click(screen.getByText(/choose/i)); expect(screen.queryByText(/no options found/i)).toBeVisible(); }); @@ -34,7 +34,7 @@ describe('SelectBase', () => { render( <> - + ); @@ -49,7 +49,7 @@ describe('SelectBase', () => { return ( <> - + ); }; @@ -63,7 +63,7 @@ describe('SelectBase', () => { describe('when openMenuOnFocus prop', () => { describe('is provided', () => { it('opens on focus', () => { - render(); + render(); fireEvent.focus(screen.getByRole('combobox')); expect(screen.queryByText(/no options found/i)).toBeVisible(); }); @@ -75,7 +75,7 @@ describe('SelectBase', () => { ${'ArrowUp'} ${' '} `('opens on arrow down/up or space', ({ key }) => { - render(); + render(); fireEvent.focus(screen.getByRole('combobox')); fireEvent.keyDown(screen.getByRole('combobox'), { key }); expect(screen.queryByText(/no options found/i)).toBeVisible(); @@ -114,7 +114,6 @@ describe('SelectBase', () => { it('should only display maxVisibleValues options, and additional number of values should be displayed as indicator', () => { render( { it('should show all selected options when menu is open', () => { render( { it('should not show all selected options when menu is open', () => { render( { it('should always show all selected options', () => { render( { describe('options', () => { it('renders menu with provided options', async () => { - render(); + render(); await userEvent.click(screen.getByText(/choose/i)); const menuOptions = screen.getAllByLabelText('Select option'); expect(menuOptions).toHaveLength(2); @@ -198,7 +194,7 @@ describe('SelectBase', () => { it('call onChange handler when option is selected', async () => { const spy = jest.fn(); - render(); + render(); const selectEl = screen.getByLabelText('My select'); expect(selectEl).toBeInTheDocument(); diff --git a/packages/grafana-ui/src/components/Select/SelectBase.tsx b/packages/grafana-ui/src/components/Select/SelectBase.tsx index 44b20ad31c3..9bf6d40fadb 100644 --- a/packages/grafana-ui/src/components/Select/SelectBase.tsx +++ b/packages/grafana-ui/src/components/Select/SelectBase.tsx @@ -119,8 +119,7 @@ export function SelectBase({ maxVisibleValues, menuPlacement = 'auto', menuPosition, - // TODO change this to default to true for Grafana 9 - menuShouldPortal = false, + menuShouldPortal = true, noOptionsMessage = 'No options found', onBlur, onChange, diff --git a/packages/grafana-ui/src/components/Select/types.ts b/packages/grafana-ui/src/components/Select/types.ts index a634735b24a..e2ecb59178a 100644 --- a/packages/grafana-ui/src/components/Select/types.ts +++ b/packages/grafana-ui/src/components/Select/types.ts @@ -48,8 +48,7 @@ export interface SelectCommonProps { menuPlacement?: 'auto' | 'bottom' | 'top'; menuPosition?: 'fixed' | 'absolute'; /** - * Setting to true will portal the menu to `document.body`. - * This property will soon default to true and portalling will be the default behavior. + * Setting to false will prevent the menu from portalling to the body. */ menuShouldPortal?: boolean; /** The message to display when no options could be found */ diff --git a/packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx b/packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx index 8fedd043c67..47797c619f6 100644 --- a/packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx +++ b/packages/grafana-ui/src/components/StatsPicker/StatsPicker.tsx @@ -68,7 +68,6 @@ export class StatsPicker extends PureComponent { const select = fieldReducers.selectOptions(stats); return ( - setSelectValue(v?.value!)} /> diff --git a/packages/grafana-ui/src/components/ValuePicker/ValuePicker.tsx b/packages/grafana-ui/src/components/ValuePicker/ValuePicker.tsx index 9edba661e15..1f0f132a0cc 100644 --- a/packages/grafana-ui/src/components/ValuePicker/ValuePicker.tsx +++ b/packages/grafana-ui/src/components/ValuePicker/ValuePicker.tsx @@ -62,7 +62,6 @@ export function ValuePicker({ {isPicking && ( {
<> - {query.data ? (
{describeDataFrame(query.data)}
) : ( diff --git a/public/app/core/components/AccessControl/AddPermission.tsx b/public/app/core/components/AccessControl/AddPermission.tsx index 48ecd803555..5a9b38ba43a 100644 --- a/public/app/core/components/AccessControl/AddPermission.tsx +++ b/public/app/core/components/AccessControl/AddPermission.tsx @@ -85,7 +85,6 @@ export const AddPermission = ({ options={targetOptions} onChange={(v) => setPermissionTarget(v.value!)} disabled={targetOptions.length === 0} - menuShouldPortal /> {target === PermissionTarget.User && canListUsers && ( @@ -100,7 +99,6 @@ export const AddPermission = ({ {target === PermissionTarget.BuiltInRole && ( p === permission)} options={permissions.map((p) => ({ label: p, value: p }))} onChange={(v) => setPermission(v.value || '')} diff --git a/public/app/core/components/AccessControl/PermissionListItem.tsx b/public/app/core/components/AccessControl/PermissionListItem.tsx index 6744c7ca167..93aaeb6ee40 100644 --- a/public/app/core/components/AccessControl/PermissionListItem.tsx +++ b/public/app/core/components/AccessControl/PermissionListItem.tsx @@ -20,7 +20,6 @@ export const PermissionListItem = ({ item, permissionLevels, canSet, onRemove, o
= ({ onChange, value, placeholder, filter }) const selected = options?.find((opt) => opt.value === value); return !loading ? ( dashboard.id === homeDashboardId)} getOptionValue={(i) => i.id} getOptionLabel={this.getFullDashName} diff --git a/public/app/core/components/TagFilter/TagFilter.tsx b/public/app/core/components/TagFilter/TagFilter.tsx index ed262ec9974..5b223b392e6 100644 --- a/public/app/core/components/TagFilter/TagFilter.tsx +++ b/public/app/core/components/TagFilter/TagFilter.tsx @@ -154,7 +154,7 @@ export const TagFilter: FC = ({ Clear tags )} - } aria-label="Tag filter" /> + } aria-label="Tag filter" />
); }; diff --git a/public/app/core/components/editors/DashboardPickerByID.tsx b/public/app/core/components/editors/DashboardPickerByID.tsx index 7bcc9bee8fb..a54279e4aba 100644 --- a/public/app/core/components/editors/DashboardPickerByID.tsx +++ b/public/app/core/components/editors/DashboardPickerByID.tsx @@ -45,7 +45,6 @@ export const DashboardPickerByID: FC = ({ return ( ({ label: key, value: key })); export function OrgRolePicker({ value, onChange, 'aria-label': ariaLabel, inputId, autoFocus, ...restProps }: Props) { return ( = ({ } control={control} rules={{ required: true }} /> diff --git a/public/app/features/alerting/components/OptionElement.tsx b/public/app/features/alerting/components/OptionElement.tsx index aa0de6ad70d..ee8472bbf92 100644 --- a/public/app/features/alerting/components/OptionElement.tsx +++ b/public/app/features/alerting/components/OptionElement.tsx @@ -31,7 +31,7 @@ export const OptionElement: FC = ({ control, option, register, invalid }) control={control} name={`${modelValue}`} render={({ field: { ref, ...field } }) => ( - )} /> ); diff --git a/public/app/features/alerting/unified/components/AlertManagerPicker.tsx b/public/app/features/alerting/unified/components/AlertManagerPicker.tsx index 19302227d16..e775215be28 100644 --- a/public/app/features/alerting/unified/components/AlertManagerPicker.tsx +++ b/public/app/features/alerting/unified/components/AlertManagerPicker.tsx @@ -44,7 +44,6 @@ export const AlertManagerPicker: FC = ({ onChange, current, disabled = fa > onChange(mapSelectValueToString(value))} @@ -170,7 +167,6 @@ export const AmRootRouteForm: FC = ({ ( = ({ onCancel, onChange={(value) => onChange(value?.value)} options={matcherFieldOptions} aria-label="Operator" - menuShouldPortal /> )} defaultValue={field.operator} @@ -153,7 +152,6 @@ export const AmRoutesExpandedForm: FC = ({ onCancel, className={formStyles.input} onChange={(value) => onChange(mapSelectValueToString(value))} options={receivers} - menuShouldPortal /> )} control={control} @@ -176,7 +174,6 @@ export const AmRoutesExpandedForm: FC = ({ onCancel, render={({ field: { onChange, ref, ...field } }) => ( = ({ onCancel, ( onChange(mapSelectValueToString(value))} @@ -313,7 +308,6 @@ export const AmRoutesExpandedForm: FC = ({ onCancel, ( = ({ render={({ field: { onChange, ref, ...field } }) => ( onChange(value?.value)} diff --git a/public/app/features/alerting/unified/components/rule-editor/ConditionField.tsx b/public/app/features/alerting/unified/components/rule-editor/ConditionField.tsx index 8d78dbe9c2f..6df7254f6ca 100644 --- a/public/app/features/alerting/unified/components/rule-editor/ConditionField.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/ConditionField.tsx @@ -53,7 +53,6 @@ export const ConditionField: FC = () => { name="condition" render={({ field: { onChange, ref, ...field } }) => ( ; + return = ({ className }) => { render={({ field: { onChange, ref, ...field } }) => ( v.value === mapping.source) || valueOptions[0]} options={valueOptions} onChange={(v: SelectableValue) => { @@ -157,7 +157,6 @@ export class AnnotationFieldMapper extends PureComponent { */} + - {linkSettings.type === 'dashboards' && ( <> @@ -113,7 +107,7 @@ export const LinkSettingsEdit: React.FC = ({ editLinkIdx, - )} diff --git a/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx b/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx index 4cb6c09d2ba..ee8a3ff4145 100644 --- a/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx +++ b/public/app/features/dashboard/components/PanelEditor/getPanelFrameOptions.tsx @@ -138,7 +138,6 @@ export function getPanelFrameCategory(props: OptionPaneRenderProps): OptionsPane const maxPerRowOptions = [2, 3, 4, 6, 8, 12].map((value) => ({ label: value.toString(), value })); return ( ; + return
{ diff --git a/public/app/features/dimensions/editors/ScalarDimensionEditor.tsx b/public/app/features/dimensions/editors/ScalarDimensionEditor.tsx index dc6bc6c4de7..e5c8ae475e6 100644 --- a/public/app/features/dimensions/editors/ScalarDimensionEditor.tsx +++ b/public/app/features/dimensions/editors/ScalarDimensionEditor.tsx @@ -87,7 +87,6 @@ export const ScalarDimensionEditor: FC v.value === mapping.specialMatch)} options={specialMatchOptions} onChange={onChangeSpecialMatch} diff --git a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx index 2d1181dba94..5b62f2faac2 100644 --- a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx @@ -192,7 +192,6 @@ export function RichHistoryQueriesTab(props: Props) { {!richHistorySettings.activeDatasourceOnly && ( { return { value: ds.name, label: ds.name }; })} @@ -213,7 +212,6 @@ export function RichHistoryQueriesTab(props: Props) {
+
diff --git a/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx b/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx index 60ed12e07e4..01aec0f4a67 100644 --- a/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryStarredTab.tsx @@ -115,7 +115,6 @@ export function RichHistoryStarredTab(props: Props) { {!richHistorySettings.activeDatasourceOnly && ( { return { value: ds.name, label: ds.name }; })} @@ -136,7 +135,6 @@ export function RichHistoryStarredTab(props: Props) {
+ = ({ condition, index, onChange, onRemoveCondi />
OF
+ + + + - - diff --git a/public/app/features/geo/editor/GazetteerPathEditor.tsx b/public/app/features/geo/editor/GazetteerPathEditor.tsx index 6a566193d7f..c85fe657f5f 100644 --- a/public/app/features/geo/editor/GazetteerPathEditor.tsx +++ b/public/app/features/geo/editor/GazetteerPathEditor.tsx @@ -62,7 +62,6 @@ export const GazetteerPathEditor: FC { options={jsonOptions} value={selected} onChange={this.onSelectChanged} - menuShouldPortal /> {this.hasPanelJSON && isPanelJSON && canEdit && ( diff --git a/public/app/features/library-panels/components/OpenLibraryPanelModal/OpenLibraryPanelModal.tsx b/public/app/features/library-panels/components/OpenLibraryPanelModal/OpenLibraryPanelModal.tsx index 6bb986fcfb0..ab43e2a1500 100644 --- a/public/app/features/library-panels/components/OpenLibraryPanelModal/OpenLibraryPanelModal.tsx +++ b/public/app/features/library-panels/components/OpenLibraryPanelModal/OpenLibraryPanelModal.tsx @@ -56,7 +56,6 @@ export function OpenLibraryPanelModal({ libraryPanel, onDismiss }: OpenLibraryPa .Please choose which dashboard to view the panel in:

= ({ onChange, value, ruleType, <> this.onPermissionChange(item, member)} diff --git a/public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap index dab73d76174..84467bdfb36 100644 --- a/public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap @@ -110,7 +110,6 @@ exports[`Render when feature toggle editorsCanAdmin is turned off should not ren isMulti={false} isSearchable={false} maxMenuHeight={300} - menuShouldPortal={true} onChange={[Function]} openMenuOnFocus={false} options={ @@ -203,7 +202,6 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p isMulti={false} isSearchable={false} maxMenuHeight={300} - menuShouldPortal={true} onChange={[Function]} openMenuOnFocus={false} options={ diff --git a/public/app/features/transformers/FilterByValueTransformer/FilterByValueFilterEditor.tsx b/public/app/features/transformers/FilterByValueTransformer/FilterByValueFilterEditor.tsx index 2afb51bd98a..219d68fd291 100644 --- a/public/app/features/transformers/FilterByValueTransformer/FilterByValueFilterEditor.tsx +++ b/public/app/features/transformers/FilterByValueTransformer/FilterByValueFilterEditor.tsx @@ -79,7 +79,6 @@ export const FilterByValueFilterEditor: React.FC = (props) => {
Field
- - @@ -96,7 +96,7 @@ export const configFromQueryTransformRegistryItem: TransformerRegistryItem
v.value === mode)} diff --git a/public/app/features/transformers/editors/ConcatenateTransformerEditor.tsx b/public/app/features/transformers/editors/ConcatenateTransformerEditor.tsx index fe60117bd59..9ab8f91239d 100644 --- a/public/app/features/transformers/editors/ConcatenateTransformerEditor.tsx +++ b/public/app/features/transformers/editors/ConcatenateTransformerEditor.tsx @@ -58,7 +58,6 @@ export class ConcatenateTransformerEditor extends React.PureComponent
Name
= ({ fieldName, con
+ + + + {row.fieldName} v.value === options.format) || formats[0]} diff --git a/public/app/features/variables/editor/VariableSelectField.tsx b/public/app/features/variables/editor/VariableSelectField.tsx index b08aca6ab5c..d464dc3f3c1 100644 --- a/public/app/features/variables/editor/VariableSelectField.tsx +++ b/public/app/features/variables/editor/VariableSelectField.tsx @@ -38,7 +38,6 @@ export function VariableSelectField({
props.onChange(value!)} value={selected} diff --git a/public/app/plugins/datasource/cloud-monitoring/components/AlignmentFunction.tsx b/public/app/plugins/datasource/cloud-monitoring/components/AlignmentFunction.tsx index fff6b5f09f0..21baefb8db5 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/AlignmentFunction.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/AlignmentFunction.tsx @@ -23,7 +23,6 @@ export const AlignmentFunction: FC = ({ inputId, query, templateVariableO return ( onChange({ ...query, alignmentPeriod: value! })} value={[...options, ...templateVariableOptions].find((s) => s.value === query.alignmentPeriod)} diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Fields.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Fields.tsx index e18b176402c..9f2495666e0 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/Fields.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/Fields.tsx @@ -24,7 +24,6 @@ export const VariableQueryField: FC = ({ return ( @@ -102,7 +101,6 @@ export const LabelFilter: FunctionComponent = ({ return ( @@ -128,7 +125,6 @@ export const LabelFilter: FunctionComponent = ({ renderControl={OperatorButton} /> s.value === service)} @@ -157,7 +156,6 @@ export function Metrics(props: Props) { `Use project: ${v}`} diff --git a/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx index bd2fd416c79..7dd2a810087 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/QueryEditor.tsx @@ -80,7 +80,6 @@ export class QueryEditor extends PureComponent { htmlFor={`${query.refId}-query-type`} > = ({ refId, query, templateVariableOption return ( { expect(namespaceSelect).toBeInTheDocument(); expect(metricsSelect).toBeInTheDocument(); - await selectEvent.select(namespaceSelect, 'n1'); - await selectEvent.select(metricsSelect, 'm1'); + await selectEvent.select(namespaceSelect, 'n1', { container: document.body }); + await selectEvent.select(metricsSelect, 'm1', { container: document.body }); expect(onChange.mock.calls).toEqual([ [{ ...propsNamespaceMetrics.metricStat, namespace: 'n1' }], // First call, namespace select @@ -167,7 +167,7 @@ describe('MetricStatEditor', () => { expect(screen.getByText('n2')).toBeInTheDocument(); expect(screen.getByText('oldNamespaceMetric')).toBeInTheDocument(); - await selectEvent.select(namespaceSelect, 'n1'); + await selectEvent.select(namespaceSelect, 'n1', { container: document.body }); expect(onChange.mock.calls).toEqual([[{ ...propsNamespaceMetrics.metricStat, metricName: '', namespace: 'n1' }]]); }); @@ -183,7 +183,7 @@ describe('MetricStatEditor', () => { expect(screen.getByText('n1')).toBeInTheDocument(); expect(screen.getByText('m1')).toBeInTheDocument(); - await selectEvent.select(namespaceSelect, 'n2'); + await selectEvent.select(namespaceSelect, 'n2', { container: document.body }); expect(onChange).toHaveBeenCalledTimes(1); expect(onChange.mock.calls).toEqual([ diff --git a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderSelectRow.tsx b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderSelectRow.tsx index 6b1a6781980..dac84f9a17e 100644 --- a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderSelectRow.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderSelectRow.tsx @@ -82,7 +82,6 @@ const SQLBuilderSelectRow: React.FC = ({ datasource, q options={namespaceOptions} allowCustomValue onChange={({ value }) => value && onNamespaceChange(setNamespace(query, value))} - menuShouldPortal /> @@ -106,7 +105,6 @@ const SQLBuilderSelectRow: React.FC = ({ datasource, q options={dimensionKeys} allowCustomValue onChange={(item) => item && onQueryChange(setSchemaLabels(query, item))} - menuShouldPortal /> )} @@ -120,7 +118,6 @@ const SQLBuilderSelectRow: React.FC = ({ datasource, q options={metricOptions} allowCustomValue onChange={({ value }) => value && onQueryChange(setMetricName(query, value))} - menuShouldPortal /> @@ -130,7 +127,6 @@ const SQLBuilderSelectRow: React.FC = ({ datasource, q value={aggregation ? toOption(aggregation) : null} options={appendTemplateVariables(datasource, AGGREGATIONS)} onChange={({ value }) => value && onQueryChange(setAggregation(query, value))} - menuShouldPortal /> diff --git a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLFilter.tsx b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLFilter.tsx index d42f7e98079..21cb4928940 100644 --- a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLFilter.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLFilter.tsx @@ -131,7 +131,6 @@ const FilterItem: React.FC = (props) => { options={dimensionKeys} allowCustomValue onChange={({ value }) => value && onChange(setOperatorExpressionProperty(filter, value))} - menuShouldPortal /> = (props) => { allowCustomValue onOpenMenu={loadOptions} onChange={({ value }) => value && onChange(setOperatorExpressionValue(filter, value))} - menuShouldPortal /> diff --git a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLGroupBy.tsx b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLGroupBy.tsx index 5a1c00d9bb4..9c6eb2ef80a 100644 --- a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLGroupBy.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLGroupBy.tsx @@ -100,7 +100,6 @@ const GroupByItem: React.FC = (props) => { options={options} allowCustomValue onChange={({ value }) => value && onChange(setGroupByField(value))} - menuShouldPortal /> diff --git a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLOrderByGroup.tsx b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLOrderByGroup.tsx index ea98f2e2d4f..0a590e72a32 100644 --- a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLOrderByGroup.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLOrderByGroup.tsx @@ -36,7 +36,6 @@ const SQLOrderByGroup: React.FC = ({ query, onQueryCha onChange={({ value }) => value && onQueryChange(setOrderBy(query, value))} options={appendTemplateVariables(datasource, STATISTICS.map(toOption))} value={orderBy ? toOption(orderBy) : null} - menuShouldPortal /> {orderBy && ( = ({ query, onQueryCha value={orderByDirection ? toOption(orderByDirection) : orderByDirections[0]} options={appendTemplateVariables(datasource, orderByDirections)} onChange={(item) => item && onQueryChange(setSql(query, { orderByDirection: item.value }))} - menuShouldPortal /> diff --git a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryField.tsx b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryField.tsx index 53a32f5db91..4c7598e5a30 100644 --- a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryField.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryField.tsx @@ -29,7 +29,6 @@ export const VariableQueryField = ({ return ( { <> dispatch(changeBucketAggregationSetting({ bucketAgg, settingName: 'order', newValue: e.value })) } @@ -49,7 +48,6 @@ export const TermsSettingsEditor = ({ bucketAgg }: Props) => { dispatch(changeBucketAggregationSetting({ bucketAgg, settingName: 'orderBy', newValue: e.value })) } diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/MovingAverageSettingsEditor.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/MovingAverageSettingsEditor.tsx index 4d0793888e5..e253d4faab7 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/MovingAverageSettingsEditor.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/MovingAverageSettingsEditor.tsx @@ -26,7 +26,6 @@ export const MovingAverageSettingsEditor = ({ metric }: Props) => { dispatch(changeMetricSetting({ metric, settingName: 'order', newValue: e.value }))} options={orderOptions} value={metric.settings?.order} diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/index.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/index.tsx index 50f7f82394a..8fde3e19f2a 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/index.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/index.tsx @@ -139,7 +139,6 @@ export const SettingsEditor = ({ metric, previousMetrics }: Props) => { <> dispatch(changeMetricSetting({ metric, settingName: 'mode', newValue: e.value }))} options={rateAggModeOptions} diff --git a/public/app/plugins/datasource/elasticsearch/components/hooks/useCreatableSelectPersistedBehaviour.test.tsx b/public/app/plugins/datasource/elasticsearch/components/hooks/useCreatableSelectPersistedBehaviour.test.tsx index 9e5cf38d389..20049f5a7ed 100644 --- a/public/app/plugins/datasource/elasticsearch/components/hooks/useCreatableSelectPersistedBehaviour.test.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/hooks/useCreatableSelectPersistedBehaviour.test.tsx @@ -11,7 +11,6 @@ describe('useCreatableSelectPersistedBehaviour', () => { const MyComp = (_: { force?: boolean }) => ( { const MyComp = (_: { force?: boolean }) => ( opt.value === credentials.authType)} options={authTypeOptions} @@ -179,7 +178,6 @@ export const AzureCredentialsForm: FunctionComponent = (props: Props) => opt.value === credentials.defaultSubscriptionId) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/FormatAsField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/FormatAsField.tsx index 75c86f58680..bf79277f4e4 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/FormatAsField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/FormatAsField.tsx @@ -32,7 +32,6 @@ const FormatAsField: React.FC = ({ query, variableOp return ( = ({ data, query, dimensio {dimensionFilters.map((filter, index) => ( = ({ return ( = ({ return ( = ({ return ( = ({ return multiSelect ? ( = ({ ) : ( {
{ { Type httpMode.value === options.jsonData.httpMode)} options={httpModes} @@ -280,7 +279,6 @@ export class ConfigEditor extends PureComponent {
{ onChange({ ...query, resultFormat: v.value }); diff --git a/public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/Seg.tsx b/public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/Seg.tsx index a64203668e0..ad2abab2a39 100644 --- a/public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/Seg.tsx +++ b/public/app/plugins/datasource/influxdb/components/VisualInfluxQLEditor/Seg.tsx @@ -78,7 +78,6 @@ const SelReload = ({ loadOptions, allowCustomValue, onChange, onClose }: SelRelo return (
version.value === value.jsonData.tsdbVersion) ?? tsdbVersions[0]} onChange={onSelectChangeHandler('tsdbVersion', value, onChange)} @@ -50,7 +49,6 @@ export const OpenTsdbDetails = (props: Props) => {
Format
opt.value === credentials.authType)} options={authTypeOptions} @@ -175,7 +174,6 @@ export const AzureCredentialsForm: FunctionComponent = (props: Props) => Azure Cloud opt.value === credentials.defaultSubscriptionId) diff --git a/public/app/plugins/datasource/prometheus/configuration/PromSettings.tsx b/public/app/plugins/datasource/prometheus/configuration/PromSettings.tsx index 11fcdf47f17..583af4c28cf 100644 --- a/public/app/plugins/datasource/prometheus/configuration/PromSettings.tsx +++ b/public/app/plugins/datasource/prometheus/configuration/PromSettings.tsx @@ -86,7 +86,6 @@ export const PromSettings = (props: Props) => { { diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/MetricSelect.test.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/MetricSelect.test.tsx index d527a581baa..10681e3b87b 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/MetricSelect.test.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/MetricSelect.test.tsx @@ -59,39 +59,39 @@ describe('MetricSelect', () => { }); it('highlights matching string', async () => { - const { container } = render(); + render(); await openMetricSelect(); const input = screen.getByRole('combobox'); await userEvent.type(input, 'more'); - await waitFor(() => expect(container.querySelectorAll('mark')).toHaveLength(1)); + await waitFor(() => expect(document.querySelectorAll('mark')).toHaveLength(1)); }); it('highlights multiple matching strings in 1 input row', async () => { - const { container } = render(); + render(); await openMetricSelect(); const input = screen.getByRole('combobox'); await userEvent.type(input, 'more metric'); - await waitFor(() => expect(container.querySelectorAll('mark')).toHaveLength(2)); + await waitFor(() => expect(document.querySelectorAll('mark')).toHaveLength(2)); }); it('highlights multiple matching strings in multiple input rows', async () => { - const { container } = render(); + render(); await openMetricSelect(); const input = screen.getByRole('combobox'); await userEvent.type(input, 'unique metric'); - await waitFor(() => expect(container.querySelectorAll('mark')).toHaveLength(4)); + await waitFor(() => expect(document.querySelectorAll('mark')).toHaveLength(4)); }); it('does not highlight matching string in create option', async () => { - const { container } = render(); + render(); await openMetricSelect(); const input = screen.getByRole('combobox'); await userEvent.type(input, 'new'); - await waitFor(() => expect(container.querySelector('mark')).not.toBeInTheDocument()); + await waitFor(() => expect(document.querySelector('mark')).not.toBeInTheDocument()); }); }); async function openMetricSelect() { - const select = await screen.getByText('Select metric').parentElement!; + const select = screen.getByText('Select metric').parentElement!; await userEvent.click(select); } diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx index f2c395e10c1..67ef8b17e7b 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx @@ -89,7 +89,6 @@ export const PromQueryBuilderOptions = React.memo(({ query, app, onChange item.value === query.scenarioId)} onChange={onScenarioChange} @@ -275,7 +274,6 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props) {scenarioId === 'grafana_api' && ( { { return ( - {query?.stream?.type === 'signal' && streamingClientFields.map(({ label, id, min, step, placeholder }) => { diff --git a/public/app/plugins/datasource/testdata/components/USAQueryEditor.tsx b/public/app/plugins/datasource/testdata/components/USAQueryEditor.tsx index 160a8223a4f..b90f94120d7 100644 --- a/public/app/plugins/datasource/testdata/components/USAQueryEditor.tsx +++ b/public/app/plugins/datasource/testdata/components/USAQueryEditor.tsx @@ -16,7 +16,6 @@ export function USAQueryEditor({ query, onChange }: Props) { + {
- s.value === scope)} onChange={this.onScopeChanged} />
{scope && (
{
{this.renderTable(data.series[currentIndex], width, height - inputHeight + padding)}
-
); diff --git a/public/app/plugins/panel/timeseries/FillBelowToEditor.tsx b/public/app/plugins/panel/timeseries/FillBelowToEditor.tsx index d2614d3ae71..207b1ec8ba6 100644 --- a/public/app/plugins/panel/timeseries/FillBelowToEditor.tsx +++ b/public/app/plugins/panel/timeseries/FillBelowToEditor.tsx @@ -38,7 +38,6 @@ export const FillBellowToEditor: React.FC> return ( - ); + return v.value === value?.frame) ?? frameNames[0]} onChange={(v) => { @@ -104,7 +103,6 @@ export const XYDimsEditor: FC - + )} diff --git a/public/app/features/admin/CrawlerStartButton.tsx b/public/app/features/admin/CrawlerStartButton.tsx index 3cceb47b7f3..69824c08f0f 100644 --- a/public/app/features/admin/CrawlerStartButton.tsx +++ b/public/app/features/admin/CrawlerStartButton.tsx @@ -39,7 +39,9 @@ export const CrawlerStartButton = () => { />
- + diff --git a/public/app/features/alerting/unified/components/admin/AddAlertManagerModal.tsx b/public/app/features/alerting/unified/components/admin/AddAlertManagerModal.tsx index 05cdfebf444..4549e02be71 100644 --- a/public/app/features/alerting/unified/components/admin/AddAlertManagerModal.tsx +++ b/public/app/features/alerting/unified/components/admin/AddAlertManagerModal.tsx @@ -82,7 +82,9 @@ export const AddAlertManagerModal: FC = ({ alertmanagers, onChangeAlertma )}
- +
)} diff --git a/public/app/features/alerting/unified/components/receivers/ReceiversSection.tsx b/public/app/features/alerting/unified/components/receivers/ReceiversSection.tsx index 93c14ce10cd..9c096dc8b60 100644 --- a/public/app/features/alerting/unified/components/receivers/ReceiversSection.tsx +++ b/public/app/features/alerting/unified/components/receivers/ReceiversSection.tsx @@ -33,7 +33,9 @@ export const ReceiversSection: FC = ({
{showButton && ( - + )}
diff --git a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx index 6d66b8ad83f..1777b4cfe35 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx @@ -152,7 +152,11 @@ export const TemplateForm: FC = ({ existing, alertManagerSourceName, conf Saving... )} - {!loading && } + {!loading && ( + + )} = ({ show, onClose, onKeyAdded, disabled })
- +
diff --git a/public/app/features/canvas/elements/button.tsx b/public/app/features/canvas/elements/button.tsx index 03f413012c4..1aebc39d5f9 100644 --- a/public/app/features/canvas/elements/button.tsx +++ b/public/app/features/canvas/elements/button.tsx @@ -27,7 +27,11 @@ class ButtonDisplay extends PureComponent{data?.text}; + return ( + + ); } } diff --git a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx index 886716a6af1..1137e6de801 100644 --- a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx @@ -41,7 +41,9 @@ const onClose = () => locationService.partial({ editview: null }); const MakeEditable = (props: { onMakeEditable: () => any }) => (
Dashboard not editable
- +
); diff --git a/public/app/features/dashboard/components/DashboardSettings/JsonEditorSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/JsonEditorSettings.tsx index 55e9317942e..7dea62f90a5 100644 --- a/public/app/features/dashboard/components/DashboardSettings/JsonEditorSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/JsonEditorSettings.tsx @@ -54,7 +54,9 @@ export const JsonEditorSettings: React.FC = ({ dashboard }) => {
{dashboard.meta.canSave && ( - + )}
diff --git a/public/app/features/dashboard/components/SaveDashboard/forms/SaveProvisionedDashboardForm.tsx b/public/app/features/dashboard/components/SaveDashboard/forms/SaveProvisionedDashboardForm.tsx index 8fc0075998e..8fdb65546d6 100644 --- a/public/app/features/dashboard/components/SaveDashboard/forms/SaveProvisionedDashboardForm.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/forms/SaveProvisionedDashboardForm.tsx @@ -67,7 +67,9 @@ export const SaveProvisionedDashboardForm: React.FC = ({ dashboardJSON} onClipboardCopy={onCopyToClipboardSuccess}> Copy JSON to clipboard - + diff --git a/public/app/features/profile/ChangePasswordForm.tsx b/public/app/features/profile/ChangePasswordForm.tsx index d86dcd1e8e9..07f2553f6eb 100644 --- a/public/app/features/profile/ChangePasswordForm.tsx +++ b/public/app/features/profile/ChangePasswordForm.tsx @@ -69,7 +69,7 @@ export const ChangePasswordForm: FC = ({ user, onChangePassword, isSaving /> - diff --git a/public/app/features/profile/UserProfileEditForm.tsx b/public/app/features/profile/UserProfileEditForm.tsx index f3c8927fbe9..c5748eb515f 100644 --- a/public/app/features/profile/UserProfileEditForm.tsx +++ b/public/app/features/profile/UserProfileEditForm.tsx @@ -74,6 +74,7 @@ export const UserProfileEditForm: FC = ({ user, isSavingUser, updateProfi variant="primary" disabled={isSavingUser} data-testid={selectors.components.UserProfile.profileSaveButton} + type="submit" > Save From dac8abfc2c9a5dfb0097440f3188e64f27f030d4 Mon Sep 17 00:00:00 2001 From: Jeff Levin Date: Wed, 4 May 2022 10:38:04 -0800 Subject: [PATCH 040/440] chore: update contribution docs (#46942) * add bus deprecation note in contribution docs --- contribute/architecture/backend/communication.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contribute/architecture/backend/communication.md b/contribute/architecture/backend/communication.md index d4836295d9e..f59f598cdf5 100644 --- a/contribute/architecture/backend/communication.md +++ b/contribute/architecture/backend/communication.md @@ -2,6 +2,8 @@ Grafana uses a _bus_ to pass messages between different parts of the application. All communication over the bus happens synchronously. +> **Deprecated:** The bus has officially been deprecated, however, we're still using the command/query objects paradigms. + There are three types of messages: _events_, _commands_, and _queries_. ## Events From 570ff074f6fc83663e35ed57b54ed7f825d38553 Mon Sep 17 00:00:00 2001 From: "Josiah (Jay) Goodson" Date: Wed, 4 May 2022 14:58:46 -0400 Subject: [PATCH 041/440] Transformations: Add an All Unique Values Reducer (#48653) --- .../src/transformations/fieldReducer.test.ts | 9 +++++++++ .../grafana-data/src/transformations/fieldReducer.ts | 10 ++++++++++ 2 files changed, 19 insertions(+) diff --git a/packages/grafana-data/src/transformations/fieldReducer.test.ts b/packages/grafana-data/src/transformations/fieldReducer.test.ts index 243479b7f2d..34de7c94f90 100644 --- a/packages/grafana-data/src/transformations/fieldReducer.test.ts +++ b/packages/grafana-data/src/transformations/fieldReducer.test.ts @@ -97,6 +97,15 @@ describe('Stats Calculators', () => { expect(stats.delta).toEqual(300); }); + it('should calculate unique values', () => { + const stats = reduceField({ + field: createField('x', [1, 2, 2, 3, 1]), + reducers: [ReducerID.uniqueValues], + }); + + expect(stats.uniqueValues).toEqual([1, 2, 3]); + }); + it('consistently check allIsNull/allIsZero', () => { const empty = createField('x'); const allNull = createField('x', [null, null, null, null]); diff --git a/packages/grafana-data/src/transformations/fieldReducer.ts b/packages/grafana-data/src/transformations/fieldReducer.ts index aaf400ba105..c593ac47346 100644 --- a/packages/grafana-data/src/transformations/fieldReducer.ts +++ b/packages/grafana-data/src/transformations/fieldReducer.ts @@ -25,6 +25,7 @@ export enum ReducerID { allIsZero = 'allIsZero', allIsNull = 'allIsNull', allValues = 'allValues', + uniqueValues = 'uniqueValues', } // Internal function @@ -237,6 +238,15 @@ export const fieldReducers = new Registry(() => [ standard: false, reduce: (field: Field) => ({ allValues: field.values.toArray() }), }, + { + id: ReducerID.uniqueValues, + name: 'All unique values', + description: 'Returns an array with all unique values', + standard: false, + reduce: (field: Field) => ({ + uniqueValues: [...new Set(field.values.toArray())], + }), + }, ]); export function doStandardCalcs(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs { From 6e6f6e3cced6277f77b670fc4294be727d2b2074 Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Wed, 4 May 2022 15:01:18 -0400 Subject: [PATCH 042/440] Converter: Add support for parsing error & warning from prometheus results (#48721) --- pkg/util/converter/prom.go | 49 +++- pkg/util/converter/prom_test.go | 7 + .../converter/testdata/prom-error-frame.json | 3 + .../converter/testdata/prom-error-golden.txt | 3 + pkg/util/converter/testdata/prom-error.json | 5 + .../testdata/prom-warnings-frame.json | 263 ++++++++++++++++++ .../testdata/prom-warnings-golden.txt | 132 +++++++++ .../converter/testdata/prom-warnings.json | 37 +++ 8 files changed, 494 insertions(+), 5 deletions(-) create mode 100644 pkg/util/converter/testdata/prom-error-frame.json create mode 100644 pkg/util/converter/testdata/prom-error-golden.txt create mode 100644 pkg/util/converter/testdata/prom-error.json create mode 100644 pkg/util/converter/testdata/prom-warnings-frame.json create mode 100644 pkg/util/converter/testdata/prom-warnings-golden.txt create mode 100644 pkg/util/converter/testdata/prom-warnings.json diff --git a/pkg/util/converter/prom.go b/pkg/util/converter/prom.go index 640dd148eac..7c238425b92 100644 --- a/pkg/util/converter/prom.go +++ b/pkg/util/converter/prom.go @@ -19,6 +19,9 @@ func logf(format string, a ...interface{}) { func ReadPrometheusStyleResult(iter *jsoniter.Iterator) *backend.DataResponse { var rsp *backend.DataResponse status := "unknown" + errorType := "" + err := "" + warnings := []data.Notice{} for l1Field := iter.ReadObject(); l1Field != ""; l1Field = iter.ReadObject() { switch l1Field { @@ -28,22 +31,58 @@ func ReadPrometheusStyleResult(iter *jsoniter.Iterator) *backend.DataResponse { case "data": rsp = readPrometheusData(iter) - // case "error": - // case "errorType": - // case "warnings": + case "error": + err = iter.ReadString() + + case "errorType": + errorType = iter.ReadString() + + case "warnings": + warnings = readWarnings(iter) + default: v := iter.Read() logf("[ROOT] TODO, support key: %s / %v\n", l1Field, v) } } - if status != "success" { - logf("ERROR: %s\n", status) + if status == "error" { + return &backend.DataResponse{ + Error: fmt.Errorf("%s: %s", errorType, err), + } + } + + if len(warnings) > 0 { + for _, frame := range rsp.Frames { + if frame.Meta == nil { + frame.Meta = &data.FrameMeta{} + } + frame.Meta.Notices = warnings + } } return rsp } +func readWarnings(iter *jsoniter.Iterator) []data.Notice { + warnings := []data.Notice{} + if iter.WhatIsNext() != jsoniter.ArrayValue { + return warnings + } + + for iter.ReadArray() { + if iter.WhatIsNext() == jsoniter.StringValue { + notice := data.Notice{ + Severity: data.NoticeSeverityWarning, + Text: iter.ReadString(), + } + warnings = append(warnings, notice) + } + } + + return warnings +} + func readPrometheusData(iter *jsoniter.Iterator) *backend.DataResponse { t := iter.WhatIsNext() if t == jsoniter.ArrayValue { diff --git a/pkg/util/converter/prom_test.go b/pkg/util/converter/prom_test.go index 04b71545364..10719e8f551 100644 --- a/pkg/util/converter/prom_test.go +++ b/pkg/util/converter/prom_test.go @@ -20,6 +20,8 @@ func TestReadPromFrames(t *testing.T) { "prom-matrix-with-nans", "prom-vector", "prom-series", + "prom-warnings", + "prom-error", "prom-exemplars", "loki-streams-a", "loki-streams-b", @@ -59,6 +61,11 @@ func TestReadPromFrames(t *testing.T) { require.NoError(t, err) } + // skip checking golden file for error response. it's not currently supported + if name == "prom-error" { + return + } + fpath = path.Join("testdata", name+"-golden.txt") err = experimental.CheckGoldenDataResponse(fpath, rsp, true) assert.NoError(t, err) diff --git a/pkg/util/converter/testdata/prom-error-frame.json b/pkg/util/converter/testdata/prom-error-frame.json new file mode 100644 index 00000000000..9fa71e9db64 --- /dev/null +++ b/pkg/util/converter/testdata/prom-error-frame.json @@ -0,0 +1,3 @@ +{ + "error": "bad_data: invalid parameter \"start\": cannot parse \"\" to a valid timestamp" +} \ No newline at end of file diff --git a/pkg/util/converter/testdata/prom-error-golden.txt b/pkg/util/converter/testdata/prom-error-golden.txt new file mode 100644 index 00000000000..c078e905cdf --- /dev/null +++ b/pkg/util/converter/testdata/prom-error-golden.txt @@ -0,0 +1,3 @@ + +ERROR: bad_data: invalid parameter "start": cannot parse "" to a valid timestamp====== TEST DATA RESPONSE (arrow base64) ====== +ERROR=bad_data: invalid parameter "start": cannot parse "" to a valid timestamp diff --git a/pkg/util/converter/testdata/prom-error.json b/pkg/util/converter/testdata/prom-error.json new file mode 100644 index 00000000000..ba451dc1b4e --- /dev/null +++ b/pkg/util/converter/testdata/prom-error.json @@ -0,0 +1,5 @@ +{ + "status": "error", + "errorType": "bad_data", + "error": "invalid parameter \"start\": cannot parse \"\" to a valid timestamp" +} \ No newline at end of file diff --git a/pkg/util/converter/testdata/prom-warnings-frame.json b/pkg/util/converter/testdata/prom-warnings-frame.json new file mode 100644 index 00000000000..6d28c2f2c85 --- /dev/null +++ b/pkg/util/converter/testdata/prom-warnings-frame.json @@ -0,0 +1,263 @@ +{ + "frames": [ + { + "schema": { + "meta": { + "type": "timeseries-many", + "notices": [ + { + "severity": "warning", + "text": "warning 1" + }, + { + "severity": "warning", + "text": "warning 2" + } + ] + }, + "fields": [ + { + "name": "Time", + "type": "time", + "typeInfo": { + "frame": "time.Time" + } + }, + { + "name": "up", + "type": "number", + "typeInfo": { + "frame": "float64" + }, + "labels": { + "instance": "localhost:9090", + "job": "prometheus" + } + } + ] + }, + "data": { + "values": [ + [ + 1435781451781 + ], + [ + 1 + ] + ] + } + }, + { + "schema": { + "meta": { + "type": "timeseries-many", + "notices": [ + { + "severity": "warning", + "text": "warning 1" + }, + { + "severity": "warning", + "text": "warning 2" + } + ] + }, + "fields": [ + { + "name": "Time", + "type": "time", + "typeInfo": { + "frame": "time.Time" + } + }, + { + "name": "up", + "type": "number", + "typeInfo": { + "frame": "float64" + }, + "labels": { + "instance": "localhost:9100", + "job": "node" + } + } + ] + }, + "data": { + "values": [ + [ + 1435781451781 + ], + [ + 0 + ] + ] + } + }, + { + "schema": { + "meta": { + "type": "timeseries-many", + "notices": [ + { + "severity": "warning", + "text": "warning 1" + }, + { + "severity": "warning", + "text": "warning 2" + } + ] + }, + "fields": [ + { + "name": "Time", + "type": "time", + "typeInfo": { + "frame": "time.Time" + } + }, + { + "name": "Value", + "type": "number", + "typeInfo": { + "frame": "float64" + }, + "labels": { + "level": "error", + "location": "moon" + } + } + ] + }, + "data": { + "values": [ + [ + 1645029699000 + ], + [ + null + ] + ], + "entities": [ + null, + { + "Inf": [ + 0 + ] + } + ] + } + }, + { + "schema": { + "meta": { + "type": "timeseries-many", + "notices": [ + { + "severity": "warning", + "text": "warning 1" + }, + { + "severity": "warning", + "text": "warning 2" + } + ] + }, + "fields": [ + { + "name": "Time", + "type": "time", + "typeInfo": { + "frame": "time.Time" + } + }, + { + "name": "Value", + "type": "number", + "typeInfo": { + "frame": "float64" + }, + "labels": { + "level": "info", + "location": "moon" + } + } + ] + }, + "data": { + "values": [ + [ + 1645029699000 + ], + [ + null + ] + ], + "entities": [ + null, + { + "NegInf": [ + 0 + ] + } + ] + } + }, + { + "schema": { + "meta": { + "type": "timeseries-many", + "notices": [ + { + "severity": "warning", + "text": "warning 1" + }, + { + "severity": "warning", + "text": "warning 2" + } + ] + }, + "fields": [ + { + "name": "Time", + "type": "time", + "typeInfo": { + "frame": "time.Time" + } + }, + { + "name": "Value", + "type": "number", + "typeInfo": { + "frame": "float64" + }, + "labels": { + "level": "debug", + "location": "moon" + } + } + ] + }, + "data": { + "values": [ + [ + 1645029699000 + ], + [ + null + ] + ], + "entities": [ + null, + { + "NaN": [ + 0 + ] + } + ] + } + } + ] +} \ No newline at end of file diff --git a/pkg/util/converter/testdata/prom-warnings-golden.txt b/pkg/util/converter/testdata/prom-warnings-golden.txt new file mode 100644 index 00000000000..83c40ec9fcd --- /dev/null +++ b/pkg/util/converter/testdata/prom-warnings-golden.txt @@ -0,0 +1,132 @@ +🌟 This was machine generated. Do not edit. 🌟 + +Frame[0] { + "type": "timeseries-many", + "notices": [ + { + "severity": "warning", + "text": "warning 1" + }, + { + "severity": "warning", + "text": "warning 2" + } + ] +} +Name: +Dimensions: 2 Fields by 1 Rows ++-----------------------------------+-------------------------------------------------+ +| Name: Time | Name: up | +| Labels: | Labels: instance=localhost:9090, job=prometheus | +| Type: []time.Time | Type: []float64 | ++-----------------------------------+-------------------------------------------------+ +| 2015-07-01 20:10:51.781 +0000 UTC | 1 | ++-----------------------------------+-------------------------------------------------+ + + + +Frame[1] { + "type": "timeseries-many", + "notices": [ + { + "severity": "warning", + "text": "warning 1" + }, + { + "severity": "warning", + "text": "warning 2" + } + ] +} +Name: +Dimensions: 2 Fields by 1 Rows ++-----------------------------------+-------------------------------------------+ +| Name: Time | Name: up | +| Labels: | Labels: instance=localhost:9100, job=node | +| Type: []time.Time | Type: []float64 | ++-----------------------------------+-------------------------------------------+ +| 2015-07-01 20:10:51.781 +0000 UTC | 0 | ++-----------------------------------+-------------------------------------------+ + + + +Frame[2] { + "type": "timeseries-many", + "notices": [ + { + "severity": "warning", + "text": "warning 1" + }, + { + "severity": "warning", + "text": "warning 2" + } + ] +} +Name: +Dimensions: 2 Fields by 1 Rows ++-------------------------------+------------------------------------+ +| Name: Time | Name: Value | +| Labels: | Labels: level=error, location=moon | +| Type: []time.Time | Type: []float64 | ++-------------------------------+------------------------------------+ +| 2022-02-16 16:41:39 +0000 UTC | +Inf | ++-------------------------------+------------------------------------+ + + + +Frame[3] { + "type": "timeseries-many", + "notices": [ + { + "severity": "warning", + "text": "warning 1" + }, + { + "severity": "warning", + "text": "warning 2" + } + ] +} +Name: +Dimensions: 2 Fields by 1 Rows ++-------------------------------+-----------------------------------+ +| Name: Time | Name: Value | +| Labels: | Labels: level=info, location=moon | +| Type: []time.Time | Type: []float64 | ++-------------------------------+-----------------------------------+ +| 2022-02-16 16:41:39 +0000 UTC | -Inf | ++-------------------------------+-----------------------------------+ + + + +Frame[4] { + "type": "timeseries-many", + "notices": [ + { + "severity": "warning", + "text": "warning 1" + }, + { + "severity": "warning", + "text": "warning 2" + } + ] +} +Name: +Dimensions: 2 Fields by 1 Rows ++-------------------------------+------------------------------------+ +| Name: Time | Name: Value | +| Labels: | Labels: level=debug, location=moon | +| Type: []time.Time | Type: []float64 | ++-------------------------------+------------------------------------+ +| 2022-02-16 16:41:39 +0000 UTC | NaN | ++-------------------------------+------------------------------------+ + + +====== TEST DATA RESPONSE (arrow base64) ====== +FRAME=QVJST1cxAAD/////UAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAOwAAAADAAAATAAAACgAAAAEAAAAQP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAABg/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAID+//8IAAAAhAAAAHoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJub3RpY2VzIjpbeyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAxIn0seyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAyIn1dfQAABAAAAG1ldGEAAAAAAgAAAMQAAAAEAAAAVv///xQAAACMAAAAjAAAAAAAAAOMAAAAAgAAACgAAAAEAAAASP///wgAAAAMAAAAAgAAAHVwAAAEAAAAbmFtZQAAAABo////CAAAADwAAAAwAAAAeyJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MDkwIiwiam9iIjoicHJvbWV0aGV1cyJ9AAAAAAYAAABsYWJlbHMAAAAAAACK////AAACAAIAAAB1cAAAAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAD/////uAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAABAAAAAAAAAAFAAAAAAAAAMEAAoAGAAMAAgABAAKAAAAFAAAAFgAAAABAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAACAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAABAS4CQn+vsEwAAAAAAAPA/EAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADwAAAAAAAQAAQAAAGACAAAAAAAAwAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAA7AAAAAMAAABMAAAAKAAAAAQAAABA/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAGD+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAgP7//wgAAACEAAAAegAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSIsIm5vdGljZXMiOlt7InNldmVyaXR5Ijoid2FybmluZyIsInRleHQiOiJ3YXJuaW5nIDEifSx7InNldmVyaXR5Ijoid2FybmluZyIsInRleHQiOiJ3YXJuaW5nIDIifV19AAAEAAAAbWV0YQAAAAACAAAAxAAAAAQAAABW////FAAAAIwAAACMAAAAAAAAA4wAAAACAAAAKAAAAAQAAABI////CAAAAAwAAAACAAAAdXAAAAQAAABuYW1lAAAAAGj///8IAAAAPAAAADAAAAB7Imluc3RhbmNlIjoibG9jYWxob3N0OjkwOTAiLCJqb2IiOiJwcm9tZXRoZXVzIn0AAAAABgAAAGxhYmVscwAAAAAAAIr///8AAAIAAgAAAHVwAAAAABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAIACAABBUlJPVzE= +FRAME=QVJST1cxAAD/////SAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAOwAAAADAAAATAAAACgAAAAEAAAASP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAABo/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAIj+//8IAAAAhAAAAHoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJub3RpY2VzIjpbeyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAxIn0seyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAyIn1dfQAABAAAAG1ldGEAAAAAAgAAALwAAAAEAAAAXv///xQAAACEAAAAhAAAAAAAAAOEAAAAAgAAACgAAAAEAAAAUP///wgAAAAMAAAAAgAAAHVwAAAEAAAAbmFtZQAAAABw////CAAAADQAAAAqAAAAeyJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MTAwIiwiam9iIjoibm9kZSJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgACAAAAdXAAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAQEuAkJ/r7BMAAAAAAAAAABAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA8AAAAAAAEAAEAAABYAgAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAOwAAAADAAAATAAAACgAAAAEAAAASP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAABo/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAIj+//8IAAAAhAAAAHoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJub3RpY2VzIjpbeyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAxIn0seyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAyIn1dfQAABAAAAG1ldGEAAAAAAgAAALwAAAAEAAAAXv///xQAAACEAAAAhAAAAAAAAAOEAAAAAgAAACgAAAAEAAAAUP///wgAAAAMAAAAAgAAAHVwAAAEAAAAbmFtZQAAAABw////CAAAADQAAAAqAAAAeyJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MTAwIiwiam9iIjoibm9kZSJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgACAAAAdXAAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAeAIAAEFSUk9XMQ== +FRAME=QVJST1cxAAD/////SAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAOwAAAADAAAATAAAACgAAAAEAAAATP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAABs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAIz+//8IAAAAhAAAAHoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJub3RpY2VzIjpbeyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAxIn0seyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAyIn1dfQAABAAAAG1ldGEAAAAAAgAAALgAAAAEAAAAYv///xQAAACAAAAAgAAAAAAAAAOAAAAAAgAAACwAAAAEAAAAVP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAeP///wgAAAAsAAAAIwAAAHsibGV2ZWwiOiJlcnJvciIsImxvY2F0aW9uIjoibW9vbiJ9AAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAN7cpctR1BYAAAAAAADwfxAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAABYAgAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAA7AAAAAMAAABMAAAAKAAAAAQAAABM/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAGz+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAjP7//wgAAACEAAAAegAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSIsIm5vdGljZXMiOlt7InNldmVyaXR5Ijoid2FybmluZyIsInRleHQiOiJ3YXJuaW5nIDEifSx7InNldmVyaXR5Ijoid2FybmluZyIsInRleHQiOiJ3YXJuaW5nIDIifV19AAAEAAAAbWV0YQAAAAACAAAAuAAAAAQAAABi////FAAAAIAAAACAAAAAAAAAA4AAAAACAAAALAAAAAQAAABU////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAAB4////CAAAACwAAAAjAAAAeyJsZXZlbCI6ImVycm9yIiwibG9jYXRpb24iOiJtb29uIn0ABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAHACAABBUlJPVzE= +FRAME=QVJST1cxAAD/////SAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAOwAAAADAAAATAAAACgAAAAEAAAATP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAABs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAIz+//8IAAAAhAAAAHoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJub3RpY2VzIjpbeyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAxIn0seyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAyIn1dfQAABAAAAG1ldGEAAAAAAgAAALgAAAAEAAAAYv///xQAAACAAAAAgAAAAAAAAAOAAAAAAgAAACwAAAAEAAAAVP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAeP///wgAAAAsAAAAIgAAAHsibGV2ZWwiOiJpbmZvIiwibG9jYXRpb24iOiJtb29uIn0AAAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAN7cpctR1BYAAAAAAADw/xAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAABYAgAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAA7AAAAAMAAABMAAAAKAAAAAQAAABM/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAGz+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAjP7//wgAAACEAAAAegAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSIsIm5vdGljZXMiOlt7InNldmVyaXR5Ijoid2FybmluZyIsInRleHQiOiJ3YXJuaW5nIDEifSx7InNldmVyaXR5Ijoid2FybmluZyIsInRleHQiOiJ3YXJuaW5nIDIifV19AAAEAAAAbWV0YQAAAAACAAAAuAAAAAQAAABi////FAAAAIAAAACAAAAAAAAAA4AAAAACAAAALAAAAAQAAABU////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAAB4////CAAAACwAAAAiAAAAeyJsZXZlbCI6ImluZm8iLCJsb2NhdGlvbiI6Im1vb24ifQAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAHACAABBUlJPVzE= +FRAME=QVJST1cxAAD/////SAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAOwAAAADAAAATAAAACgAAAAEAAAATP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAABs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAIz+//8IAAAAhAAAAHoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJub3RpY2VzIjpbeyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAxIn0seyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAyIn1dfQAABAAAAG1ldGEAAAAAAgAAALgAAAAEAAAAYv///xQAAACAAAAAgAAAAAAAAAOAAAAAAgAAACwAAAAEAAAAVP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAeP///wgAAAAsAAAAIwAAAHsibGV2ZWwiOiJkZWJ1ZyIsImxvY2F0aW9uIjoibW9vbiJ9AAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAN7cpctR1BYBAAAAAAD4fxAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAABYAgAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAA7AAAAAMAAABMAAAAKAAAAAQAAABM/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAGz+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAjP7//wgAAACEAAAAegAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSIsIm5vdGljZXMiOlt7InNldmVyaXR5Ijoid2FybmluZyIsInRleHQiOiJ3YXJuaW5nIDEifSx7InNldmVyaXR5Ijoid2FybmluZyIsInRleHQiOiJ3YXJuaW5nIDIifV19AAAEAAAAbWV0YQAAAAACAAAAuAAAAAQAAABi////FAAAAIAAAACAAAAAAAAAA4AAAAACAAAALAAAAAQAAABU////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAAB4////CAAAACwAAAAjAAAAeyJsZXZlbCI6ImRlYnVnIiwibG9jYXRpb24iOiJtb29uIn0ABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAHACAABBUlJPVzE= diff --git a/pkg/util/converter/testdata/prom-warnings.json b/pkg/util/converter/testdata/prom-warnings.json new file mode 100644 index 00000000000..28c9128b588 --- /dev/null +++ b/pkg/util/converter/testdata/prom-warnings.json @@ -0,0 +1,37 @@ +{ + "status" : "success", + "data" : { + "resultType" : "vector", + "result" : [ + { + "metric" : { + "__name__" : "up", + "job" : "prometheus", + "instance" : "localhost:9090" + }, + "value": [ 1435781451.781, "1" ] + }, + { + "metric" : { + "__name__" : "up", + "job" : "node", + "instance" : "localhost:9100" + }, + "value" : [ 1435781451.781, "0" ] + }, + { + "metric": { "level": "error", "location": "moon"}, + "value": [1645029699, "+Inf"] + }, + { + "metric": { "level": "info", "location": "moon" }, + "value": [1645029699, "-Inf"] + }, + { + "metric": { "level": "debug", "location": "moon" }, + "value": [1645029699, "NaN"] + } + ] + }, + "warnings" : ["warning 1", "warning 2"] + } From 5df91bdcf1dd612ef5f70c5919114b1a5f5e7be0 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Wed, 4 May 2022 14:15:07 -0500 Subject: [PATCH 043/440] Canvas: Inline edit (#48222) Canvas inline edit panel Co-authored-by: Ryan McKinley --- package.json | 1 + public/app/features/canvas/runtime/scene.tsx | 9 +- .../app/plugins/panel/canvas/CanvasPanel.tsx | 102 ++++++++++++++- .../app/plugins/panel/canvas/InlineEdit.tsx | 121 ++++++++++++++++++ .../plugins/panel/canvas/InlineEditBody.tsx | 99 ++++++++++++++ .../app/plugins/panel/canvas/globalStyles.ts | 11 ++ public/sass/components/_dashboard_grid.scss | 3 +- yarn.lock | 10 ++ 8 files changed, 349 insertions(+), 7 deletions(-) create mode 100644 public/app/plugins/panel/canvas/InlineEdit.tsx create mode 100644 public/app/plugins/panel/canvas/InlineEditBody.tsx create mode 100644 public/app/plugins/panel/canvas/globalStyles.ts diff --git a/package.json b/package.json index e8b82a35ed7..c266818d655 100644 --- a/package.json +++ b/package.json @@ -281,6 +281,7 @@ "@sentry/browser": "6.19.1", "@sentry/types": "6.19.1", "@sentry/utils": "6.19.1", + "@types/react-resizable": "^1.7.4", "@visx/event": "2.6.0", "@visx/gradient": "2.1.0", "@visx/group": "2.1.0", diff --git a/public/app/features/canvas/runtime/scene.tsx b/public/app/features/canvas/runtime/scene.tsx index 0b951cb2bc6..dd66f93a677 100644 --- a/public/app/features/canvas/runtime/scene.tsx +++ b/public/app/features/canvas/runtime/scene.tsx @@ -55,6 +55,7 @@ export class Scene { div?: HTMLDivElement; currentLayer?: FrameState; isEditingEnabled?: boolean; + skipNextSelectionBroadcast = false; constructor(cfg: CanvasFrameOptions, enableEditing: boolean, public onSave: (cfg: CanvasFrameOptions) => void) { this.root = this.load(cfg, enableEditing); @@ -199,7 +200,8 @@ export class Scene { }; }; - clearCurrentSelection() { + clearCurrentSelection(skipNextSelectionBroadcast = false) { + this.skipNextSelectionBroadcast = skipNextSelectionBroadcast; let event: MouseEvent = new MouseEvent('click'); this.selecto?.clickTarget(event, this.div); } @@ -256,6 +258,11 @@ export class Scene { private updateSelection = (selection: SelectionParams) => { this.moveable!.target = selection.targets; + if (this.skipNextSelectionBroadcast) { + this.skipNextSelectionBroadcast = false; + return; + } + if (selection.frame) { this.selection.next([selection.frame]); } else { diff --git a/public/app/plugins/panel/canvas/CanvasPanel.tsx b/public/app/plugins/panel/canvas/CanvasPanel.tsx index 6b1c52468e9..b04d25bc77a 100644 --- a/public/app/plugins/panel/canvas/CanvasPanel.tsx +++ b/public/app/plugins/panel/canvas/CanvasPanel.tsx @@ -1,19 +1,23 @@ -import { Component } from 'react'; -import { Subscription } from 'rxjs'; +import { css } from '@emotion/css'; +import React, { Component } from 'react'; +import { ReplaySubject, Subscription } from 'rxjs'; -import { PanelProps } from '@grafana/data'; -import { PanelContext, PanelContextRoot } from '@grafana/ui'; +import { GrafanaTheme, PanelProps } from '@grafana/data'; +import { config, locationService } from '@grafana/runtime/src'; +import { Button, PanelContext, PanelContextRoot, stylesFactory } from '@grafana/ui'; import { CanvasFrameOptions } from 'app/features/canvas'; import { ElementState } from 'app/features/canvas/runtime/element'; import { Scene } from 'app/features/canvas/runtime/scene'; import { PanelEditEnteredEvent, PanelEditExitedEvent } from 'app/types/events'; +import { InlineEdit } from './InlineEdit'; import { PanelOptions } from './models.gen'; interface Props extends PanelProps {} interface State { refresh: number; + openInlineEdit: boolean; } export interface InstanceState { @@ -21,6 +25,16 @@ export interface InstanceState { selected: ElementState[]; } +export interface SelectionAction { + panel: CanvasPanel; +} + +let canvasInstances: CanvasPanel[] = []; +let activeCanvasPanel: CanvasPanel | undefined = undefined; +let isInlineEditOpen = false; + +export const activePanelSubject = new ReplaySubject(1); + export class CanvasPanel extends Component { static contextType = PanelContextRoot; panelContext: PanelContext = {} as PanelContext; @@ -28,11 +42,14 @@ export class CanvasPanel extends Component { readonly scene: Scene; private subs = new Subscription(); needsReload = false; + styles = getStyles(config.theme); + isEditing = locationService.getSearchObject().editPanel !== undefined; constructor(props: Props) { super(props); this.state = { refresh: 0, + openInlineEdit: false, }; // Only the initial options are ever used. @@ -45,6 +62,7 @@ export class CanvasPanel extends Component { this.props.eventBus.subscribe(PanelEditEnteredEvent, (evt) => { // Remove current selection when entering edit mode for any panel in dashboard this.scene.clearCurrentSelection(); + this.inlineEditButtonClose(); }) ); @@ -58,6 +76,9 @@ export class CanvasPanel extends Component { } componentDidMount() { + activeCanvasPanel = this; + activePanelSubject.next({ panel: this }); + this.panelContext = this.context as PanelContext; if (this.panelContext.onInstanceStateChange) { this.panelContext.onInstanceStateChange({ @@ -73,14 +94,27 @@ export class CanvasPanel extends Component { selected: v, layer: this.scene.root, }); + + activeCanvasPanel = this; + activePanelSubject.next({ panel: this }); + + canvasInstances.forEach((canvasInstance) => { + if (canvasInstance !== activeCanvasPanel) { + canvasInstance.scene.clearCurrentSelection(true); + } + }); }, }) ); } + + canvasInstances.push(this); } componentWillUnmount() { this.subs.unsubscribe(); + isInlineEditOpen = false; + canvasInstances = canvasInstances.filter((ci) => ci.props.id !== activeCanvasPanel?.props.id); } // NOTE, all changes to the scene flow through this function @@ -91,6 +125,7 @@ export class CanvasPanel extends Component { ...options, root, }); + this.setState({ refresh: this.state.refresh + 1 }); // console.log('send changes', root); }; @@ -112,6 +147,10 @@ export class CanvasPanel extends Component { changed = true; } + if (this.state.openInlineEdit !== nextState.openInlineEdit) { + changed = true; + } + // After editing, the options are valid, but the scene was in a different panel or inline editing mode has changed const shouldUpdateSceneAndPanel = this.needsReload && this.props.options !== nextProps.options; const inlineEditingSwitched = this.props.options.inlineEditing !== nextProps.options.inlineEditing; @@ -130,7 +169,60 @@ export class CanvasPanel extends Component { return changed; } + inlineEditButtonClick = () => { + if (isInlineEditOpen) { + this.forceUpdate(); + this.setActivePanel(); + return; + } + + this.setActivePanel(); + this.setState({ openInlineEdit: true }); + isInlineEditOpen = true; + }; + + inlineEditButtonClose = () => { + this.setState({ openInlineEdit: false }); + isInlineEditOpen = false; + }; + + setActivePanel = () => { + activeCanvasPanel = this; + activePanelSubject.next({ panel: this }); + }; + + renderInlineEdit = () => { + return this.inlineEditButtonClose()} />; + }; + render() { - return this.scene.render(); + return ( + <> + {this.scene.render()} + {this.props.options.inlineEditing && !this.isEditing && ( +
+
+
+ {this.state.openInlineEdit && this.renderInlineEdit()} +
+ )} + + ); } } + +const getStyles = stylesFactory((theme: GrafanaTheme) => ({ + inlineEditButton: css` + position: absolute; + bottom: 8px; + left: 8px; + z-index: 999; + `, +})); diff --git a/public/app/plugins/panel/canvas/InlineEdit.tsx b/public/app/plugins/panel/canvas/InlineEdit.tsx new file mode 100644 index 00000000000..d3249248c4b --- /dev/null +++ b/public/app/plugins/panel/canvas/InlineEdit.tsx @@ -0,0 +1,121 @@ +import { css } from '@emotion/css'; +import React, { SyntheticEvent, useRef, useState } from 'react'; +import Draggable from 'react-draggable'; +import { Resizable, ResizeCallbackData } from 'react-resizable'; + +import { Dimensions2D, GrafanaTheme2 } from '@grafana/data'; +import { IconButton, Portal, useStyles2 } from '@grafana/ui'; +import store from 'app/core/store'; + +import { InlineEditBody } from './InlineEditBody'; + +type Props = { + onClose?: () => void; +}; + +const OFFSET_X = 70; + +export const InlineEdit = ({ onClose }: Props) => { + const btnInlineEdit = document.querySelector('[data-btninlineedit]')!.getBoundingClientRect(); + const ref = useRef(null); + const styles = useStyles2(getStyles); + const inlineEditKey = 'inlineEditPanel'; + + const defaultMeasurements = { width: 350, height: 400 }; + const defaultX = btnInlineEdit.x + OFFSET_X; + const defaultY = btnInlineEdit.y - defaultMeasurements.height; + + const savedPlacement = store.getObject(inlineEditKey, { + x: defaultX, + y: defaultY, + w: defaultMeasurements.width, + h: defaultMeasurements.height, + }); + const [measurements, setMeasurements] = useState({ width: savedPlacement.w, height: savedPlacement.h }); + const [placement, setPlacement] = useState({ x: savedPlacement.x, y: savedPlacement.y }); + + const onDragStop = (event: any, dragElement: any) => { + let x = dragElement.x < 0 ? 0 : dragElement.x; + let y = dragElement.y < 0 ? 0 : dragElement.y; + + setPlacement({ x: x, y: y }); + saveToStore(x, y, measurements.width, measurements.height); + }; + + const onResizeStop = (event: SyntheticEvent, data: ResizeCallbackData) => { + const { size } = data; + setMeasurements({ width: size.width, height: size.height }); + saveToStore(placement.x, placement.y, size.width, size.height); + }; + + const saveToStore = (x: number, y: number, width: number, height: number) => { + store.setObject(inlineEditKey, { x: x, y: y, w: width, h: height }); + }; + + return ( + +
+ + +
+ +
+
Canvas Inline Editor
+ +
+
+
+ +
+
+
+ + +
+ + ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + inlineEditorContainer: css` + display: flex; + flex-direction: column; + background: ${theme.v1.colors.panelBg}; + box-shadow: 5px 5px 20px -5px #000000; + z-index: 1000; + opacity: 1; + `, + draggableWrapper: css` + width: 0; + height: 0; + `, + inlineEditorHeader: css` + display: flex; + align-items: center; + justify-content: center; + background: ${theme.colors.background.canvas}; + border: 1px solid ${theme.colors.border.weak}; + height: 40px; + cursor: move; + `, + inlineEditorContent: css` + white-space: pre-wrap; + padding: 10px; + `, + inlineEditorClose: css` + margin-left: auto; + `, + placeholder: css` + width: 24px; + height: 24px; + visibility: hidden; + margin-right: auto; + `, + inlineEditorContentWrapper: css` + overflow: scroll; + `, +}); diff --git a/public/app/plugins/panel/canvas/InlineEditBody.tsx b/public/app/plugins/panel/canvas/InlineEditBody.tsx new file mode 100644 index 00000000000..68b5754ef23 --- /dev/null +++ b/public/app/plugins/panel/canvas/InlineEditBody.tsx @@ -0,0 +1,99 @@ +import { get as lodashGet } from 'lodash'; +import React, { useMemo } from 'react'; +import { useObservable } from 'react-use'; + +import { PanelOptionsEditorBuilder, StandardEditorContext } from '@grafana/data'; +import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin'; +import { NestedValueAccess } from '@grafana/data/src/utils/OptionsUIBuilders'; +import { FrameState } from 'app/features/canvas/runtime/frame'; +import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; +import { fillOptionsPaneItems } from 'app/features/dashboard/components/PanelEditor/getVisualizationOptions'; +import { setOptionImmutably } from 'app/features/dashboard/components/PanelEditor/utils'; + +import { activePanelSubject, InstanceState } from './CanvasPanel'; +import { getElementEditor } from './editor/elementEditor'; +import { getLayerEditor } from './editor/layerEditor'; + +export const InlineEditBody = () => { + const activePanel = useObservable(activePanelSubject); + const instanceState = activePanel?.panel.context?.instanceState; + + const pane = useMemo(() => { + const state: InstanceState = instanceState; + if (!state) { + return new OptionsPaneCategoryDescriptor({ id: 'root', title: 'root' }); + } + + const supplier = (builder: PanelOptionsEditorBuilder, context: StandardEditorContext) => { + builder.addNestedOptions(getLayerEditor(instanceState)); + + const selection = state.selected; + if (selection?.length === 1) { + const element = selection[0]; + if (!(element instanceof FrameState)) { + builder.addNestedOptions( + getElementEditor({ + category: [`Selected element (${element.options.name})`], + element, + scene: state.scene, + }) + ); + } + } + }; + + return getOptionsPaneCategoryDescriptor({}, supplier); + }, [instanceState]); + + return ( +
+
{pane.items.map((v) => v.render())}
+
+ {pane.categories.map((c) => { + return ( +
+
{c.props.title}
+
{c.items.map((s) => s.render())}
+
+ ); + })} +
+
+ ); +}; + +// 🤮🤮🤮🤮 this oddly does not actually do anything, but structure is required. I'll try to clean it up... +function getOptionsPaneCategoryDescriptor( + props: any, + supplier: PanelOptionsSupplier +): OptionsPaneCategoryDescriptor { + const context: StandardEditorContext = { + data: props.input, + options: props.options, + }; + + const root = new OptionsPaneCategoryDescriptor({ id: 'root', title: 'root' }); + const getOptionsPaneCategory = (categoryNames?: string[]): OptionsPaneCategoryDescriptor => { + if (categoryNames?.length) { + const key = categoryNames[0]; + let sub = root.categories.find((v) => v.props.id === key); + if (!sub) { + sub = new OptionsPaneCategoryDescriptor({ id: key, title: key }); + root.categories.push(sub); + } + return sub; + } + return root; + }; + + const access: NestedValueAccess = { + getValue: (path: string) => lodashGet(props.options, path), + onChange: (path: string, value: any) => { + props.onChange(setOptionImmutably(props.options as any, path, value)); + }, + }; + + // Use the panel options loader + fillOptionsPaneItems(supplier, access, getOptionsPaneCategory, context); + return root; +} diff --git a/public/app/plugins/panel/canvas/globalStyles.ts b/public/app/plugins/panel/canvas/globalStyles.ts new file mode 100644 index 00000000000..a90f0db0619 --- /dev/null +++ b/public/app/plugins/panel/canvas/globalStyles.ts @@ -0,0 +1,11 @@ +import { css } from '@emotion/react'; + +import { GrafanaTheme2 } from '@grafana/data'; + +export function getGlobalStyles(theme: GrafanaTheme2) { + return css` + .moveable-control-box { + z-index: 999; + } + `; +} diff --git a/public/sass/components/_dashboard_grid.scss b/public/sass/components/_dashboard_grid.scss index a0c7dd87d4e..6da0f59b2a8 100644 --- a/public/sass/components/_dashboard_grid.scss +++ b/public/sass/components/_dashboard_grid.scss @@ -6,7 +6,8 @@ visibility: hidden; } -.react-grid-item { +.react-grid-item, +#grafana-portal-container { touch-action: initial !important; &:hover { diff --git a/yarn.lock b/yarn.lock index 9045b42791a..f75b4ac59a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10804,6 +10804,15 @@ __metadata: languageName: node linkType: hard +"@types/react-resizable@npm:^1.7.4": + version: 1.7.4 + resolution: "@types/react-resizable@npm:1.7.4" + dependencies: + "@types/react": "*" + checksum: d665bb2ddf830b9f841be21204cee119602b3d983537a94ccbad40deb7cd602e04742e3e013009bbb27c2d0fe72441b29ed48bc75f7e482cfb25eaf45f281dc9 + languageName: node + linkType: hard + "@types/react-router-dom@npm:5.3.3": version: 5.3.3 resolution: "@types/react-router-dom@npm:5.3.3" @@ -20953,6 +20962,7 @@ __metadata: "@types/react-highlight-words": 0.16.4 "@types/react-loadable": 5.5.6 "@types/react-redux": 7.1.23 + "@types/react-resizable": ^1.7.4 "@types/react-router-dom": 5.3.3 "@types/react-table": ^7 "@types/react-test-renderer": 17.0.1 From 35300a816a808863be73795534b938c216830625 Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Wed, 4 May 2022 16:03:48 -0400 Subject: [PATCH 044/440] Prometheus: Add support for streaming scalar parsing (#48725) --- pkg/util/converter/prom.go | 25 ++++++++++++ pkg/util/converter/prom_test.go | 1 + .../converter/testdata/prom-scalar-frame.json | 40 +++++++++++++++++++ .../converter/testdata/prom-scalar-golden.txt | 18 +++++++++ pkg/util/converter/testdata/prom-scalar.json | 10 +++++ 5 files changed, 94 insertions(+) create mode 100644 pkg/util/converter/testdata/prom-scalar-frame.json create mode 100644 pkg/util/converter/testdata/prom-scalar-golden.txt create mode 100644 pkg/util/converter/testdata/prom-scalar.json diff --git a/pkg/util/converter/prom.go b/pkg/util/converter/prom.go index 7c238425b92..f158206b9d5 100644 --- a/pkg/util/converter/prom.go +++ b/pkg/util/converter/prom.go @@ -111,6 +111,8 @@ func readPrometheusData(iter *jsoniter.Iterator) *backend.DataResponse { rsp = readMatrixOrVector(iter) case "streams": rsp = readStream(iter) + case "scalar": + rsp = readScalar(iter) default: iter.Skip() rsp = &backend.DataResponse{ @@ -288,6 +290,29 @@ func readLabelsOrExemplars(iter *jsoniter.Iterator) (*data.Frame, [][2]string) { return frame, pairs } +func readScalar(iter *jsoniter.Iterator) *backend.DataResponse { + timeField := data.NewFieldFromFieldType(data.FieldTypeTime, 0) + timeField.Name = data.TimeSeriesTimeFieldName + valueField := data.NewFieldFromFieldType(data.FieldTypeFloat64, 0) + valueField.Name = data.TimeSeriesValueFieldName + valueField.Labels = data.Labels{} + + t, v, err := readTimeValuePair(iter) + if err == nil { + timeField.Append(t) + valueField.Append(v) + } + + frame := data.NewFrame("", timeField, valueField) + frame.Meta = &data.FrameMeta{ + Type: data.FrameTypeTimeSeriesMany, + } + + return &backend.DataResponse{ + Frames: []*data.Frame{frame}, + } +} + func readMatrixOrVector(iter *jsoniter.Iterator) *backend.DataResponse { rsp := &backend.DataResponse{} diff --git a/pkg/util/converter/prom_test.go b/pkg/util/converter/prom_test.go index 10719e8f551..c1a4f1692c7 100644 --- a/pkg/util/converter/prom_test.go +++ b/pkg/util/converter/prom_test.go @@ -19,6 +19,7 @@ func TestReadPromFrames(t *testing.T) { "prom-matrix", "prom-matrix-with-nans", "prom-vector", + "prom-scalar", "prom-series", "prom-warnings", "prom-error", diff --git a/pkg/util/converter/testdata/prom-scalar-frame.json b/pkg/util/converter/testdata/prom-scalar-frame.json new file mode 100644 index 00000000000..9c1a546bfd7 --- /dev/null +++ b/pkg/util/converter/testdata/prom-scalar-frame.json @@ -0,0 +1,40 @@ +{ + "frames": [ + { + "schema": { + "meta": { + "type": "timeseries-many" + }, + "fields": [ + { + "name": "Time", + "type": "time", + "typeInfo": { + "frame": "time.Time" + } + }, + { + "name": "Value", + "type": "number", + "typeInfo": { + "frame": "float64" + }, + "labels": { + + } + } + ] + }, + "data": { + "values": [ + [ + 1651680139104 + ], + [ + 0.00002482 + ] + ] + } + } + ] +} \ No newline at end of file diff --git a/pkg/util/converter/testdata/prom-scalar-golden.txt b/pkg/util/converter/testdata/prom-scalar-golden.txt new file mode 100644 index 00000000000..5bfe0964155 --- /dev/null +++ b/pkg/util/converter/testdata/prom-scalar-golden.txt @@ -0,0 +1,18 @@ +🌟 This was machine generated. Do not edit. 🌟 + +Frame[0] { + "type": "timeseries-many" +} +Name: +Dimensions: 2 Fields by 1 Rows ++-----------------------------------+-----------------+ +| Name: Time | Name: Value | +| Labels: | Labels: | +| Type: []time.Time | Type: []float64 | ++-----------------------------------+-----------------+ +| 2022-05-04 16:02:19.104 +0000 UTC | 2.482e-05 | ++-----------------------------------+-----------------+ + + +====== TEST DATA RESPONSE (arrow base64) ====== +FRAME=QVJST1cxAAD/////yAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAzP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAAz///8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAAJgAAAAEAAAAgv///xQAAABgAAAAYAAAAAAAAANgAAAAAgAAACwAAAAEAAAAdP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAmP///wgAAAAMAAAAAgAAAHt9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAAAAAAP////+4AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAEAAAAAAAAAAUAAAAAAAAAwQACgAYAAwACAAEAAoAAAAUAAAAWAAAAAEAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAACY3OVV8usW8VWfaZEG+j4QAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAABAABAAAA2AEAAAAAAADAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAzP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAAz///8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAAJgAAAAEAAAAgv///xQAAABgAAAAYAAAAAAAAANgAAAAAgAAACwAAAAEAAAAdP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAmP///wgAAAAMAAAAAgAAAHt9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAA8AEAAEFSUk9XMQ== diff --git a/pkg/util/converter/testdata/prom-scalar.json b/pkg/util/converter/testdata/prom-scalar.json new file mode 100644 index 00000000000..a9526760886 --- /dev/null +++ b/pkg/util/converter/testdata/prom-scalar.json @@ -0,0 +1,10 @@ +{ + "status": "success", + "data": { + "resultType": "scalar", + "result": [ + 1651680139.104, + "0.00002482" + ] + } +} \ No newline at end of file From 2d574f352cfec3b2de38f7e3c29d48292add9bd3 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 4 May 2022 16:25:27 -0700 Subject: [PATCH 045/440] Search: add actions row header to new search layout (#48735) --- .../app/features/search/page/SearchPage.tsx | 158 ++++++++++++++---- .../search/page/components/ActionRow.tsx | 107 ++++++++++++ .../SearchResultsTable.tsx} | 56 +++++-- .../page/{table => components}/columns.tsx | 38 +++-- public/app/features/search/service/types.ts | 1 + public/app/features/search/types.ts | 1 + 6 files changed, 309 insertions(+), 52 deletions(-) create mode 100644 public/app/features/search/page/components/ActionRow.tsx rename public/app/features/search/page/{table/Table.tsx => components/SearchResultsTable.tsx} (84%) rename public/app/features/search/page/{table => components}/columns.tsx (91%) diff --git a/public/app/features/search/page/SearchPage.tsx b/public/app/features/search/page/SearchPage.tsx index 58712813895..19393c7d443 100644 --- a/public/app/features/search/page/SearchPage.tsx +++ b/public/app/features/search/page/SearchPage.tsx @@ -1,30 +1,39 @@ import { css } from '@emotion/css'; -import React from 'react'; +import React, { useState } from 'react'; import { useAsync } from 'react-use'; import AutoSizer from 'react-virtualized-auto-sizer'; +import { FixedSizeGrid } from 'react-window'; -import { GrafanaTheme2, NavModelItem } from '@grafana/data'; +import { DataFrameView, GrafanaTheme2, NavModelItem } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { Input, useStyles2, Spinner, Button } from '@grafana/ui'; +import { Input, useStyles2, Spinner, InlineSwitch, InlineFieldRow, InlineField } from '@grafana/ui'; import Page from 'app/core/components/Page/Page'; -import { TagFilter, TermCount } from 'app/core/components/TagFilter/TagFilter'; +import { TermCount } from 'app/core/components/TagFilter/TagFilter'; +import { PreviewsSystemRequirements } from '../components/PreviewsSystemRequirements'; +import { SearchCard } from '../components/SearchCard'; import { useSearchQuery } from '../hooks/useSearchQuery'; -import { getGrafanaSearcher, QueryFilters } from '../service'; +import { getGrafanaSearcher, QueryFilters, QueryResult } from '../service'; import { getTermCounts } from '../service/backend'; +import { DashboardSearchItemType, DashboardSectionItem, SearchLayout } from '../types'; -import { Table } from './table/Table'; +import { ActionRow } from './components/ActionRow'; +import { SearchResultsTable } from './components/SearchResultsTable'; const node: NavModelItem = { id: 'search', - text: 'Search', + text: 'Search playground', + subTitle: 'The body below will eventually live inside existing UI layouts', icon: 'dashboard', url: 'search', }; export default function SearchPage() { const styles = useStyles2(getStyles); - const { query, onQueryChange, onTagFilterChange, onDatasourceChange } = useSearchQuery({}); + const { query, onQueryChange, onTagFilterChange, onDatasourceChange, onSortChange, onLayoutChange } = useSearchQuery( + {} + ); + const [showManage, setShowManage] = useState(false); // grid vs list view const results = useAsync(() => { const { query: searchQuery, tag: tags, datasource } = query; @@ -40,6 +49,7 @@ export default function SearchPage() { return
Unsupported
; } + // This gets the possible tags from within the query results const getTagOptions = (): Promise => { const tags = results.value?.body.fields.find((f) => f.name === 'tags'); @@ -57,33 +67,120 @@ export default function SearchPage() { onTagFilterChange(tags); }; + const onTagSelected = (tag: string) => { + onTagFilterChange([...new Set(query.tag as string[]).add(tag)]); + }; + + const showPreviews = query.layout === SearchLayout.Grid && config.featureToggles.dashboardPreviews; + return ( - + + + + setShowManage(!showManage)} /> + +
{results.loading && } {results.value?.body && (
- -
- {query.datasource && ( - - )} - - {({ width }) => { + { + if (v === SearchLayout.Folders) { + if (query.query) { + onQueryChange(''); // parent will clear the sort + } + } + onLayoutChange(v); + }} + onSortChange={onSortChange} + onTagFilterChange={onTagFilterChange} + getTagOptions={getTagOptions} + onDatasourceChange={onDatasourceChange} + query={query} + /> + + onLayoutChange(SearchLayout.List)} + /> + + + {({ width, height }) => { + if (showPreviews) { + const df = results.value?.body!; + const view = new DataFrameView(df); + + // HACK for grid view + const itemProps = { + editable: showManage, + onToggleChecked: (v: any) => { + console.log('CHECKED?', v); + }, + onTagSelected, + }; + + const numColumns = Math.ceil(width / 320); + const cellWidth = width / numColumns; + const cellHeight = (cellWidth - 64) * 0.75 + 56 + 8; + const numRows = Math.ceil(df.length / numColumns); + return ( + + {({ columnIndex, rowIndex, style }) => { + const index = rowIndex * numColumns + columnIndex; + const item = view.get(index); + const facade: DashboardSectionItem = { + uid: item.uid, + title: item.name, + url: item.url, + uri: item.url, + type: + item.kind === 'folder' + ? DashboardSearchItemType.DashFolder + : DashboardSearchItemType.DashDB, + id: 666, // do not use me! + isStarred: false, + tags: item.tags ?? [], + }; + + // The wrapper div is needed as the inner SearchItem has margin-bottom spacing + // And without this wrapper there is no room for that margin + return item ? ( +
  • + +
  • + ) : null; + }} +
    + ); + } + return ( <> - ({ font-size: 18px; `, - clearClick: css` - &:hover { - text-decoration: line-through; + virtualizedGridItemWrapper: css` + padding: 4px; + `, + wrapper: css` + display: flex; + flex-direction: column; + + > ul { + list-style: none; } - margin-bottom: 20px; `, }); diff --git a/public/app/features/search/page/components/ActionRow.tsx b/public/app/features/search/page/components/ActionRow.tsx new file mode 100644 index 00000000000..2f0fc3a5295 --- /dev/null +++ b/public/app/features/search/page/components/ActionRow.tsx @@ -0,0 +1,107 @@ +import { css } from '@emotion/css'; +import React, { FC, FormEvent } from 'react'; + +import { GrafanaTheme2, SelectableValue } from '@grafana/data'; +import { config } from '@grafana/runtime'; +import { HorizontalGroup, RadioButtonGroup, useStyles2, Checkbox, Button } from '@grafana/ui'; +import { SortPicker } from 'app/core/components/Select/SortPicker'; +import { TagFilter, TermCount } from 'app/core/components/TagFilter/TagFilter'; + +import { DashboardQuery, SearchLayout } from '../../types'; + +export const layoutOptions = [ + { value: SearchLayout.Folders, icon: 'folder', ariaLabel: 'View by folders' }, + { value: SearchLayout.List, icon: 'list-ul', ariaLabel: 'View as list' }, +]; + +if (config.featureToggles.dashboardPreviews) { + layoutOptions.push({ value: SearchLayout.Grid, icon: 'apps', ariaLabel: 'Grid view' }); +} + +interface Props { + onLayoutChange: (layout: SearchLayout) => void; + onSortChange: (value: SelectableValue) => void; + onStarredFilterChange?: (event: FormEvent) => void; + onTagFilterChange: (tags: string[]) => void; + getTagOptions: () => Promise; + onDatasourceChange: (ds?: string) => void; + query: DashboardQuery; + showStarredFilter?: boolean; + hideLayout?: boolean; +} + +function getValidQueryLayout(q: DashboardQuery): SearchLayout { + // Folders is not valid when a query exists + if (q.layout === SearchLayout.Folders) { + if (q.query || q.sort) { + return SearchLayout.List; + } + } + return q.layout; +} + +export const ActionRow: FC = ({ + onLayoutChange, + onSortChange, + onStarredFilterChange = () => {}, + onTagFilterChange, + getTagOptions, + onDatasourceChange, + query, + showStarredFilter, + hideLayout, +}) => { + const styles = useStyles2(getStyles); + + return ( +
    +
    + + {!hideLayout && ( + + )} + + +
    + + {showStarredFilter && ( +
    + +
    + )} + {query.datasource && ( + + )} + +
    +
    + ); +}; + +ActionRow.displayName = 'ActionRow'; + +const getStyles = (theme: GrafanaTheme2) => { + return { + actionRow: css` + display: none; + + @media only screen and (min-width: ${theme.v1.breakpoints.md}) { + display: flex; + justify-content: space-between; + align-items: center; + padding: ${theme.v1.spacing.lg} 0; + width: 100%; + } + `, + rowContainer: css` + margin-right: ${theme.v1.spacing.md}; + `, + checkboxWrapper: css` + label { + line-height: 1.2; + } + `, + }; +}; diff --git a/public/app/features/search/page/table/Table.tsx b/public/app/features/search/page/components/SearchResultsTable.tsx similarity index 84% rename from public/app/features/search/page/table/Table.tsx rename to public/app/features/search/page/components/SearchResultsTable.tsx index ab3afd8918e..e858451f45b 100644 --- a/public/app/features/search/page/table/Table.tsx +++ b/public/app/features/search/page/components/SearchResultsTable.tsx @@ -9,12 +9,16 @@ import { TableCell } from '@grafana/ui/src/components/Table/TableCell'; import { getTableStyles } from '@grafana/ui/src/components/Table/styles'; import { LocationInfo } from '../../service'; +import { SearchLayout } from '../../types'; import { generateColumns } from './columns'; type Props = { data: DataFrame; width: number; + height: number; + showCheckbox: boolean; + layout: SearchLayout; tags: string[]; onTagFilterChange: (tags: string[]) => void; onDatasourceChange: (datasource?: string) => void; @@ -25,6 +29,7 @@ export type TableColumn = Column & { }; export interface FieldAccess { + uid: string; // the item UID kind: string; // panel, dashboard, folder name: string; description: string; @@ -39,7 +44,18 @@ export interface FieldAccess { datasource: DataSourceRef[]; } -export const Table = ({ data, width, tags, onTagFilterChange, onDatasourceChange }: Props) => { +const skipHREF = new Set(['column-checkbox', 'column-datasource']); + +export const SearchResultsTable = ({ + data, + width, + height, + tags, + showCheckbox, + layout, + onTagFilterChange, + onDatasourceChange, +}: Props) => { const styles = useStyles2(getStyles); const tableStyles = useStyles2(getTableStyles); @@ -56,9 +72,18 @@ export const Table = ({ data, width, tags, onTagFilterChange, onDatasourceChange // React-table column definitions const access = useMemo(() => new DataFrameView(data), [data]); const memoizedColumns = useMemo(() => { - const isDashboardList = data.meta?.type === DataFrameType.DirectoryListing; - return generateColumns(access, isDashboardList, width, styles, tags, onTagFilterChange, onDatasourceChange); - }, [data.meta?.type, access, width, styles, tags, onTagFilterChange, onDatasourceChange]); + const isDashboardList = data.meta?.type === DataFrameType.DirectoryListing || layout === SearchLayout.Folders; + return generateColumns( + access, + isDashboardList, + width, + showCheckbox, + styles, + tags, + onTagFilterChange, + onDatasourceChange + ); + }, [data.meta?.type, layout, access, width, styles, tags, showCheckbox, onTagFilterChange, onDatasourceChange]); const options: TableOptions<{}> = useMemo( () => ({ @@ -80,15 +105,22 @@ export const Table = ({ data, width, tags, onTagFilterChange, onDatasourceChange return (
    {row.cells.map((cell: Cell, index: number) => { + const body = ( + + ); + if (skipHREF.has(cell.column.id)) { + return body; + } + return ( - + {body} ); })} @@ -122,7 +154,7 @@ export const Table = ({ data, width, tags, onTagFilterChange, onDatasourceChange
    {rows.length > 0 ? ( , isDashboardList: boolean, availableWidth: number, + showCheckbox: boolean, styles: { [key: string]: string }, tags: string[], onTagFilterChange: (tags: string[]) => void, onDatasourceChange: (datasource?: string) => void ): TableColumn[] => { const columns: TableColumn[] = []; - const urlField = data.fields.url!; + const uidField = data.fields.uid!; const access = data.fields; availableWidth -= 8; // ??? let width = 50; // TODO: Add optional checkbox support - if (false) { - // checkbox column + if (showCheckbox) { columns.push({ id: `column-checkbox`, Header: () => (
    - {}} /> + { + e.stopPropagation(); + e.preventDefault(); + console.log('SELECT ALL!!!', e); + }} + />
    ), - Cell: () => ( -
    - {}} /> -
    - ), - accessor: 'check', - field: urlField, width: 30, + Cell: (p) => { + const uid = uidField.values.get(p.row.index); + return ( +
    +
    + { + console.log('SELECTED!!!', uid); + }} + /> +
    +
    + ); + }, + field: uidField, }); availableWidth -= width; } diff --git a/public/app/features/search/service/types.ts b/public/app/features/search/service/types.ts index 8d13a2a076a..51ee071a1b3 100644 --- a/public/app/features/search/service/types.ts +++ b/public/app/features/search/service/types.ts @@ -3,6 +3,7 @@ import { DataFrame, DataSourceRef } from '@grafana/data'; export interface QueryResult { kind: string; // panel, dashboard, folder name: string; + uid: string; description?: string; url: string; // link to value (unique) tags?: string[]; diff --git a/public/app/features/search/types.ts b/public/app/features/search/types.ts index cb657854ab0..1ee78da5174 100644 --- a/public/app/features/search/types.ts +++ b/public/app/features/search/types.ts @@ -97,6 +97,7 @@ export type OnMoveItems = (selectedDashboards: DashboardSectionItem[], folder: F export enum SearchLayout { List = 'list', Folders = 'folders', + Grid = 'grid', // preview } export interface SearchQueryParams { From 9529c35efa7b919c1ff9e4a3b530c08cc98500bd Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Wed, 4 May 2022 20:26:32 -0400 Subject: [PATCH 046/440] Converter: Add support for parsing prometheus string (#48727) --- pkg/util/converter/prom.go | 29 ++++++++++++++ pkg/util/converter/prom_test.go | 1 + .../converter/testdata/prom-string-frame.json | 40 +++++++++++++++++++ .../converter/testdata/prom-string-golden.txt | 18 +++++++++ pkg/util/converter/testdata/prom-string.json | 10 +++++ 5 files changed, 98 insertions(+) create mode 100644 pkg/util/converter/testdata/prom-string-frame.json create mode 100644 pkg/util/converter/testdata/prom-string-golden.txt create mode 100644 pkg/util/converter/testdata/prom-string.json diff --git a/pkg/util/converter/prom.go b/pkg/util/converter/prom.go index f158206b9d5..43a7ff3e964 100644 --- a/pkg/util/converter/prom.go +++ b/pkg/util/converter/prom.go @@ -111,6 +111,8 @@ func readPrometheusData(iter *jsoniter.Iterator) *backend.DataResponse { rsp = readMatrixOrVector(iter) case "streams": rsp = readStream(iter) + case "string": + rsp = readString(iter) case "scalar": rsp = readScalar(iter) default: @@ -290,6 +292,33 @@ func readLabelsOrExemplars(iter *jsoniter.Iterator) (*data.Frame, [][2]string) { return frame, pairs } +func readString(iter *jsoniter.Iterator) *backend.DataResponse { + timeField := data.NewFieldFromFieldType(data.FieldTypeTime, 0) + timeField.Name = data.TimeSeriesTimeFieldName + valueField := data.NewFieldFromFieldType(data.FieldTypeString, 0) + valueField.Name = data.TimeSeriesValueFieldName + valueField.Labels = data.Labels{} + + iter.ReadArray() + t := iter.ReadFloat64() + iter.ReadArray() + v := iter.ReadString() + iter.ReadArray() + + tt := timeFromFloat(t) + timeField.Append(tt) + valueField.Append(v) + + frame := data.NewFrame("", timeField, valueField) + frame.Meta = &data.FrameMeta{ + Type: data.FrameTypeTimeSeriesMany, + } + + return &backend.DataResponse{ + Frames: []*data.Frame{frame}, + } +} + func readScalar(iter *jsoniter.Iterator) *backend.DataResponse { timeField := data.NewFieldFromFieldType(data.FieldTypeTime, 0) timeField.Name = data.TimeSeriesTimeFieldName diff --git a/pkg/util/converter/prom_test.go b/pkg/util/converter/prom_test.go index c1a4f1692c7..4fc854ca56f 100644 --- a/pkg/util/converter/prom_test.go +++ b/pkg/util/converter/prom_test.go @@ -19,6 +19,7 @@ func TestReadPromFrames(t *testing.T) { "prom-matrix", "prom-matrix-with-nans", "prom-vector", + "prom-string", "prom-scalar", "prom-series", "prom-warnings", diff --git a/pkg/util/converter/testdata/prom-string-frame.json b/pkg/util/converter/testdata/prom-string-frame.json new file mode 100644 index 00000000000..49f65d538b1 --- /dev/null +++ b/pkg/util/converter/testdata/prom-string-frame.json @@ -0,0 +1,40 @@ +{ + "frames": [ + { + "schema": { + "meta": { + "type": "timeseries-many" + }, + "fields": [ + { + "name": "Time", + "type": "time", + "typeInfo": { + "frame": "time.Time" + } + }, + { + "name": "Value", + "type": "string", + "typeInfo": { + "frame": "string" + }, + "labels": { + + } + } + ] + }, + "data": { + "values": [ + [ + 1651680139104 + ], + [ + "example" + ] + ] + } + } + ] +} \ No newline at end of file diff --git a/pkg/util/converter/testdata/prom-string-golden.txt b/pkg/util/converter/testdata/prom-string-golden.txt new file mode 100644 index 00000000000..3ff1517f6c4 --- /dev/null +++ b/pkg/util/converter/testdata/prom-string-golden.txt @@ -0,0 +1,18 @@ +🌟 This was machine generated. Do not edit. 🌟 + +Frame[0] { + "type": "timeseries-many" +} +Name: +Dimensions: 2 Fields by 1 Rows ++-----------------------------------+----------------+ +| Name: Time | Name: Value | +| Labels: | Labels: | +| Type: []time.Time | Type: []string | ++-----------------------------------+----------------+ +| 2022-05-04 16:02:19.104 +0000 UTC | example | ++-----------------------------------+----------------+ + + +====== TEST DATA RESPONSE (arrow base64) ====== +FRAME=QVJST1cxAAD/////yAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAzP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAAz///8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAAJgAAAAEAAAAgv///xQAAABgAAAAZAAAAAAAAAVgAAAAAgAAACwAAAAEAAAAdP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAmP///wgAAAAMAAAAAgAAAHt9AAAGAAAAbGFiZWxzAAAAAAAABAAEAAQAAAAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAAAAAAP/////IAAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAGAAAAAAAAAAUAAAAAAAAAwQACgAYAAwACAAEAAoAAAAUAAAAaAAAAAEAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAEAAAAAAAAAAHAAAAAAAAAAAAAAACAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAmNzlVfLrFgAAAAAHAAAAZXhhbXBsZQAQAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAABAABAAAA2AEAAAAAAADQAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAzP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAAz///8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAAJgAAAAEAAAAgv///xQAAABgAAAAZAAAAAAAAAVgAAAAAgAAACwAAAAEAAAAdP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAmP///wgAAAAMAAAAAgAAAHt9AAAGAAAAbGFiZWxzAAAAAAAABAAEAAQAAAAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAA8AEAAEFSUk9XMQ== diff --git a/pkg/util/converter/testdata/prom-string.json b/pkg/util/converter/testdata/prom-string.json new file mode 100644 index 00000000000..9d2e967356a --- /dev/null +++ b/pkg/util/converter/testdata/prom-string.json @@ -0,0 +1,10 @@ +{ + "status": "success", + "data": { + "resultType": "string", + "result": [ + 1651680139.104, + "example" + ] + } +} \ No newline at end of file From 83a5ca95c75fd5a61575a1347c082468bac5ff56 Mon Sep 17 00:00:00 2001 From: L-M-K-B <48948963+L-M-K-B@users.noreply.github.com> Date: Thu, 5 May 2022 08:40:21 +0200 Subject: [PATCH 047/440] Chore: transfer LiveLogs.test to testing-library (#48678) * Chore: transfer LiveLogs.test to testing-library * Chore: improve readability and third test * Chore: improve test and component after code review --- .betterer.results | 3 - public/app/features/explore/LiveLogs.test.tsx | 155 +++++++++--------- public/app/features/explore/LiveLogs.tsx | 3 +- 3 files changed, 78 insertions(+), 83 deletions(-) diff --git a/.betterer.results b/.betterer.results index 65d99476a08..865f680adee 100644 --- a/.betterer.results +++ b/.betterer.results @@ -215,9 +215,6 @@ exports[`no enzyme tests`] = { "public/app/features/dimensions/editors/ThresholdsEditor/ThresholdsEditor.test.tsx:4164297658": [ [0, 17, 13, "RegExp match", "2409514259"] ], - "public/app/features/explore/LiveLogs.test.tsx:156663779": [ - [0, 17, 13, "RegExp match", "2409514259"] - ], "public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx:3933225580": [ [0, 17, 13, "RegExp match", "2409514259"] ], diff --git a/public/app/features/explore/LiveLogs.test.tsx b/public/app/features/explore/LiveLogs.test.tsx index 901a0f0a9c7..ceebb895113 100644 --- a/public/app/features/explore/LiveLogs.test.tsx +++ b/public/app/features/explore/LiveLogs.test.tsx @@ -1,88 +1,21 @@ -import { mount } from 'enzyme'; +import { render, screen } from '@testing-library/react'; import React from 'react'; import { LogLevel, LogRowModel, MutableDataFrame } from '@grafana/data'; import { LiveLogsWithTheme } from './LiveLogs'; -describe('LiveLogs', () => { - it('renders logs', () => { - const rows: LogRowModel[] = [makeLog({ uid: '1' }), makeLog({ uid: '2' }), makeLog({ uid: '3' })]; - const wrapper = mount( - {}} - onPause={() => {}} - onResume={() => {}} - isPaused={true} - /> - ); - - expect(wrapper.contains('log message 1')).toBeTruthy(); - expect(wrapper.contains('log message 2')).toBeTruthy(); - expect(wrapper.contains('log message 3')).toBeTruthy(); - }); - - it('renders new logs only when not paused', () => { - const rows: LogRowModel[] = [makeLog({ uid: '1' }), makeLog({ uid: '2' }), makeLog({ uid: '3' })]; - const wrapper = mount( - {}} - onPause={() => {}} - onResume={() => {}} - isPaused={true} - /> - ); - - wrapper.setProps({ - ...wrapper.props(), - logRows: [makeLog({ uid: '4' }), makeLog({ uid: '5' }), makeLog({ uid: '6' })], - }); - - expect(wrapper.contains('log message 1')).toBeTruthy(); - expect(wrapper.contains('log message 2')).toBeTruthy(); - expect(wrapper.contains('log message 3')).toBeTruthy(); - - (wrapper.find('LiveLogs').instance() as any).scrollContainerRef.current.scrollTo = () => {}; - - wrapper.setProps({ - ...wrapper.props(), - isPaused: false, - }); - - expect(wrapper.contains('log message 4')).toBeTruthy(); - expect(wrapper.contains('log message 5')).toBeTruthy(); - expect(wrapper.contains('log message 6')).toBeTruthy(); - }); - - it('renders ansi logs', () => { - const rows: LogRowModel[] = [ - makeLog({ uid: '1' }), - makeLog({ hasAnsi: true, raw: 'log message \u001B[31m2\u001B[0m', uid: '2' }), - makeLog({ hasAnsi: true, raw: 'log message \u001B[31m3\u001B[0m', uid: '3' }), - ]; - const wrapper = mount( - {}} - onPause={() => {}} - onResume={() => {}} - isPaused={true} - /> - ); - - expect(wrapper.contains('log message 1')).toBeTruthy(); - expect(wrapper.contains('log message 2')).not.toBeTruthy(); - expect(wrapper.contains('log message 3')).not.toBeTruthy(); - expect(wrapper.find('LogMessageAnsi')).toHaveLength(2); - expect(wrapper.find('LogMessageAnsi').first().prop('value')).toBe('log message \u001B[31m2\u001B[0m'); - expect(wrapper.find('LogMessageAnsi').last().prop('value')).toBe('log message \u001B[31m3\u001B[0m'); - }); -}); +const setup = (rows: LogRowModel[]) => + render( + {}} + onPause={() => {}} + onResume={() => {}} + isPaused={true} + /> + ); const makeLog = (overrides: Partial): LogRowModel => { const uid = overrides.uid || '1'; @@ -106,3 +39,67 @@ const makeLog = (overrides: Partial): LogRowModel => { ...overrides, }; }; + +describe('LiveLogs', () => { + it('renders logs', () => { + setup([makeLog({ uid: '1' }), makeLog({ uid: '2' }), makeLog({ uid: '3' })]); + + expect(screen.getByRole('cell', { name: 'log message 1' })).toBeInTheDocument(); + expect(screen.getByRole('cell', { name: 'log message 2' })).toBeInTheDocument(); + expect(screen.getByRole('cell', { name: 'log message 3' })).toBeInTheDocument(); + }); + + it('renders new logs only when not paused', () => { + const { rerender } = setup([makeLog({ uid: '1' }), makeLog({ uid: '2' }), makeLog({ uid: '3' })]); + + rerender( + {}} + onPause={() => {}} + onResume={() => {}} + isPaused={true} + /> + ); + + expect(screen.getByRole('cell', { name: 'log message 1' })).toBeInTheDocument(); + expect(screen.getByRole('cell', { name: 'log message 2' })).toBeInTheDocument(); + expect(screen.getByRole('cell', { name: 'log message 3' })).toBeInTheDocument(); + expect(screen.queryByRole('cell', { name: 'log message 4' })).not.toBeInTheDocument(); + expect(screen.queryByRole('cell', { name: 'log message 5' })).not.toBeInTheDocument(); + expect(screen.queryByRole('cell', { name: 'log message 6' })).not.toBeInTheDocument(); + + rerender( + {}} + onPause={() => {}} + onResume={() => {}} + isPaused={false} + /> + ); + + expect(screen.getByRole('cell', { name: 'log message 4' })).toBeInTheDocument(); + expect(screen.getByRole('cell', { name: 'log message 5' })).toBeInTheDocument(); + expect(screen.getByRole('cell', { name: 'log message 6' })).toBeInTheDocument(); + }); + + it('renders ansi logs', () => { + setup([ + makeLog({ uid: '1' }), + makeLog({ hasAnsi: true, raw: 'log message \u001B[31m2\u001B[0m', uid: '2' }), + makeLog({ hasAnsi: true, raw: 'log message \u001B[33m3\u001B[0m', uid: '3' }), + ]); + + expect(screen.getByRole('cell', { name: 'log message 1' })).toBeInTheDocument(); + expect(screen.getByRole('cell', { name: 'log message 2' })).toBeInTheDocument(); + expect(screen.getByRole('cell', { name: 'log message 3' })).toBeInTheDocument(); + + const logList = screen.getAllByTestId('ansiLogLine'); + expect(logList).toHaveLength(2); + expect(logList[0]).toHaveAttribute('style', 'color: rgb(204, 0, 0);'); + expect(logList[1]).toHaveAttribute('style', 'color: rgb(204, 102, 0);'); + }); +}); diff --git a/public/app/features/explore/LiveLogs.tsx b/public/app/features/explore/LiveLogs.tsx index bf07dd72aeb..0dbef3c0829 100644 --- a/public/app/features/explore/LiveLogs.tsx +++ b/public/app/features/explore/LiveLogs.tsx @@ -135,7 +135,8 @@ class LiveLogs extends PureComponent { this.liveEndDiv = element; // This is triggered on every update so on every new row. It keeps the view scrolled at the bottom by // default. - if (this.liveEndDiv && !isPaused) { + // As scrollTo is not implemented in JSDOM it needs to be part of the condition + if (this.liveEndDiv && this.scrollContainerRef.current?.scrollTo && !isPaused) { this.scrollContainerRef.current?.scrollTo(0, this.scrollContainerRef.current.scrollHeight); } }} From 46b40b6e82417661078be5f1c4a9d802e440d9ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Thu, 5 May 2022 08:47:40 +0200 Subject: [PATCH 048/440] Loki: backend: use streaming JSON parser (#47656) * converter: remove __name__ customization because Loki does not do that Loki does not handle __name__ in a special way. for Prometheus, the caller can implement the formatting by themselves * converter: change labels-formatting the labels.String() method does not handle strange values well * loki: backend: use streaming-json parser * more idiomatic code Co-authored-by: Todd Treece <360020+toddtreece@users.noreply.github.com> * simpler row-length check * simpler code Co-authored-by: Todd Treece <360020+toddtreece@users.noreply.github.com> --- pkg/tsdb/loki/api.go | 16 +- pkg/tsdb/loki/frame.go | 112 ++++++++-- pkg/tsdb/loki/frame_test.go | 114 ++++++++-- pkg/tsdb/loki/loki.go | 13 +- pkg/tsdb/loki/parse_response.go | 206 ------------------ pkg/tsdb/loki/parse_response_test.go | 183 ---------------- pkg/util/converter/prom.go | 34 ++- .../testdata/loki-streams-a-frame.json | 42 ++-- .../testdata/loki-streams-a-golden.txt | 26 +-- .../testdata/loki-streams-b-frame.json | 40 ++-- .../testdata/loki-streams-b-golden.txt | 24 +- .../converter/testdata/prom-matrix-frame.json | 8 +- .../converter/testdata/prom-matrix-golden.txt | 40 ++-- .../converter/testdata/prom-vector-frame.json | 8 +- .../converter/testdata/prom-vector-golden.txt | 32 +-- .../loki/backendResultTransformer.test.ts | 2 +- 16 files changed, 348 insertions(+), 552 deletions(-) delete mode 100644 pkg/tsdb/loki/parse_response.go delete mode 100644 pkg/tsdb/loki/parse_response_test.go diff --git a/pkg/tsdb/loki/api.go b/pkg/tsdb/loki/api.go index d1d1eb6f813..bca3eec28c6 100644 --- a/pkg/tsdb/loki/api.go +++ b/pkg/tsdb/loki/api.go @@ -10,8 +10,9 @@ import ( "net/url" "strconv" + "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/loki/pkg/loghttp" + "github.com/grafana/grafana/pkg/util/converter" jsoniter "github.com/json-iterator/go" ) @@ -135,7 +136,7 @@ func makeLokiError(body io.ReadCloser) error { return fmt.Errorf("%v", errorMessage) } -func (api *LokiAPI) DataQuery(ctx context.Context, query lokiQuery) (*loghttp.QueryResponse, error) { +func (api *LokiAPI) DataQuery(ctx context.Context, query lokiQuery) (data.Frames, error) { req, err := makeDataRequest(ctx, api.url, query) if err != nil { return nil, err @@ -156,13 +157,14 @@ func (api *LokiAPI) DataQuery(ctx context.Context, query lokiQuery) (*loghttp.Qu return nil, makeLokiError(resp.Body) } - var response loghttp.QueryResponse - err = jsoniter.NewDecoder(resp.Body).Decode(&response) - if err != nil { - return nil, err + iter := jsoniter.Parse(jsoniter.ConfigDefault, resp.Body, 1024) + res := converter.ReadPrometheusStyleResult(iter) + + if res.Error != nil { + return nil, res.Error } - return &response, nil + return res.Frames, nil } func makeRawRequest(ctx context.Context, lokiDsUrl string, resourceURL string) (*http.Request, error) { diff --git a/pkg/tsdb/loki/frame.go b/pkg/tsdb/loki/frame.go index ebfe4247ee1..10affc4dede 100644 --- a/pkg/tsdb/loki/frame.go +++ b/pkg/tsdb/loki/frame.go @@ -6,7 +6,6 @@ import ( "hash/fnv" "sort" "strings" - "time" "github.com/grafana/grafana-plugin-sdk-go/data" ) @@ -57,6 +56,9 @@ func adjustMetricFrame(frame *data.Frame, query *lokiQuery) error { frame.Meta = &data.FrameMeta{} } + frame.Meta.Stats = parseStats(frame.Meta.Custom) + frame.Meta.Custom = nil + if isMetricRange { frame.Meta.ExecutedQueryString = "Expr: " + query.Expr + "\n" + "Step: " + query.Step.String() } else { @@ -81,53 +83,55 @@ func adjustMetricFrame(frame *data.Frame, query *lokiQuery) error { func adjustLogsFrame(frame *data.Frame, query *lokiQuery) error { // we check if the fields are of correct type and length fields := frame.Fields - if len(fields) != 3 { + if len(fields) != 4 { return fmt.Errorf("invalid fields in logs frame") } labelsField := fields[0] timeField := fields[1] lineField := fields[2] + stringTimeField := fields[3] - if (timeField.Type() != data.FieldTypeTime) || (lineField.Type() != data.FieldTypeString) || (labelsField.Type() != data.FieldTypeJSON) { + if (timeField.Type() != data.FieldTypeTime) || (lineField.Type() != data.FieldTypeString) || (labelsField.Type() != data.FieldTypeJSON) || (stringTimeField.Type() != data.FieldTypeString) { return fmt.Errorf("invalid fields in logs frame") } - if (timeField.Len() != lineField.Len()) || (timeField.Len() != labelsField.Len()) { + if (timeField.Len() != lineField.Len()) || (timeField.Len() != labelsField.Len()) || (timeField.Len() != stringTimeField.Len()) { return fmt.Errorf("invalid fields in logs frame") } + // this returns an error when the length of fields do not match + _, err := frame.RowLen() + if err != nil { + return err + } + + labelsField.Name = "labels" + stringTimeField.Name = "tsNs" + if frame.Meta == nil { frame.Meta = &data.FrameMeta{} } + frame.Meta.Stats = parseStats(frame.Meta.Custom) + frame.Meta.Custom = nil + frame.Meta.ExecutedQueryString = "Expr: " + query.Expr // we need to send to the browser the nanosecond-precision timestamp too. // usually timestamps become javascript-date-objects in the browser automatically, which only // have millisecond-precision. - // so we send a separate timestamp-as-string field too. - stringTimeField := makeStringTimeField(timeField) + // so we send a separate timestamp-as-string field too. it is provided by the + // loki-json-parser-code idField, err := makeIdField(stringTimeField, lineField, labelsField, frame.RefID) if err != nil { return err } - frame.Fields = append(frame.Fields, stringTimeField, idField) + frame.Fields = append(frame.Fields, idField) return nil } -func makeStringTimeField(timeField *data.Field) *data.Field { - length := timeField.Len() - stringTimestamps := make([]string, length) - - for i := 0; i < length; i++ { - nsNumber := timeField.At(i).(time.Time).UnixNano() - stringTimestamps[i] = fmt.Sprintf("%d", nsNumber) - } - return data.NewField("tsNs", timeField.Labels.Copy(), stringTimestamps) -} - func calculateCheckSum(time string, line string, labels []byte) (string, error) { input := []byte(line + "_") input = append(input, labels...) @@ -211,3 +215,75 @@ func getFrameLabels(frame *data.Frame) map[string]string { return labels } + +func parseStats(frameMetaCustom interface{}) []data.QueryStat { + customMap, ok := frameMetaCustom.(map[string]interface{}) + if !ok { + return nil + } + rawStats, ok := customMap["stats"].(map[string]interface{}) + if !ok { + return nil + } + + var stats []data.QueryStat + + summary, ok := rawStats["summary"].(map[string]interface{}) + if ok { + stats = append(stats, + makeStat("Summary: bytes processed per second", summary["bytesProcessedPerSecond"], "Bps"), + makeStat("Summary: lines processed per second", summary["linesProcessedPerSecond"], ""), + makeStat("Summary: total bytes processed", summary["totalBytesProcessed"], "decbytes"), + makeStat("Summary: total lines processed", summary["totalLinesProcessed"], ""), + makeStat("Summary: exec time", summary["execTime"], "s")) + } + + store, ok := rawStats["store"].(map[string]interface{}) + if ok { + stats = append(stats, + makeStat("Store: total chunks ref", store["totalChunksRef"], ""), + makeStat("Store: total chunks downloaded", store["totalChunksDownloaded"], ""), + makeStat("Store: chunks download time", store["chunksDownloadTime"], "s"), + makeStat("Store: head chunk bytes", store["headChunkBytes"], "decbytes"), + makeStat("Store: head chunk lines", store["headChunkLines"], ""), + makeStat("Store: decompressed bytes", store["decompressedBytes"], "decbytes"), + makeStat("Store: decompressed lines", store["decompressedLines"], ""), + makeStat("Store: compressed bytes", store["compressedBytes"], "decbytes"), + makeStat("Store: total duplicates", store["totalDuplicates"], "")) + } + + ingester, ok := rawStats["ingester"].(map[string]interface{}) + if ok { + stats = append(stats, + makeStat("Ingester: total reached", ingester["totalReached"], ""), + makeStat("Ingester: total chunks matched", ingester["totalChunksMatched"], ""), + makeStat("Ingester: total batches", ingester["totalBatches"], ""), + makeStat("Ingester: total lines sent", ingester["totalLinesSent"], ""), + makeStat("Ingester: head chunk bytes", ingester["headChunkBytes"], "decbytes"), + makeStat("Ingester: head chunk lines", ingester["headChunkLines"], ""), + makeStat("Ingester: decompressed bytes", ingester["decompressedBytes"], "decbytes"), + makeStat("Ingester: decompressed lines", ingester["decompressedLines"], ""), + makeStat("Ingester: compressed bytes", ingester["compressedBytes"], "decbytes"), + makeStat("Ingester: total duplicates", ingester["totalDuplicates"], "")) + } + + return stats +} + +func makeStat(name string, interfaceValue interface{}, unit string) data.QueryStat { + var value float64 + switch v := interfaceValue.(type) { + case float64: + value = v + case int: + value = float64(v) + } + + return data.QueryStat{ + FieldConfig: data.FieldConfig{ + DisplayName: name, + Unit: unit, + }, + Value: value, + } +} diff --git a/pkg/tsdb/loki/frame_test.go b/pkg/tsdb/loki/frame_test.go index f4da77bb71a..430db3b1330 100644 --- a/pkg/tsdb/loki/frame_test.go +++ b/pkg/tsdb/loki/frame_test.go @@ -2,6 +2,7 @@ package loki import ( "encoding/json" + "strconv" "testing" "time" @@ -39,20 +40,30 @@ func TestFormatName(t *testing.T) { func TestAdjustFrame(t *testing.T) { t.Run("logs-frame metadata should be set correctly", func(t *testing.T) { + time1 := time.Date(2022, 1, 2, 3, 4, 5, 6, time.UTC) + time2 := time.Date(2022, 1, 2, 3, 5, 5, 6, time.UTC) + time3 := time.Date(2022, 1, 2, 3, 5, 5, 6, time.UTC) + time4 := time.Date(2022, 1, 2, 3, 6, 5, 6, time.UTC) + + timeNs1 := strconv.FormatInt(time1.UnixNano(), 10) + timeNs2 := strconv.FormatInt(time2.UnixNano(), 10) + timeNs3 := strconv.FormatInt(time3.UnixNano(), 10) + timeNs4 := strconv.FormatInt(time4.UnixNano(), 10) + frame := data.NewFrame("", - data.NewField("labels", nil, []json.RawMessage{ + data.NewField("__labels", nil, []json.RawMessage{ json.RawMessage(`{"level":"info"}`), json.RawMessage(`{"level":"error"}`), json.RawMessage(`{"level":"error"}`), json.RawMessage(`{"level":"info"}`), }), - data.NewField("time", nil, []time.Time{ - time.Date(2022, 1, 2, 3, 4, 5, 6, time.UTC), - time.Date(2022, 1, 2, 3, 5, 5, 6, time.UTC), - time.Date(2022, 1, 2, 3, 5, 5, 6, time.UTC), - time.Date(2022, 1, 2, 3, 6, 5, 6, time.UTC), + data.NewField("Time", nil, []time.Time{ + time1, time2, time3, time4, + }), + data.NewField("Line", nil, []string{"line1", "line2", "line2", "line3"}), + data.NewField("TS", nil, []string{ + timeNs1, timeNs2, timeNs3, timeNs4, }), - data.NewField("line", nil, []string{"line1", "line2", "line2", "line3"}), ) frame.RefID = "A" @@ -68,14 +79,6 @@ func TestAdjustFrame(t *testing.T) { fields := frame.Fields require.Equal(t, 5, len(fields)) - tsNsField := fields[3] - require.Equal(t, "tsNs", tsNsField.Name) - require.Equal(t, data.FieldTypeString, tsNsField.Type()) - require.Equal(t, 4, tsNsField.Len()) - require.Equal(t, "1641092645000000006", tsNsField.At(0)) - require.Equal(t, "1641092705000000006", tsNsField.At(1)) - require.Equal(t, "1641092705000000006", tsNsField.At(2)) - require.Equal(t, "1641092765000000006", tsNsField.At(3)) idField := fields[4] require.Equal(t, "id", idField.Name) @@ -136,4 +139,85 @@ func TestAdjustFrame(t *testing.T) { require.NotNil(t, timeFieldConfig) require.Equal(t, float64(42000), timeFieldConfig.Interval) }) + + t.Run("should parse response stats", func(t *testing.T) { + stats := map[string]interface{}{ + "summary": map[string]interface{}{ + "bytesProcessedPerSecond": 1, + "linesProcessedPerSecond": 2, + "totalBytesProcessed": 3, + "totalLinesProcessed": 4, + "execTime": 5.5, + }, + + "store": map[string]interface{}{ + "totalChunksRef": 6, + "totalChunksDownloaded": 7, + "chunksDownloadTime": 8.8, + "headChunkBytes": 9, + "headChunkLines": 10, + "decompressedBytes": 11, + "decompressedLines": 12, + "compressedBytes": 13, + "totalDuplicates": 14, + }, + + "ingester": map[string]interface{}{ + "totalReached": 15, + "totalChunksMatched": 16, + "totalBatches": 17, + "totalLinesSent": 18, + "headChunkBytes": 19, + "headChunkLines": 20, + "decompressedBytes": 21, + "decompressedLines": 22, + "compressedBytes": 23, + "totalDuplicates": 24, + }, + } + + meta := data.FrameMeta{ + Custom: map[string]interface{}{ + "stats": stats, + }, + } + + expected := []data.QueryStat{ + {FieldConfig: data.FieldConfig{DisplayName: "Summary: bytes processed per second", Unit: "Bps"}, Value: 1}, + {FieldConfig: data.FieldConfig{DisplayName: "Summary: lines processed per second", Unit: ""}, Value: 2}, + {FieldConfig: data.FieldConfig{DisplayName: "Summary: total bytes processed", Unit: "decbytes"}, Value: 3}, + {FieldConfig: data.FieldConfig{DisplayName: "Summary: total lines processed", Unit: ""}, Value: 4}, + {FieldConfig: data.FieldConfig{DisplayName: "Summary: exec time", Unit: "s"}, Value: 5.5}, + + {FieldConfig: data.FieldConfig{DisplayName: "Store: total chunks ref", Unit: ""}, Value: 6}, + {FieldConfig: data.FieldConfig{DisplayName: "Store: total chunks downloaded", Unit: ""}, Value: 7}, + {FieldConfig: data.FieldConfig{DisplayName: "Store: chunks download time", Unit: "s"}, Value: 8.8}, + {FieldConfig: data.FieldConfig{DisplayName: "Store: head chunk bytes", Unit: "decbytes"}, Value: 9}, + {FieldConfig: data.FieldConfig{DisplayName: "Store: head chunk lines", Unit: ""}, Value: 10}, + {FieldConfig: data.FieldConfig{DisplayName: "Store: decompressed bytes", Unit: "decbytes"}, Value: 11}, + {FieldConfig: data.FieldConfig{DisplayName: "Store: decompressed lines", Unit: ""}, Value: 12}, + {FieldConfig: data.FieldConfig{DisplayName: "Store: compressed bytes", Unit: "decbytes"}, Value: 13}, + {FieldConfig: data.FieldConfig{DisplayName: "Store: total duplicates", Unit: ""}, Value: 14}, + + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total reached", Unit: ""}, Value: 15}, + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total chunks matched", Unit: ""}, Value: 16}, + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total batches", Unit: ""}, Value: 17}, + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total lines sent", Unit: ""}, Value: 18}, + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: head chunk bytes", Unit: "decbytes"}, Value: 19}, + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: head chunk lines", Unit: ""}, Value: 20}, + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: decompressed bytes", Unit: "decbytes"}, Value: 21}, + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: decompressed lines", Unit: ""}, Value: 22}, + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: compressed bytes", Unit: "decbytes"}, Value: 23}, + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total duplicates", Unit: ""}, Value: 24}, + } + + result := parseStats(meta.Custom) + + // NOTE: i compare it item-by-item otherwise the test-fail-error-message is very hard to read + require.Len(t, result, len(expected)) + + for i := 0; i < len(result); i++ { + require.Equal(t, expected[i], result[i]) + } + }) } diff --git a/pkg/tsdb/loki/loki.go b/pkg/tsdb/loki/loki.go index 4404d86d961..e8b0b1f9484 100644 --- a/pkg/tsdb/loki/loki.go +++ b/pkg/tsdb/loki/loki.go @@ -166,12 +166,21 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) // we extracted this part of the functionality to make it easy to unit-test it func runQuery(ctx context.Context, api *LokiAPI, query *lokiQuery) (data.Frames, error) { - value, err := api.DataQuery(ctx, *query) + frames, err := api.DataQuery(ctx, *query) if err != nil { return data.Frames{}, err } - return parseResponse(value, query) + for _, frame := range frames { + if err = adjustFrame(frame, query); err != nil { + return data.Frames{}, err + } + if err != nil { + return data.Frames{}, err + } + } + + return frames, nil } func (s *Service) getDSInfo(pluginCtx backend.PluginContext) (*datasourceInfo, error) { diff --git a/pkg/tsdb/loki/parse_response.go b/pkg/tsdb/loki/parse_response.go deleted file mode 100644 index 4665709912e..00000000000 --- a/pkg/tsdb/loki/parse_response.go +++ /dev/null @@ -1,206 +0,0 @@ -package loki - -import ( - "encoding/json" - "fmt" - "time" - - "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/loki/pkg/loghttp" - "github.com/grafana/loki/pkg/logqlmodel/stats" - jsoniter "github.com/json-iterator/go" -) - -func parseResponse(value *loghttp.QueryResponse, query *lokiQuery) (data.Frames, error) { - frames, err := lokiResponseToDataFrames(value, query) - - if err != nil { - return nil, err - } - - for _, frame := range frames { - err = adjustFrame(frame, query) - if err != nil { - return nil, err - } - } - - return frames, nil -} - -func lokiResponseToDataFrames(value *loghttp.QueryResponse, query *lokiQuery) (data.Frames, error) { - stats := parseStats(value.Data.Statistics) - switch res := value.Data.Result.(type) { - case loghttp.Matrix: - return lokiMatrixToDataFrames(res, query, stats), nil - case loghttp.Vector: - return lokiVectorToDataFrames(res, query, stats), nil - case loghttp.Streams: - return lokiStreamsToDataFrames(res, query, stats) - default: - return nil, fmt.Errorf("resultType %T not supported{", res) - } -} - -func lokiMatrixToDataFrames(matrix loghttp.Matrix, query *lokiQuery, stats []data.QueryStat) data.Frames { - frames := data.Frames{} - - for i, v := range matrix { - tags := make(map[string]string, len(v.Metric)) - timeVector := make([]time.Time, 0, len(v.Values)) - values := make([]float64, 0, len(v.Values)) - - for k, v := range v.Metric { - tags[string(k)] = string(v) - } - - for _, k := range v.Values { - timeVector = append(timeVector, k.Timestamp.Time().UTC()) - values = append(values, float64(k.Value)) - } - - timeField := data.NewField(data.TimeSeriesTimeFieldName, nil, timeVector) - valueField := data.NewField(data.TimeSeriesValueFieldName, tags, values) - - frame := data.NewFrame("", timeField, valueField) - frame.SetMeta(&data.FrameMeta{ - Type: data.FrameTypeTimeSeriesMany, - }) - - // only add the stats to the first dataframe - if i == 0 { - frame.Meta.Stats = stats - } - - frames = append(frames, frame) - } - - return frames -} - -func lokiVectorToDataFrames(vector loghttp.Vector, query *lokiQuery, stats []data.QueryStat) data.Frames { - frames := data.Frames{} - - for i, v := range vector { - tags := make(map[string]string, len(v.Metric)) - timeVector := []time.Time{v.Timestamp.Time().UTC()} - values := []float64{float64(v.Value)} - - for k, v := range v.Metric { - tags[string(k)] = string(v) - } - timeField := data.NewField(data.TimeSeriesTimeFieldName, nil, timeVector) - valueField := data.NewField(data.TimeSeriesValueFieldName, tags, values) - - frame := data.NewFrame("", timeField, valueField) - frame.SetMeta(&data.FrameMeta{ - Type: data.FrameTypeTimeSeriesMany, - }) - - // only add the stats to the first dataframe - if i == 0 { - frame.Meta.Stats = stats - } - - frames = append(frames, frame) - } - - return frames -} - -// we serialize the labels as an ordered list of pairs -func labelsToRawJson(labels data.Labels) (json.RawMessage, error) { - // data.Labels when converted to JSON keep the fields sorted - bytes, err := jsoniter.Marshal(labels) - if err != nil { - return nil, err - } - - return json.RawMessage(bytes), nil -} - -func lokiStreamsToDataFrames(streams loghttp.Streams, query *lokiQuery, stats []data.QueryStat) (data.Frames, error) { - var timeVector []time.Time - var values []string - var labelsVector []json.RawMessage - - for _, v := range streams { - labelsJson, err := labelsToRawJson(v.Labels.Map()) - if err != nil { - return nil, err - } - - for _, k := range v.Entries { - timeVector = append(timeVector, k.Timestamp.UTC()) - values = append(values, k.Line) - labelsVector = append(labelsVector, labelsJson) - } - } - - timeField := data.NewField(data.TimeSeriesTimeFieldName, nil, timeVector) - valueField := data.NewField("Line", nil, values) - labelsField := data.NewField("labels", nil, labelsVector) - - frame := data.NewFrame("", labelsField, timeField, valueField) - frame.SetMeta(&data.FrameMeta{ - Stats: stats, - }) - - return data.Frames{frame}, nil -} - -func parseStats(result stats.Result) []data.QueryStat { - data := []data.QueryStat{ - makeStat("Summary: bytes processed per second", float64(result.Summary.BytesProcessedPerSecond), "Bps"), - makeStat("Summary: lines processed per second", float64(result.Summary.LinesProcessedPerSecond), ""), - makeStat("Summary: total bytes processed", float64(result.Summary.TotalBytesProcessed), "decbytes"), - makeStat("Summary: total lines processed", float64(result.Summary.TotalLinesProcessed), ""), - makeStat("Summary: exec time", result.Summary.ExecTime, "s"), - makeStat("Store: total chunks ref", float64(result.Store.TotalChunksRef), ""), - makeStat("Store: total chunks downloaded", float64(result.Store.TotalChunksDownloaded), ""), - makeStat("Store: chunks download time", result.Store.ChunksDownloadTime, "s"), - makeStat("Store: head chunk bytes", float64(result.Store.HeadChunkBytes), "decbytes"), - makeStat("Store: head chunk lines", float64(result.Store.HeadChunkLines), ""), - makeStat("Store: decompressed bytes", float64(result.Store.DecompressedBytes), "decbytes"), - makeStat("Store: decompressed lines", float64(result.Store.DecompressedLines), ""), - makeStat("Store: compressed bytes", float64(result.Store.CompressedBytes), "decbytes"), - makeStat("Store: total duplicates", float64(result.Store.TotalDuplicates), ""), - makeStat("Ingester: total reached", float64(result.Ingester.TotalReached), ""), - makeStat("Ingester: total chunks matched", float64(result.Ingester.TotalChunksMatched), ""), - makeStat("Ingester: total batches", float64(result.Ingester.TotalBatches), ""), - makeStat("Ingester: total lines sent", float64(result.Ingester.TotalLinesSent), ""), - makeStat("Ingester: head chunk bytes", float64(result.Ingester.HeadChunkBytes), "decbytes"), - makeStat("Ingester: head chunk lines", float64(result.Ingester.HeadChunkLines), ""), - makeStat("Ingester: decompressed bytes", float64(result.Ingester.DecompressedBytes), "decbytes"), - makeStat("Ingester: decompressed lines", float64(result.Ingester.DecompressedLines), ""), - makeStat("Ingester: compressed bytes", float64(result.Ingester.CompressedBytes), "decbytes"), - makeStat("Ingester: total duplicates", float64(result.Ingester.TotalDuplicates), ""), - } - - // it is not possible to know whether the given statistics was missing, or - // it's value was zero. - // we do a heuristic here, if every stat-value is zero, we assume we got no stats-data - allStatsZero := true - for _, stat := range data { - if stat.Value > 0 { - allStatsZero = false - break - } - } - - if allStatsZero { - return nil - } - - return data -} - -func makeStat(name string, value float64, unit string) data.QueryStat { - return data.QueryStat{ - FieldConfig: data.FieldConfig{ - DisplayName: name, - Unit: unit, - }, - Value: value, - } -} diff --git a/pkg/tsdb/loki/parse_response_test.go b/pkg/tsdb/loki/parse_response_test.go deleted file mode 100644 index 07166a45bbd..00000000000 --- a/pkg/tsdb/loki/parse_response_test.go +++ /dev/null @@ -1,183 +0,0 @@ -package loki - -import ( - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/loki/pkg/loghttp" - "github.com/grafana/loki/pkg/logqlmodel/stats" - p "github.com/prometheus/common/model" - "github.com/stretchr/testify/require" -) - -func TestParseResponse(t *testing.T) { - t.Run("value is not of supported type", func(t *testing.T) { - value := loghttp.QueryResponse{ - Data: loghttp.QueryResponseData{ - Result: loghttp.Scalar{}, - }, - } - res, err := parseResponse(&value, nil) - require.Equal(t, len(res), 0) - require.Error(t, err) - }) - - t.Run("response should be parsed normally", func(t *testing.T) { - values := []p.SamplePair{ - {Value: 1, Timestamp: 1000}, - {Value: 2, Timestamp: 2000}, - {Value: 3, Timestamp: 3000}, - {Value: 4, Timestamp: 4000}, - {Value: 5, Timestamp: 5000}, - } - value := loghttp.QueryResponse{ - Data: loghttp.QueryResponseData{ - Result: loghttp.Matrix{ - p.SampleStream{ - Metric: p.Metric{"app": "Application", "tag2": "tag2"}, - Values: values, - }, - }, - }, - } - - query := &lokiQuery{ - Expr: "up(ALERTS)", - QueryType: QueryTypeRange, - LegendFormat: "legend {{app}}", - Step: time.Second * 42, - } - frame, err := parseResponse(&value, query) - require.NoError(t, err) - - labels, err := data.LabelsFromString("app=Application, tag2=tag2") - require.NoError(t, err) - field1 := data.NewField("Time", nil, []time.Time{ - time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC), - time.Date(1970, 1, 1, 0, 0, 2, 0, time.UTC), - time.Date(1970, 1, 1, 0, 0, 3, 0, time.UTC), - time.Date(1970, 1, 1, 0, 0, 4, 0, time.UTC), - time.Date(1970, 1, 1, 0, 0, 5, 0, time.UTC), - }) - field1.Config = &data.FieldConfig{Interval: float64(42000)} - field2 := data.NewField("Value", labels, []float64{1, 2, 3, 4, 5}) - field2.SetConfig(&data.FieldConfig{DisplayNameFromDS: "legend Application"}) - testFrame := data.NewFrame("legend Application", field1, field2) - testFrame.SetMeta(&data.FrameMeta{ - ExecutedQueryString: "Expr: up(ALERTS)\nStep: 42s", - Type: data.FrameTypeTimeSeriesMany, - }) - - if diff := cmp.Diff(testFrame, frame[0], data.FrameTestCompareOptions()...); diff != "" { - t.Errorf("Result mismatch (-want +got):\n%s", diff) - } - }) - - t.Run("should set interval-attribute in response", func(t *testing.T) { - values := []p.SamplePair{ - {Value: 1, Timestamp: 1000}, - } - value := loghttp.QueryResponse{ - Data: loghttp.QueryResponseData{ - Result: loghttp.Matrix{ - p.SampleStream{ - Values: values, - }, - }, - }, - } - - query := &lokiQuery{ - Step: time.Second * 42, - QueryType: QueryTypeRange, - } - - frames, err := parseResponse(&value, query) - require.NoError(t, err) - - // to keep the test simple, we assume the - // first field is the time-field - timeField := frames[0].Fields[0] - require.NotNil(t, timeField) - require.Equal(t, data.FieldTypeTime, timeField.Type()) - - timeFieldConfig := timeField.Config - require.NotNil(t, timeFieldConfig) - require.Equal(t, float64(42000), timeFieldConfig.Interval) - }) - - t.Run("should parse response stats", func(t *testing.T) { - stats := stats.Result{ - Summary: stats.Summary{ - BytesProcessedPerSecond: 1, - LinesProcessedPerSecond: 2, - TotalBytesProcessed: 3, - TotalLinesProcessed: 4, - ExecTime: 5.5, - }, - Store: stats.Store{ - TotalChunksRef: 6, - TotalChunksDownloaded: 7, - ChunksDownloadTime: 8.8, - HeadChunkBytes: 9, - HeadChunkLines: 10, - DecompressedBytes: 11, - DecompressedLines: 12, - CompressedBytes: 13, - TotalDuplicates: 14, - }, - Ingester: stats.Ingester{ - TotalReached: 15, - TotalChunksMatched: 16, - TotalBatches: 17, - TotalLinesSent: 18, - HeadChunkBytes: 19, - HeadChunkLines: 20, - DecompressedBytes: 21, - DecompressedLines: 22, - CompressedBytes: 23, - TotalDuplicates: 24, - }, - } - - expected := []data.QueryStat{ - {FieldConfig: data.FieldConfig{DisplayName: "Summary: bytes processed per second", Unit: "Bps"}, Value: 1}, - {FieldConfig: data.FieldConfig{DisplayName: "Summary: lines processed per second", Unit: ""}, Value: 2}, - {FieldConfig: data.FieldConfig{DisplayName: "Summary: total bytes processed", Unit: "decbytes"}, Value: 3}, - {FieldConfig: data.FieldConfig{DisplayName: "Summary: total lines processed", Unit: ""}, Value: 4}, - {FieldConfig: data.FieldConfig{DisplayName: "Summary: exec time", Unit: "s"}, Value: 5.5}, - - {FieldConfig: data.FieldConfig{DisplayName: "Store: total chunks ref", Unit: ""}, Value: 6}, - {FieldConfig: data.FieldConfig{DisplayName: "Store: total chunks downloaded", Unit: ""}, Value: 7}, - {FieldConfig: data.FieldConfig{DisplayName: "Store: chunks download time", Unit: "s"}, Value: 8.8}, - {FieldConfig: data.FieldConfig{DisplayName: "Store: head chunk bytes", Unit: "decbytes"}, Value: 9}, - {FieldConfig: data.FieldConfig{DisplayName: "Store: head chunk lines", Unit: ""}, Value: 10}, - {FieldConfig: data.FieldConfig{DisplayName: "Store: decompressed bytes", Unit: "decbytes"}, Value: 11}, - {FieldConfig: data.FieldConfig{DisplayName: "Store: decompressed lines", Unit: ""}, Value: 12}, - {FieldConfig: data.FieldConfig{DisplayName: "Store: compressed bytes", Unit: "decbytes"}, Value: 13}, - {FieldConfig: data.FieldConfig{DisplayName: "Store: total duplicates", Unit: ""}, Value: 14}, - - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total reached", Unit: ""}, Value: 15}, - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total chunks matched", Unit: ""}, Value: 16}, - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total batches", Unit: ""}, Value: 17}, - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total lines sent", Unit: ""}, Value: 18}, - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: head chunk bytes", Unit: "decbytes"}, Value: 19}, - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: head chunk lines", Unit: ""}, Value: 20}, - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: decompressed bytes", Unit: "decbytes"}, Value: 21}, - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: decompressed lines", Unit: ""}, Value: 22}, - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: compressed bytes", Unit: "decbytes"}, Value: 23}, - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total duplicates", Unit: ""}, Value: 24}, - } - - result := parseStats((stats)) - - // NOTE: i compare it item-by-item otherwise the test-fail-error-message is very hard to read - require.Len(t, result, len(expected)) - - for i := 0; i < len(result); i++ { - require.Equal(t, expected[i], result[i]) - } - }) -} diff --git a/pkg/util/converter/prom.go b/pkg/util/converter/prom.go index 43a7ff3e964..dcde552106c 100644 --- a/pkg/util/converter/prom.go +++ b/pkg/util/converter/prom.go @@ -1,6 +1,7 @@ package converter import ( + "encoding/json" "fmt" "strconv" "time" @@ -349,6 +350,7 @@ func readMatrixOrVector(iter *jsoniter.Iterator) *backend.DataResponse { timeField := data.NewFieldFromFieldType(data.FieldTypeTime, 0) timeField.Name = data.TimeSeriesTimeFieldName valueField := data.NewFieldFromFieldType(data.FieldTypeFloat64, 0) + valueField.Name = data.TimeSeriesValueFieldName valueField.Labels = data.Labels{} for l1Field := iter.ReadObject(); l1Field != ""; l1Field = iter.ReadObject() { @@ -375,14 +377,6 @@ func readMatrixOrVector(iter *jsoniter.Iterator) *backend.DataResponse { } } - name, ok := valueField.Labels["__name__"] - if ok { - valueField.Name = name - delete(valueField.Labels, "__name__") - } else { - valueField.Name = data.TimeSeriesValueFieldName - } - frame := data.NewFrame("", timeField, valueField) frame.Meta = &data.FrameMeta{ Type: data.FrameTypeTimeSeriesMany, @@ -408,7 +402,7 @@ func readTimeValuePair(iter *jsoniter.Iterator) (time.Time, float64, error) { func readStream(iter *jsoniter.Iterator) *backend.DataResponse { rsp := &backend.DataResponse{} - labelsField := data.NewFieldFromFieldType(data.FieldTypeString, 0) + labelsField := data.NewFieldFromFieldType(data.FieldTypeJSON, 0) labelsField.Name = "__labels" // avoid automatically spreading this by labels timeField := data.NewFieldFromFieldType(data.FieldTypeTime, 0) @@ -422,14 +416,20 @@ func readStream(iter *jsoniter.Iterator) *backend.DataResponse { tsField.Name = "TS" labels := data.Labels{} - labelString := labels.String() + labelJson, err := labelsToRawJson(labels) + if err != nil { + return &backend.DataResponse{Error: err} + } for iter.ReadArray() { for l1Field := iter.ReadObject(); l1Field != ""; l1Field = iter.ReadObject() { switch l1Field { case "stream": iter.ReadVal(&labels) - labelString = labels.String() + labelJson, err = labelsToRawJson(labels) + if err != nil { + return &backend.DataResponse{Error: err} + } case "values": for iter.ReadArray() { @@ -441,7 +441,7 @@ func readStream(iter *jsoniter.Iterator) *backend.DataResponse { t := timeFromLokiString(ts) - labelsField.Append(labelString) + labelsField.Append(labelJson) timeField.Append(t) lineField.Append(line) tsField.Append(ts) @@ -477,3 +477,13 @@ func timeFromLokiString(str string) time.Time { ns, _ := strconv.ParseInt(str[10:], 10, 64) return time.Unix(ss, ns).UTC() } + +func labelsToRawJson(labels data.Labels) (json.RawMessage, error) { + // data.Labels when converted to JSON keep the fields sorted + bytes, err := jsoniter.Marshal(labels) + if err != nil { + return nil, err + } + + return json.RawMessage(bytes), nil +} diff --git a/pkg/util/converter/testdata/loki-streams-a-frame.json b/pkg/util/converter/testdata/loki-streams-a-frame.json index bd5df94989e..622d46826a1 100644 --- a/pkg/util/converter/testdata/loki-streams-a-frame.json +++ b/pkg/util/converter/testdata/loki-streams-a-frame.json @@ -5,17 +5,28 @@ "meta": { "custom": { "stats": { - "ingester": { - "totalChunksMatched": 0, - "totalBatches": 0, - "totalLinesSent": 0, + "store": { "headChunkBytes": 0, "headChunkLines": 0, + "compressedBytes": 31432, + "decompressedBytes": 7772, + "decompressedLines": 55, + "totalDuplicates": 0, + "totalChunksRef": 2, + "totalChunksDownloaded": 2, + "chunksDownloadTime": 0.000390958 + }, + "ingester": { + "totalReached": 0, + "headChunkBytes": 0, + "totalDuplicates": 0, + "headChunkLines": 0, + "decompressedBytes": 0, "decompressedLines": 0, "compressedBytes": 0, - "totalReached": 0, - "totalDuplicates": 0, - "decompressedBytes": 0 + "totalChunksMatched": 0, + "totalBatches": 0, + "totalLinesSent": 0 }, "summary": { "bytesProcessedPerSecond": 3507022, @@ -23,17 +34,6 @@ "totalBytesProcessed": 7772, "totalLinesProcessed": 55, "execTime": 0.002216125 - }, - "store": { - "totalChunksDownloaded": 2, - "headChunkBytes": 0, - "decompressedLines": 55, - "totalDuplicates": 0, - "totalChunksRef": 2, - "headChunkLines": 0, - "decompressedBytes": 7772, - "compressedBytes": 31432, - "chunksDownloadTime": 0.000390958 } } } @@ -41,9 +41,9 @@ "fields": [ { "name": "__labels", - "type": "string", + "type": "other", "typeInfo": { - "frame": "string" + "frame": "json.RawMessage" } }, { @@ -72,7 +72,7 @@ "data": { "values": [ [ - "level=error, location=moon🌙","level=info, location=moon🌙","level=info, location=moon🌙","level=info, location=moon🌙","level=info, location=moon🌙","level=info, location=moon🌙" + {"level":"error","location":"moon🌙"},{"level":"info","location":"moon🌙"},{"level":"info","location":"moon🌙"},{"level":"info","location":"moon🌙"},{"level":"info","location":"moon🌙"},{"level":"info","location":"moon🌙"} ], [ 1645030244810,1645030247027,1645030246277,1645030246277,1645030245539,1645030244091 diff --git a/pkg/util/converter/testdata/loki-streams-a-golden.txt b/pkg/util/converter/testdata/loki-streams-a-golden.txt index 727f13947ae..9d1fdaa4caf 100644 --- a/pkg/util/converter/testdata/loki-streams-a-golden.txt +++ b/pkg/util/converter/testdata/loki-streams-a-golden.txt @@ -38,19 +38,19 @@ Frame[0] { } Name: Dimensions: 4 Fields by 6 Rows -+------------------------------+-----------------------------------------+------------------+---------------------+ -| Name: __labels | Name: Time | Name: Line | Name: TS | -| Labels: | Labels: | Labels: | Labels: | -| Type: []string | Type: []time.Time | Type: []string | Type: []string | -+------------------------------+-----------------------------------------+------------------+---------------------+ -| level=error, location=moon🌙 | 2022-02-16 16:50:44.81075712 +0000 UTC | log line error 1 | 1645030244810757120 | -| level=info, location=moon🌙 | 2022-02-16 16:50:47.02773504 +0000 UTC | log line info 1 | 1645030247027735040 | -| level=info, location=moon🌙 | 2022-02-16 16:50:46.277587968 +0000 UTC | log line info 2 | 1645030246277587968 | -| level=info, location=moon🌙 | 2022-02-16 16:50:46.277587968 +0000 UTC | log line info 2 | 1645030246277587968 | -| level=info, location=moon🌙 | 2022-02-16 16:50:45.539423744 +0000 UTC | log line info 3 | 1645030245539423744 | -| level=info, location=moon🌙 | 2022-02-16 16:50:44.091700992 +0000 UTC | log line info 4 | 1645030244091700992 | -+------------------------------+-----------------------------------------+------------------+---------------------+ ++---------------------------------------+-----------------------------------------+------------------+---------------------+ +| Name: __labels | Name: Time | Name: Line | Name: TS | +| Labels: | Labels: | Labels: | Labels: | +| Type: []json.RawMessage | Type: []time.Time | Type: []string | Type: []string | ++---------------------------------------+-----------------------------------------+------------------+---------------------+ +| {"level":"error","location":"moon🌙"} | 2022-02-16 16:50:44.81075712 +0000 UTC | log line error 1 | 1645030244810757120 | +| {"level":"info","location":"moon🌙"} | 2022-02-16 16:50:47.02773504 +0000 UTC | log line info 1 | 1645030247027735040 | +| {"level":"info","location":"moon🌙"} | 2022-02-16 16:50:46.277587968 +0000 UTC | log line info 2 | 1645030246277587968 | +| {"level":"info","location":"moon🌙"} | 2022-02-16 16:50:46.277587968 +0000 UTC | log line info 2 | 1645030246277587968 | +| {"level":"info","location":"moon🌙"} | 2022-02-16 16:50:45.539423744 +0000 UTC | log line info 3 | 1645030245539423744 | +| {"level":"info","location":"moon🌙"} | 2022-02-16 16:50:44.091700992 +0000 UTC | log line info 4 | 1645030244091700992 | ++---------------------------------------+-----------------------------------------+------------------+---------------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=QVJST1cxAAD/////oAQAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAANACAAADAAAATAAAACgAAAAEAAAA9Pv//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAAAU/P//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAADT8//8IAAAAaAIAAFwCAAB7ImN1c3RvbSI6eyJzdGF0cyI6eyJpbmdlc3RlciI6eyJjb21wcmVzc2VkQnl0ZXMiOjAsImRlY29tcHJlc3NlZEJ5dGVzIjowLCJkZWNvbXByZXNzZWRMaW5lcyI6MCwiaGVhZENodW5rQnl0ZXMiOjAsImhlYWRDaHVua0xpbmVzIjowLCJ0b3RhbEJhdGNoZXMiOjAsInRvdGFsQ2h1bmtzTWF0Y2hlZCI6MCwidG90YWxEdXBsaWNhdGVzIjowLCJ0b3RhbExpbmVzU2VudCI6MCwidG90YWxSZWFjaGVkIjowfSwic3RvcmUiOnsiY2h1bmtzRG93bmxvYWRUaW1lIjowLjAwMDM5MDk1OCwiY29tcHJlc3NlZEJ5dGVzIjozMTQzMiwiZGVjb21wcmVzc2VkQnl0ZXMiOjc3NzIsImRlY29tcHJlc3NlZExpbmVzIjo1NSwiaGVhZENodW5rQnl0ZXMiOjAsImhlYWRDaHVua0xpbmVzIjowLCJ0b3RhbENodW5rc0Rvd25sb2FkZWQiOjIsInRvdGFsQ2h1bmtzUmVmIjoyLCJ0b3RhbER1cGxpY2F0ZXMiOjB9LCJzdW1tYXJ5Ijp7ImJ5dGVzUHJvY2Vzc2VkUGVyU2Vjb25kIjozNTA3MDIyLCJleGVjVGltZSI6MC4wMDIyMTYxMjUsImxpbmVzUHJvY2Vzc2VkUGVyU2Vjb25kIjoyNDgxOCwidG90YWxCeXRlc1Byb2Nlc3NlZCI6Nzc3MiwidG90YWxMaW5lc1Byb2Nlc3NlZCI6NTV9fX19AAAAAAQAAABtZXRhAAAAAAQAAAAsAQAAtAAAAFgAAAAEAAAA9v7//xQAAAA4AAAAOAAAAAAAAAU0AAAAAQAAAAQAAADk/v//CAAAAAwAAAACAAAAVFMAAAQAAABuYW1lAAAAAAAAAADU/v//AgAAAFRTAABG////FAAAADwAAAA8AAAAAAAABTgAAAABAAAABAAAADT///8IAAAAEAAAAAQAAABMaW5lAAAAAAQAAABuYW1lAAAAAAAAAAAo////BAAAAExpbmUAAAAAnv///xQAAAA8AAAARAAAAAAAAApEAAAAAQAAAAQAAACM////CAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEgAAABMAAAAAAAABUgAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAUAAAACAAAAF9fbGFiZWxzAAAAAAQAAABuYW1lAAAAAAAAAAAEAAQABAAAAAgAAABfX2xhYmVscwAAAAAAAAAA/////0gBAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAYAgAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAADIAAAABgAAAAAAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAAAAAIAAAAAAAAACvAAAAAAAAANAAAAAAAAAAAAAAAAAAAADQAAAAAAAAADAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAHAAAAAAAAAAgAQAAAAAAAFsAAAAAAAAAgAEAAAAAAAAAAAAAAAAAAIABAAAAAAAAHAAAAAAAAACgAQAAAAAAAHIAAAAAAAAAAAAAAAQAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAA7AAAAWAAAAHUAAACSAAAArwAAAAAAAABsZXZlbD1lcnJvciwgbG9jYXRpb249bW9vbvCfjJlsZXZlbD1pbmZvLCBsb2NhdGlvbj1tb29u8J+MmWxldmVsPWluZm8sIGxvY2F0aW9uPW1vb27wn4yZbGV2ZWw9aW5mbywgbG9jYXRpb249bW9vbvCfjJlsZXZlbD1pbmZvLCBsb2NhdGlvbj1tb29u8J+MmWxldmVsPWluZm8sIGxvY2F0aW9uPW1vb27wn4yZAAAUuLpKUtQWAHrcPktS1BYAJCYSS1LUFgAkJhJLUtQWAKYm5kpS1BYAJ9yPSlLUFgAAAAAQAAAAHwAAAC4AAAA9AAAATAAAAFsAAAAAAAAAbG9nIGxpbmUgZXJyb3IgMWxvZyBsaW5lIGluZm8gMWxvZyBsaW5lIGluZm8gMmxvZyBsaW5lIGluZm8gMmxvZyBsaW5lIGluZm8gM2xvZyBsaW5lIGluZm8gNAAAAAAAAAAAABMAAAAmAAAAOQAAAEwAAABfAAAAcgAAAAAAAAAxNjQ1MDMwMjQ0ODEwNzU3MTIwMTY0NTAzMDI0NzAyNzczNTA0MDE2NDUwMzAyNDYyNzc1ODc5NjgxNjQ1MDMwMjQ2Mjc3NTg3OTY4MTY0NTAzMDI0NTUzOTQyMzc0NDE2NDUwMzAyNDQwOTE3MDA5OTIAAAAAAAAQAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAABAABAAAAsAQAAAAAAABQAQAAAAAAABgCAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAANACAAADAAAATAAAACgAAAAEAAAA9Pv//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAAAU/P//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAADT8//8IAAAAaAIAAFwCAAB7ImN1c3RvbSI6eyJzdGF0cyI6eyJpbmdlc3RlciI6eyJjb21wcmVzc2VkQnl0ZXMiOjAsImRlY29tcHJlc3NlZEJ5dGVzIjowLCJkZWNvbXByZXNzZWRMaW5lcyI6MCwiaGVhZENodW5rQnl0ZXMiOjAsImhlYWRDaHVua0xpbmVzIjowLCJ0b3RhbEJhdGNoZXMiOjAsInRvdGFsQ2h1bmtzTWF0Y2hlZCI6MCwidG90YWxEdXBsaWNhdGVzIjowLCJ0b3RhbExpbmVzU2VudCI6MCwidG90YWxSZWFjaGVkIjowfSwic3RvcmUiOnsiY2h1bmtzRG93bmxvYWRUaW1lIjowLjAwMDM5MDk1OCwiY29tcHJlc3NlZEJ5dGVzIjozMTQzMiwiZGVjb21wcmVzc2VkQnl0ZXMiOjc3NzIsImRlY29tcHJlc3NlZExpbmVzIjo1NSwiaGVhZENodW5rQnl0ZXMiOjAsImhlYWRDaHVua0xpbmVzIjowLCJ0b3RhbENodW5rc0Rvd25sb2FkZWQiOjIsInRvdGFsQ2h1bmtzUmVmIjoyLCJ0b3RhbER1cGxpY2F0ZXMiOjB9LCJzdW1tYXJ5Ijp7ImJ5dGVzUHJvY2Vzc2VkUGVyU2Vjb25kIjozNTA3MDIyLCJleGVjVGltZSI6MC4wMDIyMTYxMjUsImxpbmVzUHJvY2Vzc2VkUGVyU2Vjb25kIjoyNDgxOCwidG90YWxCeXRlc1Byb2Nlc3NlZCI6Nzc3MiwidG90YWxMaW5lc1Byb2Nlc3NlZCI6NTV9fX19AAAAAAQAAABtZXRhAAAAAAQAAAAsAQAAtAAAAFgAAAAEAAAA9v7//xQAAAA4AAAAOAAAAAAAAAU0AAAAAQAAAAQAAADk/v//CAAAAAwAAAACAAAAVFMAAAQAAABuYW1lAAAAAAAAAADU/v//AgAAAFRTAABG////FAAAADwAAAA8AAAAAAAABTgAAAABAAAABAAAADT///8IAAAAEAAAAAQAAABMaW5lAAAAAAQAAABuYW1lAAAAAAAAAAAo////BAAAAExpbmUAAAAAnv///xQAAAA8AAAARAAAAAAAAApEAAAAAQAAAAQAAACM////CAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEgAAABMAAAAAAAABUgAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAUAAAACAAAAF9fbGFiZWxzAAAAAAQAAABuYW1lAAAAAAAAAAAEAAQABAAAAAgAAABfX2xhYmVscwAAAADIBAAAQVJST1cx +FRAME=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 diff --git a/pkg/util/converter/testdata/loki-streams-b-frame.json b/pkg/util/converter/testdata/loki-streams-b-frame.json index 28ef33d2768..7c8c7dd3610 100644 --- a/pkg/util/converter/testdata/loki-streams-b-frame.json +++ b/pkg/util/converter/testdata/loki-streams-b-frame.json @@ -6,34 +6,34 @@ "custom": { "stats": { "summary": { - "execTime": 0.002216125, "bytesProcessedPerSecond": 3507022, "linesProcessedPerSecond": 24818, "totalBytesProcessed": 7772, - "totalLinesProcessed": 55 + "totalLinesProcessed": 55, + "execTime": 0.002216125 }, "store": { - "headChunkBytes": 0, - "decompressedLines": 55, - "compressedBytes": 31432, - "totalDuplicates": 0, - "totalChunksDownloaded": 2, + "totalChunksRef": 2, "chunksDownloadTime": 0.000390958, "headChunkLines": 0, + "decompressedLines": 55, + "totalDuplicates": 0, + "totalChunksDownloaded": 2, + "headChunkBytes": 0, "decompressedBytes": 7772, - "totalChunksRef": 2 + "compressedBytes": 31432 }, "ingester": { - "headChunkBytes": 0, - "decompressedBytes": 0, - "totalBatches": 0, - "totalLinesSent": 0, - "headChunkLines": 0, - "decompressedLines": 0, - "compressedBytes": 0, - "totalDuplicates": 0, "totalReached": 0, - "totalChunksMatched": 0 + "totalChunksMatched": 0, + "totalLinesSent": 0, + "headChunkBytes": 0, + "decompressedLines": 0, + "totalBatches": 0, + "headChunkLines": 0, + "decompressedBytes": 0, + "compressedBytes": 0, + "totalDuplicates": 0 } } } @@ -41,9 +41,9 @@ "fields": [ { "name": "__labels", - "type": "string", + "type": "other", "typeInfo": { - "frame": "string" + "frame": "json.RawMessage" } }, { @@ -72,7 +72,7 @@ "data": { "values": [ [ - "level=error, location=moon","level=info, location=moon","level=info, location=moon","level=info, location=moon","level=info, location=moon" + {"level":"error","location":"moon"},{"level":"info","location":"moon"},{"level":"info","location":"moon"},{"level":"info","location":"moon"},{"level":"info","location":"moon"} ], [ 1645030244810,1645030247027,1645030246277,1645030245539,1645030244091 diff --git a/pkg/util/converter/testdata/loki-streams-b-golden.txt b/pkg/util/converter/testdata/loki-streams-b-golden.txt index c5291a2e1a5..d8b09e5d00a 100644 --- a/pkg/util/converter/testdata/loki-streams-b-golden.txt +++ b/pkg/util/converter/testdata/loki-streams-b-golden.txt @@ -38,18 +38,18 @@ Frame[0] { } Name: Dimensions: 4 Fields by 5 Rows -+----------------------------+-----------------------------------------+------------------+---------------------+ -| Name: __labels | Name: Time | Name: Line | Name: TS | -| Labels: | Labels: | Labels: | Labels: | -| Type: []string | Type: []time.Time | Type: []string | Type: []string | -+----------------------------+-----------------------------------------+------------------+---------------------+ -| level=error, location=moon | 2022-02-16 16:50:44.81075712 +0000 UTC | log line error 1 | 1645030244810757120 | -| level=info, location=moon | 2022-02-16 16:50:47.02773504 +0000 UTC | log line info 1 | 1645030247027735040 | -| level=info, location=moon | 2022-02-16 16:50:46.277587968 +0000 UTC | log line info 2 | 1645030246277587968 | -| level=info, location=moon | 2022-02-16 16:50:45.539423744 +0000 UTC | log line info 3 | 1645030245539423744 | -| level=info, location=moon | 2022-02-16 16:50:44.091700992 +0000 UTC | log line info 4 | 1645030244091700992 | -+----------------------------+-----------------------------------------+------------------+---------------------+ ++-------------------------------------+-----------------------------------------+------------------+---------------------+ +| Name: __labels | Name: Time | Name: Line | Name: TS | +| Labels: | Labels: | Labels: | Labels: | +| Type: []json.RawMessage | Type: []time.Time | Type: []string | Type: []string | ++-------------------------------------+-----------------------------------------+------------------+---------------------+ +| {"level":"error","location":"moon"} | 2022-02-16 16:50:44.81075712 +0000 UTC | log line error 1 | 1645030244810757120 | +| {"level":"info","location":"moon"} | 2022-02-16 16:50:47.02773504 +0000 UTC | log line info 1 | 1645030247027735040 | +| {"level":"info","location":"moon"} | 2022-02-16 16:50:46.277587968 +0000 UTC | log line info 2 | 1645030246277587968 | +| {"level":"info","location":"moon"} | 2022-02-16 16:50:45.539423744 +0000 UTC | log line info 3 | 1645030245539423744 | +| {"level":"info","location":"moon"} | 2022-02-16 16:50:44.091700992 +0000 UTC | log line info 4 | 1645030244091700992 | ++-------------------------------------+-----------------------------------------+------------------+---------------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=QVJST1cxAAD/////oAQAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAANACAAADAAAATAAAACgAAAAEAAAA9Pv//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAAAU/P//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAADT8//8IAAAAaAIAAFwCAAB7ImN1c3RvbSI6eyJzdGF0cyI6eyJpbmdlc3RlciI6eyJjb21wcmVzc2VkQnl0ZXMiOjAsImRlY29tcHJlc3NlZEJ5dGVzIjowLCJkZWNvbXByZXNzZWRMaW5lcyI6MCwiaGVhZENodW5rQnl0ZXMiOjAsImhlYWRDaHVua0xpbmVzIjowLCJ0b3RhbEJhdGNoZXMiOjAsInRvdGFsQ2h1bmtzTWF0Y2hlZCI6MCwidG90YWxEdXBsaWNhdGVzIjowLCJ0b3RhbExpbmVzU2VudCI6MCwidG90YWxSZWFjaGVkIjowfSwic3RvcmUiOnsiY2h1bmtzRG93bmxvYWRUaW1lIjowLjAwMDM5MDk1OCwiY29tcHJlc3NlZEJ5dGVzIjozMTQzMiwiZGVjb21wcmVzc2VkQnl0ZXMiOjc3NzIsImRlY29tcHJlc3NlZExpbmVzIjo1NSwiaGVhZENodW5rQnl0ZXMiOjAsImhlYWRDaHVua0xpbmVzIjowLCJ0b3RhbENodW5rc0Rvd25sb2FkZWQiOjIsInRvdGFsQ2h1bmtzUmVmIjoyLCJ0b3RhbER1cGxpY2F0ZXMiOjB9LCJzdW1tYXJ5Ijp7ImJ5dGVzUHJvY2Vzc2VkUGVyU2Vjb25kIjozNTA3MDIyLCJleGVjVGltZSI6MC4wMDIyMTYxMjUsImxpbmVzUHJvY2Vzc2VkUGVyU2Vjb25kIjoyNDgxOCwidG90YWxCeXRlc1Byb2Nlc3NlZCI6Nzc3MiwidG90YWxMaW5lc1Byb2Nlc3NlZCI6NTV9fX19AAAAAAQAAABtZXRhAAAAAAQAAAAsAQAAtAAAAFgAAAAEAAAA9v7//xQAAAA4AAAAOAAAAAAAAAU0AAAAAQAAAAQAAADk/v//CAAAAAwAAAACAAAAVFMAAAQAAABuYW1lAAAAAAAAAADU/v//AgAAAFRTAABG////FAAAADwAAAA8AAAAAAAABTgAAAABAAAABAAAADT///8IAAAAEAAAAAQAAABMaW5lAAAAAAQAAABuYW1lAAAAAAAAAAAo////BAAAAExpbmUAAAAAnv///xQAAAA8AAAARAAAAAAAAApEAAAAAQAAAAQAAACM////CAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEgAAABMAAAAAAAABEgAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAUAAAACAAAAF9fbGFiZWxzAAAAAAQAAABuYW1lAAAAAAAAAAAEAAQABAAAAAgAAABfX2xhYmVscwAAAAAAAAAA/////0gBAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAADQAQAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAADIAAAABQAAAAAAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAGAAAAAAAAACrAAAAAAAAAMgAAAAAAAAAAAAAAAAAAADIAAAAAAAAACgAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAGAAAAAAAAAAIAQAAAAAAAEwAAAAAAAAAWAEAAAAAAAAAAAAAAAAAAFgBAAAAAAAAGAAAAAAAAABwAQAAAAAAAF8AAAAAAAAAAAAAAAQAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAACMAAABFAAAAZwAAAIkAAACrAAAAeyJsZXZlbCI6ImVycm9yIiwibG9jYXRpb24iOiJtb29uIn17ImxldmVsIjoiaW5mbyIsImxvY2F0aW9uIjoibW9vbiJ9eyJsZXZlbCI6ImluZm8iLCJsb2NhdGlvbiI6Im1vb24ifXsibGV2ZWwiOiJpbmZvIiwibG9jYXRpb24iOiJtb29uIn17ImxldmVsIjoiaW5mbyIsImxvY2F0aW9uIjoibW9vbiJ9AAAAAAAAFLi6SlLUFgB63D5LUtQWACQmEktS1BYApibmSlLUFgAn3I9KUtQWAAAAABAAAAAfAAAALgAAAD0AAABMAAAAbG9nIGxpbmUgZXJyb3IgMWxvZyBsaW5lIGluZm8gMWxvZyBsaW5lIGluZm8gMmxvZyBsaW5lIGluZm8gM2xvZyBsaW5lIGluZm8gNAAAAAAAAAAAEwAAACYAAAA5AAAATAAAAF8AAAAxNjQ1MDMwMjQ0ODEwNzU3MTIwMTY0NTAzMDI0NzAyNzczNTA0MDE2NDUwMzAyNDYyNzc1ODc5NjgxNjQ1MDMwMjQ1NTM5NDIzNzQ0MTY0NTAzMDI0NDA5MTcwMDk5MgAQAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAABAABAAAAsAQAAAAAAABQAQAAAAAAANABAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAANACAAADAAAATAAAACgAAAAEAAAA9Pv//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAAAU/P//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAADT8//8IAAAAaAIAAFwCAAB7ImN1c3RvbSI6eyJzdGF0cyI6eyJpbmdlc3RlciI6eyJjb21wcmVzc2VkQnl0ZXMiOjAsImRlY29tcHJlc3NlZEJ5dGVzIjowLCJkZWNvbXByZXNzZWRMaW5lcyI6MCwiaGVhZENodW5rQnl0ZXMiOjAsImhlYWRDaHVua0xpbmVzIjowLCJ0b3RhbEJhdGNoZXMiOjAsInRvdGFsQ2h1bmtzTWF0Y2hlZCI6MCwidG90YWxEdXBsaWNhdGVzIjowLCJ0b3RhbExpbmVzU2VudCI6MCwidG90YWxSZWFjaGVkIjowfSwic3RvcmUiOnsiY2h1bmtzRG93bmxvYWRUaW1lIjowLjAwMDM5MDk1OCwiY29tcHJlc3NlZEJ5dGVzIjozMTQzMiwiZGVjb21wcmVzc2VkQnl0ZXMiOjc3NzIsImRlY29tcHJlc3NlZExpbmVzIjo1NSwiaGVhZENodW5rQnl0ZXMiOjAsImhlYWRDaHVua0xpbmVzIjowLCJ0b3RhbENodW5rc0Rvd25sb2FkZWQiOjIsInRvdGFsQ2h1bmtzUmVmIjoyLCJ0b3RhbER1cGxpY2F0ZXMiOjB9LCJzdW1tYXJ5Ijp7ImJ5dGVzUHJvY2Vzc2VkUGVyU2Vjb25kIjozNTA3MDIyLCJleGVjVGltZSI6MC4wMDIyMTYxMjUsImxpbmVzUHJvY2Vzc2VkUGVyU2Vjb25kIjoyNDgxOCwidG90YWxCeXRlc1Byb2Nlc3NlZCI6Nzc3MiwidG90YWxMaW5lc1Byb2Nlc3NlZCI6NTV9fX19AAAAAAQAAABtZXRhAAAAAAQAAAAsAQAAtAAAAFgAAAAEAAAA9v7//xQAAAA4AAAAOAAAAAAAAAU0AAAAAQAAAAQAAADk/v//CAAAAAwAAAACAAAAVFMAAAQAAABuYW1lAAAAAAAAAADU/v//AgAAAFRTAABG////FAAAADwAAAA8AAAAAAAABTgAAAABAAAABAAAADT///8IAAAAEAAAAAQAAABMaW5lAAAAAAQAAABuYW1lAAAAAAAAAAAo////BAAAAExpbmUAAAAAnv///xQAAAA8AAAARAAAAAAAAApEAAAAAQAAAAQAAACM////CAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEgAAABMAAAAAAAABEgAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAUAAAACAAAAF9fbGFiZWxzAAAAAAQAAABuYW1lAAAAAAAAAAAEAAQABAAAAAgAAABfX2xhYmVscwAAAADIBAAAQVJST1cx diff --git a/pkg/util/converter/testdata/prom-matrix-frame.json b/pkg/util/converter/testdata/prom-matrix-frame.json index af91613cb20..11484d37e8a 100644 --- a/pkg/util/converter/testdata/prom-matrix-frame.json +++ b/pkg/util/converter/testdata/prom-matrix-frame.json @@ -14,14 +14,15 @@ } }, { - "name": "up", + "name": "Value", "type": "number", "typeInfo": { "frame": "float64" }, "labels": { "job": "prometheus", - "instance": "localhost:9090" + "instance": "localhost:9090", + "__name__": "up" } } ] @@ -51,12 +52,13 @@ } }, { - "name": "up", + "name": "Value", "type": "number", "typeInfo": { "frame": "float64" }, "labels": { + "__name__": "up", "job": "node", "instance": "localhost:9091" } diff --git a/pkg/util/converter/testdata/prom-matrix-golden.txt b/pkg/util/converter/testdata/prom-matrix-golden.txt index 83088450f9d..c9cf2dc5e93 100644 --- a/pkg/util/converter/testdata/prom-matrix-golden.txt +++ b/pkg/util/converter/testdata/prom-matrix-golden.txt @@ -5,15 +5,15 @@ Frame[0] { } Name: Dimensions: 2 Fields by 3 Rows -+-----------------------------------+-------------------------------------------------+ -| Name: Time | Name: up | -| Labels: | Labels: instance=localhost:9090, job=prometheus | -| Type: []time.Time | Type: []float64 | -+-----------------------------------+-------------------------------------------------+ -| 2015-07-01 20:10:30.781 +0000 UTC | 1 | -| 2015-07-01 20:10:45.781 +0000 UTC | 1 | -| 2015-07-01 20:11:00.781 +0000 UTC | 1 | -+-----------------------------------+-------------------------------------------------+ ++-----------------------------------+--------------------------------------------------------------+ +| Name: Time | Name: Value | +| Labels: | Labels: __name__=up, instance=localhost:9090, job=prometheus | +| Type: []time.Time | Type: []float64 | ++-----------------------------------+--------------------------------------------------------------+ +| 2015-07-01 20:10:30.781 +0000 UTC | 1 | +| 2015-07-01 20:10:45.781 +0000 UTC | 1 | +| 2015-07-01 20:11:00.781 +0000 UTC | 1 | ++-----------------------------------+--------------------------------------------------------------+ @@ -22,17 +22,17 @@ Frame[1] { } Name: Dimensions: 2 Fields by 3 Rows -+-----------------------------------+-------------------------------------------+ -| Name: Time | Name: up | -| Labels: | Labels: instance=localhost:9091, job=node | -| Type: []time.Time | Type: []float64 | -+-----------------------------------+-------------------------------------------+ -| 2015-07-01 20:10:30.781 +0000 UTC | 0 | -| 2015-07-01 20:10:45.781 +0000 UTC | 0 | -| 2015-07-01 20:11:00.781 +0000 UTC | 1 | -+-----------------------------------+-------------------------------------------+ ++-----------------------------------+--------------------------------------------------------+ +| Name: Time | Name: Value | +| Labels: | Labels: __name__=up, instance=localhost:9091, job=node | +| Type: []time.Time | Type: []float64 | ++-----------------------------------+--------------------------------------------------------+ +| 2015-07-01 20:10:30.781 +0000 UTC | 0 | +| 2015-07-01 20:10:45.781 +0000 UTC | 0 | +| 2015-07-01 20:11:00.781 +0000 UTC | 1 | ++-----------------------------------+--------------------------------------------------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=QVJST1cxAAD/////8AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAoP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADA/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOD+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAAMQAAAAEAAAAVv///xQAAACMAAAAjAAAAAAAAAOMAAAAAgAAACgAAAAEAAAASP///wgAAAAMAAAAAgAAAHVwAAAEAAAAbmFtZQAAAABo////CAAAADwAAAAwAAAAeyJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MDkwIiwiam9iIjoicHJvbWV0aGV1cyJ9AAAAAAYAAABsYWJlbHMAAAAAAACK////AAACAAIAAAB1cAAAAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAD/////uAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAADAAAAAAAAAAFAAAAAAAAAMEAAoAGAAMAAgABAAKAAAAFAAAAFgAAAADAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAYAAAAAAAAAAAAAAACAAAAAwAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAABAuc2smuvsE0CP3yqe6+wTQGXxqKHr7BMAAAAAAADwPwAAAAAAAPA/AAAAAAAA8D8QAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAPAAAAAAABAABAAAAAAIAAAAAAADAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAwAAAAIAAQACgAAAAgAAACMAAAAAwAAAEwAAAAoAAAABAAAAKD+//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAAwP7//wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAADg/v//CAAAACQAAAAaAAAAeyJ0eXBlIjoidGltZXNlcmllcy1tYW55In0AAAQAAABtZXRhAAAAAAIAAADEAAAABAAAAFb///8UAAAAjAAAAIwAAAAAAAADjAAAAAIAAAAoAAAABAAAAEj///8IAAAADAAAAAIAAAB1cAAABAAAAG5hbWUAAAAAaP///wgAAAA8AAAAMAAAAHsiaW5zdGFuY2UiOiJsb2NhbGhvc3Q6OTA5MCIsImpvYiI6InByb21ldGhldXMifQAAAAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgACAAAAdXAAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAIAIAAEFSUk9XMQ== -FRAME=QVJST1cxAAD/////6AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAqP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADI/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOj+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAALwAAAAEAAAAXv///xQAAACEAAAAhAAAAAAAAAOEAAAAAgAAACgAAAAEAAAAUP///wgAAAAMAAAAAgAAAHVwAAAEAAAAbmFtZQAAAABw////CAAAADQAAAAqAAAAeyJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MDkxIiwiam9iIjoibm9kZSJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgACAAAAdXAAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAwAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAwAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAGAAAAAAAAAAAAAAAAgAAAAMAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAQLnNrJrr7BNAj98qnuvsE0Bl8aih6+wTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPA/EAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADwAAAAAAAQAAQAAAPgBAAAAAAAAwAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAjAAAAAMAAABMAAAAKAAAAAQAAACo/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAMj+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA6P7//wgAAAAkAAAAGgAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSJ9AAAEAAAAbWV0YQAAAAACAAAAvAAAAAQAAABe////FAAAAIQAAACEAAAAAAAAA4QAAAACAAAAKAAAAAQAAABQ////CAAAAAwAAAACAAAAdXAAAAQAAABuYW1lAAAAAHD///8IAAAANAAAACoAAAB7Imluc3RhbmNlIjoibG9jYWxob3N0OjkwOTEiLCJqb2IiOiJub2RlIn0AAAYAAABsYWJlbHMAAAAAAACK////AAACAAIAAAB1cAAAAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAYAgAAQVJST1cx +FRAME=QVJST1cxAAD/////CAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAjP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAMz+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAANgAAAAEAAAAQv///xQAAACgAAAAoAAAAAAAAAOgAAAAAgAAACwAAAAEAAAANP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAWP///wgAAABMAAAAQAAAAHsiX19uYW1lX18iOiJ1cCIsImluc3RhbmNlIjoibG9jYWxob3N0OjkwOTAiLCJqb2IiOiJwcm9tZXRoZXVzIn0AAAAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAAAAAAD/////uAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAADAAAAAAAAAAFAAAAAAAAAMEAAoAGAAMAAgABAAKAAAAFAAAAFgAAAADAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAYAAAAAAAAAAAAAAACAAAAAwAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAABAuc2smuvsE0CP3yqe6+wTQGXxqKHr7BMAAAAAAADwPwAAAAAAAPA/AAAAAAAA8D8QAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAABAABAAAAGAIAAAAAAADAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAjP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAMz+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAANgAAAAEAAAAQv///xQAAACgAAAAoAAAAAAAAAOgAAAAAgAAACwAAAAEAAAANP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAWP///wgAAABMAAAAQAAAAHsiX19uYW1lX18iOiJ1cCIsImluc3RhbmNlIjoibG9jYWxob3N0OjkwOTAiLCJqb2IiOiJwcm9tZXRoZXVzIn0AAAAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAADACAABBUlJPVzE= +FRAME=QVJST1cxAAD/////AAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAlP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAAC0/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAANT+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAANAAAAAEAAAASv///xQAAACYAAAAmAAAAAAAAAOYAAAAAgAAACwAAAAEAAAAPP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAYP///wgAAABEAAAAOgAAAHsiX19uYW1lX18iOiJ1cCIsImluc3RhbmNlIjoibG9jYWxob3N0OjkwOTEiLCJqb2IiOiJub2RlIn0AAAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAwAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAwAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAGAAAAAAAAAAAAAAAAgAAAAMAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAQLnNrJrr7BNAj98qnuvsE0Bl8aih6+wTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPA/EAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADgAAAAAAAQAAQAAABACAAAAAAAAwAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAKAAwAAAAIAAQACgAAAAgAAACMAAAAAwAAAEwAAAAoAAAABAAAAJT+//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAAtP7//wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAADU/v//CAAAACQAAAAaAAAAeyJ0eXBlIjoidGltZXNlcmllcy1tYW55In0AAAQAAABtZXRhAAAAAAIAAADQAAAABAAAAEr///8UAAAAmAAAAJgAAAAAAAADmAAAAAIAAAAsAAAABAAAADz///8IAAAAEAAAAAUAAABWYWx1ZQAAAAQAAABuYW1lAAAAAGD///8IAAAARAAAADoAAAB7Il9fbmFtZV9fIjoidXAiLCJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MDkxIiwiam9iIjoibm9kZSJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAKAIAAEFSUk9XMQ== diff --git a/pkg/util/converter/testdata/prom-vector-frame.json b/pkg/util/converter/testdata/prom-vector-frame.json index 8d81e0ba3ca..eaff88b9a5a 100644 --- a/pkg/util/converter/testdata/prom-vector-frame.json +++ b/pkg/util/converter/testdata/prom-vector-frame.json @@ -14,12 +14,13 @@ } }, { - "name": "up", + "name": "Value", "type": "number", "typeInfo": { "frame": "float64" }, "labels": { + "__name__": "up", "job": "prometheus", "instance": "localhost:9090" } @@ -51,14 +52,15 @@ } }, { - "name": "up", + "name": "Value", "type": "number", "typeInfo": { "frame": "float64" }, "labels": { "job": "node", - "instance": "localhost:9100" + "instance": "localhost:9100", + "__name__": "up" } } ] diff --git a/pkg/util/converter/testdata/prom-vector-golden.txt b/pkg/util/converter/testdata/prom-vector-golden.txt index a1dfe5665b5..0c4c0d3150e 100644 --- a/pkg/util/converter/testdata/prom-vector-golden.txt +++ b/pkg/util/converter/testdata/prom-vector-golden.txt @@ -5,13 +5,13 @@ Frame[0] { } Name: Dimensions: 2 Fields by 1 Rows -+-----------------------------------+-------------------------------------------------+ -| Name: Time | Name: up | -| Labels: | Labels: instance=localhost:9090, job=prometheus | -| Type: []time.Time | Type: []float64 | -+-----------------------------------+-------------------------------------------------+ -| 2015-07-01 20:10:51.781 +0000 UTC | 1 | -+-----------------------------------+-------------------------------------------------+ ++-----------------------------------+--------------------------------------------------------------+ +| Name: Time | Name: Value | +| Labels: | Labels: __name__=up, instance=localhost:9090, job=prometheus | +| Type: []time.Time | Type: []float64 | ++-----------------------------------+--------------------------------------------------------------+ +| 2015-07-01 20:10:51.781 +0000 UTC | 1 | ++-----------------------------------+--------------------------------------------------------------+ @@ -20,13 +20,13 @@ Frame[1] { } Name: Dimensions: 2 Fields by 1 Rows -+-----------------------------------+-------------------------------------------+ -| Name: Time | Name: up | -| Labels: | Labels: instance=localhost:9100, job=node | -| Type: []time.Time | Type: []float64 | -+-----------------------------------+-------------------------------------------+ -| 2015-07-01 20:10:51.781 +0000 UTC | 0 | -+-----------------------------------+-------------------------------------------+ ++-----------------------------------+--------------------------------------------------------+ +| Name: Time | Name: Value | +| Labels: | Labels: __name__=up, instance=localhost:9100, job=node | +| Type: []time.Time | Type: []float64 | ++-----------------------------------+--------------------------------------------------------+ +| 2015-07-01 20:10:51.781 +0000 UTC | 0 | ++-----------------------------------+--------------------------------------------------------+ @@ -75,8 +75,8 @@ Dimensions: 2 Fields by 1 Rows ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=QVJST1cxAAD/////8AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAoP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADA/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOD+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAAMQAAAAEAAAAVv///xQAAACMAAAAjAAAAAAAAAOMAAAAAgAAACgAAAAEAAAASP///wgAAAAMAAAAAgAAAHVwAAAEAAAAbmFtZQAAAABo////CAAAADwAAAAwAAAAeyJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MDkwIiwiam9iIjoicHJvbWV0aGV1cyJ9AAAAAAYAAABsYWJlbHMAAAAAAACK////AAACAAIAAAB1cAAAAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAD/////uAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAABAAAAAAAAAAFAAAAAAAAAMEAAoAGAAMAAgABAAKAAAAFAAAAFgAAAABAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAACAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAABAS4CQn+vsEwAAAAAAAPA/EAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADwAAAAAAAQAAQAAAAACAAAAAAAAwAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAjAAAAAMAAABMAAAAKAAAAAQAAACg/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAMD+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA4P7//wgAAAAkAAAAGgAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSJ9AAAEAAAAbWV0YQAAAAACAAAAxAAAAAQAAABW////FAAAAIwAAACMAAAAAAAAA4wAAAACAAAAKAAAAAQAAABI////CAAAAAwAAAACAAAAdXAAAAQAAABuYW1lAAAAAGj///8IAAAAPAAAADAAAAB7Imluc3RhbmNlIjoibG9jYWxob3N0OjkwOTAiLCJqb2IiOiJwcm9tZXRoZXVzIn0AAAAABgAAAGxhYmVscwAAAAAAAIr///8AAAIAAgAAAHVwAAAAABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAACACAABBUlJPVzE= -FRAME=QVJST1cxAAD/////6AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAqP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADI/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOj+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAALwAAAAEAAAAXv///xQAAACEAAAAhAAAAAAAAAOEAAAAAgAAACgAAAAEAAAAUP///wgAAAAMAAAAAgAAAHVwAAAEAAAAbmFtZQAAAABw////CAAAADQAAAAqAAAAeyJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MTAwIiwiam9iIjoibm9kZSJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgACAAAAdXAAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAQEuAkJ/r7BMAAAAAAAAAABAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA8AAAAAAAEAAEAAAD4AQAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAqP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADI/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOj+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAALwAAAAEAAAAXv///xQAAACEAAAAhAAAAAAAAAOEAAAAAgAAACgAAAAEAAAAUP///wgAAAAMAAAAAgAAAHVwAAAEAAAAbmFtZQAAAABw////CAAAADQAAAAqAAAAeyJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MTAwIiwiam9iIjoibm9kZSJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgACAAAAdXAAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAGAIAAEFSUk9XMQ== +FRAME=QVJST1cxAAD/////CAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAjP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAMz+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAANgAAAAEAAAAQv///xQAAACgAAAAoAAAAAAAAAOgAAAAAgAAACwAAAAEAAAANP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAWP///wgAAABMAAAAQAAAAHsiX19uYW1lX18iOiJ1cCIsImluc3RhbmNlIjoibG9jYWxob3N0OjkwOTAiLCJqb2IiOiJwcm9tZXRoZXVzIn0AAAAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAAAAAAD/////uAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAABAAAAAAAAAAFAAAAAAAAAMEAAoAGAAMAAgABAAKAAAAFAAAAFgAAAABAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAACAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAABAS4CQn+vsEwAAAAAAAPA/EAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADgAAAAAAAQAAQAAABgCAAAAAAAAwAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAKAAwAAAAIAAQACgAAAAgAAACMAAAAAwAAAEwAAAAoAAAABAAAAIz+//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAArP7//wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAADM/v//CAAAACQAAAAaAAAAeyJ0eXBlIjoidGltZXNlcmllcy1tYW55In0AAAQAAABtZXRhAAAAAAIAAADYAAAABAAAAEL///8UAAAAoAAAAKAAAAAAAAADoAAAAAIAAAAsAAAABAAAADT///8IAAAAEAAAAAUAAABWYWx1ZQAAAAQAAABuYW1lAAAAAFj///8IAAAATAAAAEAAAAB7Il9fbmFtZV9fIjoidXAiLCJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MDkwIiwiam9iIjoicHJvbWV0aGV1cyJ9AAAAAAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAwAgAAQVJST1cx +FRAME=QVJST1cxAAD/////AAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAlP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAAC0/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAANT+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAANAAAAAEAAAASv///xQAAACYAAAAmAAAAAAAAAOYAAAAAgAAACwAAAAEAAAAPP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAYP///wgAAABEAAAAOgAAAHsiX19uYW1lX18iOiJ1cCIsImluc3RhbmNlIjoibG9jYWxob3N0OjkxMDAiLCJqb2IiOiJub2RlIn0AAAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAQEuAkJ/r7BMAAAAAAAAAABAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAAAQAgAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAjAAAAAMAAABMAAAAKAAAAAQAAACU/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAALT+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA1P7//wgAAAAkAAAAGgAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSJ9AAAEAAAAbWV0YQAAAAACAAAA0AAAAAQAAABK////FAAAAJgAAACYAAAAAAAAA5gAAAACAAAALAAAAAQAAAA8////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAABg////CAAAAEQAAAA6AAAAeyJfX25hbWVfXyI6InVwIiwiaW5zdGFuY2UiOiJsb2NhbGhvc3Q6OTEwMCIsImpvYiI6Im5vZGUifQAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAACgCAABBUlJPVzE= FRAME=QVJST1cxAAD/////6AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAArP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADM/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOz+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAALgAAAAEAAAAYv///xQAAACAAAAAgAAAAAAAAAOAAAAAAgAAACwAAAAEAAAAVP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAeP///wgAAAAsAAAAIwAAAHsibGV2ZWwiOiJlcnJvciIsImxvY2F0aW9uIjoibW9vbiJ9AAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAN7cpctR1BYAAAAAAADwfxAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAAD4AQAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAjAAAAAMAAABMAAAAKAAAAAQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAMz+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA7P7//wgAAAAkAAAAGgAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSJ9AAAEAAAAbWV0YQAAAAACAAAAuAAAAAQAAABi////FAAAAIAAAACAAAAAAAAAA4AAAAACAAAALAAAAAQAAABU////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAAB4////CAAAACwAAAAjAAAAeyJsZXZlbCI6ImVycm9yIiwibG9jYXRpb24iOiJtb29uIn0ABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAABACAABBUlJPVzE= FRAME=QVJST1cxAAD/////6AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAArP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADM/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOz+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAALgAAAAEAAAAYv///xQAAACAAAAAgAAAAAAAAAOAAAAAAgAAACwAAAAEAAAAVP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAeP///wgAAAAsAAAAIgAAAHsibGV2ZWwiOiJpbmZvIiwibG9jYXRpb24iOiJtb29uIn0AAAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAN7cpctR1BYAAAAAAADw/xAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAAD4AQAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAjAAAAAMAAABMAAAAKAAAAAQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAMz+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA7P7//wgAAAAkAAAAGgAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSJ9AAAEAAAAbWV0YQAAAAACAAAAuAAAAAQAAABi////FAAAAIAAAACAAAAAAAAAA4AAAAACAAAALAAAAAQAAABU////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAAB4////CAAAACwAAAAiAAAAeyJsZXZlbCI6ImluZm8iLCJsb2NhdGlvbiI6Im1vb24ifQAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAABACAABBUlJPVzE= FRAME=QVJST1cxAAD/////6AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAArP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADM/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOz+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAALgAAAAEAAAAYv///xQAAACAAAAAgAAAAAAAAAOAAAAAAgAAACwAAAAEAAAAVP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAeP///wgAAAAsAAAAIwAAAHsibGV2ZWwiOiJkZWJ1ZyIsImxvY2F0aW9uIjoibW9vbiJ9AAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAN7cpctR1BYBAAAAAAD4fxAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAAD4AQAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAjAAAAAMAAABMAAAAKAAAAAQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAMz+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA7P7//wgAAAAkAAAAGgAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSJ9AAAEAAAAbWV0YQAAAAACAAAAuAAAAAQAAABi////FAAAAIAAAACAAAAAAAAAA4AAAAACAAAALAAAAAQAAABU////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAAB4////CAAAACwAAAAjAAAAeyJsZXZlbCI6ImRlYnVnIiwibG9jYXRpb24iOiJtb29uIn0ABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAABACAABBUlJPVzE= diff --git a/public/app/plugins/datasource/loki/backendResultTransformer.test.ts b/public/app/plugins/datasource/loki/backendResultTransformer.test.ts index 6179df79462..2d01b584b63 100644 --- a/public/app/plugins/datasource/loki/backendResultTransformer.test.ts +++ b/public/app/plugins/datasource/loki/backendResultTransformer.test.ts @@ -31,7 +31,7 @@ const inputFrame: DataFrame = { json: true, }, }, - values: new ArrayVector([`[["level", "info"],["code", "41🌙"]]`, `[["level", "error"],["code", "41🌙"]]`]), + values: new ArrayVector(['{ "level": "info", "code": "41🌙" }', '{ "level": "error", "code": "41🌙" }']), }, { name: 'tsNs', From 60cabaea0a554b2dbc02b25719f2cebd001add06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Thu, 5 May 2022 09:43:36 +0200 Subject: [PATCH 049/440] loki: use metadataRequest in testDatasource (#48431) --- .../datasource/loki/datasource.test.ts | 84 ++++++++----------- .../app/plugins/datasource/loki/datasource.ts | 63 ++++++-------- 2 files changed, 60 insertions(+), 87 deletions(-) diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index b790bd86f42..c71756edd43 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -534,74 +534,56 @@ describe('LokiDatasource', () => { }); describe('when performing testDataSource', () => { - describe('and call succeeds', () => { - it('should return successfully', async () => { - fetchMock.mockImplementation(() => of(createFetchResponse({ values: ['avalue'] }))); - const ds = createLokiDSForTests({} as TemplateSrv); + it('should return successfully when call succeeds with labels', async () => { + const ds = createLokiDSForTests({} as TemplateSrv); + ds.metadataRequest = () => Promise.resolve(['avalue']); - const result = await ds.testDatasource(); + const result = await ds.testDatasource(); - expect(result.status).toBe('success'); + expect(result).toStrictEqual({ + status: 'success', + message: 'Data source connected and labels found.', }); }); - describe('and call fails with 401 error', () => { - it('should return error status and a detailed error message', async () => { - fetchMock.mockImplementation(() => - throwError({ - statusText: 'Unauthorized', - status: 401, - data: { - message: 'Unauthorized', - }, - }) - ); - const ds = createLokiDSForTests({} as TemplateSrv); + it('should return error when call succeeds without labels', async () => { + const ds = createLokiDSForTests({} as TemplateSrv); + ds.metadataRequest = () => Promise.resolve([]); - const result = await ds.testDatasource(); + const result = await ds.testDatasource(); - expect(result.status).toEqual('error'); - expect(result.message).toBe('Loki: Unauthorized. 401. Unauthorized'); + expect(result).toStrictEqual({ + status: 'error', + message: 'Data source connected, but no labels received. Verify that Loki and Promtail is configured properly.', }); }); - describe('and call fails with 404 error', () => { - it('should return error status and a detailed error message', async () => { - fetchMock.mockImplementation(() => - throwError({ - statusText: 'Not found', - status: 404, - data: { - message: '404 page not found', - }, - }) - ); + it('should return error status with no details when call fails with no details', async () => { + const ds = createLokiDSForTests({} as TemplateSrv); + ds.metadataRequest = () => Promise.reject({}); - const ds = createLokiDSForTests({} as TemplateSrv); + const result = await ds.testDatasource(); - const result = await ds.testDatasource(); - - expect(result.status).toEqual('error'); - expect(result.message).toBe('Loki: Not found. 404. 404 page not found'); + expect(result).toStrictEqual({ + status: 'error', + message: 'Unable to fetch labels from Loki, please check the server logs for more details', }); }); - describe('and call fails with 502 error', () => { - it('should return error status and a detailed error message', async () => { - fetchMock.mockImplementation(() => - throwError({ - statusText: 'Bad Gateway', - status: 502, - data: '', - }) - ); + it('should return error status with details when call fails with details', async () => { + const ds = createLokiDSForTests({} as TemplateSrv); + ds.metadataRequest = () => + Promise.reject({ + data: { + message: 'error42', + }, + }); - const ds = createLokiDSForTests({} as TemplateSrv); + const result = await ds.testDatasource(); - const result = await ds.testDatasource(); - - expect(result.status).toEqual('error'); - expect(result.message).toBe('Loki: Bad Gateway. 502'); + expect(result).toStrictEqual({ + status: 'error', + message: 'Unable to fetch labels from Loki (error42), please check the server logs for more details', }); }); }); diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 70f2b40d074..a1d220b9394 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -689,44 +689,35 @@ export class LokiDatasource }; }; - testDatasource() { + testDatasource(): Promise<{ status: string; message: string }> { // Consider only last 10 minutes otherwise request takes too long - const startMs = Date.now() - 10 * 60 * 1000; - const start = `${startMs}000000`; // API expects nanoseconds - return lastValueFrom( - this._request(`${LOKI_ENDPOINT}/label`, { start }).pipe( - map((res) => { - const values: any[] = res?.data?.data || res?.data?.values || []; - const testResult = - values.length > 0 - ? { status: 'success', message: 'Data source connected and labels found.' } - : { - status: 'error', - message: - 'Data source connected, but no labels received. Verify that Loki and Promtail is configured properly.', - }; - return testResult; - }), - catchError((err: any) => { - let message = 'Loki: '; - if (err.statusText) { - message += err.statusText; - } else { - message += 'Cannot connect to Loki'; - } + const nowMs = Date.now(); + const params = { + start: (nowMs - 10 * 60 * 1000) * NS_IN_MS, + end: nowMs * NS_IN_MS, + }; - if (err.status) { - message += `. ${err.status}`; - } - - if (err.data && err.data.message) { - message += `. ${err.data.message}`; - } else if (err.data) { - message += `. ${err.data}`; - } - return of({ status: 'error', message: message }); - }) - ) + return this.metadataRequest('labels', params).then( + (values) => { + return values.length > 0 + ? { status: 'success', message: 'Data source connected and labels found.' } + : { + status: 'error', + message: + 'Data source connected, but no labels received. Verify that Loki and Promtail is configured properly.', + }; + }, + (err) => { + // we did a resource-call that failed. + // the only info we have, if exists, is err.data.message + // (when in development-mode, err.data.error exists too, but not in production-mode) + // things like err.status & err.statusText does not help, + // because those will only describe how the request between browser<>server failed + const info: string = err?.data?.message ?? ''; + const infoInParentheses = info !== '' ? ` (${info})` : ''; + const message = `Unable to fetch labels from Loki${infoInParentheses}, please check the server logs for more details`; + return { status: 'error', message: message }; + } ); } From 0d14c27eb99d327fb0b284ed4d2dfab0bf174f60 Mon Sep 17 00:00:00 2001 From: ying-jeanne <74549700+ying-jeanne@users.noreply.github.com> Date: Thu, 5 May 2022 10:04:54 +0200 Subject: [PATCH 050/440] Chore: add Folderuid into panel-library API (#48577) * add folderuid into interface * panellibrary id/uid things * modify doc * update doc * correct some comments --- docs/sources/http_api/library_element.md | 10 ++- pkg/services/libraryelements/api.go | 44 +++++++++++ pkg/services/libraryelements/database.go | 3 +- .../libraryelements_create_test.go | 6 ++ .../libraryelements_patch_test.go | 4 + pkg/services/libraryelements/models.go | 5 ++ public/api-merged.json | 74 +++++++++++-------- public/api-spec.json | 74 +++++++++++-------- 8 files changed, 156 insertions(+), 64 deletions(-) diff --git a/docs/sources/http_api/library_element.md b/docs/sources/http_api/library_element.md index 3b500f3837a..7b40d5d2087 100644 --- a/docs/sources/http_api/library_element.md +++ b/docs/sources/http_api/library_element.md @@ -269,7 +269,8 @@ Creates a new library element. JSON Body schema: -- **folderId** – Optional, the ID of the folder where the library element is stored. +- **folderId** – ID of the folder where the library element is stored. It is deprecated since Grafana v9 +- **folderUid** – Optional, the UID of the folder where the library element is stored, empty string when it is General folder - **name** – Optional, the name of the library element. - **model** – The JSON model for the library element. - **kind** – Kind of element to create, Use `1` for library panels or `2` for library variables. @@ -285,7 +286,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk { "uid": "nErXDvCkzz", - "folderId": 0, + "folderUid": "", "name": "Example library panel", "model": {...}, "kind": 1 @@ -303,6 +304,7 @@ Content-Type: application/json "id": 28, "orgId": 1, "folderId": 0, + "folderUid": "", "uid": "nErXDvCkzz", "name": "Example library panel", "kind": 1, @@ -346,7 +348,8 @@ Updates an existing library element identified by uid. JSON Body schema: -- **folderId** – ID of the folder where the library element is stored. +- **folderId** – ID of the folder where the library element is stored. It is deprecated since Grafana v9 +- **folderUid** – UID of the folder where the library element is stored, empty string when it is General folder. - **name** – Name of the library element. - **model** – The JSON model for the library element. - **kind** – Kind of element to create. Use `1` for library panels or `2` for library variables. @@ -379,6 +382,7 @@ Content-Type: application/json "id": 28, "orgId": 1, "folderId": 0, + "folderUid": "", "uid": "nErXDvCkzz", "name": "Renamed library panel", "kind": 1, diff --git a/pkg/services/libraryelements/api.go b/pkg/services/libraryelements/api.go index f14c876b8e4..d78cc817fb3 100644 --- a/pkg/services/libraryelements/api.go +++ b/pkg/services/libraryelements/api.go @@ -30,11 +30,33 @@ func (l *LibraryElementService) createHandler(c *models.ReqContext) response.Res return response.Error(http.StatusBadRequest, "bad request data", err) } + if cmd.FolderUID != nil { + if *cmd.FolderUID == "" { + cmd.FolderID = 0 + } else { + folder, err := l.folderService.GetFolderByUID(c.Req.Context(), c.SignedInUser, c.OrgId, *cmd.FolderUID) + if err != nil || folder == nil { + return response.Error(http.StatusBadRequest, "failed to get folder", err) + } + cmd.FolderID = folder.Id + } + } + element, err := l.createLibraryElement(c.Req.Context(), c.SignedInUser, cmd) if err != nil { return toLibraryElementError(err, "Failed to create library element") } + if element.FolderID != 0 { + folder, err := l.folderService.GetFolderByID(c.Req.Context(), c.SignedInUser, element.FolderID, c.OrgId) + if err != nil { + return response.Error(http.StatusInternalServerError, "failed to get folder", err) + } + element.FolderUID = folder.Uid + element.Meta.FolderUID = folder.Uid + element.Meta.FolderName = folder.Title + } + return response.JSON(http.StatusOK, LibraryElementResponse{Result: element}) } @@ -88,11 +110,33 @@ func (l *LibraryElementService) patchHandler(c *models.ReqContext) response.Resp return response.Error(http.StatusBadRequest, "bad request data", err) } + if cmd.FolderUID != nil { + if *cmd.FolderUID == "" { + cmd.FolderID = 0 + } else { + folder, err := l.folderService.GetFolderByUID(c.Req.Context(), c.SignedInUser, c.OrgId, *cmd.FolderUID) + if err != nil || folder == nil { + return response.Error(http.StatusBadRequest, "failed to get folder", err) + } + cmd.FolderID = folder.Id + } + } + element, err := l.patchLibraryElement(c.Req.Context(), c.SignedInUser, cmd, web.Params(c.Req)[":uid"]) if err != nil { return toLibraryElementError(err, "Failed to update library element") } + if element.FolderID != 0 { + folder, err := l.folderService.GetFolderByID(c.Req.Context(), c.SignedInUser, element.FolderID, c.OrgId) + if err != nil { + return response.Error(http.StatusInternalServerError, "failed to get folder", err) + } + element.FolderUID = folder.Uid + element.Meta.FolderUID = folder.Uid + element.Meta.FolderName = folder.Title + } + return response.JSON(http.StatusOK, LibraryElementResponse{Result: element}) } diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index 88b95a957ee..9b4470224c6 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -245,6 +245,7 @@ func getLibraryElements(c context.Context, store *sqlstore.SQLStore, signedInUse ID: libraryElement.ID, OrgID: libraryElement.OrgID, FolderID: libraryElement.FolderID, + FolderUID: libraryElement.FolderUID, UID: libraryElement.UID, Name: libraryElement.Name, Kind: libraryElement.Kind, @@ -357,6 +358,7 @@ func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedI ID: element.ID, OrgID: element.OrgID, FolderID: element.FolderID, + FolderUID: element.FolderUID, UID: element.UID, Name: element.Name, Kind: element.Kind, @@ -530,7 +532,6 @@ func (l *LibraryElementService) patchLibraryElement(c context.Context, signedInU }, }, } - return nil }) diff --git a/pkg/services/libraryelements/libraryelements_create_test.go b/pkg/services/libraryelements/libraryelements_create_test.go index fdcbd730e1d..afde9ceaf09 100644 --- a/pkg/services/libraryelements/libraryelements_create_test.go +++ b/pkg/services/libraryelements/libraryelements_create_test.go @@ -40,6 +40,8 @@ func TestCreateLibraryElement(t *testing.T) { }, Version: 1, Meta: LibraryElementDTOMeta{ + FolderName: "ScenarioFolder", + FolderUID: "ScenarioFolder", ConnectedDashboards: 0, Created: sc.initialResult.Result.Meta.Created, Updated: sc.initialResult.Result.Meta.Updated, @@ -87,6 +89,8 @@ func TestCreateLibraryElement(t *testing.T) { }, Version: 1, Meta: LibraryElementDTOMeta{ + FolderName: "ScenarioFolder", + FolderUID: "ScenarioFolder", ConnectedDashboards: 0, Created: result.Result.Meta.Created, Updated: result.Result.Meta.Updated, @@ -160,6 +164,8 @@ func TestCreateLibraryElement(t *testing.T) { }, Version: 1, Meta: LibraryElementDTOMeta{ + FolderName: "ScenarioFolder", + FolderUID: "ScenarioFolder", ConnectedDashboards: 0, Created: result.Result.Meta.Created, Updated: result.Result.Meta.Updated, diff --git a/pkg/services/libraryelements/libraryelements_patch_test.go b/pkg/services/libraryelements/libraryelements_patch_test.go index 0633a014593..6ee59606c44 100644 --- a/pkg/services/libraryelements/libraryelements_patch_test.go +++ b/pkg/services/libraryelements/libraryelements_patch_test.go @@ -63,6 +63,8 @@ func TestPatchLibraryElement(t *testing.T) { }, Version: 2, Meta: LibraryElementDTOMeta{ + FolderName: "NewFolder", + FolderUID: "NewFolder", ConnectedDashboards: 0, Created: sc.initialResult.Result.Meta.Created, Updated: result.Result.Meta.Updated, @@ -102,6 +104,8 @@ func TestPatchLibraryElement(t *testing.T) { sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar sc.initialResult.Result.Meta.Updated = result.Result.Meta.Updated sc.initialResult.Result.Version = 2 + sc.initialResult.Result.Meta.FolderName = "NewFolder" + sc.initialResult.Result.Meta.FolderUID = "NewFolder" if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" { t.Fatalf("Result mismatch (-want +got):\n%s", diff) } diff --git a/pkg/services/libraryelements/models.go b/pkg/services/libraryelements/models.go index 3765141c792..a684f8075fe 100644 --- a/pkg/services/libraryelements/models.go +++ b/pkg/services/libraryelements/models.go @@ -64,6 +64,7 @@ type LibraryElementDTO struct { ID int64 `json:"id"` OrgID int64 `json:"orgId"` FolderID int64 `json:"folderId"` + FolderUID string `json:"folderUid"` UID string `json:"uid"` Name string `json:"name"` Kind int64 `json:"kind"` @@ -162,6 +163,8 @@ var ( type CreateLibraryElementCommand struct { // ID of the folder where the library element is stored. FolderID int64 `json:"folderId"` + // UID of the folder where the library element is stored. + FolderUID *string `json:"folderUid"` // Name of the library element. Name string `json:"name"` // The JSON model for the library element. @@ -181,6 +184,8 @@ type CreateLibraryElementCommand struct { type PatchLibraryElementCommand struct { // ID of the folder where the library element is stored. FolderID int64 `json:"folderId" binding:"Default(-1)"` + // UID of the folder where the library element is stored. + FolderUID *string `json:"folderUid"` // Name of the library element. Name string `json:"name"` // The JSON model for the library element. diff --git a/public/api-merged.json b/public/api-merged.json index adcdd972323..876ebc32d7c 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -506,14 +506,6 @@ "summary": "Add a user role assignment.", "operationId": "addUserRole", "parameters": [ - { - "type": "integer", - "format": "int64", - "x-go-name": "UserID", - "name": "user_id", - "in": "path", - "required": true - }, { "x-go-name": "Body", "name": "body", @@ -522,6 +514,14 @@ "schema": { "$ref": "#/definitions/AddUserRoleCommand" } + }, + { + "type": "integer", + "format": "int64", + "x-go-name": "UserID", + "name": "user_id", + "in": "path", + "required": true } ], "responses": { @@ -547,14 +547,6 @@ "summary": "Remove a user role assignment.", "operationId": "removeUserRole", "parameters": [ - { - "type": "integer", - "format": "int64", - "x-go-name": "UserID", - "name": "user_id", - "in": "path", - "required": true - }, { "type": "string", "x-go-name": "RoleUID", @@ -568,6 +560,14 @@ "description": "A flag indicating if the assignment is global or not. If set to false, the default org ID of the authenticated user will be used from the request to remove assignment.", "name": "global", "in": "query" + }, + { + "type": "integer", + "format": "int64", + "x-go-name": "UserID", + "name": "user_id", + "in": "path", + "required": true } ], "responses": { @@ -8261,6 +8261,14 @@ "summary": "Add External Group.", "operationId": "addTeamGroupApi", "parameters": [ + { + "type": "integer", + "format": "int64", + "x-go-name": "TeamID", + "name": "teamId", + "in": "path", + "required": true + }, { "x-go-name": "Body", "name": "body", @@ -8269,14 +8277,6 @@ "schema": { "$ref": "#/definitions/TeamGroupMapping" } - }, - { - "type": "integer", - "format": "int64", - "x-go-name": "TeamID", - "name": "teamId", - "in": "path", - "required": true } ], "responses": { @@ -8310,16 +8310,16 @@ { "type": "integer", "format": "int64", - "x-go-name": "GroupID", - "name": "groupId", + "x-go-name": "TeamID", + "name": "teamId", "in": "path", "required": true }, { "type": "integer", "format": "int64", - "x-go-name": "TeamID", - "name": "teamId", + "x-go-name": "GroupID", + "name": "groupId", "in": "path", "required": true } @@ -10919,6 +10919,11 @@ "format": "int64", "x-go-name": "FolderID" }, + "folderUID": { + "description": "UID of the folder where the library element is stored.", + "type": "string", + "x-go-name": "FolderUID" + }, "kind": { "description": "Kind of element to create, Use 1 for library panels or 2 for c.\nDescription:\n1 - library panels\n2 - library variables", "type": "integer", @@ -13326,6 +13331,10 @@ "format": "int64", "x-go-name": "FolderID" }, + "folderUID": { + "type": "string", + "x-go-name": "FolderUID" + }, "id": { "type": "integer", "format": "int64", @@ -13638,7 +13647,7 @@ "properties": { "id": { "type": "string", - "x-go-name": "ID" + "x-go-name": "Id" }, "target": { "type": "string", @@ -13653,7 +13662,7 @@ "x-go-name": "Url" } }, - "x-go-package": "github.com/grafana/grafana/pkg/services/preference" + "x-go-package": "github.com/grafana/grafana/pkg/models" }, "NavbarPreference": { "type": "object", @@ -14113,6 +14122,11 @@ "format": "int64", "x-go-name": "FolderID" }, + "folderUID": { + "description": "UID of the folder where the library element is stored.", + "type": "string", + "x-go-name": "FolderUID" + }, "kind": { "description": "Kind of element to create, Use 1 for library panels or 2 for c.\nDescription:\n1 - library panels\n2 - library variables", "type": "integer", diff --git a/public/api-spec.json b/public/api-spec.json index c9f744551dd..32d01405a32 100644 --- a/public/api-spec.json +++ b/public/api-spec.json @@ -506,14 +506,6 @@ "summary": "Add a user role assignment.", "operationId": "addUserRole", "parameters": [ - { - "type": "integer", - "format": "int64", - "x-go-name": "UserID", - "name": "user_id", - "in": "path", - "required": true - }, { "x-go-name": "Body", "name": "body", @@ -522,6 +514,14 @@ "schema": { "$ref": "#/definitions/AddUserRoleCommand" } + }, + { + "type": "integer", + "format": "int64", + "x-go-name": "UserID", + "name": "user_id", + "in": "path", + "required": true } ], "responses": { @@ -547,14 +547,6 @@ "summary": "Remove a user role assignment.", "operationId": "removeUserRole", "parameters": [ - { - "type": "integer", - "format": "int64", - "x-go-name": "UserID", - "name": "user_id", - "in": "path", - "required": true - }, { "type": "string", "x-go-name": "RoleUID", @@ -568,6 +560,14 @@ "description": "A flag indicating if the assignment is global or not. If set to false, the default org ID of the authenticated user will be used from the request to remove assignment.", "name": "global", "in": "query" + }, + { + "type": "integer", + "format": "int64", + "x-go-name": "UserID", + "name": "user_id", + "in": "path", + "required": true } ], "responses": { @@ -6670,6 +6670,14 @@ "summary": "Add External Group.", "operationId": "addTeamGroupApi", "parameters": [ + { + "type": "integer", + "format": "int64", + "x-go-name": "TeamID", + "name": "teamId", + "in": "path", + "required": true + }, { "x-go-name": "Body", "name": "body", @@ -6678,14 +6686,6 @@ "schema": { "$ref": "#/definitions/TeamGroupMapping" } - }, - { - "type": "integer", - "format": "int64", - "x-go-name": "TeamID", - "name": "teamId", - "in": "path", - "required": true } ], "responses": { @@ -6719,16 +6719,16 @@ { "type": "integer", "format": "int64", - "x-go-name": "GroupID", - "name": "groupId", + "x-go-name": "TeamID", + "name": "teamId", "in": "path", "required": true }, { "type": "integer", "format": "int64", - "x-go-name": "TeamID", - "name": "teamId", + "x-go-name": "GroupID", + "name": "groupId", "in": "path", "required": true } @@ -8906,6 +8906,11 @@ "format": "int64", "x-go-name": "FolderID" }, + "folderUID": { + "description": "UID of the folder where the library element is stored.", + "type": "string", + "x-go-name": "FolderUID" + }, "kind": { "description": "Kind of element to create, Use 1 for library panels or 2 for c.\nDescription:\n1 - library panels\n2 - library variables", "type": "integer", @@ -10493,6 +10498,10 @@ "format": "int64", "x-go-name": "FolderID" }, + "folderUID": { + "type": "string", + "x-go-name": "FolderUID" + }, "id": { "type": "integer", "format": "int64", @@ -10721,7 +10730,7 @@ "properties": { "id": { "type": "string", - "x-go-name": "ID" + "x-go-name": "Id" }, "target": { "type": "string", @@ -10736,7 +10745,7 @@ "x-go-name": "Url" } }, - "x-go-package": "github.com/grafana/grafana/pkg/services/preference" + "x-go-package": "github.com/grafana/grafana/pkg/models" }, "NavbarPreference": { "type": "object", @@ -10938,6 +10947,11 @@ "format": "int64", "x-go-name": "FolderID" }, + "folderUID": { + "description": "UID of the folder where the library element is stored.", + "type": "string", + "x-go-name": "FolderUID" + }, "kind": { "description": "Kind of element to create, Use 1 for library panels or 2 for c.\nDescription:\n1 - library panels\n2 - library variables", "type": "integer", From 3372cb78978270ea9885893e0772c8a77f8d841e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20L=C3=B3pez?= Date: Thu, 5 May 2022 10:06:21 +0200 Subject: [PATCH 051/440] make @grafana/ui run properly in SSR environments (#46288) * allow SSR * fix rollup commonjs default flag changed breaking in SSR * revert wrong change * avoid using dynamic imports * fix test * allow icon load in packaged version * fix SelectBase to run on SSR * add extra check for fixing tests * revert wrong change * allow SSR * revert wrong change * don't include emotion in the bundle * fix wrong merge changes * remove unneeded icon change * use forked version of uplot * remove unneeded bundle exceptions * fix typescript issues * update to latest uplot --- packages/grafana-ui/rollup.config.ts | 3 +++ .../ClickOutsideWrapper/ClickOutsideWrapper.tsx | 2 +- packages/grafana-ui/src/components/Icon/iconBundle.ts | 2 +- .../JSONFormatter/json_explorer/json_explorer.ts | 2 +- packages/grafana-ui/src/components/Select/SelectBase.tsx | 2 +- packages/grafana-ui/src/utils/debug.ts | 8 +++++--- packages/grafana-ui/src/utils/dom.ts | 2 +- packages/grafana-ui/src/utils/measureText.ts | 9 +++++++-- 8 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/grafana-ui/rollup.config.ts b/packages/grafana-ui/rollup.config.ts index a19e76cb0eb..9a1d647c43c 100644 --- a/packages/grafana-ui/rollup.config.ts +++ b/packages/grafana-ui/rollup.config.ts @@ -35,10 +35,13 @@ const buildCjsPackage = ({ env }) => { 'moment', 'jquery', // required to use jquery.plot, which is assigned externally 'react-inlinesvg', // required to mock Icon svg loading in tests + '@emotion/react', + '@emotion/css', ], plugins: [ commonjs({ include: /node_modules/, + ignoreTryCatch: false, }), resolve(), svg({ stringify: true }), diff --git a/packages/grafana-ui/src/components/ClickOutsideWrapper/ClickOutsideWrapper.tsx b/packages/grafana-ui/src/components/ClickOutsideWrapper/ClickOutsideWrapper.tsx index e976629d1c6..12a22968f3a 100644 --- a/packages/grafana-ui/src/components/ClickOutsideWrapper/ClickOutsideWrapper.tsx +++ b/packages/grafana-ui/src/components/ClickOutsideWrapper/ClickOutsideWrapper.tsx @@ -24,7 +24,7 @@ interface State { export class ClickOutsideWrapper extends PureComponent { static defaultProps = { includeButtonPress: true, - parent: window, + parent: typeof window !== 'undefined' ? window : null, useCapture: false, }; myRef = createRef(); diff --git a/packages/grafana-ui/src/components/Icon/iconBundle.ts b/packages/grafana-ui/src/components/Icon/iconBundle.ts index 88e9fb7c5ac..8bea9dba083 100644 --- a/packages/grafana-ui/src/components/Icon/iconBundle.ts +++ b/packages/grafana-ui/src/components/Icon/iconBundle.ts @@ -170,7 +170,7 @@ export function initIconCache() { // This function needs to be called after index.js loads to give the // application time to modify __webpack_public_path__ with a CDN path - const grafanaPublicPath = (window as any).__grafana_public_path__; + const grafanaPublicPath = typeof window !== 'undefined' && (window as any).__grafana_public_path__; if (grafanaPublicPath) { iconRoot = grafanaPublicPath + 'img/icons/'; } diff --git a/packages/grafana-ui/src/components/JSONFormatter/json_explorer/json_explorer.ts b/packages/grafana-ui/src/components/JSONFormatter/json_explorer/json_explorer.ts index c6eecd5da31..8deb06ed099 100644 --- a/packages/grafana-ui/src/components/JSONFormatter/json_explorer/json_explorer.ts +++ b/packages/grafana-ui/src/components/JSONFormatter/json_explorer/json_explorer.ts @@ -14,7 +14,7 @@ const JSON_DATE_REGEX = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; const MAX_ANIMATED_TOGGLE_ITEMS = 10; const requestAnimationFrame = - window.requestAnimationFrame || + (typeof window !== 'undefined' && window.requestAnimationFrame) || ((cb: () => void) => { cb(); return 0; diff --git a/packages/grafana-ui/src/components/Select/SelectBase.tsx b/packages/grafana-ui/src/components/Select/SelectBase.tsx index 9bf6d40fadb..28a41837458 100644 --- a/packages/grafana-ui/src/components/Select/SelectBase.tsx +++ b/packages/grafana-ui/src/components/Select/SelectBase.tsx @@ -228,7 +228,7 @@ export function SelectBase({ menuPlacement: menuPlacement === 'auto' && closeToBottom ? 'top' : menuPlacement, menuPosition, menuShouldBlockScroll: true, - menuPortalTarget: menuShouldPortal ? document.body : undefined, + menuPortalTarget: menuShouldPortal && typeof document !== 'undefined' ? document.body : undefined, menuShouldScrollIntoView: false, onBlur, onChange: onChangeWithEmpty, diff --git a/packages/grafana-ui/src/utils/debug.ts b/packages/grafana-ui/src/utils/debug.ts index 4b60bb998bf..a4fdf99cb8c 100644 --- a/packages/grafana-ui/src/utils/debug.ts +++ b/packages/grafana-ui/src/utils/debug.ts @@ -15,8 +15,10 @@ export function attachDebugger(key: string, thebugger?: any, logger?: Logger) { } // @ts-ignore - let debugGlobal = window['_debug'] ?? {}; + let debugGlobal = (typeof window !== 'undefined' && window['_debug']) ?? {}; debugGlobal[key] = completeDebugger; - // @ts-ignore - window['_debug'] = debugGlobal; + if (typeof window !== 'undefined') { + // @ts-ignore + window['_debug'] = debugGlobal; + } } diff --git a/packages/grafana-ui/src/utils/dom.ts b/packages/grafana-ui/src/utils/dom.ts index c81d80af9d8..0b5e74fce22 100644 --- a/packages/grafana-ui/src/utils/dom.ts +++ b/packages/grafana-ui/src/utils/dom.ts @@ -1,5 +1,5 @@ // Node.closest() polyfill -if ('Element' in window && !Element.prototype.closest) { +if (typeof window !== 'undefined' && 'Element' in window && !Element.prototype.closest) { Element.prototype.closest = function (this: any, s: string) { const matches = (this.document || this.ownerDocument).querySelectorAll(s); let el = this; diff --git a/packages/grafana-ui/src/utils/measureText.ts b/packages/grafana-ui/src/utils/measureText.ts index caf76afb1de..321178967fa 100644 --- a/packages/grafana-ui/src/utils/measureText.ts +++ b/packages/grafana-ui/src/utils/measureText.ts @@ -1,4 +1,4 @@ -const context = document.createElement('canvas').getContext('2d')!; +let _context: CanvasRenderingContext2D; const cache = new Map(); const cacheLimit = 500; let ctxFontStyle = ''; @@ -7,7 +7,10 @@ let ctxFontStyle = ''; * @internal */ export function getCanvasContext() { - return context; + if (!_context) { + _context = document.createElement('canvas').getContext('2d')!; + } + return _context; } /** @@ -22,6 +25,8 @@ export function measureText(text: string, fontSize: number): TextMetrics { return fromCache; } + const context = getCanvasContext(); + if (ctxFontStyle !== fontStyle) { context.font = ctxFontStyle = fontStyle; } From abb161829162d486c1e44f5f7928512ec03801ed Mon Sep 17 00:00:00 2001 From: idafurjes <36131195+idafurjes@users.noreply.github.com> Date: Thu, 5 May 2022 10:37:26 +0200 Subject: [PATCH 052/440] Add OTLP exporter for OpenTelemetry (#47987) * Add OTLP exporter for OpenTelemtry * Fix lint * Refactore parse settings * Add configuration for propagation + fix tests * Fix tests and lint * Fix alerting tests * Add coments to config * Add propagation to custom.ini --- conf/defaults.ini | 11 +- conf/sample.ini | 9 ++ go.mod | 12 +- go.sum | 25 +++- pkg/infra/tracing/opentelemetry_tracing.go | 107 ++++++++++++++++-- pkg/infra/tracing/test_helper.go | 8 +- pkg/infra/tracing/tracing.go | 36 +++--- .../api/alerting/api_alertmanager_test.go | 73 ++++++++---- .../alerting/api_notification_channel_test.go | 5 +- 9 files changed, 232 insertions(+), 54 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index a36fdede73c..4dd435a7775 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -975,7 +975,16 @@ disable_shared_zipkin_spans = false [tracing.opentelemetry.jaeger] # jaeger destination (ex http://localhost:14268/api/traces) -address = +address = +# Propagation specifies the text map propagation format: w3c, jaeger +propagation = + +# This is a configuration for OTLP exporter with GRPC protocol +[tracing.opentelemetry.otlp] +# otlp destination (ex localhost:4317) +address = +# Propagation specifies the text map propagation format: w3c, jaeger +propagation = #################################### External Image Storage ############## [external_image_storage] diff --git a/conf/sample.ini b/conf/sample.ini index 2fea29cbefb..9793db0812f 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -958,6 +958,15 @@ [tracing.opentelemetry.jaeger] # jaeger destination (ex http://localhost:14268/api/traces) ; address = http://localhost:14268/api/traces +# Propagation specifies the text map propagation format: w3c, jaeger +; propagation = jaeger + +# This is a configuration for OTLP exporter with GRPC protocol +[tracing.opentelemetry.otlp] +# otlp destination (ex localhost:4317) +; address = localhost:4317 +# Propagation specifies the text map propagation format: w3c, jaeger +; propagation = w3c #################################### External image storage ########################## [external_image_storage] diff --git a/go.mod b/go.mod index 22b20d5d96e..5d7ab9c25e3 100644 --- a/go.mod +++ b/go.mod @@ -99,7 +99,7 @@ require ( go.opentelemetry.io/collector/model v0.31.0 go.opentelemetry.io/otel v1.6.3 go.opentelemetry.io/otel/exporters/jaeger v1.0.0 - go.opentelemetry.io/otel/sdk v1.3.0 + go.opentelemetry.io/otel/sdk v1.6.3 go.opentelemetry.io/otel/trace v1.6.3 golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 golang.org/x/exp v0.0.0-20210220032938-85be41e4509f @@ -172,7 +172,7 @@ require ( github.com/gogo/status v1.1.0 // indirect github.com/golang-jwt/jwt/v4 v4.4.1 // indirect github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect - github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect + github.com/golang/glog v1.0.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/gomodule/redigo v2.0.0+incompatible // indirect @@ -249,6 +249,10 @@ require ( github.com/golang-migrate/migrate/v4 v4.7.0 github.com/grafana/dskit v0.0.0-20211011144203-3a88ec0b675f github.com/grafana/thema v0.0.0-20220413232647-fc54c169b508 + go.etcd.io/etcd v3.3.25+incompatible + go.opentelemetry.io/contrib/propagators/jaeger v1.6.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.6.3 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.6.3 gocloud.dev v0.25.0 ) @@ -272,12 +276,14 @@ require ( github.com/Microsoft/go-winio v0.5.2 // indirect github.com/chromedp/cdproto v0.0.0-20220208224320-6efb837e6bc2 // indirect github.com/containerd/containerd v1.6.2 // indirect + github.com/coreos/go-semver v0.3.0 // indirect github.com/elazarl/goproxy v0.0.0-20220115173737-adb46da277ac // indirect github.com/getkin/kin-openapi v0.94.0 // indirect github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/klauspost/compress v1.15.1 // indirect github.com/kylelemons/godebug v1.1.0 // indirect @@ -285,6 +291,8 @@ require ( github.com/pierrec/lz4/v4 v4.1.8 // indirect github.com/segmentio/asm v1.1.1 // indirect github.com/yudai/pp v2.0.1+incompatible // indirect + go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.6.3 // indirect + go.opentelemetry.io/proto/otlp v0.15.0 // indirect k8s.io/api v0.22.5 // indirect k8s.io/apimachinery v0.22.5 // indirect k8s.io/klog/v2 v2.30.0 // indirect diff --git a/go.sum b/go.sum index a01d3f917c5..f038edcc4fc 100644 --- a/go.sum +++ b/go.sum @@ -680,6 +680,7 @@ github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFE github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= @@ -1216,8 +1217,9 @@ github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZ github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -1455,7 +1457,10 @@ github.com/grpc-ecosystem/grpc-gateway v1.12.1/go.mod h1:8XEsbTttt/W+VvjtQhLACqC github.com/grpc-ecosystem/grpc-gateway v1.14.4/go.mod h1:6CwZWGDSPRJidgKAtJVvND6soZe6fT7iteq8wDPdhb0= github.com/grpc-ecosystem/grpc-gateway v1.14.6/go.mod h1:zdiPV4Yse/1gnckTHtghG4GkDEdKCRJduHpTxT3/jcw= github.com/grpc-ecosystem/grpc-gateway v1.15.0/go.mod h1:vO11I9oWA+KsxmfFQPhLnnIb1VDE24M+pdxZFiuZcA8= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 h1:BZHcxBETFHIdVyhyEfOvn/RdU/QGdLI4y34qQGjGWO0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok= @@ -2721,6 +2726,7 @@ go.etcd.io/etcd v0.0.0-20190709142735-eb7dd97135a5/go.mod h1:N0RPWo9FXJYZQI4BTkD go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.etcd.io/etcd v0.5.0-alpha.5.0.20200520232829-54ba9589114f/go.mod h1:skWido08r9w6Lq/w70DO5XYIKMu4QFu1+4VsqLQuJy8= go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= +go.etcd.io/etcd v3.3.25+incompatible h1:V1RzkZJj9LqsJRy+TUBgpWSbZXITLB819lstuTFoZOY= go.etcd.io/etcd v3.3.25+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI= go.etcd.io/etcd/api/v3 v3.5.0-alpha.0/go.mod h1:mPcW6aZJukV6Aa81LSKpBjQXTWlXB5r74ymPoSWa3Sw= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= @@ -2771,26 +2777,37 @@ go.opentelemetry.io/collector v0.31.0/go.mod h1:A9vKmEa2MI/vJXNUoRinq9w25ZMmxWJL go.opentelemetry.io/collector/model v0.31.0 h1:IgMOkSBd/n/gV4EQQ1nJ+/ylddOlqTfMGjku91yC0d8= go.opentelemetry.io/collector/model v0.31.0/go.mod h1:PcHNnM+RUl0uD8VkSn93PO78N7kQYhfqpI/eki57pl4= go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= +go.opentelemetry.io/contrib v0.21.0 h1:RMJ6GlUVzLYp/zmItxTTdAmr1gnpO/HHMFmvjAhvJQM= go.opentelemetry.io/contrib v0.21.0/go.mod h1:EH4yDYeNoaTqn/8yCWQmfNB78VHfGX2Jt2bvnvzBlGM= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.21.0/go.mod h1:Vm5u/mtkj1OMhtao0v+BGo2LUoLCgHYXvRmj0jWITlE= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0/go.mod h1:vEhqr0m4eTc+DWxfsXoXue2GBgV2uUwVznkGIHW/e5w= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.21.0/go.mod h1:JQAtechjxLEL81EjmbRwxBq/XEzGaHcsPuDHAx54hg4= +go.opentelemetry.io/contrib/propagators/jaeger v1.6.0 h1:tCc+sWgHVeOMp4zmUxHHTaoA5vQlGO089zfg97d+BvU= +go.opentelemetry.io/contrib/propagators/jaeger v1.6.0/go.mod h1:cqu1XdBYBXqXHxZLJdK00G9rT5Hda7Fa938I8LVYz/Y= go.opentelemetry.io/contrib/zpages v0.0.0-20210722161726-7668016acb73/go.mod h1:NAkejuYm41lpyL43Fu1XdnCOYxN5NVV80/MJ03JQ/X8= go.opentelemetry.io/otel v0.11.0/go.mod h1:G8UCk+KooF2HLkgo8RHX9epABH/aRGYET7gQOqBVdB0= go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= go.opentelemetry.io/otel v1.0.0-RC1/go.mod h1:x9tRa9HK4hSSq7jf2TKbqFbtt58/TGk0f9XiEYISI1I= go.opentelemetry.io/otel v1.0.0/go.mod h1:AjRVh9A5/5DE7S+mZtTR6t8vpKKryam+0lREnfmS4cg= go.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs= +go.opentelemetry.io/otel v1.6.1/go.mod h1:blzUabWHkX6LJewxvadmzafgh/wnvBSDBdOuwkAtrWQ= go.opentelemetry.io/otel v1.6.3 h1:FLOfo8f9JzFVFVyU+MSRJc2HdEAXQgm7pIv2uFKRSZE= go.opentelemetry.io/otel v1.6.3/go.mod h1:7BgNga5fNlF/iZjG06hM3yofffp0ofKCDwSXx1GC4dI= go.opentelemetry.io/otel/exporters/jaeger v1.0.0 h1:cLhx8llHw02h5JTqGqaRbYn+QVKHmrzD9vEbKnSPk5U= go.opentelemetry.io/otel/exporters/jaeger v1.0.0/go.mod h1:q10N1AolE1JjqKrFJK2tYw0iZpmX+HBaXBtuCzRnBGQ= +go.opentelemetry.io/otel/exporters/otlp v0.20.0 h1:PTNgq9MRmQqqJY0REVbZFvwkYOA85vbdQU/nVfxDyqg= go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0/go.mod h1:VpP4/RMn8bv8gNo9uK7/IMY4mtWLELsS+JIP0inH0h4= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.6.3 h1:nAmg1WgsUXoXf46dJG9eS/AzOcvkCTK4xJSUYpWyHYg= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.6.3/go.mod h1:NEu79Xo32iVb+0gVNV8PMd7GoWqnyDXRlj04yFjqz40= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0/go.mod h1:hO1KLR7jcKaDDKDkvI9dP/FIhpmna5lkqPUQdEjFAM8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.6.3 h1:4/UjHWMVVc5VwX/KAtqJOHErKigMCH8NexChMuanb/o= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.6.3/go.mod h1:UJmXdiVVBaZ63umRUTwJuCMAV//GCMvDiQwn703/GoY= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.3.0/go.mod h1:keUU7UfnwWTWpJ+FWnyqmogPa82nuU5VUANFq49hlMY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.6.3 h1:leYDq5psbM3K4QNcZ2juCj30LjUnvxjuYQj1mkGjXFM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.6.3/go.mod h1:ycItY/esVj8c0dKgYTOztTERXtPzcfDU/0o8EdwCjoA= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0/go.mod h1:QNX1aly8ehqqX1LEa6YniTU7VY9I6R3X/oPxhGdTceE= go.opentelemetry.io/otel/internal/metric v0.21.0/go.mod h1:iOfAaY2YycsXfYD4kaRSbLx2LKmfpKObWBEv9QK5zFo= go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= @@ -2800,18 +2817,22 @@ go.opentelemetry.io/otel/oteltest v1.0.0-RC1/go.mod h1:+eoIG0gdEOaPNftuy1YScLr1G go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= go.opentelemetry.io/otel/sdk v1.0.0-RC1/go.mod h1:kj6yPn7Pgt5ByRuwesbaWcRLA+V7BSDg3Hf8xRvsvf8= go.opentelemetry.io/otel/sdk v1.0.0/go.mod h1:PCrDHlSy5x1kjezSdL37PhbFUMjrsLRshJ2zCzeXwbM= -go.opentelemetry.io/otel/sdk v1.3.0 h1:3278edCoH89MEJ0Ky8WQXVmDQv3FX4ZJ3Pp+9fJreAI= go.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs= +go.opentelemetry.io/otel/sdk v1.6.3 h1:prSHYdwCQOX5DrsEzxowH3nLhoAzEBdZhvrR79scfLs= +go.opentelemetry.io/otel/sdk v1.6.3/go.mod h1:A4iWF7HTXa+GWL/AaqESz28VuSBIcZ+0CV+IzJ5NMiQ= go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= go.opentelemetry.io/otel/trace v1.0.0-RC1/go.mod h1:86UHmyHWFEtWjfWPSbu0+d0Pf9Q6e1U+3ViBOc+NXAg= go.opentelemetry.io/otel/trace v1.0.0/go.mod h1:PXTWqayeFUlJV1YDNhsJYB184+IvAH814St6o6ajzIs= go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk= +go.opentelemetry.io/otel/trace v1.6.1/go.mod h1:RkFRM1m0puWIq10oxImnGEduNBzxiN7TXluRBtE+5j0= go.opentelemetry.io/otel/trace v1.6.3 h1:IqN4L+5b0mPNjdXIiZ90Ni4Bl5BRkDQywePLWemd9bc= go.opentelemetry.io/otel/trace v1.6.3/go.mod h1:GNJQusJlUgZl9/TQBPKU/Y/ty+0iVB5fjhKeJGZPGFs= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.11.0/go.mod h1:QpEjXPrNQzrFDZgoTo49dgHR9RYRSrg3NAKnUGl9YpQ= +go.opentelemetry.io/proto/otlp v0.15.0 h1:h0bKrvdrT/9sBwEJ6iWUqT/N/xPcS66bL4u3isneJ6w= +go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.starlark.net v0.0.0-20200901195727-6e684ef5eeee/go.mod h1:f0znQkUKRrkk36XxWbGjMqQM8wGv/xHBVE2qc3B5oFU= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= diff --git a/pkg/infra/tracing/opentelemetry_tracing.go b/pkg/infra/tracing/opentelemetry_tracing.go index 61b62a850e0..15200cbf529 100644 --- a/pkg/infra/tracing/opentelemetry_tracing.go +++ b/pkg/infra/tracing/opentelemetry_tracing.go @@ -6,11 +6,16 @@ import ( "time" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/log/level" "github.com/grafana/grafana/pkg/setting" + "go.etcd.io/etcd/version" + jaegerpropagator "go.opentelemetry.io/contrib/propagators/jaeger" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/exporters/jaeger" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" tracesdk "go.opentelemetry.io/otel/sdk/trace" @@ -18,6 +23,13 @@ import ( trace "go.opentelemetry.io/otel/trace" ) +const ( + jaegerExporter string = "jaeger" + otlpExporter string = "otlp" + jaegerPropagator string = "jaeger" + w3cPropagator string = "w3c" +) + type Tracer interface { Run(context.Context) error Start(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, Span) @@ -34,9 +46,10 @@ type Span interface { } type Opentelemetry struct { - enabled bool - address string - log log.Logger + enabled string + address string + propagation string + log log.Logger tracerProvider *tracesdk.TracerProvider tracer trace.Tracer @@ -53,6 +66,12 @@ type EventValue struct { Num int64 } +type otelErrHandler func(err error) + +func (o otelErrHandler) Handle(err error) { + o(err) +} + func (ots *Opentelemetry) parseSettingsOpentelemetry() error { section, err := ots.Cfg.Raw.GetSection("tracing.opentelemetry.jaeger") if err != nil { @@ -61,13 +80,26 @@ func (ots *Opentelemetry) parseSettingsOpentelemetry() error { ots.address = section.Key("address").MustString("") if ots.address != "" { - ots.enabled = true + ots.enabled = jaegerExporter + return nil } + ots.propagation = section.Key("propagation").MustString("") + + section, err = ots.Cfg.Raw.GetSection("tracing.opentelemetry.otlp") + if err != nil { + return err + } + + ots.address = section.Key("address").MustString("") + if ots.address != "" { + ots.enabled = otlpExporter + } + ots.propagation = section.Key("propagation").MustString("") return nil } -func (ots *Opentelemetry) initTracerProvider() (*tracesdk.TracerProvider, error) { +func (ots *Opentelemetry) initJaegerTracerProvider() (*tracesdk.TracerProvider, error) { // Create the Jaeger exporter exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(ots.address))) if err != nil { @@ -86,18 +118,69 @@ func (ots *Opentelemetry) initTracerProvider() (*tracesdk.TracerProvider, error) return tp, nil } -func (ots *Opentelemetry) initOpentelemetryTracer() error { - tp, err := ots.initTracerProvider() +func (ots *Opentelemetry) initOTLPTracerProvider() (*tracesdk.TracerProvider, error) { + client := otlptracegrpc.NewClient(otlptracegrpc.WithEndpoint(ots.address), otlptracegrpc.WithInsecure()) + exp, err := otlptrace.New(context.Background(), client) if err != nil { - return err + return nil, err } + + res, err := resource.New( + context.Background(), + resource.WithAttributes( + semconv.ServiceNameKey.String("grafana"), + semconv.ServiceVersionKey.String(version.Version), + ), + resource.WithProcessRuntimeDescription(), + resource.WithTelemetrySDK(), + ) + if err != nil { + return nil, err + } + + tp := tracesdk.NewTracerProvider( + tracesdk.WithBatcher(exp), + tracesdk.WithSampler(tracesdk.ParentBased( + tracesdk.AlwaysSample(), + )), + tracesdk.WithResource(res), + ) + return tp, nil +} + +func (ots *Opentelemetry) initOpentelemetryTracer() error { + var tp *tracesdk.TracerProvider + var err error + switch ots.enabled { + case jaegerExporter: + tp, err = ots.initJaegerTracerProvider() + if err != nil { + return err + } + case otlpExporter: + tp, err = ots.initOTLPTracerProvider() + if err != nil { + return err + } + default: + ots.log.Error("invalid trace exporter") + } + // Register our TracerProvider as the global so any imported // instrumentation in the future will default to using it // only if tracing is enabled - if ots.enabled { + if ots.enabled != "" { otel.SetTracerProvider(tp) } + switch ots.propagation { + case w3cPropagator: + otel.SetTextMapPropagator(propagation.TraceContext{}) + case jaegerPropagator: + otel.SetTextMapPropagator(jaegerpropagator.Jaeger{}) + default: + otel.SetTextMapPropagator(propagation.TraceContext{}) + } ots.tracerProvider = tp ots.tracer = otel.GetTracerProvider().Tracer("component-main") @@ -105,6 +188,12 @@ func (ots *Opentelemetry) initOpentelemetryTracer() error { } func (ots *Opentelemetry) Run(ctx context.Context) error { + otel.SetErrorHandler(otelErrHandler(func(err error) { + err = level.Error(ots.log).Log("msg", "OpenTelemetry handler returned an error", "err", err) + if err != nil { + ots.log.Error("OpenTelemetry log returning error", err) + } + })) <-ctx.Done() ots.log.Info("Closing tracing") diff --git a/pkg/infra/tracing/test_helper.go b/pkg/infra/tracing/test_helper.go index 88474c700a9..0b28e11b816 100644 --- a/pkg/infra/tracing/test_helper.go +++ b/pkg/infra/tracing/test_helper.go @@ -1,7 +1,9 @@ package tracing func InitializeTracerForTest() (Tracer, error) { - ots := &Opentelemetry{} + ots := &Opentelemetry{ + enabled: "jaeger", + } err := ots.initOpentelemetryTracer() if err != nil { return ots, err @@ -10,7 +12,9 @@ func InitializeTracerForTest() (Tracer, error) { } func InitializeForBus() Tracer { - ots := &Opentelemetry{} + ots := &Opentelemetry{ + enabled: "jaeger", + } _ = ots.initOpentelemetryTracer() return ots } diff --git a/pkg/infra/tracing/tracing.go b/pkg/infra/tracing/tracing.go index c13e353e94e..5b2e4ce628d 100644 --- a/pkg/infra/tracing/tracing.go +++ b/pkg/infra/tracing/tracing.go @@ -28,29 +28,37 @@ const ( ) func ProvideService(cfg *setting.Cfg) (Tracer, error) { - ts := &Opentracing{ - Cfg: cfg, - log: log.New("tracing"), - } - - if err := ts.parseSettings(); err != nil { + ts, ots, err := parseSettings(cfg) + if err != nil { return nil, err } if ts.enabled { - return ts, ts.initGlobalTracer() + return ts, ts.initJaegerGlobalTracer() + } + + return ots, ots.initOpentelemetryTracer() +} + +func parseSettings(cfg *setting.Cfg) (*Opentracing, *Opentelemetry, error) { + ts := &Opentracing{ + Cfg: cfg, + log: log.New("tracing"), + } + err := ts.parseSettings() + if err != nil { + return ts, nil, err + } + if ts.enabled { + return ts, nil, nil } ots := &Opentelemetry{ Cfg: cfg, log: log.New("tracing"), } - - if err := ots.parseSettingsOpentelemetry(); err != nil { - return nil, err - } - - return ots, ots.initOpentelemetryTracer() + err = ots.parseSettingsOpentelemetry() + return ts, ots, err } type traceKey struct{} @@ -136,7 +144,7 @@ func (ts *Opentracing) initJaegerCfg() (jaegercfg.Configuration, error) { return cfg, nil } -func (ts *Opentracing) initGlobalTracer() error { +func (ts *Opentracing) initJaegerGlobalTracer() error { cfg, err := ts.initJaegerCfg() if err != nil { return err diff --git a/pkg/tests/api/alerting/api_alertmanager_test.go b/pkg/tests/api/alerting/api_alertmanager_test.go index 1ed1cde55c8..fabef255d2a 100644 --- a/pkg/tests/api/alerting/api_alertmanager_test.go +++ b/pkg/tests/api/alerting/api_alertmanager_test.go @@ -27,6 +27,11 @@ import ( "github.com/grafana/grafana/pkg/tests/testinfra" ) +type Response struct { + Message string `json:"message"` + TraceID string `json:"traceID"` +} + func TestAMConfigAccess(t *testing.T) { _, err := tracing.InitializeTracerForTest() require.NoError(t, err) @@ -880,11 +885,11 @@ func TestAlertRuleCRUD(t *testing.T) { // Now, let's try to create some invalid alert rules. { testCases := []struct { - desc string - rulegroup string - interval model.Duration - rule apimodels.PostableExtendedRuleNode - expectedResponse string + desc string + rulegroup string + interval model.Duration + rule apimodels.PostableExtendedRuleNode + expectedMessage string }{ { desc: "alert rule without queries and expressions", @@ -900,7 +905,7 @@ func TestAlertRuleCRUD(t *testing.T) { Data: []ngmodels.AlertQuery{}, }, }, - expectedResponse: `{"message": "invalid rule specification at index [0]: invalid alert rule: no queries or expressions are found", "traceID":"00000000000000000000000000000000"}`, + expectedMessage: "invalid rule specification at index [0]: invalid alert rule: no queries or expressions are found", }, { desc: "alert rule with empty title", @@ -930,7 +935,7 @@ func TestAlertRuleCRUD(t *testing.T) { }, }, }, - expectedResponse: `{"message": "invalid rule specification at index [0]: alert rule title cannot be empty", "traceID":"00000000000000000000000000000000"}`, + expectedMessage: "invalid rule specification at index [0]: alert rule title cannot be empty", }, { desc: "alert rule with too long name", @@ -960,7 +965,7 @@ func TestAlertRuleCRUD(t *testing.T) { }, }, }, - expectedResponse: `{"message": "invalid rule specification at index [0]: alert rule title is too long. Max length is 190", "traceID":"00000000000000000000000000000000"}`, + expectedMessage: "invalid rule specification at index [0]: alert rule title is too long. Max length is 190", }, { desc: "alert rule with too long rulegroup", @@ -990,7 +995,7 @@ func TestAlertRuleCRUD(t *testing.T) { }, }, }, - expectedResponse: `{"message": "rule group name is too long. Max length is 190", "traceID":"00000000000000000000000000000000"}`, + expectedMessage: "rule group name is too long. Max length is 190", }, { desc: "alert rule with invalid interval", @@ -1021,8 +1026,7 @@ func TestAlertRuleCRUD(t *testing.T) { }, }, }, - expectedResponse: `{"message": "rule evaluation interval (1 second) should be positive ` + - `number that is multiple of the base interval of 10 seconds", "traceID":"00000000000000000000000000000000"}`, + expectedMessage: "rule evaluation interval (1 second) should be positive number that is multiple of the base interval of 10 seconds", }, { desc: "alert rule with unknown datasource", @@ -1052,8 +1056,7 @@ func TestAlertRuleCRUD(t *testing.T) { }, }, }, - expectedResponse: `{"message": "invalid rule specification at index [0]: failed to validate condition of alert rule AlwaysFiring:` + - ` invalid query A: data source not found: unknown", "traceID":"00000000000000000000000000000000"}`, + expectedMessage: "invalid rule specification at index [0]: failed to validate condition of alert rule AlwaysFiring: invalid query A: data source not found: unknown", }, { desc: "alert rule with invalid condition", @@ -1083,8 +1086,7 @@ func TestAlertRuleCRUD(t *testing.T) { }, }, }, - expectedResponse: `{"message": "invalid rule specification at index [0]: failed to validate condition of alert rule AlwaysFiring: ` + - `condition B not found in any query or expression: it should be one of: [A]", "traceID":"00000000000000000000000000000000"}`, + expectedMessage: "invalid rule specification at index [0]: failed to validate condition of alert rule AlwaysFiring: condition B not found in any query or expression: it should be one of: [A]", }, } @@ -1113,8 +1115,14 @@ func TestAlertRuleCRUD(t *testing.T) { b, err := ioutil.ReadAll(resp.Body) require.NoError(t, err) + res := &Response{} + err = json.Unmarshal(b, &res) + require.NoError(t, err) + + assert.Equal(t, res.Message, tc.expectedMessage) + assert.NotEmpty(t, res.TraceID) + assert.Equal(t, resp.StatusCode, http.StatusBadRequest) - require.JSONEq(t, tc.expectedResponse, string(b)) }) } } @@ -2266,6 +2274,7 @@ func TestEval(t *testing.T) { payload string expectedStatusCode int expectedResponse string + expectedMessage string }{ { desc: "alerting condition", @@ -2414,8 +2423,7 @@ func TestEval(t *testing.T) { } `, expectedStatusCode: http.StatusBadRequest, - expectedResponse: `{"message": "invalid condition: condition B not found in any query or expression: it should be one of: [A]",` + - `"traceID": "00000000000000000000000000000000"}`, + expectedMessage: "invalid condition: condition B not found in any query or expression: it should be one of: [A]", }, { desc: "unknown query datasource", @@ -2440,7 +2448,7 @@ func TestEval(t *testing.T) { } `, expectedStatusCode: http.StatusBadRequest, - expectedResponse: `{"message": "invalid condition: invalid query A: data source not found: unknown", "traceID": "00000000000000000000000000000000"}`, + expectedMessage: "invalid condition: invalid query A: data source not found: unknown", }, } @@ -2457,9 +2465,18 @@ func TestEval(t *testing.T) { }) b, err := ioutil.ReadAll(resp.Body) require.NoError(t, err) + res := Response{} + err = json.Unmarshal(b, &res) + require.NoError(t, err) assert.Equal(t, tc.expectedStatusCode, resp.StatusCode) - require.JSONEq(t, tc.expectedResponse, string(b)) + if tc.expectedResponse != "" { + require.JSONEq(t, tc.expectedResponse, string(b)) + } + if tc.expectedMessage != "" { + assert.Equal(t, tc.expectedMessage, res.Message) + assert.NotEmpty(t, res.TraceID) + } }) } @@ -2469,6 +2486,7 @@ func TestEval(t *testing.T) { payload string expectedStatusCode int expectedResponse string + expectedMessage string }{ { desc: "alerting condition", @@ -2596,8 +2614,7 @@ func TestEval(t *testing.T) { } `, expectedStatusCode: http.StatusBadRequest, - expectedResponse: `{"message": "invalid queries or expressions: invalid query A: data source not found: unknown",` + - `"traceID": "00000000000000000000000000000000"}`, + expectedMessage: "invalid queries or expressions: invalid query A: data source not found: unknown", }, } @@ -2614,9 +2631,19 @@ func TestEval(t *testing.T) { }) b, err := ioutil.ReadAll(resp.Body) require.NoError(t, err) + res := Response{} + err = json.Unmarshal(b, &res) + require.NoError(t, err) assert.Equal(t, tc.expectedStatusCode, resp.StatusCode) - require.JSONEq(t, tc.expectedResponse, string(b)) + if tc.expectedResponse != "" { + require.JSONEq(t, tc.expectedResponse, string(b)) + } + + if tc.expectedMessage != "" { + require.Equal(t, tc.expectedMessage, res.Message) + require.NotEmpty(t, res.TraceID) + } }) } } diff --git a/pkg/tests/api/alerting/api_notification_channel_test.go b/pkg/tests/api/alerting/api_notification_channel_test.go index c535065d216..333775b6370 100644 --- a/pkg/tests/api/alerting/api_notification_channel_test.go +++ b/pkg/tests/api/alerting/api_notification_channel_test.go @@ -62,7 +62,10 @@ func TestTestReceivers(t *testing.T) { b, err := ioutil.ReadAll(resp.Body) require.NoError(t, err) - require.JSONEq(t, `{"traceID":"00000000000000000000000000000000"}`, string(b)) + res := Response{} + err = json.Unmarshal(b, &res) + require.NoError(t, err) + require.NotEmpty(t, res.TraceID) }) t.Run("assert working receiver returns OK", func(t *testing.T) { From 1cd9175075cd31bad241808440b15aff851d27b4 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Thu, 5 May 2022 09:42:39 +0100 Subject: [PATCH 053/440] PanelEdit: Add gap between variables, like in Dashboard view (#48718) --- .../features/dashboard/components/PanelEditor/PanelEditor.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx index db35c2ecb38..8221172b50e 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx @@ -509,6 +509,7 @@ export const getStyles = stylesFactory((theme: GrafanaTheme2, props: Props) => { display: flex; flex-grow: 1; flex-wrap: wrap; + gap: ${theme.spacing(1, 2)}; `, panelWrapper: css` flex: 1 1 0; From 143810bb95af196174f05fd3b8d59b8518e6277e Mon Sep 17 00:00:00 2001 From: Giordano Ricci Date: Thu, 5 May 2022 10:36:50 +0100 Subject: [PATCH 054/440] Explore: simplify support for multiple query editors (#48701) * Explore: simplify support for multiple query editors * CloudWatch: remove usage of deprecated plugin methods * Apply suggestions from code review Co-authored-by: JitaC <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/developers/plugins/add-support-for-explore-queries.md Co-authored-by: JitaC <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/developers/plugins/add-support-for-explore-queries.md Co-authored-by: JitaC <70489351+achatterjee-grafana@users.noreply.github.com> * run prettier Co-authored-by: JitaC <70489351+achatterjee-grafana@users.noreply.github.com> --- .../plugins/add-query-editor-help.md | 1 - .../add-support-for-explore-queries.md | 61 +++++-------------- packages/grafana-data/src/types/datasource.ts | 6 ++ .../plugins/datasource/cloudwatch/module.tsx | 5 +- 4 files changed, 23 insertions(+), 50 deletions(-) diff --git a/docs/sources/developers/plugins/add-query-editor-help.md b/docs/sources/developers/plugins/add-query-editor-help.md index 6eca73a4687..13d4687641c 100644 --- a/docs/sources/developers/plugins/add-query-editor-help.md +++ b/docs/sources/developers/plugins/add-query-editor-help.md @@ -27,7 +27,6 @@ By adding a help component to your plugin, you can for example create "cheat she export const plugin = new DataSourcePlugin(DataSource) .setConfigEditor(ConfigEditor) .setQueryEditor(QueryEditor) - .setExploreQueryField(ExploreQueryEditor) .setQueryEditorHelp(QueryEditorHelp); ``` diff --git a/docs/sources/developers/plugins/add-support-for-explore-queries.md b/docs/sources/developers/plugins/add-support-for-explore-queries.md index be4ef28037f..4c9e357222e 100644 --- a/docs/sources/developers/plugins/add-support-for-explore-queries.md +++ b/docs/sources/developers/plugins/add-support-for-explore-queries.md @@ -10,11 +10,11 @@ This guide assumes that you're already familiar with how to [Build a data source With Explore, users can make ad-hoc queries without the use of a dashboard. This is useful when users want to troubleshoot or to learn more about the data. -Your data source already supports Explore by default, and will use the existing query editor for the data source. If you want to offer extended Explore functionality for your data source however, you can define a Explore-specific query editor. +Your data source supports Explore by default and uses the existing query editor for the data source. -## Add a query editor for Explore +## Add an Explore-specific query editor -The query editor for Explore is similar to the query editor for the data source itself. In fact, you'll probably reuse the same components for both query editors. +To extend Explore functionality for your data source, you can define an Explore-specific query editor. 1. Create a file `ExploreQueryEditor.tsx` in the `src` directory of your plugin, with the following content: @@ -26,60 +26,31 @@ The query editor for Explore is similar to the query editor for the data source import { DataSource } from './DataSource'; import { MyQuery, MyDataSourceOptions } from './types'; - export type Props = QueryEditorProps; + type Props = QueryEditorProps; export default (props: Props) => { - return

    My query editor

    ; + return

    My Explore-specific query editor

    ; }; ``` -1. Configure the plugin to use the `ExploreQueryEditor`. +1. Modify your base query editor in `QueryEditor.tsx` to render the Explore-specific query editor. For example: ```ts + // [...] + import { CoreApp } from '@grafana/data'; import ExploreQueryEditor from './ExploreQueryEditor'; - ``` - ```ts - export const plugin = new DataSourcePlugin(DataSource) - .setConfigEditor(ConfigEditor) - .setQueryEditor(QueryEditor) - .setExploreQueryField(ExploreQueryEditor); - ``` + type Props = QueryEditorProps; -1. Add a `QueryField` to `ExploreQueryEditor`. - - ```ts - import { QueryField } from '@grafana/ui'; - ``` - - ```ts export default (props: Props) => { - const { query } = props; + const { app } = props; - const onQueryChange = (value: string, override?: boolean) => { - const { query, onChange, onRunQuery } = props; - - if (onChange) { - // Update the query whenever the query field changes. - onChange({ ...query, queryText: value }); - - // Run the query on Enter. - if (override && onRunQuery) { - onRunQuery(); - } - } - }; - - return ( - - ); + switch (app) { + case CoreApp.Explore: + return ; + default: + return
    My base query editor
    ; + } }; ``` diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts index d05fc6985e6..8d84a51751e 100644 --- a/packages/grafana-data/src/types/datasource.ts +++ b/packages/grafana-data/src/types/datasource.ts @@ -65,16 +65,19 @@ export class DataSourcePlugin< return this; } + /** @deprecated Use `setQueryEditor` instead. When using Explore `props.app` is equal to `CoreApp.Explore` */ setExploreQueryField(ExploreQueryField: ComponentType>) { this.components.ExploreQueryField = ExploreQueryField; return this; } + /** @deprecated Use `setQueryEditor` instead. */ setExploreMetricsQueryField(ExploreQueryField: ComponentType>) { this.components.ExploreMetricsQueryField = ExploreQueryField; return this; } + /** @deprecated Use `setQueryEditor` instead. */ setExploreLogsQueryField(ExploreQueryField: ComponentType>) { this.components.ExploreLogsQueryField = ExploreQueryField; return this; @@ -151,8 +154,11 @@ export interface DataSourcePluginComponents< AnnotationsQueryCtrl?: any; VariableQueryEditor?: any; QueryEditor?: ComponentType>; + /** @deprecated it will be removed in a future release and `QueryEditor` will be used instead. */ ExploreQueryField?: ComponentType>; + /** @deprecated it will be removed in a future release and `QueryEditor` will be used instead. */ ExploreMetricsQueryField?: ComponentType>; + /** @deprecated it will be removed in a future release and `QueryEditor` will be used instead. */ ExploreLogsQueryField?: ComponentType>; QueryEditorHelp?: ComponentType>; ConfigEditor?: ComponentType>; diff --git a/public/app/plugins/datasource/cloudwatch/module.tsx b/public/app/plugins/datasource/cloudwatch/module.tsx index a4f63d4bca4..b295691232c 100644 --- a/public/app/plugins/datasource/cloudwatch/module.tsx +++ b/public/app/plugins/datasource/cloudwatch/module.tsx @@ -2,7 +2,6 @@ import { DataSourcePlugin } from '@grafana/data'; import { ConfigEditor } from './components/ConfigEditor'; import LogsCheatSheet from './components/LogsCheatSheet'; -import { CloudWatchLogsQueryEditor } from './components/LogsQueryEditor'; import { MetaInspector } from './components/MetaInspector'; import { PanelQueryEditor } from './components/PanelQueryEditor'; import { CloudWatchDatasource } from './datasource'; @@ -14,6 +13,4 @@ export const plugin = new DataSourcePlugin Date: Thu, 5 May 2022 11:38:46 +0200 Subject: [PATCH 055/440] Chore: Add PR Check action enforcing changelog decision (#48728) --- .github/pr-checks.json | 43 +++++++++++++++++++++++++++----- .github/workflows/pr-checks.yml | 3 ++- contribute/merge-pull-request.md | 26 ++++++++++++++++--- 3 files changed, 62 insertions(+), 10 deletions(-) diff --git a/.github/pr-checks.json b/.github/pr-checks.json index de313e7b313..b0ea1c5c639 100644 --- a/.github/pr-checks.json +++ b/.github/pr-checks.json @@ -7,12 +7,43 @@ "failure": "Milestone not set" }, { - "type": "check-backport", + "type": "check-label", "title": "Backport Check", - "backportEnabled": "Backport enabled", - "backportSkipped": "Backport skipped", - "failure": "Backport decision needed", - "targetUrl": "https://github.com/grafana/grafana/blob/main/contribute/merge-pull-request.md#should-the-pull-request-be-backported", - "skipLabels": [ "backport", "no-backport"] + "labels": { + "exists": "Backport enabled", + "notExists": "Backport decision needed", + "matches": [ + "backport v*" + ] + }, + "skip": { + "message": "Backport skipped", + "matches": [ + "backport", + "no-backport" + ] + }, + "targetUrl": "https://github.com/grafana/grafana/blob/main/contribute/merge-pull-request.md#should-the-pull-request-be-backported" + }, + { + "type": "check-changelog", + "title": "Changelog Check", + "labels": { + "exists": "Changelog enabled", + "notExists": "Changelog decision needed", + "matches": [ + "add to changelog" + ] + }, + "breakingChangeLabels": [ + "breaking change" + ], + "skip": { + "message": "Changelog skipped", + "matches": [ + "no-changelog" + ] + }, + "targetUrl": "https://github.com/grafana/grafana/blob/main/contribute/merge-pull-request.md#include-in-changelog-and-release-notes" } ] \ No newline at end of file diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index bf41c507509..ee978b31931 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -5,9 +5,10 @@ on: - opened - reopened - synchronize - - ready_for_review + - ready_for_review - labeled - unlabeled + - edited issues: types: - milestoned diff --git a/contribute/merge-pull-request.md b/contribute/merge-pull-request.md index 0383c4df4fb..b6e403ad597 100644 --- a/contribute/merge-pull-request.md +++ b/contribute/merge-pull-request.md @@ -43,11 +43,31 @@ This makes it easier to track what changes go into a certain release. Without th At Grafana we generate the [changelog](https://github.com/grafana/grafana/blob/main/CHANGELOG.md) and [release notes](https://grafana.com/docs/grafana/latest/release-notes/) based on merged pull requests. Including changes in the changelog/release notes is very important to provide a somewhat complete picture of what changes a Grafana release actually includes. -Exactly what changes should be added to the changelog is hard to answer but some general guidance would be any change that you think would be interesting for the community as a whole. Use your best judgement and/or ask other maintainers for advice. - There's a GitHub action available in the repository named [Update changelog](https://github.com/grafana/grafana/blob/main/.github/workflows/update-changelog.yml) that can manually be triggered to re-generate the changelog and release notes for any release. -To include a pull request in the changelog/release notes the general rule of thumb is that a milestone should be assigned and labeled with `add to changelog`. +Exactly what changes should be added to the changelog is hard to answer but here's some general guidance: + +- Include any bug fix in general. +- Include any change that you think would be interesting for the community as a whole. +- Skip larger features divided in multiple pull requests since they might go into the release blog post/What's New article. +- Use your best judgement and/or ask other maintainers for advice. +- Including a change in error rather than skipping one that should have been there is better. +- Always keep [Format the pull request title](#format-the-pull-request-title) in mind. + +An active decision to include change in changelog/release notes needs to be taken for every pull request. There's a pull request check named **Changelog Check** that will enforce this. By adding/removing labels on the pull request or updating the pull request title/description the check will be re-evaluated. + +#### Skip changelog + +If you don't want to include your change in changelog/release notes you need to add a label named **no-changelog** to the pull request. + +#### Include in changelog/release notes + +To include a pull request in the changelog/release notes you need to add a label named `add to changelog` to the pull request. Then additional validation rules is checked: + +- Title need to be formatted according to [Format the pull request title](#format-the-pull-request-title) +- Description needs to include a breaking change notice if change is labeled to be a breaking change, see Breaking changes below for more information. + +Not complying with above rules can make the **Changelog Check** fail with validation errors. The changelog/release notes are divided into sections and here's a description of how you make a pull request show up in a certain section. From 3cade2f6696378b622d1bf97756c57662aaed3ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Thu, 5 May 2022 12:02:53 +0200 Subject: [PATCH 056/440] Revert "Loki: backend: use streaming JSON parser (#47656)" (#48747) This reverts commit 46b40b6e82417661078be5f1c4a9d802e440d9ec. --- pkg/tsdb/loki/api.go | 16 +- pkg/tsdb/loki/frame.go | 112 ++-------- pkg/tsdb/loki/frame_test.go | 114 ++-------- pkg/tsdb/loki/loki.go | 13 +- pkg/tsdb/loki/parse_response.go | 206 ++++++++++++++++++ pkg/tsdb/loki/parse_response_test.go | 183 ++++++++++++++++ pkg/util/converter/prom.go | 34 +-- .../testdata/loki-streams-a-frame.json | 44 ++-- .../testdata/loki-streams-a-golden.txt | 26 +-- .../testdata/loki-streams-b-frame.json | 34 +-- .../testdata/loki-streams-b-golden.txt | 24 +- .../converter/testdata/prom-matrix-frame.json | 8 +- .../converter/testdata/prom-matrix-golden.txt | 40 ++-- .../converter/testdata/prom-vector-frame.json | 8 +- .../converter/testdata/prom-vector-golden.txt | 32 +-- .../loki/backendResultTransformer.test.ts | 2 +- 16 files changed, 550 insertions(+), 346 deletions(-) create mode 100644 pkg/tsdb/loki/parse_response.go create mode 100644 pkg/tsdb/loki/parse_response_test.go diff --git a/pkg/tsdb/loki/api.go b/pkg/tsdb/loki/api.go index bca3eec28c6..d1d1eb6f813 100644 --- a/pkg/tsdb/loki/api.go +++ b/pkg/tsdb/loki/api.go @@ -10,9 +10,8 @@ import ( "net/url" "strconv" - "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/util/converter" + "github.com/grafana/loki/pkg/loghttp" jsoniter "github.com/json-iterator/go" ) @@ -136,7 +135,7 @@ func makeLokiError(body io.ReadCloser) error { return fmt.Errorf("%v", errorMessage) } -func (api *LokiAPI) DataQuery(ctx context.Context, query lokiQuery) (data.Frames, error) { +func (api *LokiAPI) DataQuery(ctx context.Context, query lokiQuery) (*loghttp.QueryResponse, error) { req, err := makeDataRequest(ctx, api.url, query) if err != nil { return nil, err @@ -157,14 +156,13 @@ func (api *LokiAPI) DataQuery(ctx context.Context, query lokiQuery) (data.Frames return nil, makeLokiError(resp.Body) } - iter := jsoniter.Parse(jsoniter.ConfigDefault, resp.Body, 1024) - res := converter.ReadPrometheusStyleResult(iter) - - if res.Error != nil { - return nil, res.Error + var response loghttp.QueryResponse + err = jsoniter.NewDecoder(resp.Body).Decode(&response) + if err != nil { + return nil, err } - return res.Frames, nil + return &response, nil } func makeRawRequest(ctx context.Context, lokiDsUrl string, resourceURL string) (*http.Request, error) { diff --git a/pkg/tsdb/loki/frame.go b/pkg/tsdb/loki/frame.go index 10affc4dede..ebfe4247ee1 100644 --- a/pkg/tsdb/loki/frame.go +++ b/pkg/tsdb/loki/frame.go @@ -6,6 +6,7 @@ import ( "hash/fnv" "sort" "strings" + "time" "github.com/grafana/grafana-plugin-sdk-go/data" ) @@ -56,9 +57,6 @@ func adjustMetricFrame(frame *data.Frame, query *lokiQuery) error { frame.Meta = &data.FrameMeta{} } - frame.Meta.Stats = parseStats(frame.Meta.Custom) - frame.Meta.Custom = nil - if isMetricRange { frame.Meta.ExecutedQueryString = "Expr: " + query.Expr + "\n" + "Step: " + query.Step.String() } else { @@ -83,55 +81,53 @@ func adjustMetricFrame(frame *data.Frame, query *lokiQuery) error { func adjustLogsFrame(frame *data.Frame, query *lokiQuery) error { // we check if the fields are of correct type and length fields := frame.Fields - if len(fields) != 4 { + if len(fields) != 3 { return fmt.Errorf("invalid fields in logs frame") } labelsField := fields[0] timeField := fields[1] lineField := fields[2] - stringTimeField := fields[3] - if (timeField.Type() != data.FieldTypeTime) || (lineField.Type() != data.FieldTypeString) || (labelsField.Type() != data.FieldTypeJSON) || (stringTimeField.Type() != data.FieldTypeString) { + if (timeField.Type() != data.FieldTypeTime) || (lineField.Type() != data.FieldTypeString) || (labelsField.Type() != data.FieldTypeJSON) { return fmt.Errorf("invalid fields in logs frame") } - if (timeField.Len() != lineField.Len()) || (timeField.Len() != labelsField.Len()) || (timeField.Len() != stringTimeField.Len()) { + if (timeField.Len() != lineField.Len()) || (timeField.Len() != labelsField.Len()) { return fmt.Errorf("invalid fields in logs frame") } - // this returns an error when the length of fields do not match - _, err := frame.RowLen() - if err != nil { - return err - } - - labelsField.Name = "labels" - stringTimeField.Name = "tsNs" - if frame.Meta == nil { frame.Meta = &data.FrameMeta{} } - frame.Meta.Stats = parseStats(frame.Meta.Custom) - frame.Meta.Custom = nil - frame.Meta.ExecutedQueryString = "Expr: " + query.Expr // we need to send to the browser the nanosecond-precision timestamp too. // usually timestamps become javascript-date-objects in the browser automatically, which only // have millisecond-precision. - // so we send a separate timestamp-as-string field too. it is provided by the - // loki-json-parser-code + // so we send a separate timestamp-as-string field too. + stringTimeField := makeStringTimeField(timeField) idField, err := makeIdField(stringTimeField, lineField, labelsField, frame.RefID) if err != nil { return err } - frame.Fields = append(frame.Fields, idField) + frame.Fields = append(frame.Fields, stringTimeField, idField) return nil } +func makeStringTimeField(timeField *data.Field) *data.Field { + length := timeField.Len() + stringTimestamps := make([]string, length) + + for i := 0; i < length; i++ { + nsNumber := timeField.At(i).(time.Time).UnixNano() + stringTimestamps[i] = fmt.Sprintf("%d", nsNumber) + } + return data.NewField("tsNs", timeField.Labels.Copy(), stringTimestamps) +} + func calculateCheckSum(time string, line string, labels []byte) (string, error) { input := []byte(line + "_") input = append(input, labels...) @@ -215,75 +211,3 @@ func getFrameLabels(frame *data.Frame) map[string]string { return labels } - -func parseStats(frameMetaCustom interface{}) []data.QueryStat { - customMap, ok := frameMetaCustom.(map[string]interface{}) - if !ok { - return nil - } - rawStats, ok := customMap["stats"].(map[string]interface{}) - if !ok { - return nil - } - - var stats []data.QueryStat - - summary, ok := rawStats["summary"].(map[string]interface{}) - if ok { - stats = append(stats, - makeStat("Summary: bytes processed per second", summary["bytesProcessedPerSecond"], "Bps"), - makeStat("Summary: lines processed per second", summary["linesProcessedPerSecond"], ""), - makeStat("Summary: total bytes processed", summary["totalBytesProcessed"], "decbytes"), - makeStat("Summary: total lines processed", summary["totalLinesProcessed"], ""), - makeStat("Summary: exec time", summary["execTime"], "s")) - } - - store, ok := rawStats["store"].(map[string]interface{}) - if ok { - stats = append(stats, - makeStat("Store: total chunks ref", store["totalChunksRef"], ""), - makeStat("Store: total chunks downloaded", store["totalChunksDownloaded"], ""), - makeStat("Store: chunks download time", store["chunksDownloadTime"], "s"), - makeStat("Store: head chunk bytes", store["headChunkBytes"], "decbytes"), - makeStat("Store: head chunk lines", store["headChunkLines"], ""), - makeStat("Store: decompressed bytes", store["decompressedBytes"], "decbytes"), - makeStat("Store: decompressed lines", store["decompressedLines"], ""), - makeStat("Store: compressed bytes", store["compressedBytes"], "decbytes"), - makeStat("Store: total duplicates", store["totalDuplicates"], "")) - } - - ingester, ok := rawStats["ingester"].(map[string]interface{}) - if ok { - stats = append(stats, - makeStat("Ingester: total reached", ingester["totalReached"], ""), - makeStat("Ingester: total chunks matched", ingester["totalChunksMatched"], ""), - makeStat("Ingester: total batches", ingester["totalBatches"], ""), - makeStat("Ingester: total lines sent", ingester["totalLinesSent"], ""), - makeStat("Ingester: head chunk bytes", ingester["headChunkBytes"], "decbytes"), - makeStat("Ingester: head chunk lines", ingester["headChunkLines"], ""), - makeStat("Ingester: decompressed bytes", ingester["decompressedBytes"], "decbytes"), - makeStat("Ingester: decompressed lines", ingester["decompressedLines"], ""), - makeStat("Ingester: compressed bytes", ingester["compressedBytes"], "decbytes"), - makeStat("Ingester: total duplicates", ingester["totalDuplicates"], "")) - } - - return stats -} - -func makeStat(name string, interfaceValue interface{}, unit string) data.QueryStat { - var value float64 - switch v := interfaceValue.(type) { - case float64: - value = v - case int: - value = float64(v) - } - - return data.QueryStat{ - FieldConfig: data.FieldConfig{ - DisplayName: name, - Unit: unit, - }, - Value: value, - } -} diff --git a/pkg/tsdb/loki/frame_test.go b/pkg/tsdb/loki/frame_test.go index 430db3b1330..f4da77bb71a 100644 --- a/pkg/tsdb/loki/frame_test.go +++ b/pkg/tsdb/loki/frame_test.go @@ -2,7 +2,6 @@ package loki import ( "encoding/json" - "strconv" "testing" "time" @@ -40,30 +39,20 @@ func TestFormatName(t *testing.T) { func TestAdjustFrame(t *testing.T) { t.Run("logs-frame metadata should be set correctly", func(t *testing.T) { - time1 := time.Date(2022, 1, 2, 3, 4, 5, 6, time.UTC) - time2 := time.Date(2022, 1, 2, 3, 5, 5, 6, time.UTC) - time3 := time.Date(2022, 1, 2, 3, 5, 5, 6, time.UTC) - time4 := time.Date(2022, 1, 2, 3, 6, 5, 6, time.UTC) - - timeNs1 := strconv.FormatInt(time1.UnixNano(), 10) - timeNs2 := strconv.FormatInt(time2.UnixNano(), 10) - timeNs3 := strconv.FormatInt(time3.UnixNano(), 10) - timeNs4 := strconv.FormatInt(time4.UnixNano(), 10) - frame := data.NewFrame("", - data.NewField("__labels", nil, []json.RawMessage{ + data.NewField("labels", nil, []json.RawMessage{ json.RawMessage(`{"level":"info"}`), json.RawMessage(`{"level":"error"}`), json.RawMessage(`{"level":"error"}`), json.RawMessage(`{"level":"info"}`), }), - data.NewField("Time", nil, []time.Time{ - time1, time2, time3, time4, - }), - data.NewField("Line", nil, []string{"line1", "line2", "line2", "line3"}), - data.NewField("TS", nil, []string{ - timeNs1, timeNs2, timeNs3, timeNs4, + data.NewField("time", nil, []time.Time{ + time.Date(2022, 1, 2, 3, 4, 5, 6, time.UTC), + time.Date(2022, 1, 2, 3, 5, 5, 6, time.UTC), + time.Date(2022, 1, 2, 3, 5, 5, 6, time.UTC), + time.Date(2022, 1, 2, 3, 6, 5, 6, time.UTC), }), + data.NewField("line", nil, []string{"line1", "line2", "line2", "line3"}), ) frame.RefID = "A" @@ -79,6 +68,14 @@ func TestAdjustFrame(t *testing.T) { fields := frame.Fields require.Equal(t, 5, len(fields)) + tsNsField := fields[3] + require.Equal(t, "tsNs", tsNsField.Name) + require.Equal(t, data.FieldTypeString, tsNsField.Type()) + require.Equal(t, 4, tsNsField.Len()) + require.Equal(t, "1641092645000000006", tsNsField.At(0)) + require.Equal(t, "1641092705000000006", tsNsField.At(1)) + require.Equal(t, "1641092705000000006", tsNsField.At(2)) + require.Equal(t, "1641092765000000006", tsNsField.At(3)) idField := fields[4] require.Equal(t, "id", idField.Name) @@ -139,85 +136,4 @@ func TestAdjustFrame(t *testing.T) { require.NotNil(t, timeFieldConfig) require.Equal(t, float64(42000), timeFieldConfig.Interval) }) - - t.Run("should parse response stats", func(t *testing.T) { - stats := map[string]interface{}{ - "summary": map[string]interface{}{ - "bytesProcessedPerSecond": 1, - "linesProcessedPerSecond": 2, - "totalBytesProcessed": 3, - "totalLinesProcessed": 4, - "execTime": 5.5, - }, - - "store": map[string]interface{}{ - "totalChunksRef": 6, - "totalChunksDownloaded": 7, - "chunksDownloadTime": 8.8, - "headChunkBytes": 9, - "headChunkLines": 10, - "decompressedBytes": 11, - "decompressedLines": 12, - "compressedBytes": 13, - "totalDuplicates": 14, - }, - - "ingester": map[string]interface{}{ - "totalReached": 15, - "totalChunksMatched": 16, - "totalBatches": 17, - "totalLinesSent": 18, - "headChunkBytes": 19, - "headChunkLines": 20, - "decompressedBytes": 21, - "decompressedLines": 22, - "compressedBytes": 23, - "totalDuplicates": 24, - }, - } - - meta := data.FrameMeta{ - Custom: map[string]interface{}{ - "stats": stats, - }, - } - - expected := []data.QueryStat{ - {FieldConfig: data.FieldConfig{DisplayName: "Summary: bytes processed per second", Unit: "Bps"}, Value: 1}, - {FieldConfig: data.FieldConfig{DisplayName: "Summary: lines processed per second", Unit: ""}, Value: 2}, - {FieldConfig: data.FieldConfig{DisplayName: "Summary: total bytes processed", Unit: "decbytes"}, Value: 3}, - {FieldConfig: data.FieldConfig{DisplayName: "Summary: total lines processed", Unit: ""}, Value: 4}, - {FieldConfig: data.FieldConfig{DisplayName: "Summary: exec time", Unit: "s"}, Value: 5.5}, - - {FieldConfig: data.FieldConfig{DisplayName: "Store: total chunks ref", Unit: ""}, Value: 6}, - {FieldConfig: data.FieldConfig{DisplayName: "Store: total chunks downloaded", Unit: ""}, Value: 7}, - {FieldConfig: data.FieldConfig{DisplayName: "Store: chunks download time", Unit: "s"}, Value: 8.8}, - {FieldConfig: data.FieldConfig{DisplayName: "Store: head chunk bytes", Unit: "decbytes"}, Value: 9}, - {FieldConfig: data.FieldConfig{DisplayName: "Store: head chunk lines", Unit: ""}, Value: 10}, - {FieldConfig: data.FieldConfig{DisplayName: "Store: decompressed bytes", Unit: "decbytes"}, Value: 11}, - {FieldConfig: data.FieldConfig{DisplayName: "Store: decompressed lines", Unit: ""}, Value: 12}, - {FieldConfig: data.FieldConfig{DisplayName: "Store: compressed bytes", Unit: "decbytes"}, Value: 13}, - {FieldConfig: data.FieldConfig{DisplayName: "Store: total duplicates", Unit: ""}, Value: 14}, - - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total reached", Unit: ""}, Value: 15}, - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total chunks matched", Unit: ""}, Value: 16}, - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total batches", Unit: ""}, Value: 17}, - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total lines sent", Unit: ""}, Value: 18}, - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: head chunk bytes", Unit: "decbytes"}, Value: 19}, - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: head chunk lines", Unit: ""}, Value: 20}, - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: decompressed bytes", Unit: "decbytes"}, Value: 21}, - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: decompressed lines", Unit: ""}, Value: 22}, - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: compressed bytes", Unit: "decbytes"}, Value: 23}, - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total duplicates", Unit: ""}, Value: 24}, - } - - result := parseStats(meta.Custom) - - // NOTE: i compare it item-by-item otherwise the test-fail-error-message is very hard to read - require.Len(t, result, len(expected)) - - for i := 0; i < len(result); i++ { - require.Equal(t, expected[i], result[i]) - } - }) } diff --git a/pkg/tsdb/loki/loki.go b/pkg/tsdb/loki/loki.go index e8b0b1f9484..4404d86d961 100644 --- a/pkg/tsdb/loki/loki.go +++ b/pkg/tsdb/loki/loki.go @@ -166,21 +166,12 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) // we extracted this part of the functionality to make it easy to unit-test it func runQuery(ctx context.Context, api *LokiAPI, query *lokiQuery) (data.Frames, error) { - frames, err := api.DataQuery(ctx, *query) + value, err := api.DataQuery(ctx, *query) if err != nil { return data.Frames{}, err } - for _, frame := range frames { - if err = adjustFrame(frame, query); err != nil { - return data.Frames{}, err - } - if err != nil { - return data.Frames{}, err - } - } - - return frames, nil + return parseResponse(value, query) } func (s *Service) getDSInfo(pluginCtx backend.PluginContext) (*datasourceInfo, error) { diff --git a/pkg/tsdb/loki/parse_response.go b/pkg/tsdb/loki/parse_response.go new file mode 100644 index 00000000000..4665709912e --- /dev/null +++ b/pkg/tsdb/loki/parse_response.go @@ -0,0 +1,206 @@ +package loki + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/loki/pkg/loghttp" + "github.com/grafana/loki/pkg/logqlmodel/stats" + jsoniter "github.com/json-iterator/go" +) + +func parseResponse(value *loghttp.QueryResponse, query *lokiQuery) (data.Frames, error) { + frames, err := lokiResponseToDataFrames(value, query) + + if err != nil { + return nil, err + } + + for _, frame := range frames { + err = adjustFrame(frame, query) + if err != nil { + return nil, err + } + } + + return frames, nil +} + +func lokiResponseToDataFrames(value *loghttp.QueryResponse, query *lokiQuery) (data.Frames, error) { + stats := parseStats(value.Data.Statistics) + switch res := value.Data.Result.(type) { + case loghttp.Matrix: + return lokiMatrixToDataFrames(res, query, stats), nil + case loghttp.Vector: + return lokiVectorToDataFrames(res, query, stats), nil + case loghttp.Streams: + return lokiStreamsToDataFrames(res, query, stats) + default: + return nil, fmt.Errorf("resultType %T not supported{", res) + } +} + +func lokiMatrixToDataFrames(matrix loghttp.Matrix, query *lokiQuery, stats []data.QueryStat) data.Frames { + frames := data.Frames{} + + for i, v := range matrix { + tags := make(map[string]string, len(v.Metric)) + timeVector := make([]time.Time, 0, len(v.Values)) + values := make([]float64, 0, len(v.Values)) + + for k, v := range v.Metric { + tags[string(k)] = string(v) + } + + for _, k := range v.Values { + timeVector = append(timeVector, k.Timestamp.Time().UTC()) + values = append(values, float64(k.Value)) + } + + timeField := data.NewField(data.TimeSeriesTimeFieldName, nil, timeVector) + valueField := data.NewField(data.TimeSeriesValueFieldName, tags, values) + + frame := data.NewFrame("", timeField, valueField) + frame.SetMeta(&data.FrameMeta{ + Type: data.FrameTypeTimeSeriesMany, + }) + + // only add the stats to the first dataframe + if i == 0 { + frame.Meta.Stats = stats + } + + frames = append(frames, frame) + } + + return frames +} + +func lokiVectorToDataFrames(vector loghttp.Vector, query *lokiQuery, stats []data.QueryStat) data.Frames { + frames := data.Frames{} + + for i, v := range vector { + tags := make(map[string]string, len(v.Metric)) + timeVector := []time.Time{v.Timestamp.Time().UTC()} + values := []float64{float64(v.Value)} + + for k, v := range v.Metric { + tags[string(k)] = string(v) + } + timeField := data.NewField(data.TimeSeriesTimeFieldName, nil, timeVector) + valueField := data.NewField(data.TimeSeriesValueFieldName, tags, values) + + frame := data.NewFrame("", timeField, valueField) + frame.SetMeta(&data.FrameMeta{ + Type: data.FrameTypeTimeSeriesMany, + }) + + // only add the stats to the first dataframe + if i == 0 { + frame.Meta.Stats = stats + } + + frames = append(frames, frame) + } + + return frames +} + +// we serialize the labels as an ordered list of pairs +func labelsToRawJson(labels data.Labels) (json.RawMessage, error) { + // data.Labels when converted to JSON keep the fields sorted + bytes, err := jsoniter.Marshal(labels) + if err != nil { + return nil, err + } + + return json.RawMessage(bytes), nil +} + +func lokiStreamsToDataFrames(streams loghttp.Streams, query *lokiQuery, stats []data.QueryStat) (data.Frames, error) { + var timeVector []time.Time + var values []string + var labelsVector []json.RawMessage + + for _, v := range streams { + labelsJson, err := labelsToRawJson(v.Labels.Map()) + if err != nil { + return nil, err + } + + for _, k := range v.Entries { + timeVector = append(timeVector, k.Timestamp.UTC()) + values = append(values, k.Line) + labelsVector = append(labelsVector, labelsJson) + } + } + + timeField := data.NewField(data.TimeSeriesTimeFieldName, nil, timeVector) + valueField := data.NewField("Line", nil, values) + labelsField := data.NewField("labels", nil, labelsVector) + + frame := data.NewFrame("", labelsField, timeField, valueField) + frame.SetMeta(&data.FrameMeta{ + Stats: stats, + }) + + return data.Frames{frame}, nil +} + +func parseStats(result stats.Result) []data.QueryStat { + data := []data.QueryStat{ + makeStat("Summary: bytes processed per second", float64(result.Summary.BytesProcessedPerSecond), "Bps"), + makeStat("Summary: lines processed per second", float64(result.Summary.LinesProcessedPerSecond), ""), + makeStat("Summary: total bytes processed", float64(result.Summary.TotalBytesProcessed), "decbytes"), + makeStat("Summary: total lines processed", float64(result.Summary.TotalLinesProcessed), ""), + makeStat("Summary: exec time", result.Summary.ExecTime, "s"), + makeStat("Store: total chunks ref", float64(result.Store.TotalChunksRef), ""), + makeStat("Store: total chunks downloaded", float64(result.Store.TotalChunksDownloaded), ""), + makeStat("Store: chunks download time", result.Store.ChunksDownloadTime, "s"), + makeStat("Store: head chunk bytes", float64(result.Store.HeadChunkBytes), "decbytes"), + makeStat("Store: head chunk lines", float64(result.Store.HeadChunkLines), ""), + makeStat("Store: decompressed bytes", float64(result.Store.DecompressedBytes), "decbytes"), + makeStat("Store: decompressed lines", float64(result.Store.DecompressedLines), ""), + makeStat("Store: compressed bytes", float64(result.Store.CompressedBytes), "decbytes"), + makeStat("Store: total duplicates", float64(result.Store.TotalDuplicates), ""), + makeStat("Ingester: total reached", float64(result.Ingester.TotalReached), ""), + makeStat("Ingester: total chunks matched", float64(result.Ingester.TotalChunksMatched), ""), + makeStat("Ingester: total batches", float64(result.Ingester.TotalBatches), ""), + makeStat("Ingester: total lines sent", float64(result.Ingester.TotalLinesSent), ""), + makeStat("Ingester: head chunk bytes", float64(result.Ingester.HeadChunkBytes), "decbytes"), + makeStat("Ingester: head chunk lines", float64(result.Ingester.HeadChunkLines), ""), + makeStat("Ingester: decompressed bytes", float64(result.Ingester.DecompressedBytes), "decbytes"), + makeStat("Ingester: decompressed lines", float64(result.Ingester.DecompressedLines), ""), + makeStat("Ingester: compressed bytes", float64(result.Ingester.CompressedBytes), "decbytes"), + makeStat("Ingester: total duplicates", float64(result.Ingester.TotalDuplicates), ""), + } + + // it is not possible to know whether the given statistics was missing, or + // it's value was zero. + // we do a heuristic here, if every stat-value is zero, we assume we got no stats-data + allStatsZero := true + for _, stat := range data { + if stat.Value > 0 { + allStatsZero = false + break + } + } + + if allStatsZero { + return nil + } + + return data +} + +func makeStat(name string, value float64, unit string) data.QueryStat { + return data.QueryStat{ + FieldConfig: data.FieldConfig{ + DisplayName: name, + Unit: unit, + }, + Value: value, + } +} diff --git a/pkg/tsdb/loki/parse_response_test.go b/pkg/tsdb/loki/parse_response_test.go new file mode 100644 index 00000000000..07166a45bbd --- /dev/null +++ b/pkg/tsdb/loki/parse_response_test.go @@ -0,0 +1,183 @@ +package loki + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/loki/pkg/loghttp" + "github.com/grafana/loki/pkg/logqlmodel/stats" + p "github.com/prometheus/common/model" + "github.com/stretchr/testify/require" +) + +func TestParseResponse(t *testing.T) { + t.Run("value is not of supported type", func(t *testing.T) { + value := loghttp.QueryResponse{ + Data: loghttp.QueryResponseData{ + Result: loghttp.Scalar{}, + }, + } + res, err := parseResponse(&value, nil) + require.Equal(t, len(res), 0) + require.Error(t, err) + }) + + t.Run("response should be parsed normally", func(t *testing.T) { + values := []p.SamplePair{ + {Value: 1, Timestamp: 1000}, + {Value: 2, Timestamp: 2000}, + {Value: 3, Timestamp: 3000}, + {Value: 4, Timestamp: 4000}, + {Value: 5, Timestamp: 5000}, + } + value := loghttp.QueryResponse{ + Data: loghttp.QueryResponseData{ + Result: loghttp.Matrix{ + p.SampleStream{ + Metric: p.Metric{"app": "Application", "tag2": "tag2"}, + Values: values, + }, + }, + }, + } + + query := &lokiQuery{ + Expr: "up(ALERTS)", + QueryType: QueryTypeRange, + LegendFormat: "legend {{app}}", + Step: time.Second * 42, + } + frame, err := parseResponse(&value, query) + require.NoError(t, err) + + labels, err := data.LabelsFromString("app=Application, tag2=tag2") + require.NoError(t, err) + field1 := data.NewField("Time", nil, []time.Time{ + time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC), + time.Date(1970, 1, 1, 0, 0, 2, 0, time.UTC), + time.Date(1970, 1, 1, 0, 0, 3, 0, time.UTC), + time.Date(1970, 1, 1, 0, 0, 4, 0, time.UTC), + time.Date(1970, 1, 1, 0, 0, 5, 0, time.UTC), + }) + field1.Config = &data.FieldConfig{Interval: float64(42000)} + field2 := data.NewField("Value", labels, []float64{1, 2, 3, 4, 5}) + field2.SetConfig(&data.FieldConfig{DisplayNameFromDS: "legend Application"}) + testFrame := data.NewFrame("legend Application", field1, field2) + testFrame.SetMeta(&data.FrameMeta{ + ExecutedQueryString: "Expr: up(ALERTS)\nStep: 42s", + Type: data.FrameTypeTimeSeriesMany, + }) + + if diff := cmp.Diff(testFrame, frame[0], data.FrameTestCompareOptions()...); diff != "" { + t.Errorf("Result mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("should set interval-attribute in response", func(t *testing.T) { + values := []p.SamplePair{ + {Value: 1, Timestamp: 1000}, + } + value := loghttp.QueryResponse{ + Data: loghttp.QueryResponseData{ + Result: loghttp.Matrix{ + p.SampleStream{ + Values: values, + }, + }, + }, + } + + query := &lokiQuery{ + Step: time.Second * 42, + QueryType: QueryTypeRange, + } + + frames, err := parseResponse(&value, query) + require.NoError(t, err) + + // to keep the test simple, we assume the + // first field is the time-field + timeField := frames[0].Fields[0] + require.NotNil(t, timeField) + require.Equal(t, data.FieldTypeTime, timeField.Type()) + + timeFieldConfig := timeField.Config + require.NotNil(t, timeFieldConfig) + require.Equal(t, float64(42000), timeFieldConfig.Interval) + }) + + t.Run("should parse response stats", func(t *testing.T) { + stats := stats.Result{ + Summary: stats.Summary{ + BytesProcessedPerSecond: 1, + LinesProcessedPerSecond: 2, + TotalBytesProcessed: 3, + TotalLinesProcessed: 4, + ExecTime: 5.5, + }, + Store: stats.Store{ + TotalChunksRef: 6, + TotalChunksDownloaded: 7, + ChunksDownloadTime: 8.8, + HeadChunkBytes: 9, + HeadChunkLines: 10, + DecompressedBytes: 11, + DecompressedLines: 12, + CompressedBytes: 13, + TotalDuplicates: 14, + }, + Ingester: stats.Ingester{ + TotalReached: 15, + TotalChunksMatched: 16, + TotalBatches: 17, + TotalLinesSent: 18, + HeadChunkBytes: 19, + HeadChunkLines: 20, + DecompressedBytes: 21, + DecompressedLines: 22, + CompressedBytes: 23, + TotalDuplicates: 24, + }, + } + + expected := []data.QueryStat{ + {FieldConfig: data.FieldConfig{DisplayName: "Summary: bytes processed per second", Unit: "Bps"}, Value: 1}, + {FieldConfig: data.FieldConfig{DisplayName: "Summary: lines processed per second", Unit: ""}, Value: 2}, + {FieldConfig: data.FieldConfig{DisplayName: "Summary: total bytes processed", Unit: "decbytes"}, Value: 3}, + {FieldConfig: data.FieldConfig{DisplayName: "Summary: total lines processed", Unit: ""}, Value: 4}, + {FieldConfig: data.FieldConfig{DisplayName: "Summary: exec time", Unit: "s"}, Value: 5.5}, + + {FieldConfig: data.FieldConfig{DisplayName: "Store: total chunks ref", Unit: ""}, Value: 6}, + {FieldConfig: data.FieldConfig{DisplayName: "Store: total chunks downloaded", Unit: ""}, Value: 7}, + {FieldConfig: data.FieldConfig{DisplayName: "Store: chunks download time", Unit: "s"}, Value: 8.8}, + {FieldConfig: data.FieldConfig{DisplayName: "Store: head chunk bytes", Unit: "decbytes"}, Value: 9}, + {FieldConfig: data.FieldConfig{DisplayName: "Store: head chunk lines", Unit: ""}, Value: 10}, + {FieldConfig: data.FieldConfig{DisplayName: "Store: decompressed bytes", Unit: "decbytes"}, Value: 11}, + {FieldConfig: data.FieldConfig{DisplayName: "Store: decompressed lines", Unit: ""}, Value: 12}, + {FieldConfig: data.FieldConfig{DisplayName: "Store: compressed bytes", Unit: "decbytes"}, Value: 13}, + {FieldConfig: data.FieldConfig{DisplayName: "Store: total duplicates", Unit: ""}, Value: 14}, + + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total reached", Unit: ""}, Value: 15}, + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total chunks matched", Unit: ""}, Value: 16}, + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total batches", Unit: ""}, Value: 17}, + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total lines sent", Unit: ""}, Value: 18}, + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: head chunk bytes", Unit: "decbytes"}, Value: 19}, + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: head chunk lines", Unit: ""}, Value: 20}, + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: decompressed bytes", Unit: "decbytes"}, Value: 21}, + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: decompressed lines", Unit: ""}, Value: 22}, + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: compressed bytes", Unit: "decbytes"}, Value: 23}, + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total duplicates", Unit: ""}, Value: 24}, + } + + result := parseStats((stats)) + + // NOTE: i compare it item-by-item otherwise the test-fail-error-message is very hard to read + require.Len(t, result, len(expected)) + + for i := 0; i < len(result); i++ { + require.Equal(t, expected[i], result[i]) + } + }) +} diff --git a/pkg/util/converter/prom.go b/pkg/util/converter/prom.go index dcde552106c..43a7ff3e964 100644 --- a/pkg/util/converter/prom.go +++ b/pkg/util/converter/prom.go @@ -1,7 +1,6 @@ package converter import ( - "encoding/json" "fmt" "strconv" "time" @@ -350,7 +349,6 @@ func readMatrixOrVector(iter *jsoniter.Iterator) *backend.DataResponse { timeField := data.NewFieldFromFieldType(data.FieldTypeTime, 0) timeField.Name = data.TimeSeriesTimeFieldName valueField := data.NewFieldFromFieldType(data.FieldTypeFloat64, 0) - valueField.Name = data.TimeSeriesValueFieldName valueField.Labels = data.Labels{} for l1Field := iter.ReadObject(); l1Field != ""; l1Field = iter.ReadObject() { @@ -377,6 +375,14 @@ func readMatrixOrVector(iter *jsoniter.Iterator) *backend.DataResponse { } } + name, ok := valueField.Labels["__name__"] + if ok { + valueField.Name = name + delete(valueField.Labels, "__name__") + } else { + valueField.Name = data.TimeSeriesValueFieldName + } + frame := data.NewFrame("", timeField, valueField) frame.Meta = &data.FrameMeta{ Type: data.FrameTypeTimeSeriesMany, @@ -402,7 +408,7 @@ func readTimeValuePair(iter *jsoniter.Iterator) (time.Time, float64, error) { func readStream(iter *jsoniter.Iterator) *backend.DataResponse { rsp := &backend.DataResponse{} - labelsField := data.NewFieldFromFieldType(data.FieldTypeJSON, 0) + labelsField := data.NewFieldFromFieldType(data.FieldTypeString, 0) labelsField.Name = "__labels" // avoid automatically spreading this by labels timeField := data.NewFieldFromFieldType(data.FieldTypeTime, 0) @@ -416,20 +422,14 @@ func readStream(iter *jsoniter.Iterator) *backend.DataResponse { tsField.Name = "TS" labels := data.Labels{} - labelJson, err := labelsToRawJson(labels) - if err != nil { - return &backend.DataResponse{Error: err} - } + labelString := labels.String() for iter.ReadArray() { for l1Field := iter.ReadObject(); l1Field != ""; l1Field = iter.ReadObject() { switch l1Field { case "stream": iter.ReadVal(&labels) - labelJson, err = labelsToRawJson(labels) - if err != nil { - return &backend.DataResponse{Error: err} - } + labelString = labels.String() case "values": for iter.ReadArray() { @@ -441,7 +441,7 @@ func readStream(iter *jsoniter.Iterator) *backend.DataResponse { t := timeFromLokiString(ts) - labelsField.Append(labelJson) + labelsField.Append(labelString) timeField.Append(t) lineField.Append(line) tsField.Append(ts) @@ -477,13 +477,3 @@ func timeFromLokiString(str string) time.Time { ns, _ := strconv.ParseInt(str[10:], 10, 64) return time.Unix(ss, ns).UTC() } - -func labelsToRawJson(labels data.Labels) (json.RawMessage, error) { - // data.Labels when converted to JSON keep the fields sorted - bytes, err := jsoniter.Marshal(labels) - if err != nil { - return nil, err - } - - return json.RawMessage(bytes), nil -} diff --git a/pkg/util/converter/testdata/loki-streams-a-frame.json b/pkg/util/converter/testdata/loki-streams-a-frame.json index 622d46826a1..bd5df94989e 100644 --- a/pkg/util/converter/testdata/loki-streams-a-frame.json +++ b/pkg/util/converter/testdata/loki-streams-a-frame.json @@ -5,28 +5,17 @@ "meta": { "custom": { "stats": { - "store": { - "headChunkBytes": 0, - "headChunkLines": 0, - "compressedBytes": 31432, - "decompressedBytes": 7772, - "decompressedLines": 55, - "totalDuplicates": 0, - "totalChunksRef": 2, - "totalChunksDownloaded": 2, - "chunksDownloadTime": 0.000390958 - }, "ingester": { - "totalReached": 0, - "headChunkBytes": 0, - "totalDuplicates": 0, - "headChunkLines": 0, - "decompressedBytes": 0, - "decompressedLines": 0, - "compressedBytes": 0, "totalChunksMatched": 0, "totalBatches": 0, - "totalLinesSent": 0 + "totalLinesSent": 0, + "headChunkBytes": 0, + "headChunkLines": 0, + "decompressedLines": 0, + "compressedBytes": 0, + "totalReached": 0, + "totalDuplicates": 0, + "decompressedBytes": 0 }, "summary": { "bytesProcessedPerSecond": 3507022, @@ -34,6 +23,17 @@ "totalBytesProcessed": 7772, "totalLinesProcessed": 55, "execTime": 0.002216125 + }, + "store": { + "totalChunksDownloaded": 2, + "headChunkBytes": 0, + "decompressedLines": 55, + "totalDuplicates": 0, + "totalChunksRef": 2, + "headChunkLines": 0, + "decompressedBytes": 7772, + "compressedBytes": 31432, + "chunksDownloadTime": 0.000390958 } } } @@ -41,9 +41,9 @@ "fields": [ { "name": "__labels", - "type": "other", + "type": "string", "typeInfo": { - "frame": "json.RawMessage" + "frame": "string" } }, { @@ -72,7 +72,7 @@ "data": { "values": [ [ - {"level":"error","location":"moon🌙"},{"level":"info","location":"moon🌙"},{"level":"info","location":"moon🌙"},{"level":"info","location":"moon🌙"},{"level":"info","location":"moon🌙"},{"level":"info","location":"moon🌙"} + "level=error, location=moon🌙","level=info, location=moon🌙","level=info, location=moon🌙","level=info, location=moon🌙","level=info, location=moon🌙","level=info, location=moon🌙" ], [ 1645030244810,1645030247027,1645030246277,1645030246277,1645030245539,1645030244091 diff --git a/pkg/util/converter/testdata/loki-streams-a-golden.txt b/pkg/util/converter/testdata/loki-streams-a-golden.txt index 9d1fdaa4caf..727f13947ae 100644 --- a/pkg/util/converter/testdata/loki-streams-a-golden.txt +++ b/pkg/util/converter/testdata/loki-streams-a-golden.txt @@ -38,19 +38,19 @@ Frame[0] { } Name: Dimensions: 4 Fields by 6 Rows -+---------------------------------------+-----------------------------------------+------------------+---------------------+ -| Name: __labels | Name: Time | Name: Line | Name: TS | -| Labels: | Labels: | Labels: | Labels: | -| Type: []json.RawMessage | Type: []time.Time | Type: []string | Type: []string | -+---------------------------------------+-----------------------------------------+------------------+---------------------+ -| {"level":"error","location":"moon🌙"} | 2022-02-16 16:50:44.81075712 +0000 UTC | log line error 1 | 1645030244810757120 | -| {"level":"info","location":"moon🌙"} | 2022-02-16 16:50:47.02773504 +0000 UTC | log line info 1 | 1645030247027735040 | -| {"level":"info","location":"moon🌙"} | 2022-02-16 16:50:46.277587968 +0000 UTC | log line info 2 | 1645030246277587968 | -| {"level":"info","location":"moon🌙"} | 2022-02-16 16:50:46.277587968 +0000 UTC | log line info 2 | 1645030246277587968 | -| {"level":"info","location":"moon🌙"} | 2022-02-16 16:50:45.539423744 +0000 UTC | log line info 3 | 1645030245539423744 | -| {"level":"info","location":"moon🌙"} | 2022-02-16 16:50:44.091700992 +0000 UTC | log line info 4 | 1645030244091700992 | -+---------------------------------------+-----------------------------------------+------------------+---------------------+ ++------------------------------+-----------------------------------------+------------------+---------------------+ +| Name: __labels | Name: Time | Name: Line | Name: TS | +| Labels: | Labels: | Labels: | Labels: | +| Type: []string | Type: []time.Time | Type: []string | Type: []string | ++------------------------------+-----------------------------------------+------------------+---------------------+ +| level=error, location=moon🌙 | 2022-02-16 16:50:44.81075712 +0000 UTC | log line error 1 | 1645030244810757120 | +| level=info, location=moon🌙 | 2022-02-16 16:50:47.02773504 +0000 UTC | log line info 1 | 1645030247027735040 | +| level=info, location=moon🌙 | 2022-02-16 16:50:46.277587968 +0000 UTC | log line info 2 | 1645030246277587968 | +| level=info, location=moon🌙 | 2022-02-16 16:50:46.277587968 +0000 UTC | log line info 2 | 1645030246277587968 | +| level=info, location=moon🌙 | 2022-02-16 16:50:45.539423744 +0000 UTC | log line info 3 | 1645030245539423744 | +| level=info, location=moon🌙 | 2022-02-16 16:50:44.091700992 +0000 UTC | log line info 4 | 1645030244091700992 | ++------------------------------+-----------------------------------------+------------------+---------------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=QVJST1cxAAD/////oAQAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAANACAAADAAAATAAAACgAAAAEAAAA9Pv//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAAAU/P//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAADT8//8IAAAAaAIAAFwCAAB7ImN1c3RvbSI6eyJzdGF0cyI6eyJpbmdlc3RlciI6eyJjb21wcmVzc2VkQnl0ZXMiOjAsImRlY29tcHJlc3NlZEJ5dGVzIjowLCJkZWNvbXByZXNzZWRMaW5lcyI6MCwiaGVhZENodW5rQnl0ZXMiOjAsImhlYWRDaHVua0xpbmVzIjowLCJ0b3RhbEJhdGNoZXMiOjAsInRvdGFsQ2h1bmtzTWF0Y2hlZCI6MCwidG90YWxEdXBsaWNhdGVzIjowLCJ0b3RhbExpbmVzU2VudCI6MCwidG90YWxSZWFjaGVkIjowfSwic3RvcmUiOnsiY2h1bmtzRG93bmxvYWRUaW1lIjowLjAwMDM5MDk1OCwiY29tcHJlc3NlZEJ5dGVzIjozMTQzMiwiZGVjb21wcmVzc2VkQnl0ZXMiOjc3NzIsImRlY29tcHJlc3NlZExpbmVzIjo1NSwiaGVhZENodW5rQnl0ZXMiOjAsImhlYWRDaHVua0xpbmVzIjowLCJ0b3RhbENodW5rc0Rvd25sb2FkZWQiOjIsInRvdGFsQ2h1bmtzUmVmIjoyLCJ0b3RhbER1cGxpY2F0ZXMiOjB9LCJzdW1tYXJ5Ijp7ImJ5dGVzUHJvY2Vzc2VkUGVyU2Vjb25kIjozNTA3MDIyLCJleGVjVGltZSI6MC4wMDIyMTYxMjUsImxpbmVzUHJvY2Vzc2VkUGVyU2Vjb25kIjoyNDgxOCwidG90YWxCeXRlc1Byb2Nlc3NlZCI6Nzc3MiwidG90YWxMaW5lc1Byb2Nlc3NlZCI6NTV9fX19AAAAAAQAAABtZXRhAAAAAAQAAAAsAQAAtAAAAFgAAAAEAAAA9v7//xQAAAA4AAAAOAAAAAAAAAU0AAAAAQAAAAQAAADk/v//CAAAAAwAAAACAAAAVFMAAAQAAABuYW1lAAAAAAAAAADU/v//AgAAAFRTAABG////FAAAADwAAAA8AAAAAAAABTgAAAABAAAABAAAADT///8IAAAAEAAAAAQAAABMaW5lAAAAAAQAAABuYW1lAAAAAAAAAAAo////BAAAAExpbmUAAAAAnv///xQAAAA8AAAARAAAAAAAAApEAAAAAQAAAAQAAACM////CAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEgAAABMAAAAAAAABUgAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAUAAAACAAAAF9fbGFiZWxzAAAAAAQAAABuYW1lAAAAAAAAAAAEAAQABAAAAAgAAABfX2xhYmVscwAAAAAAAAAA/////0gBAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAYAgAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAADIAAAABgAAAAAAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAAAAAIAAAAAAAAACvAAAAAAAAANAAAAAAAAAAAAAAAAAAAADQAAAAAAAAADAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAHAAAAAAAAAAgAQAAAAAAAFsAAAAAAAAAgAEAAAAAAAAAAAAAAAAAAIABAAAAAAAAHAAAAAAAAACgAQAAAAAAAHIAAAAAAAAAAAAAAAQAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAA7AAAAWAAAAHUAAACSAAAArwAAAAAAAABsZXZlbD1lcnJvciwgbG9jYXRpb249bW9vbvCfjJlsZXZlbD1pbmZvLCBsb2NhdGlvbj1tb29u8J+MmWxldmVsPWluZm8sIGxvY2F0aW9uPW1vb27wn4yZbGV2ZWw9aW5mbywgbG9jYXRpb249bW9vbvCfjJlsZXZlbD1pbmZvLCBsb2NhdGlvbj1tb29u8J+MmWxldmVsPWluZm8sIGxvY2F0aW9uPW1vb27wn4yZAAAUuLpKUtQWAHrcPktS1BYAJCYSS1LUFgAkJhJLUtQWAKYm5kpS1BYAJ9yPSlLUFgAAAAAQAAAAHwAAAC4AAAA9AAAATAAAAFsAAAAAAAAAbG9nIGxpbmUgZXJyb3IgMWxvZyBsaW5lIGluZm8gMWxvZyBsaW5lIGluZm8gMmxvZyBsaW5lIGluZm8gMmxvZyBsaW5lIGluZm8gM2xvZyBsaW5lIGluZm8gNAAAAAAAAAAAABMAAAAmAAAAOQAAAEwAAABfAAAAcgAAAAAAAAAxNjQ1MDMwMjQ0ODEwNzU3MTIwMTY0NTAzMDI0NzAyNzczNTA0MDE2NDUwMzAyNDYyNzc1ODc5NjgxNjQ1MDMwMjQ2Mjc3NTg3OTY4MTY0NTAzMDI0NTUzOTQyMzc0NDE2NDUwMzAyNDQwOTE3MDA5OTIAAAAAAAAQAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAABAABAAAAsAQAAAAAAABQAQAAAAAAABgCAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAANACAAADAAAATAAAACgAAAAEAAAA9Pv//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAAAU/P//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAADT8//8IAAAAaAIAAFwCAAB7ImN1c3RvbSI6eyJzdGF0cyI6eyJpbmdlc3RlciI6eyJjb21wcmVzc2VkQnl0ZXMiOjAsImRlY29tcHJlc3NlZEJ5dGVzIjowLCJkZWNvbXByZXNzZWRMaW5lcyI6MCwiaGVhZENodW5rQnl0ZXMiOjAsImhlYWRDaHVua0xpbmVzIjowLCJ0b3RhbEJhdGNoZXMiOjAsInRvdGFsQ2h1bmtzTWF0Y2hlZCI6MCwidG90YWxEdXBsaWNhdGVzIjowLCJ0b3RhbExpbmVzU2VudCI6MCwidG90YWxSZWFjaGVkIjowfSwic3RvcmUiOnsiY2h1bmtzRG93bmxvYWRUaW1lIjowLjAwMDM5MDk1OCwiY29tcHJlc3NlZEJ5dGVzIjozMTQzMiwiZGVjb21wcmVzc2VkQnl0ZXMiOjc3NzIsImRlY29tcHJlc3NlZExpbmVzIjo1NSwiaGVhZENodW5rQnl0ZXMiOjAsImhlYWRDaHVua0xpbmVzIjowLCJ0b3RhbENodW5rc0Rvd25sb2FkZWQiOjIsInRvdGFsQ2h1bmtzUmVmIjoyLCJ0b3RhbER1cGxpY2F0ZXMiOjB9LCJzdW1tYXJ5Ijp7ImJ5dGVzUHJvY2Vzc2VkUGVyU2Vjb25kIjozNTA3MDIyLCJleGVjVGltZSI6MC4wMDIyMTYxMjUsImxpbmVzUHJvY2Vzc2VkUGVyU2Vjb25kIjoyNDgxOCwidG90YWxCeXRlc1Byb2Nlc3NlZCI6Nzc3MiwidG90YWxMaW5lc1Byb2Nlc3NlZCI6NTV9fX19AAAAAAQAAABtZXRhAAAAAAQAAAAsAQAAtAAAAFgAAAAEAAAA9v7//xQAAAA4AAAAOAAAAAAAAAU0AAAAAQAAAAQAAADk/v//CAAAAAwAAAACAAAAVFMAAAQAAABuYW1lAAAAAAAAAADU/v//AgAAAFRTAABG////FAAAADwAAAA8AAAAAAAABTgAAAABAAAABAAAADT///8IAAAAEAAAAAQAAABMaW5lAAAAAAQAAABuYW1lAAAAAAAAAAAo////BAAAAExpbmUAAAAAnv///xQAAAA8AAAARAAAAAAAAApEAAAAAQAAAAQAAACM////CAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEgAAABMAAAAAAAABUgAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAUAAAACAAAAF9fbGFiZWxzAAAAAAQAAABuYW1lAAAAAAAAAAAEAAQABAAAAAgAAABfX2xhYmVscwAAAADIBAAAQVJST1cx diff --git a/pkg/util/converter/testdata/loki-streams-b-frame.json b/pkg/util/converter/testdata/loki-streams-b-frame.json index 7c8c7dd3610..28ef33d2768 100644 --- a/pkg/util/converter/testdata/loki-streams-b-frame.json +++ b/pkg/util/converter/testdata/loki-streams-b-frame.json @@ -6,34 +6,34 @@ "custom": { "stats": { "summary": { + "execTime": 0.002216125, "bytesProcessedPerSecond": 3507022, "linesProcessedPerSecond": 24818, "totalBytesProcessed": 7772, - "totalLinesProcessed": 55, - "execTime": 0.002216125 + "totalLinesProcessed": 55 }, "store": { - "totalChunksRef": 2, - "chunksDownloadTime": 0.000390958, - "headChunkLines": 0, + "headChunkBytes": 0, "decompressedLines": 55, + "compressedBytes": 31432, "totalDuplicates": 0, "totalChunksDownloaded": 2, - "headChunkBytes": 0, + "chunksDownloadTime": 0.000390958, + "headChunkLines": 0, "decompressedBytes": 7772, - "compressedBytes": 31432 + "totalChunksRef": 2 }, "ingester": { - "totalReached": 0, - "totalChunksMatched": 0, - "totalLinesSent": 0, "headChunkBytes": 0, - "decompressedLines": 0, - "totalBatches": 0, - "headChunkLines": 0, "decompressedBytes": 0, + "totalBatches": 0, + "totalLinesSent": 0, + "headChunkLines": 0, + "decompressedLines": 0, "compressedBytes": 0, - "totalDuplicates": 0 + "totalDuplicates": 0, + "totalReached": 0, + "totalChunksMatched": 0 } } } @@ -41,9 +41,9 @@ "fields": [ { "name": "__labels", - "type": "other", + "type": "string", "typeInfo": { - "frame": "json.RawMessage" + "frame": "string" } }, { @@ -72,7 +72,7 @@ "data": { "values": [ [ - {"level":"error","location":"moon"},{"level":"info","location":"moon"},{"level":"info","location":"moon"},{"level":"info","location":"moon"},{"level":"info","location":"moon"} + "level=error, location=moon","level=info, location=moon","level=info, location=moon","level=info, location=moon","level=info, location=moon" ], [ 1645030244810,1645030247027,1645030246277,1645030245539,1645030244091 diff --git a/pkg/util/converter/testdata/loki-streams-b-golden.txt b/pkg/util/converter/testdata/loki-streams-b-golden.txt index d8b09e5d00a..c5291a2e1a5 100644 --- a/pkg/util/converter/testdata/loki-streams-b-golden.txt +++ b/pkg/util/converter/testdata/loki-streams-b-golden.txt @@ -38,18 +38,18 @@ Frame[0] { } Name: Dimensions: 4 Fields by 5 Rows -+-------------------------------------+-----------------------------------------+------------------+---------------------+ -| Name: __labels | Name: Time | Name: Line | Name: TS | -| Labels: | Labels: | Labels: | Labels: | -| Type: []json.RawMessage | Type: []time.Time | Type: []string | Type: []string | -+-------------------------------------+-----------------------------------------+------------------+---------------------+ -| {"level":"error","location":"moon"} | 2022-02-16 16:50:44.81075712 +0000 UTC | log line error 1 | 1645030244810757120 | -| {"level":"info","location":"moon"} | 2022-02-16 16:50:47.02773504 +0000 UTC | log line info 1 | 1645030247027735040 | -| {"level":"info","location":"moon"} | 2022-02-16 16:50:46.277587968 +0000 UTC | log line info 2 | 1645030246277587968 | -| {"level":"info","location":"moon"} | 2022-02-16 16:50:45.539423744 +0000 UTC | log line info 3 | 1645030245539423744 | -| {"level":"info","location":"moon"} | 2022-02-16 16:50:44.091700992 +0000 UTC | log line info 4 | 1645030244091700992 | -+-------------------------------------+-----------------------------------------+------------------+---------------------+ ++----------------------------+-----------------------------------------+------------------+---------------------+ +| Name: __labels | Name: Time | Name: Line | Name: TS | +| Labels: | Labels: | Labels: | Labels: | +| Type: []string | Type: []time.Time | Type: []string | Type: []string | ++----------------------------+-----------------------------------------+------------------+---------------------+ +| level=error, location=moon | 2022-02-16 16:50:44.81075712 +0000 UTC | log line error 1 | 1645030244810757120 | +| level=info, location=moon | 2022-02-16 16:50:47.02773504 +0000 UTC | log line info 1 | 1645030247027735040 | +| level=info, location=moon | 2022-02-16 16:50:46.277587968 +0000 UTC | log line info 2 | 1645030246277587968 | +| level=info, location=moon | 2022-02-16 16:50:45.539423744 +0000 UTC | log line info 3 | 1645030245539423744 | +| level=info, location=moon | 2022-02-16 16:50:44.091700992 +0000 UTC | log line info 4 | 1645030244091700992 | ++----------------------------+-----------------------------------------+------------------+---------------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=QVJST1cxAAD/////oAQAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAANACAAADAAAATAAAACgAAAAEAAAA9Pv//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAAAU/P//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAADT8//8IAAAAaAIAAFwCAAB7ImN1c3RvbSI6eyJzdGF0cyI6eyJpbmdlc3RlciI6eyJjb21wcmVzc2VkQnl0ZXMiOjAsImRlY29tcHJlc3NlZEJ5dGVzIjowLCJkZWNvbXByZXNzZWRMaW5lcyI6MCwiaGVhZENodW5rQnl0ZXMiOjAsImhlYWRDaHVua0xpbmVzIjowLCJ0b3RhbEJhdGNoZXMiOjAsInRvdGFsQ2h1bmtzTWF0Y2hlZCI6MCwidG90YWxEdXBsaWNhdGVzIjowLCJ0b3RhbExpbmVzU2VudCI6MCwidG90YWxSZWFjaGVkIjowfSwic3RvcmUiOnsiY2h1bmtzRG93bmxvYWRUaW1lIjowLjAwMDM5MDk1OCwiY29tcHJlc3NlZEJ5dGVzIjozMTQzMiwiZGVjb21wcmVzc2VkQnl0ZXMiOjc3NzIsImRlY29tcHJlc3NlZExpbmVzIjo1NSwiaGVhZENodW5rQnl0ZXMiOjAsImhlYWRDaHVua0xpbmVzIjowLCJ0b3RhbENodW5rc0Rvd25sb2FkZWQiOjIsInRvdGFsQ2h1bmtzUmVmIjoyLCJ0b3RhbER1cGxpY2F0ZXMiOjB9LCJzdW1tYXJ5Ijp7ImJ5dGVzUHJvY2Vzc2VkUGVyU2Vjb25kIjozNTA3MDIyLCJleGVjVGltZSI6MC4wMDIyMTYxMjUsImxpbmVzUHJvY2Vzc2VkUGVyU2Vjb25kIjoyNDgxOCwidG90YWxCeXRlc1Byb2Nlc3NlZCI6Nzc3MiwidG90YWxMaW5lc1Byb2Nlc3NlZCI6NTV9fX19AAAAAAQAAABtZXRhAAAAAAQAAAAsAQAAtAAAAFgAAAAEAAAA9v7//xQAAAA4AAAAOAAAAAAAAAU0AAAAAQAAAAQAAADk/v//CAAAAAwAAAACAAAAVFMAAAQAAABuYW1lAAAAAAAAAADU/v//AgAAAFRTAABG////FAAAADwAAAA8AAAAAAAABTgAAAABAAAABAAAADT///8IAAAAEAAAAAQAAABMaW5lAAAAAAQAAABuYW1lAAAAAAAAAAAo////BAAAAExpbmUAAAAAnv///xQAAAA8AAAARAAAAAAAAApEAAAAAQAAAAQAAACM////CAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEgAAABMAAAAAAAABEgAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAUAAAACAAAAF9fbGFiZWxzAAAAAAQAAABuYW1lAAAAAAAAAAAEAAQABAAAAAgAAABfX2xhYmVscwAAAAAAAAAA/////0gBAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAADQAQAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAADIAAAABQAAAAAAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAGAAAAAAAAACrAAAAAAAAAMgAAAAAAAAAAAAAAAAAAADIAAAAAAAAACgAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAGAAAAAAAAAAIAQAAAAAAAEwAAAAAAAAAWAEAAAAAAAAAAAAAAAAAAFgBAAAAAAAAGAAAAAAAAABwAQAAAAAAAF8AAAAAAAAAAAAAAAQAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAACMAAABFAAAAZwAAAIkAAACrAAAAeyJsZXZlbCI6ImVycm9yIiwibG9jYXRpb24iOiJtb29uIn17ImxldmVsIjoiaW5mbyIsImxvY2F0aW9uIjoibW9vbiJ9eyJsZXZlbCI6ImluZm8iLCJsb2NhdGlvbiI6Im1vb24ifXsibGV2ZWwiOiJpbmZvIiwibG9jYXRpb24iOiJtb29uIn17ImxldmVsIjoiaW5mbyIsImxvY2F0aW9uIjoibW9vbiJ9AAAAAAAAFLi6SlLUFgB63D5LUtQWACQmEktS1BYApibmSlLUFgAn3I9KUtQWAAAAABAAAAAfAAAALgAAAD0AAABMAAAAbG9nIGxpbmUgZXJyb3IgMWxvZyBsaW5lIGluZm8gMWxvZyBsaW5lIGluZm8gMmxvZyBsaW5lIGluZm8gM2xvZyBsaW5lIGluZm8gNAAAAAAAAAAAEwAAACYAAAA5AAAATAAAAF8AAAAxNjQ1MDMwMjQ0ODEwNzU3MTIwMTY0NTAzMDI0NzAyNzczNTA0MDE2NDUwMzAyNDYyNzc1ODc5NjgxNjQ1MDMwMjQ1NTM5NDIzNzQ0MTY0NTAzMDI0NDA5MTcwMDk5MgAQAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAABAABAAAAsAQAAAAAAABQAQAAAAAAANABAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAANACAAADAAAATAAAACgAAAAEAAAA9Pv//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAAAU/P//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAADT8//8IAAAAaAIAAFwCAAB7ImN1c3RvbSI6eyJzdGF0cyI6eyJpbmdlc3RlciI6eyJjb21wcmVzc2VkQnl0ZXMiOjAsImRlY29tcHJlc3NlZEJ5dGVzIjowLCJkZWNvbXByZXNzZWRMaW5lcyI6MCwiaGVhZENodW5rQnl0ZXMiOjAsImhlYWRDaHVua0xpbmVzIjowLCJ0b3RhbEJhdGNoZXMiOjAsInRvdGFsQ2h1bmtzTWF0Y2hlZCI6MCwidG90YWxEdXBsaWNhdGVzIjowLCJ0b3RhbExpbmVzU2VudCI6MCwidG90YWxSZWFjaGVkIjowfSwic3RvcmUiOnsiY2h1bmtzRG93bmxvYWRUaW1lIjowLjAwMDM5MDk1OCwiY29tcHJlc3NlZEJ5dGVzIjozMTQzMiwiZGVjb21wcmVzc2VkQnl0ZXMiOjc3NzIsImRlY29tcHJlc3NlZExpbmVzIjo1NSwiaGVhZENodW5rQnl0ZXMiOjAsImhlYWRDaHVua0xpbmVzIjowLCJ0b3RhbENodW5rc0Rvd25sb2FkZWQiOjIsInRvdGFsQ2h1bmtzUmVmIjoyLCJ0b3RhbER1cGxpY2F0ZXMiOjB9LCJzdW1tYXJ5Ijp7ImJ5dGVzUHJvY2Vzc2VkUGVyU2Vjb25kIjozNTA3MDIyLCJleGVjVGltZSI6MC4wMDIyMTYxMjUsImxpbmVzUHJvY2Vzc2VkUGVyU2Vjb25kIjoyNDgxOCwidG90YWxCeXRlc1Byb2Nlc3NlZCI6Nzc3MiwidG90YWxMaW5lc1Byb2Nlc3NlZCI6NTV9fX19AAAAAAQAAABtZXRhAAAAAAQAAAAsAQAAtAAAAFgAAAAEAAAA9v7//xQAAAA4AAAAOAAAAAAAAAU0AAAAAQAAAAQAAADk/v//CAAAAAwAAAACAAAAVFMAAAQAAABuYW1lAAAAAAAAAADU/v//AgAAAFRTAABG////FAAAADwAAAA8AAAAAAAABTgAAAABAAAABAAAADT///8IAAAAEAAAAAQAAABMaW5lAAAAAAQAAABuYW1lAAAAAAAAAAAo////BAAAAExpbmUAAAAAnv///xQAAAA8AAAARAAAAAAAAApEAAAAAQAAAAQAAACM////CAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEgAAABMAAAAAAAABEgAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAUAAAACAAAAF9fbGFiZWxzAAAAAAQAAABuYW1lAAAAAAAAAAAEAAQABAAAAAgAAABfX2xhYmVscwAAAADIBAAAQVJST1cx +FRAME=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 diff --git a/pkg/util/converter/testdata/prom-matrix-frame.json b/pkg/util/converter/testdata/prom-matrix-frame.json index 11484d37e8a..af91613cb20 100644 --- a/pkg/util/converter/testdata/prom-matrix-frame.json +++ b/pkg/util/converter/testdata/prom-matrix-frame.json @@ -14,15 +14,14 @@ } }, { - "name": "Value", + "name": "up", "type": "number", "typeInfo": { "frame": "float64" }, "labels": { "job": "prometheus", - "instance": "localhost:9090", - "__name__": "up" + "instance": "localhost:9090" } } ] @@ -52,13 +51,12 @@ } }, { - "name": "Value", + "name": "up", "type": "number", "typeInfo": { "frame": "float64" }, "labels": { - "__name__": "up", "job": "node", "instance": "localhost:9091" } diff --git a/pkg/util/converter/testdata/prom-matrix-golden.txt b/pkg/util/converter/testdata/prom-matrix-golden.txt index c9cf2dc5e93..83088450f9d 100644 --- a/pkg/util/converter/testdata/prom-matrix-golden.txt +++ b/pkg/util/converter/testdata/prom-matrix-golden.txt @@ -5,15 +5,15 @@ Frame[0] { } Name: Dimensions: 2 Fields by 3 Rows -+-----------------------------------+--------------------------------------------------------------+ -| Name: Time | Name: Value | -| Labels: | Labels: __name__=up, instance=localhost:9090, job=prometheus | -| Type: []time.Time | Type: []float64 | -+-----------------------------------+--------------------------------------------------------------+ -| 2015-07-01 20:10:30.781 +0000 UTC | 1 | -| 2015-07-01 20:10:45.781 +0000 UTC | 1 | -| 2015-07-01 20:11:00.781 +0000 UTC | 1 | -+-----------------------------------+--------------------------------------------------------------+ ++-----------------------------------+-------------------------------------------------+ +| Name: Time | Name: up | +| Labels: | Labels: instance=localhost:9090, job=prometheus | +| Type: []time.Time | Type: []float64 | ++-----------------------------------+-------------------------------------------------+ +| 2015-07-01 20:10:30.781 +0000 UTC | 1 | +| 2015-07-01 20:10:45.781 +0000 UTC | 1 | +| 2015-07-01 20:11:00.781 +0000 UTC | 1 | ++-----------------------------------+-------------------------------------------------+ @@ -22,17 +22,17 @@ Frame[1] { } Name: Dimensions: 2 Fields by 3 Rows -+-----------------------------------+--------------------------------------------------------+ -| Name: Time | Name: Value | -| Labels: | Labels: __name__=up, instance=localhost:9091, job=node | -| Type: []time.Time | Type: []float64 | -+-----------------------------------+--------------------------------------------------------+ -| 2015-07-01 20:10:30.781 +0000 UTC | 0 | -| 2015-07-01 20:10:45.781 +0000 UTC | 0 | -| 2015-07-01 20:11:00.781 +0000 UTC | 1 | -+-----------------------------------+--------------------------------------------------------+ ++-----------------------------------+-------------------------------------------+ +| Name: Time | Name: up | +| Labels: | Labels: instance=localhost:9091, job=node | +| Type: []time.Time | Type: []float64 | ++-----------------------------------+-------------------------------------------+ +| 2015-07-01 20:10:30.781 +0000 UTC | 0 | +| 2015-07-01 20:10:45.781 +0000 UTC | 0 | +| 2015-07-01 20:11:00.781 +0000 UTC | 1 | ++-----------------------------------+-------------------------------------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=QVJST1cxAAD/////CAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAjP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAMz+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAANgAAAAEAAAAQv///xQAAACgAAAAoAAAAAAAAAOgAAAAAgAAACwAAAAEAAAANP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAWP///wgAAABMAAAAQAAAAHsiX19uYW1lX18iOiJ1cCIsImluc3RhbmNlIjoibG9jYWxob3N0OjkwOTAiLCJqb2IiOiJwcm9tZXRoZXVzIn0AAAAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAAAAAAD/////uAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAADAAAAAAAAAAFAAAAAAAAAMEAAoAGAAMAAgABAAKAAAAFAAAAFgAAAADAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAYAAAAAAAAAAAAAAACAAAAAwAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAABAuc2smuvsE0CP3yqe6+wTQGXxqKHr7BMAAAAAAADwPwAAAAAAAPA/AAAAAAAA8D8QAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAABAABAAAAGAIAAAAAAADAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAjP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAMz+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAANgAAAAEAAAAQv///xQAAACgAAAAoAAAAAAAAAOgAAAAAgAAACwAAAAEAAAANP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAWP///wgAAABMAAAAQAAAAHsiX19uYW1lX18iOiJ1cCIsImluc3RhbmNlIjoibG9jYWxob3N0OjkwOTAiLCJqb2IiOiJwcm9tZXRoZXVzIn0AAAAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAADACAABBUlJPVzE= -FRAME=QVJST1cxAAD/////AAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAlP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAAC0/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAANT+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAANAAAAAEAAAASv///xQAAACYAAAAmAAAAAAAAAOYAAAAAgAAACwAAAAEAAAAPP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAYP///wgAAABEAAAAOgAAAHsiX19uYW1lX18iOiJ1cCIsImluc3RhbmNlIjoibG9jYWxob3N0OjkwOTEiLCJqb2IiOiJub2RlIn0AAAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAwAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAwAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAGAAAAAAAAAAAAAAAAgAAAAMAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAQLnNrJrr7BNAj98qnuvsE0Bl8aih6+wTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPA/EAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADgAAAAAAAQAAQAAABACAAAAAAAAwAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAKAAwAAAAIAAQACgAAAAgAAACMAAAAAwAAAEwAAAAoAAAABAAAAJT+//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAAtP7//wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAADU/v//CAAAACQAAAAaAAAAeyJ0eXBlIjoidGltZXNlcmllcy1tYW55In0AAAQAAABtZXRhAAAAAAIAAADQAAAABAAAAEr///8UAAAAmAAAAJgAAAAAAAADmAAAAAIAAAAsAAAABAAAADz///8IAAAAEAAAAAUAAABWYWx1ZQAAAAQAAABuYW1lAAAAAGD///8IAAAARAAAADoAAAB7Il9fbmFtZV9fIjoidXAiLCJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MDkxIiwiam9iIjoibm9kZSJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAKAIAAEFSUk9XMQ== +FRAME=QVJST1cxAAD/////8AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAoP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADA/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOD+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAAMQAAAAEAAAAVv///xQAAACMAAAAjAAAAAAAAAOMAAAAAgAAACgAAAAEAAAASP///wgAAAAMAAAAAgAAAHVwAAAEAAAAbmFtZQAAAABo////CAAAADwAAAAwAAAAeyJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MDkwIiwiam9iIjoicHJvbWV0aGV1cyJ9AAAAAAYAAABsYWJlbHMAAAAAAACK////AAACAAIAAAB1cAAAAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAD/////uAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAADAAAAAAAAAAFAAAAAAAAAMEAAoAGAAMAAgABAAKAAAAFAAAAFgAAAADAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAYAAAAAAAAAAAAAAACAAAAAwAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAABAuc2smuvsE0CP3yqe6+wTQGXxqKHr7BMAAAAAAADwPwAAAAAAAPA/AAAAAAAA8D8QAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAPAAAAAAABAABAAAAAAIAAAAAAADAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAwAAAAIAAQACgAAAAgAAACMAAAAAwAAAEwAAAAoAAAABAAAAKD+//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAAwP7//wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAADg/v//CAAAACQAAAAaAAAAeyJ0eXBlIjoidGltZXNlcmllcy1tYW55In0AAAQAAABtZXRhAAAAAAIAAADEAAAABAAAAFb///8UAAAAjAAAAIwAAAAAAAADjAAAAAIAAAAoAAAABAAAAEj///8IAAAADAAAAAIAAAB1cAAABAAAAG5hbWUAAAAAaP///wgAAAA8AAAAMAAAAHsiaW5zdGFuY2UiOiJsb2NhbGhvc3Q6OTA5MCIsImpvYiI6InByb21ldGhldXMifQAAAAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgACAAAAdXAAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAIAIAAEFSUk9XMQ== +FRAME=QVJST1cxAAD/////6AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAqP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADI/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOj+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAALwAAAAEAAAAXv///xQAAACEAAAAhAAAAAAAAAOEAAAAAgAAACgAAAAEAAAAUP///wgAAAAMAAAAAgAAAHVwAAAEAAAAbmFtZQAAAABw////CAAAADQAAAAqAAAAeyJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MDkxIiwiam9iIjoibm9kZSJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgACAAAAdXAAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAwAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAwAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAGAAAAAAAAAAAAAAAAgAAAAMAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAQLnNrJrr7BNAj98qnuvsE0Bl8aih6+wTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPA/EAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADwAAAAAAAQAAQAAAPgBAAAAAAAAwAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAjAAAAAMAAABMAAAAKAAAAAQAAACo/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAMj+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA6P7//wgAAAAkAAAAGgAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSJ9AAAEAAAAbWV0YQAAAAACAAAAvAAAAAQAAABe////FAAAAIQAAACEAAAAAAAAA4QAAAACAAAAKAAAAAQAAABQ////CAAAAAwAAAACAAAAdXAAAAQAAABuYW1lAAAAAHD///8IAAAANAAAACoAAAB7Imluc3RhbmNlIjoibG9jYWxob3N0OjkwOTEiLCJqb2IiOiJub2RlIn0AAAYAAABsYWJlbHMAAAAAAACK////AAACAAIAAAB1cAAAAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAYAgAAQVJST1cx diff --git a/pkg/util/converter/testdata/prom-vector-frame.json b/pkg/util/converter/testdata/prom-vector-frame.json index eaff88b9a5a..8d81e0ba3ca 100644 --- a/pkg/util/converter/testdata/prom-vector-frame.json +++ b/pkg/util/converter/testdata/prom-vector-frame.json @@ -14,13 +14,12 @@ } }, { - "name": "Value", + "name": "up", "type": "number", "typeInfo": { "frame": "float64" }, "labels": { - "__name__": "up", "job": "prometheus", "instance": "localhost:9090" } @@ -52,15 +51,14 @@ } }, { - "name": "Value", + "name": "up", "type": "number", "typeInfo": { "frame": "float64" }, "labels": { "job": "node", - "instance": "localhost:9100", - "__name__": "up" + "instance": "localhost:9100" } } ] diff --git a/pkg/util/converter/testdata/prom-vector-golden.txt b/pkg/util/converter/testdata/prom-vector-golden.txt index 0c4c0d3150e..a1dfe5665b5 100644 --- a/pkg/util/converter/testdata/prom-vector-golden.txt +++ b/pkg/util/converter/testdata/prom-vector-golden.txt @@ -5,13 +5,13 @@ Frame[0] { } Name: Dimensions: 2 Fields by 1 Rows -+-----------------------------------+--------------------------------------------------------------+ -| Name: Time | Name: Value | -| Labels: | Labels: __name__=up, instance=localhost:9090, job=prometheus | -| Type: []time.Time | Type: []float64 | -+-----------------------------------+--------------------------------------------------------------+ -| 2015-07-01 20:10:51.781 +0000 UTC | 1 | -+-----------------------------------+--------------------------------------------------------------+ ++-----------------------------------+-------------------------------------------------+ +| Name: Time | Name: up | +| Labels: | Labels: instance=localhost:9090, job=prometheus | +| Type: []time.Time | Type: []float64 | ++-----------------------------------+-------------------------------------------------+ +| 2015-07-01 20:10:51.781 +0000 UTC | 1 | ++-----------------------------------+-------------------------------------------------+ @@ -20,13 +20,13 @@ Frame[1] { } Name: Dimensions: 2 Fields by 1 Rows -+-----------------------------------+--------------------------------------------------------+ -| Name: Time | Name: Value | -| Labels: | Labels: __name__=up, instance=localhost:9100, job=node | -| Type: []time.Time | Type: []float64 | -+-----------------------------------+--------------------------------------------------------+ -| 2015-07-01 20:10:51.781 +0000 UTC | 0 | -+-----------------------------------+--------------------------------------------------------+ ++-----------------------------------+-------------------------------------------+ +| Name: Time | Name: up | +| Labels: | Labels: instance=localhost:9100, job=node | +| Type: []time.Time | Type: []float64 | ++-----------------------------------+-------------------------------------------+ +| 2015-07-01 20:10:51.781 +0000 UTC | 0 | ++-----------------------------------+-------------------------------------------+ @@ -75,8 +75,8 @@ Dimensions: 2 Fields by 1 Rows ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=QVJST1cxAAD/////CAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAjP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAMz+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAANgAAAAEAAAAQv///xQAAACgAAAAoAAAAAAAAAOgAAAAAgAAACwAAAAEAAAANP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAWP///wgAAABMAAAAQAAAAHsiX19uYW1lX18iOiJ1cCIsImluc3RhbmNlIjoibG9jYWxob3N0OjkwOTAiLCJqb2IiOiJwcm9tZXRoZXVzIn0AAAAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAAAAAAD/////uAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAABAAAAAAAAAAFAAAAAAAAAMEAAoAGAAMAAgABAAKAAAAFAAAAFgAAAABAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAACAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAABAS4CQn+vsEwAAAAAAAPA/EAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADgAAAAAAAQAAQAAABgCAAAAAAAAwAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAKAAwAAAAIAAQACgAAAAgAAACMAAAAAwAAAEwAAAAoAAAABAAAAIz+//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAArP7//wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAADM/v//CAAAACQAAAAaAAAAeyJ0eXBlIjoidGltZXNlcmllcy1tYW55In0AAAQAAABtZXRhAAAAAAIAAADYAAAABAAAAEL///8UAAAAoAAAAKAAAAAAAAADoAAAAAIAAAAsAAAABAAAADT///8IAAAAEAAAAAUAAABWYWx1ZQAAAAQAAABuYW1lAAAAAFj///8IAAAATAAAAEAAAAB7Il9fbmFtZV9fIjoidXAiLCJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MDkwIiwiam9iIjoicHJvbWV0aGV1cyJ9AAAAAAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAwAgAAQVJST1cx -FRAME=QVJST1cxAAD/////AAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAlP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAAC0/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAANT+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAANAAAAAEAAAASv///xQAAACYAAAAmAAAAAAAAAOYAAAAAgAAACwAAAAEAAAAPP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAYP///wgAAABEAAAAOgAAAHsiX19uYW1lX18iOiJ1cCIsImluc3RhbmNlIjoibG9jYWxob3N0OjkxMDAiLCJqb2IiOiJub2RlIn0AAAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAQEuAkJ/r7BMAAAAAAAAAABAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAAAQAgAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAjAAAAAMAAABMAAAAKAAAAAQAAACU/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAALT+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA1P7//wgAAAAkAAAAGgAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSJ9AAAEAAAAbWV0YQAAAAACAAAA0AAAAAQAAABK////FAAAAJgAAACYAAAAAAAAA5gAAAACAAAALAAAAAQAAAA8////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAABg////CAAAAEQAAAA6AAAAeyJfX25hbWVfXyI6InVwIiwiaW5zdGFuY2UiOiJsb2NhbGhvc3Q6OTEwMCIsImpvYiI6Im5vZGUifQAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAACgCAABBUlJPVzE= +FRAME=QVJST1cxAAD/////8AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAoP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADA/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOD+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAAMQAAAAEAAAAVv///xQAAACMAAAAjAAAAAAAAAOMAAAAAgAAACgAAAAEAAAASP///wgAAAAMAAAAAgAAAHVwAAAEAAAAbmFtZQAAAABo////CAAAADwAAAAwAAAAeyJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MDkwIiwiam9iIjoicHJvbWV0aGV1cyJ9AAAAAAYAAABsYWJlbHMAAAAAAACK////AAACAAIAAAB1cAAAAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAD/////uAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAABAAAAAAAAAAFAAAAAAAAAMEAAoAGAAMAAgABAAKAAAAFAAAAFgAAAABAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAACAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAABAS4CQn+vsEwAAAAAAAPA/EAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADwAAAAAAAQAAQAAAAACAAAAAAAAwAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAjAAAAAMAAABMAAAAKAAAAAQAAACg/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAMD+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA4P7//wgAAAAkAAAAGgAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSJ9AAAEAAAAbWV0YQAAAAACAAAAxAAAAAQAAABW////FAAAAIwAAACMAAAAAAAAA4wAAAACAAAAKAAAAAQAAABI////CAAAAAwAAAACAAAAdXAAAAQAAABuYW1lAAAAAGj///8IAAAAPAAAADAAAAB7Imluc3RhbmNlIjoibG9jYWxob3N0OjkwOTAiLCJqb2IiOiJwcm9tZXRoZXVzIn0AAAAABgAAAGxhYmVscwAAAAAAAIr///8AAAIAAgAAAHVwAAAAABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAACACAABBUlJPVzE= +FRAME=QVJST1cxAAD/////6AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAqP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADI/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOj+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAALwAAAAEAAAAXv///xQAAACEAAAAhAAAAAAAAAOEAAAAAgAAACgAAAAEAAAAUP///wgAAAAMAAAAAgAAAHVwAAAEAAAAbmFtZQAAAABw////CAAAADQAAAAqAAAAeyJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MTAwIiwiam9iIjoibm9kZSJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgACAAAAdXAAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAQEuAkJ/r7BMAAAAAAAAAABAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA8AAAAAAAEAAEAAAD4AQAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAqP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADI/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOj+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAALwAAAAEAAAAXv///xQAAACEAAAAhAAAAAAAAAOEAAAAAgAAACgAAAAEAAAAUP///wgAAAAMAAAAAgAAAHVwAAAEAAAAbmFtZQAAAABw////CAAAADQAAAAqAAAAeyJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MTAwIiwiam9iIjoibm9kZSJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgACAAAAdXAAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAGAIAAEFSUk9XMQ== FRAME=QVJST1cxAAD/////6AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAArP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADM/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOz+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAALgAAAAEAAAAYv///xQAAACAAAAAgAAAAAAAAAOAAAAAAgAAACwAAAAEAAAAVP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAeP///wgAAAAsAAAAIwAAAHsibGV2ZWwiOiJlcnJvciIsImxvY2F0aW9uIjoibW9vbiJ9AAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAN7cpctR1BYAAAAAAADwfxAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAAD4AQAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAjAAAAAMAAABMAAAAKAAAAAQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAMz+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA7P7//wgAAAAkAAAAGgAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSJ9AAAEAAAAbWV0YQAAAAACAAAAuAAAAAQAAABi////FAAAAIAAAACAAAAAAAAAA4AAAAACAAAALAAAAAQAAABU////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAAB4////CAAAACwAAAAjAAAAeyJsZXZlbCI6ImVycm9yIiwibG9jYXRpb24iOiJtb29uIn0ABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAABACAABBUlJPVzE= FRAME=QVJST1cxAAD/////6AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAArP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADM/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOz+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAALgAAAAEAAAAYv///xQAAACAAAAAgAAAAAAAAAOAAAAAAgAAACwAAAAEAAAAVP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAeP///wgAAAAsAAAAIgAAAHsibGV2ZWwiOiJpbmZvIiwibG9jYXRpb24iOiJtb29uIn0AAAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAN7cpctR1BYAAAAAAADw/xAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAAD4AQAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAjAAAAAMAAABMAAAAKAAAAAQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAMz+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA7P7//wgAAAAkAAAAGgAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSJ9AAAEAAAAbWV0YQAAAAACAAAAuAAAAAQAAABi////FAAAAIAAAACAAAAAAAAAA4AAAAACAAAALAAAAAQAAABU////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAAB4////CAAAACwAAAAiAAAAeyJsZXZlbCI6ImluZm8iLCJsb2NhdGlvbiI6Im1vb24ifQAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAABACAABBUlJPVzE= FRAME=QVJST1cxAAD/////6AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAArP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADM/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOz+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAALgAAAAEAAAAYv///xQAAACAAAAAgAAAAAAAAAOAAAAAAgAAACwAAAAEAAAAVP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAeP///wgAAAAsAAAAIwAAAHsibGV2ZWwiOiJkZWJ1ZyIsImxvY2F0aW9uIjoibW9vbiJ9AAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAN7cpctR1BYBAAAAAAD4fxAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAAD4AQAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAjAAAAAMAAABMAAAAKAAAAAQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAMz+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA7P7//wgAAAAkAAAAGgAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSJ9AAAEAAAAbWV0YQAAAAACAAAAuAAAAAQAAABi////FAAAAIAAAACAAAAAAAAAA4AAAAACAAAALAAAAAQAAABU////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAAB4////CAAAACwAAAAjAAAAeyJsZXZlbCI6ImRlYnVnIiwibG9jYXRpb24iOiJtb29uIn0ABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAABACAABBUlJPVzE= diff --git a/public/app/plugins/datasource/loki/backendResultTransformer.test.ts b/public/app/plugins/datasource/loki/backendResultTransformer.test.ts index 2d01b584b63..6179df79462 100644 --- a/public/app/plugins/datasource/loki/backendResultTransformer.test.ts +++ b/public/app/plugins/datasource/loki/backendResultTransformer.test.ts @@ -31,7 +31,7 @@ const inputFrame: DataFrame = { json: true, }, }, - values: new ArrayVector(['{ "level": "info", "code": "41🌙" }', '{ "level": "error", "code": "41🌙" }']), + values: new ArrayVector([`[["level", "info"],["code", "41🌙"]]`, `[["level", "error"],["code", "41🌙"]]`]), }, { name: 'tsNs', From 9b8cdab1231d325bcdc3897ab13a5269dfbddf14 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Thu, 5 May 2022 13:37:04 +0300 Subject: [PATCH 057/440] TagsInput: Do not trigger submit on Enter (#48743) * TagsInput: Do not trigger submit on Enter * Comment --- packages/grafana-ui/src/components/TagsInput/TagsInput.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/grafana-ui/src/components/TagsInput/TagsInput.tsx b/packages/grafana-ui/src/components/TagsInput/TagsInput.tsx index 5e1246e09a3..017b1bc8364 100644 --- a/packages/grafana-ui/src/components/TagsInput/TagsInput.tsx +++ b/packages/grafana-ui/src/components/TagsInput/TagsInput.tsx @@ -85,6 +85,13 @@ export const TagsInput: FC = ({ onChange={onNameChange} value={newTagName} onKeyUp={onKeyboardAdd} + onKeyDown={(e) => { + // onKeyDown is triggered before onKeyUp, triggering submit behaviour on Enter press if this component + // is used inside forms. Moving onKeyboardAdd callback here doesn't work since text input is not captured in onKeyDown + if (e.key === 'Enter') { + e.preventDefault(); + } + }} onBlur={onBlur} invalid={invalid} suffix={ From 02aa1cd1c54430010f60520d0979b7b73ae6cecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Thu, 5 May 2022 12:42:50 +0200 Subject: [PATCH 058/440] loki backend mode forward-oauth (#48401) * loki: backend: add forward oauth credentials functionality * removed obsolete comment --- pkg/tsdb/loki/api.go | 42 +++++--- pkg/tsdb/loki/api_mock.go | 41 ++++++++ pkg/tsdb/loki/framing_test.go | 31 +----- pkg/tsdb/loki/loki.go | 75 +++++++++++--- pkg/tsdb/loki/loki_bench_test.go | 2 +- pkg/tsdb/loki/oauth_test.go | 164 +++++++++++++++++++++++++++++++ 6 files changed, 293 insertions(+), 62 deletions(-) create mode 100644 pkg/tsdb/loki/api_mock.go create mode 100644 pkg/tsdb/loki/oauth_test.go diff --git a/pkg/tsdb/loki/api.go b/pkg/tsdb/loki/api.go index d1d1eb6f813..63c99bd99b6 100644 --- a/pkg/tsdb/loki/api.go +++ b/pkg/tsdb/loki/api.go @@ -16,16 +16,23 @@ import ( ) type LokiAPI struct { - client *http.Client - url string - log log.Logger + client *http.Client + url string + log log.Logger + oauthToken string } -func newLokiAPI(client *http.Client, url string, log log.Logger) *LokiAPI { - return &LokiAPI{client: client, url: url, log: log} +func newLokiAPI(client *http.Client, url string, log log.Logger, oauthToken string) *LokiAPI { + return &LokiAPI{client: client, url: url, log: log, oauthToken: oauthToken} } -func makeDataRequest(ctx context.Context, lokiDsUrl string, query lokiQuery) (*http.Request, error) { +func addOauthHeader(req *http.Request, oauthToken string) { + if oauthToken != "" { + req.Header.Set("Authorization", oauthToken) + } +} + +func makeDataRequest(ctx context.Context, lokiDsUrl string, query lokiQuery, oauthToken string) (*http.Request, error) { qs := url.Values{} qs.Set("query", query.Expr) @@ -78,12 +85,7 @@ func makeDataRequest(ctx context.Context, lokiDsUrl string, query lokiQuery) (*h return nil, err } - // NOTE: - // 1. we are missing "dynamic" http params, like OAuth data. - // this never worked before (and it is not needed for alerting scenarios), - // so it is not a regression. - // twe need to have that when we migrate to backend-queries. - // + addOauthHeader(req, oauthToken) if query.VolumeQuery { req.Header.Set("X-Query-Tags", "Source=logvolhist") @@ -136,7 +138,7 @@ func makeLokiError(body io.ReadCloser) error { } func (api *LokiAPI) DataQuery(ctx context.Context, query lokiQuery) (*loghttp.QueryResponse, error) { - req, err := makeDataRequest(ctx, api.url, query) + req, err := makeDataRequest(ctx, api.url, query, api.oauthToken) if err != nil { return nil, err } @@ -165,7 +167,7 @@ func (api *LokiAPI) DataQuery(ctx context.Context, query lokiQuery) (*loghttp.Qu return &response, nil } -func makeRawRequest(ctx context.Context, lokiDsUrl string, resourceURL string) (*http.Request, error) { +func makeRawRequest(ctx context.Context, lokiDsUrl string, resourceURL string, oauthToken string) (*http.Request, error) { lokiUrl, err := url.Parse(lokiDsUrl) if err != nil { return nil, err @@ -176,11 +178,19 @@ func makeRawRequest(ctx context.Context, lokiDsUrl string, resourceURL string) ( return nil, err } - return http.NewRequestWithContext(ctx, "GET", url.String(), nil) + req, err := http.NewRequestWithContext(ctx, "GET", url.String(), nil) + + if err != nil { + return nil, err + } + + addOauthHeader(req, oauthToken) + + return req, nil } func (api *LokiAPI) RawQuery(ctx context.Context, resourceURL string) ([]byte, error) { - req, err := makeRawRequest(ctx, api.url, resourceURL) + req, err := makeRawRequest(ctx, api.url, resourceURL, api.oauthToken) if err != nil { return nil, err } diff --git a/pkg/tsdb/loki/api_mock.go b/pkg/tsdb/loki/api_mock.go new file mode 100644 index 00000000000..a48633677e7 --- /dev/null +++ b/pkg/tsdb/loki/api_mock.go @@ -0,0 +1,41 @@ +package loki + +import ( + "bytes" + "io" + "net/http" + + "github.com/grafana/grafana/pkg/infra/log" +) + +type mockRequestCallback func(req *http.Request) + +type mockedRoundTripper struct { + statusCode int + responseBytes []byte + contentType string + requestCallback mockRequestCallback +} + +func (mockedRT *mockedRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + requestCallback := mockedRT.requestCallback + if requestCallback != nil { + requestCallback(req) + } + + header := http.Header{} + header.Add("Content-Type", mockedRT.contentType) + return &http.Response{ + StatusCode: mockedRT.statusCode, + Header: header, + Body: io.NopCloser(bytes.NewReader(mockedRT.responseBytes)), + }, nil +} + +func makeMockedAPI(statusCode int, contentType string, responseBytes []byte, requestCallback mockRequestCallback) *LokiAPI { + client := http.Client{ + Transport: &mockedRoundTripper{statusCode: statusCode, contentType: contentType, responseBytes: responseBytes, requestCallback: requestCallback}, + } + + return newLokiAPI(&client, "http://localhost:9999", log.New("test"), "") +} diff --git a/pkg/tsdb/loki/framing_test.go b/pkg/tsdb/loki/framing_test.go index 8525a7f0673..187595484bf 100644 --- a/pkg/tsdb/loki/framing_test.go +++ b/pkg/tsdb/loki/framing_test.go @@ -1,9 +1,7 @@ package loki import ( - "bytes" "context" - "io/ioutil" "net/http" "os" "path/filepath" @@ -12,7 +10,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/experimental" - "github.com/grafana/grafana/pkg/infra/log" "github.com/stretchr/testify/require" ) @@ -59,7 +56,7 @@ func TestSuccessResponse(t *testing.T) { bytes, err := os.ReadFile(responseFileName) require.NoError(t, err) - frames, err := runQuery(context.Background(), makeMockedAPI(http.StatusOK, "application/json", bytes), &test.query) + frames, err := runQuery(context.Background(), makeMockedAPI(http.StatusOK, "application/json", bytes, nil), &test.query) require.NoError(t, err) dr := &backend.DataResponse{ @@ -119,7 +116,7 @@ func TestErrorResponse(t *testing.T) { for _, test := range tt { t.Run(test.name, func(t *testing.T) { - frames, err := runQuery(context.Background(), makeMockedAPI(400, test.contentType, test.body), &lokiQuery{QueryType: QueryTypeRange, Direction: DirectionBackward}) + frames, err := runQuery(context.Background(), makeMockedAPI(400, test.contentType, test.body, nil), &lokiQuery{QueryType: QueryTypeRange, Direction: DirectionBackward}) require.Len(t, frames, 0) require.Error(t, err) @@ -127,27 +124,3 @@ func TestErrorResponse(t *testing.T) { }) } } - -type mockedRoundTripper struct { - statusCode int - responseBytes []byte - contentType string -} - -func (mockedRT *mockedRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - header := http.Header{} - header.Add("Content-Type", mockedRT.contentType) - return &http.Response{ - StatusCode: mockedRT.statusCode, - Header: header, - Body: ioutil.NopCloser(bytes.NewReader(mockedRT.responseBytes)), - }, nil -} - -func makeMockedAPI(statusCode int, contentType string, responseBytes []byte) *LokiAPI { - client := http.Client{ - Transport: &mockedRoundTripper{statusCode: statusCode, contentType: contentType, responseBytes: responseBytes}, - } - - return newLokiAPI(&client, "http://localhost:9999", log.New("test")) -} diff --git a/pkg/tsdb/loki/loki.go b/pkg/tsdb/loki/loki.go index 4404d86d961..6a274296f69 100644 --- a/pkg/tsdb/loki/loki.go +++ b/pkg/tsdb/loki/loki.go @@ -44,8 +44,9 @@ var ( ) type datasourceInfo struct { - HTTPClient *http.Client - URL string + HTTPClient *http.Client + URL string + OauthPassThru bool // open streams streams map[string]data.FrameJSONCache @@ -64,6 +65,10 @@ type QueryJSONModel struct { VolumeQuery bool `json:"volumeQuery"` } +type DataSourceJSONModel struct { + OauthPassThru bool `json:"oauthPassThru"` +} + func parseQueryModel(raw json.RawMessage) (*QueryJSONModel, error) { model := &QueryJSONModel{} err := json.Unmarshal(raw, model) @@ -82,16 +87,54 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst return nil, err } + jsonModel := DataSourceJSONModel{} + err = json.Unmarshal(settings.JSONData, &jsonModel) + if err != nil { + return nil, err + } + model := &datasourceInfo{ - HTTPClient: client, - URL: settings.URL, - streams: make(map[string]data.FrameJSONCache), + HTTPClient: client, + URL: settings.URL, + OauthPassThru: jsonModel.OauthPassThru, + streams: make(map[string]data.FrameJSONCache), } return model, nil } } +func getOauthTokenForQueryData(dsInfo *datasourceInfo, headers map[string]string) string { + if !dsInfo.OauthPassThru { + return "" + } + + return headers["Authorization"] +} + +func getOauthTokenForCallResource(dsInfo *datasourceInfo, headers map[string][]string) string { + if !dsInfo.OauthPassThru { + return "" + } + + accessValues := headers["Authorization"] + + if len(accessValues) == 0 { + return "" + } + + return accessValues[0] +} + func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { + dsInfo, err := s.getDSInfo(req.PluginContext) + if err != nil { + return err + } + + return callResource(ctx, req, sender, dsInfo, s.plog) +} + +func callResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender, dsInfo *datasourceInfo, plog log.Logger) error { url := req.URL // a very basic is-this-url-valid check @@ -105,12 +148,7 @@ func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceReq } lokiURL := fmt.Sprintf("/loki/api/v1/%s", url) - dsInfo, err := s.getDSInfo(req.PluginContext) - if err != nil { - return err - } - - api := newLokiAPI(dsInfo.HTTPClient, dsInfo.URL, s.plog) + api := newLokiAPI(dsInfo.HTTPClient, dsInfo.URL, plog, getOauthTokenForCallResource(dsInfo, req.Headers)) bytes, err := api.RawQuery(ctx, lokiURL) if err != nil { @@ -127,14 +165,19 @@ func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceReq } func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - result := backend.NewQueryDataResponse() - dsInfo, err := s.getDSInfo(req.PluginContext) if err != nil { + result := backend.NewQueryDataResponse() return result, err } - api := newLokiAPI(dsInfo.HTTPClient, dsInfo.URL, s.plog) + return queryData(ctx, req, dsInfo, s.plog, s.tracer) +} + +func queryData(ctx context.Context, req *backend.QueryDataRequest, dsInfo *datasourceInfo, plog log.Logger, tracer tracing.Tracer) (*backend.QueryDataResponse, error) { + result := backend.NewQueryDataResponse() + + api := newLokiAPI(dsInfo.HTTPClient, dsInfo.URL, plog, getOauthTokenForQueryData(dsInfo, req.Headers)) queries, err := parseQuery(req) if err != nil { @@ -142,8 +185,8 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) } for _, query := range queries { - s.plog.Debug("Sending query", "start", query.Start, "end", query.End, "step", query.Step, "query", query.Expr) - _, span := s.tracer.Start(ctx, "alerting.loki") + plog.Debug("Sending query", "start", query.Start, "end", query.End, "step", query.Step, "query", query.Expr) + _, span := tracer.Start(ctx, "alerting.loki") span.SetAttributes("expr", query.Expr, attribute.Key("expr").String(query.Expr)) span.SetAttributes("start_unixnano", query.Start, attribute.Key("start_unixnano").Int64(query.Start.UnixNano())) span.SetAttributes("stop_unixnano", query.End, attribute.Key("stop_unixnano").Int64(query.End.UnixNano())) diff --git a/pkg/tsdb/loki/loki_bench_test.go b/pkg/tsdb/loki/loki_bench_test.go index de0b6d50a17..05e4b6f9273 100644 --- a/pkg/tsdb/loki/loki_bench_test.go +++ b/pkg/tsdb/loki/loki_bench_test.go @@ -17,7 +17,7 @@ func BenchmarkMatrixJson(b *testing.B) { b.ResetTimer() for n := 0; n < b.N; n++ { - _, _ = runQuery(context.Background(), makeMockedAPI(http.StatusOK, "application/json", bytes), &lokiQuery{}) + _, _ = runQuery(context.Background(), makeMockedAPI(http.StatusOK, "application/json", bytes, nil), &lokiQuery{}) } } diff --git a/pkg/tsdb/loki/oauth_test.go b/pkg/tsdb/loki/oauth_test.go new file mode 100644 index 00000000000..641c29cc59c --- /dev/null +++ b/pkg/tsdb/loki/oauth_test.go @@ -0,0 +1,164 @@ +package loki + +import ( + "bytes" + "context" + "io" + "net/http" + "testing" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/stretchr/testify/require" +) + +type mockedRoundTripperForOauth struct { + requestCallback func(req *http.Request) + body []byte +} + +func (mockedRT *mockedRoundTripperForOauth) RoundTrip(req *http.Request) (*http.Response, error) { + mockedRT.requestCallback(req) + return &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{}, + Body: io.NopCloser(bytes.NewReader(mockedRT.body)), + }, nil +} + +type mockedCallResourceResponseSenderForOauth struct { + Response *backend.CallResourceResponse +} + +func (s *mockedCallResourceResponseSenderForOauth) Send(resp *backend.CallResourceResponse) error { + s.Response = resp + return nil +} + +func makeMockedDsInfoForOauth(oauthPassThru bool, body []byte, requestCallback func(req *http.Request)) datasourceInfo { + client := http.Client{ + Transport: &mockedRoundTripperForOauth{requestCallback: requestCallback, body: body}, + } + + return datasourceInfo{ + HTTPClient: &client, + OauthPassThru: oauthPassThru, + } +} + +func TestOauthForwardIdentity(t *testing.T) { + tt := []struct { + name string + oauthPassThru bool + headerGiven bool + headerSent bool + }{ + {name: "when enabled and headers exist => add headers", oauthPassThru: true, headerGiven: true, headerSent: true}, + {name: "when disabled and headers exist => do not add headers", oauthPassThru: false, headerGiven: true, headerSent: false}, + {name: "when enabled and no headers exist => do not add headers", oauthPassThru: true, headerGiven: false, headerSent: false}, + {name: "when disabled and no headers exist => do not add headers", oauthPassThru: false, headerGiven: false, headerSent: false}, + } + + authName := "Authorization" + authValue := "auth" + + for _, test := range tt { + t.Run("QueryData: "+test.name, func(t *testing.T) { + response := []byte(` + { + "status": "success", + "data": { + "resultType": "streams", + "result": [ + { + "stream": {}, + "values": [ + ["1", "line1"] + ] + } + ] + } + } + `) + + clientUsed := false + dsInfo := makeMockedDsInfoForOauth(test.oauthPassThru, response, func(req *http.Request) { + clientUsed = true + if test.headerSent { + require.Equal(t, authValue, req.Header.Get(authName)) + } else { + require.Equal(t, "", req.Header.Get(authName)) + } + }) + + req := backend.QueryDataRequest{ + Headers: map[string]string{}, + Queries: []backend.DataQuery{ + { + RefID: "A", + JSON: []byte("{}"), + }, + }, + } + + if test.headerGiven { + req.Headers[authName] = authValue + } + + tracer, err := tracing.InitializeTracerForTest() + require.NoError(t, err) + + data, err := queryData(context.Background(), &req, &dsInfo, log.New("testlog"), tracer) + // we do a basic check that the result is OK + require.NoError(t, err) + require.Len(t, data.Responses, 1) + res := data.Responses["A"] + require.NoError(t, res.Error) + require.Len(t, res.Frames, 1) + require.Equal(t, "line1", res.Frames[0].Fields[2].At(0)) + + // we need to be sure the client-callback was triggered + require.True(t, clientUsed) + }) + } + + for _, test := range tt { + t.Run("CallResource: "+test.name, func(t *testing.T) { + response := []byte("mocked resource response") + + clientUsed := false + dsInfo := makeMockedDsInfoForOauth(test.oauthPassThru, response, func(req *http.Request) { + clientUsed = true + if test.headerSent { + require.Equal(t, authValue, req.Header.Get(authName)) + } else { + require.Equal(t, "", req.Header.Get(authName)) + } + }) + + req := backend.CallResourceRequest{ + Headers: map[string][]string{}, + Method: "GET", + URL: "labels?", + } + + if test.headerGiven { + req.Headers[authName] = []string{authValue} + } + + sender := &mockedCallResourceResponseSenderForOauth{} + + err := callResource(context.Background(), &req, sender, &dsInfo, log.New("testlog")) + // we do a basic check that the result is OK + require.NoError(t, err) + sent := sender.Response + require.NotNil(t, sent) + require.Equal(t, http.StatusOK, sent.Status) + require.Equal(t, response, sent.Body) + + // we need to be sure the client-callback was triggered + require.True(t, clientUsed) + }) + } +} From 8ed3fb1f2de8b77842c55d663aca232d28371d96 Mon Sep 17 00:00:00 2001 From: Jguer Date: Thu, 5 May 2022 11:02:34 +0000 Subject: [PATCH 059/440] AccessControl: don't pull builtin role assignments when refactor is enabled (#48675) --- public/app/core/services/context_srv.ts | 4 ++++ public/app/features/serviceaccounts/state/actions.ts | 1 + public/app/features/users/UsersTable.tsx | 5 ++++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index 6930600ad3d..9cc71e90ba0 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -112,6 +112,10 @@ export class ContextSrv { return Boolean(config.featureToggles['accesscontrol']); } + accessControlBuiltinRefactorEnabled(): boolean { + return Boolean(config.featureToggles['accesscontrol-builtins']); + } + licensedAccessControlEnabled(): boolean { return featureEnabled('accesscontrol') && Boolean(config.featureToggles['accesscontrol']); } diff --git a/public/app/features/serviceaccounts/state/actions.ts b/public/app/features/serviceaccounts/state/actions.ts index 9eb5ccb3d9b..6626edad0cc 100644 --- a/public/app/features/serviceaccounts/state/actions.ts +++ b/public/app/features/serviceaccounts/state/actions.ts @@ -32,6 +32,7 @@ export function fetchACOptions(): ThunkResult { dispatch(acOptionsLoaded(options)); } if ( + !contextSrv.accessControlBuiltinRefactorEnabled() && contextSrv.licensedAccessControlEnabled() && contextSrv.hasPermission(AccessControlAction.ActionBuiltinRolesList) ) { diff --git a/public/app/features/users/UsersTable.tsx b/public/app/features/users/UsersTable.tsx index aee80c96897..054e0402f77 100644 --- a/public/app/features/users/UsersTable.tsx +++ b/public/app/features/users/UsersTable.tsx @@ -30,7 +30,10 @@ const UsersTable: FC = (props) => { setRoleOptions(options); } - if (contextSrv.hasPermission(AccessControlAction.ActionBuiltinRolesList)) { + if ( + !contextSrv.accessControlBuiltinRefactorEnabled() && + contextSrv.hasPermission(AccessControlAction.ActionBuiltinRolesList) + ) { const builtInRoles = await fetchBuiltinRoles(orgId); setBuiltinRoles(builtInRoles); } From da74dba7c8d3da59d4187ae9414c7257985f9e48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Thu, 5 May 2022 13:09:01 +0200 Subject: [PATCH 060/440] Loki: backend: use streaming JSON parser, try2 (#48752) * converter: remove __name__ customization because Loki does not do that Loki does not handle __name__ in a special way. for Prometheus, the caller can implement the formatting by themselves * converter: change labels-formatting the labels.String() method does not handle strange values well * loki: backend: use streaming-json parser * more idiomatic code Co-authored-by: Todd Treece <360020+toddtreece@users.noreply.github.com> * simpler row-length check * simpler code * fixed converter/prom tests Co-authored-by: Todd Treece <360020+toddtreece@users.noreply.github.com> --- pkg/tsdb/loki/api.go | 16 +- pkg/tsdb/loki/frame.go | 112 ++++++++-- pkg/tsdb/loki/frame_test.go | 114 ++++++++-- pkg/tsdb/loki/loki.go | 13 +- pkg/tsdb/loki/parse_response.go | 206 ------------------ pkg/tsdb/loki/parse_response_test.go | 183 ---------------- pkg/util/converter/prom.go | 34 ++- .../testdata/loki-streams-a-frame.json | 42 ++-- .../testdata/loki-streams-a-golden.txt | 26 +-- .../testdata/loki-streams-b-frame.json | 40 ++-- .../testdata/loki-streams-b-golden.txt | 24 +- .../converter/testdata/prom-matrix-frame.json | 8 +- .../converter/testdata/prom-matrix-golden.txt | 40 ++-- .../converter/testdata/prom-vector-frame.json | 8 +- .../converter/testdata/prom-vector-golden.txt | 32 +-- .../testdata/prom-warnings-frame.json | 6 +- .../testdata/prom-warnings-golden.txt | 32 +-- .../loki/backendResultTransformer.test.ts | 2 +- 18 files changed, 368 insertions(+), 570 deletions(-) delete mode 100644 pkg/tsdb/loki/parse_response.go delete mode 100644 pkg/tsdb/loki/parse_response_test.go diff --git a/pkg/tsdb/loki/api.go b/pkg/tsdb/loki/api.go index 63c99bd99b6..609ae9308f3 100644 --- a/pkg/tsdb/loki/api.go +++ b/pkg/tsdb/loki/api.go @@ -10,8 +10,9 @@ import ( "net/url" "strconv" + "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/loki/pkg/loghttp" + "github.com/grafana/grafana/pkg/util/converter" jsoniter "github.com/json-iterator/go" ) @@ -137,7 +138,7 @@ func makeLokiError(body io.ReadCloser) error { return fmt.Errorf("%v", errorMessage) } -func (api *LokiAPI) DataQuery(ctx context.Context, query lokiQuery) (*loghttp.QueryResponse, error) { +func (api *LokiAPI) DataQuery(ctx context.Context, query lokiQuery) (data.Frames, error) { req, err := makeDataRequest(ctx, api.url, query, api.oauthToken) if err != nil { return nil, err @@ -158,13 +159,14 @@ func (api *LokiAPI) DataQuery(ctx context.Context, query lokiQuery) (*loghttp.Qu return nil, makeLokiError(resp.Body) } - var response loghttp.QueryResponse - err = jsoniter.NewDecoder(resp.Body).Decode(&response) - if err != nil { - return nil, err + iter := jsoniter.Parse(jsoniter.ConfigDefault, resp.Body, 1024) + res := converter.ReadPrometheusStyleResult(iter) + + if res.Error != nil { + return nil, res.Error } - return &response, nil + return res.Frames, nil } func makeRawRequest(ctx context.Context, lokiDsUrl string, resourceURL string, oauthToken string) (*http.Request, error) { diff --git a/pkg/tsdb/loki/frame.go b/pkg/tsdb/loki/frame.go index ebfe4247ee1..10affc4dede 100644 --- a/pkg/tsdb/loki/frame.go +++ b/pkg/tsdb/loki/frame.go @@ -6,7 +6,6 @@ import ( "hash/fnv" "sort" "strings" - "time" "github.com/grafana/grafana-plugin-sdk-go/data" ) @@ -57,6 +56,9 @@ func adjustMetricFrame(frame *data.Frame, query *lokiQuery) error { frame.Meta = &data.FrameMeta{} } + frame.Meta.Stats = parseStats(frame.Meta.Custom) + frame.Meta.Custom = nil + if isMetricRange { frame.Meta.ExecutedQueryString = "Expr: " + query.Expr + "\n" + "Step: " + query.Step.String() } else { @@ -81,53 +83,55 @@ func adjustMetricFrame(frame *data.Frame, query *lokiQuery) error { func adjustLogsFrame(frame *data.Frame, query *lokiQuery) error { // we check if the fields are of correct type and length fields := frame.Fields - if len(fields) != 3 { + if len(fields) != 4 { return fmt.Errorf("invalid fields in logs frame") } labelsField := fields[0] timeField := fields[1] lineField := fields[2] + stringTimeField := fields[3] - if (timeField.Type() != data.FieldTypeTime) || (lineField.Type() != data.FieldTypeString) || (labelsField.Type() != data.FieldTypeJSON) { + if (timeField.Type() != data.FieldTypeTime) || (lineField.Type() != data.FieldTypeString) || (labelsField.Type() != data.FieldTypeJSON) || (stringTimeField.Type() != data.FieldTypeString) { return fmt.Errorf("invalid fields in logs frame") } - if (timeField.Len() != lineField.Len()) || (timeField.Len() != labelsField.Len()) { + if (timeField.Len() != lineField.Len()) || (timeField.Len() != labelsField.Len()) || (timeField.Len() != stringTimeField.Len()) { return fmt.Errorf("invalid fields in logs frame") } + // this returns an error when the length of fields do not match + _, err := frame.RowLen() + if err != nil { + return err + } + + labelsField.Name = "labels" + stringTimeField.Name = "tsNs" + if frame.Meta == nil { frame.Meta = &data.FrameMeta{} } + frame.Meta.Stats = parseStats(frame.Meta.Custom) + frame.Meta.Custom = nil + frame.Meta.ExecutedQueryString = "Expr: " + query.Expr // we need to send to the browser the nanosecond-precision timestamp too. // usually timestamps become javascript-date-objects in the browser automatically, which only // have millisecond-precision. - // so we send a separate timestamp-as-string field too. - stringTimeField := makeStringTimeField(timeField) + // so we send a separate timestamp-as-string field too. it is provided by the + // loki-json-parser-code idField, err := makeIdField(stringTimeField, lineField, labelsField, frame.RefID) if err != nil { return err } - frame.Fields = append(frame.Fields, stringTimeField, idField) + frame.Fields = append(frame.Fields, idField) return nil } -func makeStringTimeField(timeField *data.Field) *data.Field { - length := timeField.Len() - stringTimestamps := make([]string, length) - - for i := 0; i < length; i++ { - nsNumber := timeField.At(i).(time.Time).UnixNano() - stringTimestamps[i] = fmt.Sprintf("%d", nsNumber) - } - return data.NewField("tsNs", timeField.Labels.Copy(), stringTimestamps) -} - func calculateCheckSum(time string, line string, labels []byte) (string, error) { input := []byte(line + "_") input = append(input, labels...) @@ -211,3 +215,75 @@ func getFrameLabels(frame *data.Frame) map[string]string { return labels } + +func parseStats(frameMetaCustom interface{}) []data.QueryStat { + customMap, ok := frameMetaCustom.(map[string]interface{}) + if !ok { + return nil + } + rawStats, ok := customMap["stats"].(map[string]interface{}) + if !ok { + return nil + } + + var stats []data.QueryStat + + summary, ok := rawStats["summary"].(map[string]interface{}) + if ok { + stats = append(stats, + makeStat("Summary: bytes processed per second", summary["bytesProcessedPerSecond"], "Bps"), + makeStat("Summary: lines processed per second", summary["linesProcessedPerSecond"], ""), + makeStat("Summary: total bytes processed", summary["totalBytesProcessed"], "decbytes"), + makeStat("Summary: total lines processed", summary["totalLinesProcessed"], ""), + makeStat("Summary: exec time", summary["execTime"], "s")) + } + + store, ok := rawStats["store"].(map[string]interface{}) + if ok { + stats = append(stats, + makeStat("Store: total chunks ref", store["totalChunksRef"], ""), + makeStat("Store: total chunks downloaded", store["totalChunksDownloaded"], ""), + makeStat("Store: chunks download time", store["chunksDownloadTime"], "s"), + makeStat("Store: head chunk bytes", store["headChunkBytes"], "decbytes"), + makeStat("Store: head chunk lines", store["headChunkLines"], ""), + makeStat("Store: decompressed bytes", store["decompressedBytes"], "decbytes"), + makeStat("Store: decompressed lines", store["decompressedLines"], ""), + makeStat("Store: compressed bytes", store["compressedBytes"], "decbytes"), + makeStat("Store: total duplicates", store["totalDuplicates"], "")) + } + + ingester, ok := rawStats["ingester"].(map[string]interface{}) + if ok { + stats = append(stats, + makeStat("Ingester: total reached", ingester["totalReached"], ""), + makeStat("Ingester: total chunks matched", ingester["totalChunksMatched"], ""), + makeStat("Ingester: total batches", ingester["totalBatches"], ""), + makeStat("Ingester: total lines sent", ingester["totalLinesSent"], ""), + makeStat("Ingester: head chunk bytes", ingester["headChunkBytes"], "decbytes"), + makeStat("Ingester: head chunk lines", ingester["headChunkLines"], ""), + makeStat("Ingester: decompressed bytes", ingester["decompressedBytes"], "decbytes"), + makeStat("Ingester: decompressed lines", ingester["decompressedLines"], ""), + makeStat("Ingester: compressed bytes", ingester["compressedBytes"], "decbytes"), + makeStat("Ingester: total duplicates", ingester["totalDuplicates"], "")) + } + + return stats +} + +func makeStat(name string, interfaceValue interface{}, unit string) data.QueryStat { + var value float64 + switch v := interfaceValue.(type) { + case float64: + value = v + case int: + value = float64(v) + } + + return data.QueryStat{ + FieldConfig: data.FieldConfig{ + DisplayName: name, + Unit: unit, + }, + Value: value, + } +} diff --git a/pkg/tsdb/loki/frame_test.go b/pkg/tsdb/loki/frame_test.go index f4da77bb71a..430db3b1330 100644 --- a/pkg/tsdb/loki/frame_test.go +++ b/pkg/tsdb/loki/frame_test.go @@ -2,6 +2,7 @@ package loki import ( "encoding/json" + "strconv" "testing" "time" @@ -39,20 +40,30 @@ func TestFormatName(t *testing.T) { func TestAdjustFrame(t *testing.T) { t.Run("logs-frame metadata should be set correctly", func(t *testing.T) { + time1 := time.Date(2022, 1, 2, 3, 4, 5, 6, time.UTC) + time2 := time.Date(2022, 1, 2, 3, 5, 5, 6, time.UTC) + time3 := time.Date(2022, 1, 2, 3, 5, 5, 6, time.UTC) + time4 := time.Date(2022, 1, 2, 3, 6, 5, 6, time.UTC) + + timeNs1 := strconv.FormatInt(time1.UnixNano(), 10) + timeNs2 := strconv.FormatInt(time2.UnixNano(), 10) + timeNs3 := strconv.FormatInt(time3.UnixNano(), 10) + timeNs4 := strconv.FormatInt(time4.UnixNano(), 10) + frame := data.NewFrame("", - data.NewField("labels", nil, []json.RawMessage{ + data.NewField("__labels", nil, []json.RawMessage{ json.RawMessage(`{"level":"info"}`), json.RawMessage(`{"level":"error"}`), json.RawMessage(`{"level":"error"}`), json.RawMessage(`{"level":"info"}`), }), - data.NewField("time", nil, []time.Time{ - time.Date(2022, 1, 2, 3, 4, 5, 6, time.UTC), - time.Date(2022, 1, 2, 3, 5, 5, 6, time.UTC), - time.Date(2022, 1, 2, 3, 5, 5, 6, time.UTC), - time.Date(2022, 1, 2, 3, 6, 5, 6, time.UTC), + data.NewField("Time", nil, []time.Time{ + time1, time2, time3, time4, + }), + data.NewField("Line", nil, []string{"line1", "line2", "line2", "line3"}), + data.NewField("TS", nil, []string{ + timeNs1, timeNs2, timeNs3, timeNs4, }), - data.NewField("line", nil, []string{"line1", "line2", "line2", "line3"}), ) frame.RefID = "A" @@ -68,14 +79,6 @@ func TestAdjustFrame(t *testing.T) { fields := frame.Fields require.Equal(t, 5, len(fields)) - tsNsField := fields[3] - require.Equal(t, "tsNs", tsNsField.Name) - require.Equal(t, data.FieldTypeString, tsNsField.Type()) - require.Equal(t, 4, tsNsField.Len()) - require.Equal(t, "1641092645000000006", tsNsField.At(0)) - require.Equal(t, "1641092705000000006", tsNsField.At(1)) - require.Equal(t, "1641092705000000006", tsNsField.At(2)) - require.Equal(t, "1641092765000000006", tsNsField.At(3)) idField := fields[4] require.Equal(t, "id", idField.Name) @@ -136,4 +139,85 @@ func TestAdjustFrame(t *testing.T) { require.NotNil(t, timeFieldConfig) require.Equal(t, float64(42000), timeFieldConfig.Interval) }) + + t.Run("should parse response stats", func(t *testing.T) { + stats := map[string]interface{}{ + "summary": map[string]interface{}{ + "bytesProcessedPerSecond": 1, + "linesProcessedPerSecond": 2, + "totalBytesProcessed": 3, + "totalLinesProcessed": 4, + "execTime": 5.5, + }, + + "store": map[string]interface{}{ + "totalChunksRef": 6, + "totalChunksDownloaded": 7, + "chunksDownloadTime": 8.8, + "headChunkBytes": 9, + "headChunkLines": 10, + "decompressedBytes": 11, + "decompressedLines": 12, + "compressedBytes": 13, + "totalDuplicates": 14, + }, + + "ingester": map[string]interface{}{ + "totalReached": 15, + "totalChunksMatched": 16, + "totalBatches": 17, + "totalLinesSent": 18, + "headChunkBytes": 19, + "headChunkLines": 20, + "decompressedBytes": 21, + "decompressedLines": 22, + "compressedBytes": 23, + "totalDuplicates": 24, + }, + } + + meta := data.FrameMeta{ + Custom: map[string]interface{}{ + "stats": stats, + }, + } + + expected := []data.QueryStat{ + {FieldConfig: data.FieldConfig{DisplayName: "Summary: bytes processed per second", Unit: "Bps"}, Value: 1}, + {FieldConfig: data.FieldConfig{DisplayName: "Summary: lines processed per second", Unit: ""}, Value: 2}, + {FieldConfig: data.FieldConfig{DisplayName: "Summary: total bytes processed", Unit: "decbytes"}, Value: 3}, + {FieldConfig: data.FieldConfig{DisplayName: "Summary: total lines processed", Unit: ""}, Value: 4}, + {FieldConfig: data.FieldConfig{DisplayName: "Summary: exec time", Unit: "s"}, Value: 5.5}, + + {FieldConfig: data.FieldConfig{DisplayName: "Store: total chunks ref", Unit: ""}, Value: 6}, + {FieldConfig: data.FieldConfig{DisplayName: "Store: total chunks downloaded", Unit: ""}, Value: 7}, + {FieldConfig: data.FieldConfig{DisplayName: "Store: chunks download time", Unit: "s"}, Value: 8.8}, + {FieldConfig: data.FieldConfig{DisplayName: "Store: head chunk bytes", Unit: "decbytes"}, Value: 9}, + {FieldConfig: data.FieldConfig{DisplayName: "Store: head chunk lines", Unit: ""}, Value: 10}, + {FieldConfig: data.FieldConfig{DisplayName: "Store: decompressed bytes", Unit: "decbytes"}, Value: 11}, + {FieldConfig: data.FieldConfig{DisplayName: "Store: decompressed lines", Unit: ""}, Value: 12}, + {FieldConfig: data.FieldConfig{DisplayName: "Store: compressed bytes", Unit: "decbytes"}, Value: 13}, + {FieldConfig: data.FieldConfig{DisplayName: "Store: total duplicates", Unit: ""}, Value: 14}, + + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total reached", Unit: ""}, Value: 15}, + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total chunks matched", Unit: ""}, Value: 16}, + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total batches", Unit: ""}, Value: 17}, + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total lines sent", Unit: ""}, Value: 18}, + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: head chunk bytes", Unit: "decbytes"}, Value: 19}, + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: head chunk lines", Unit: ""}, Value: 20}, + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: decompressed bytes", Unit: "decbytes"}, Value: 21}, + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: decompressed lines", Unit: ""}, Value: 22}, + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: compressed bytes", Unit: "decbytes"}, Value: 23}, + {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total duplicates", Unit: ""}, Value: 24}, + } + + result := parseStats(meta.Custom) + + // NOTE: i compare it item-by-item otherwise the test-fail-error-message is very hard to read + require.Len(t, result, len(expected)) + + for i := 0; i < len(result); i++ { + require.Equal(t, expected[i], result[i]) + } + }) } diff --git a/pkg/tsdb/loki/loki.go b/pkg/tsdb/loki/loki.go index 6a274296f69..4a1a59e832d 100644 --- a/pkg/tsdb/loki/loki.go +++ b/pkg/tsdb/loki/loki.go @@ -209,12 +209,21 @@ func queryData(ctx context.Context, req *backend.QueryDataRequest, dsInfo *datas // we extracted this part of the functionality to make it easy to unit-test it func runQuery(ctx context.Context, api *LokiAPI, query *lokiQuery) (data.Frames, error) { - value, err := api.DataQuery(ctx, *query) + frames, err := api.DataQuery(ctx, *query) if err != nil { return data.Frames{}, err } - return parseResponse(value, query) + for _, frame := range frames { + if err = adjustFrame(frame, query); err != nil { + return data.Frames{}, err + } + if err != nil { + return data.Frames{}, err + } + } + + return frames, nil } func (s *Service) getDSInfo(pluginCtx backend.PluginContext) (*datasourceInfo, error) { diff --git a/pkg/tsdb/loki/parse_response.go b/pkg/tsdb/loki/parse_response.go deleted file mode 100644 index 4665709912e..00000000000 --- a/pkg/tsdb/loki/parse_response.go +++ /dev/null @@ -1,206 +0,0 @@ -package loki - -import ( - "encoding/json" - "fmt" - "time" - - "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/loki/pkg/loghttp" - "github.com/grafana/loki/pkg/logqlmodel/stats" - jsoniter "github.com/json-iterator/go" -) - -func parseResponse(value *loghttp.QueryResponse, query *lokiQuery) (data.Frames, error) { - frames, err := lokiResponseToDataFrames(value, query) - - if err != nil { - return nil, err - } - - for _, frame := range frames { - err = adjustFrame(frame, query) - if err != nil { - return nil, err - } - } - - return frames, nil -} - -func lokiResponseToDataFrames(value *loghttp.QueryResponse, query *lokiQuery) (data.Frames, error) { - stats := parseStats(value.Data.Statistics) - switch res := value.Data.Result.(type) { - case loghttp.Matrix: - return lokiMatrixToDataFrames(res, query, stats), nil - case loghttp.Vector: - return lokiVectorToDataFrames(res, query, stats), nil - case loghttp.Streams: - return lokiStreamsToDataFrames(res, query, stats) - default: - return nil, fmt.Errorf("resultType %T not supported{", res) - } -} - -func lokiMatrixToDataFrames(matrix loghttp.Matrix, query *lokiQuery, stats []data.QueryStat) data.Frames { - frames := data.Frames{} - - for i, v := range matrix { - tags := make(map[string]string, len(v.Metric)) - timeVector := make([]time.Time, 0, len(v.Values)) - values := make([]float64, 0, len(v.Values)) - - for k, v := range v.Metric { - tags[string(k)] = string(v) - } - - for _, k := range v.Values { - timeVector = append(timeVector, k.Timestamp.Time().UTC()) - values = append(values, float64(k.Value)) - } - - timeField := data.NewField(data.TimeSeriesTimeFieldName, nil, timeVector) - valueField := data.NewField(data.TimeSeriesValueFieldName, tags, values) - - frame := data.NewFrame("", timeField, valueField) - frame.SetMeta(&data.FrameMeta{ - Type: data.FrameTypeTimeSeriesMany, - }) - - // only add the stats to the first dataframe - if i == 0 { - frame.Meta.Stats = stats - } - - frames = append(frames, frame) - } - - return frames -} - -func lokiVectorToDataFrames(vector loghttp.Vector, query *lokiQuery, stats []data.QueryStat) data.Frames { - frames := data.Frames{} - - for i, v := range vector { - tags := make(map[string]string, len(v.Metric)) - timeVector := []time.Time{v.Timestamp.Time().UTC()} - values := []float64{float64(v.Value)} - - for k, v := range v.Metric { - tags[string(k)] = string(v) - } - timeField := data.NewField(data.TimeSeriesTimeFieldName, nil, timeVector) - valueField := data.NewField(data.TimeSeriesValueFieldName, tags, values) - - frame := data.NewFrame("", timeField, valueField) - frame.SetMeta(&data.FrameMeta{ - Type: data.FrameTypeTimeSeriesMany, - }) - - // only add the stats to the first dataframe - if i == 0 { - frame.Meta.Stats = stats - } - - frames = append(frames, frame) - } - - return frames -} - -// we serialize the labels as an ordered list of pairs -func labelsToRawJson(labels data.Labels) (json.RawMessage, error) { - // data.Labels when converted to JSON keep the fields sorted - bytes, err := jsoniter.Marshal(labels) - if err != nil { - return nil, err - } - - return json.RawMessage(bytes), nil -} - -func lokiStreamsToDataFrames(streams loghttp.Streams, query *lokiQuery, stats []data.QueryStat) (data.Frames, error) { - var timeVector []time.Time - var values []string - var labelsVector []json.RawMessage - - for _, v := range streams { - labelsJson, err := labelsToRawJson(v.Labels.Map()) - if err != nil { - return nil, err - } - - for _, k := range v.Entries { - timeVector = append(timeVector, k.Timestamp.UTC()) - values = append(values, k.Line) - labelsVector = append(labelsVector, labelsJson) - } - } - - timeField := data.NewField(data.TimeSeriesTimeFieldName, nil, timeVector) - valueField := data.NewField("Line", nil, values) - labelsField := data.NewField("labels", nil, labelsVector) - - frame := data.NewFrame("", labelsField, timeField, valueField) - frame.SetMeta(&data.FrameMeta{ - Stats: stats, - }) - - return data.Frames{frame}, nil -} - -func parseStats(result stats.Result) []data.QueryStat { - data := []data.QueryStat{ - makeStat("Summary: bytes processed per second", float64(result.Summary.BytesProcessedPerSecond), "Bps"), - makeStat("Summary: lines processed per second", float64(result.Summary.LinesProcessedPerSecond), ""), - makeStat("Summary: total bytes processed", float64(result.Summary.TotalBytesProcessed), "decbytes"), - makeStat("Summary: total lines processed", float64(result.Summary.TotalLinesProcessed), ""), - makeStat("Summary: exec time", result.Summary.ExecTime, "s"), - makeStat("Store: total chunks ref", float64(result.Store.TotalChunksRef), ""), - makeStat("Store: total chunks downloaded", float64(result.Store.TotalChunksDownloaded), ""), - makeStat("Store: chunks download time", result.Store.ChunksDownloadTime, "s"), - makeStat("Store: head chunk bytes", float64(result.Store.HeadChunkBytes), "decbytes"), - makeStat("Store: head chunk lines", float64(result.Store.HeadChunkLines), ""), - makeStat("Store: decompressed bytes", float64(result.Store.DecompressedBytes), "decbytes"), - makeStat("Store: decompressed lines", float64(result.Store.DecompressedLines), ""), - makeStat("Store: compressed bytes", float64(result.Store.CompressedBytes), "decbytes"), - makeStat("Store: total duplicates", float64(result.Store.TotalDuplicates), ""), - makeStat("Ingester: total reached", float64(result.Ingester.TotalReached), ""), - makeStat("Ingester: total chunks matched", float64(result.Ingester.TotalChunksMatched), ""), - makeStat("Ingester: total batches", float64(result.Ingester.TotalBatches), ""), - makeStat("Ingester: total lines sent", float64(result.Ingester.TotalLinesSent), ""), - makeStat("Ingester: head chunk bytes", float64(result.Ingester.HeadChunkBytes), "decbytes"), - makeStat("Ingester: head chunk lines", float64(result.Ingester.HeadChunkLines), ""), - makeStat("Ingester: decompressed bytes", float64(result.Ingester.DecompressedBytes), "decbytes"), - makeStat("Ingester: decompressed lines", float64(result.Ingester.DecompressedLines), ""), - makeStat("Ingester: compressed bytes", float64(result.Ingester.CompressedBytes), "decbytes"), - makeStat("Ingester: total duplicates", float64(result.Ingester.TotalDuplicates), ""), - } - - // it is not possible to know whether the given statistics was missing, or - // it's value was zero. - // we do a heuristic here, if every stat-value is zero, we assume we got no stats-data - allStatsZero := true - for _, stat := range data { - if stat.Value > 0 { - allStatsZero = false - break - } - } - - if allStatsZero { - return nil - } - - return data -} - -func makeStat(name string, value float64, unit string) data.QueryStat { - return data.QueryStat{ - FieldConfig: data.FieldConfig{ - DisplayName: name, - Unit: unit, - }, - Value: value, - } -} diff --git a/pkg/tsdb/loki/parse_response_test.go b/pkg/tsdb/loki/parse_response_test.go deleted file mode 100644 index 07166a45bbd..00000000000 --- a/pkg/tsdb/loki/parse_response_test.go +++ /dev/null @@ -1,183 +0,0 @@ -package loki - -import ( - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/loki/pkg/loghttp" - "github.com/grafana/loki/pkg/logqlmodel/stats" - p "github.com/prometheus/common/model" - "github.com/stretchr/testify/require" -) - -func TestParseResponse(t *testing.T) { - t.Run("value is not of supported type", func(t *testing.T) { - value := loghttp.QueryResponse{ - Data: loghttp.QueryResponseData{ - Result: loghttp.Scalar{}, - }, - } - res, err := parseResponse(&value, nil) - require.Equal(t, len(res), 0) - require.Error(t, err) - }) - - t.Run("response should be parsed normally", func(t *testing.T) { - values := []p.SamplePair{ - {Value: 1, Timestamp: 1000}, - {Value: 2, Timestamp: 2000}, - {Value: 3, Timestamp: 3000}, - {Value: 4, Timestamp: 4000}, - {Value: 5, Timestamp: 5000}, - } - value := loghttp.QueryResponse{ - Data: loghttp.QueryResponseData{ - Result: loghttp.Matrix{ - p.SampleStream{ - Metric: p.Metric{"app": "Application", "tag2": "tag2"}, - Values: values, - }, - }, - }, - } - - query := &lokiQuery{ - Expr: "up(ALERTS)", - QueryType: QueryTypeRange, - LegendFormat: "legend {{app}}", - Step: time.Second * 42, - } - frame, err := parseResponse(&value, query) - require.NoError(t, err) - - labels, err := data.LabelsFromString("app=Application, tag2=tag2") - require.NoError(t, err) - field1 := data.NewField("Time", nil, []time.Time{ - time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC), - time.Date(1970, 1, 1, 0, 0, 2, 0, time.UTC), - time.Date(1970, 1, 1, 0, 0, 3, 0, time.UTC), - time.Date(1970, 1, 1, 0, 0, 4, 0, time.UTC), - time.Date(1970, 1, 1, 0, 0, 5, 0, time.UTC), - }) - field1.Config = &data.FieldConfig{Interval: float64(42000)} - field2 := data.NewField("Value", labels, []float64{1, 2, 3, 4, 5}) - field2.SetConfig(&data.FieldConfig{DisplayNameFromDS: "legend Application"}) - testFrame := data.NewFrame("legend Application", field1, field2) - testFrame.SetMeta(&data.FrameMeta{ - ExecutedQueryString: "Expr: up(ALERTS)\nStep: 42s", - Type: data.FrameTypeTimeSeriesMany, - }) - - if diff := cmp.Diff(testFrame, frame[0], data.FrameTestCompareOptions()...); diff != "" { - t.Errorf("Result mismatch (-want +got):\n%s", diff) - } - }) - - t.Run("should set interval-attribute in response", func(t *testing.T) { - values := []p.SamplePair{ - {Value: 1, Timestamp: 1000}, - } - value := loghttp.QueryResponse{ - Data: loghttp.QueryResponseData{ - Result: loghttp.Matrix{ - p.SampleStream{ - Values: values, - }, - }, - }, - } - - query := &lokiQuery{ - Step: time.Second * 42, - QueryType: QueryTypeRange, - } - - frames, err := parseResponse(&value, query) - require.NoError(t, err) - - // to keep the test simple, we assume the - // first field is the time-field - timeField := frames[0].Fields[0] - require.NotNil(t, timeField) - require.Equal(t, data.FieldTypeTime, timeField.Type()) - - timeFieldConfig := timeField.Config - require.NotNil(t, timeFieldConfig) - require.Equal(t, float64(42000), timeFieldConfig.Interval) - }) - - t.Run("should parse response stats", func(t *testing.T) { - stats := stats.Result{ - Summary: stats.Summary{ - BytesProcessedPerSecond: 1, - LinesProcessedPerSecond: 2, - TotalBytesProcessed: 3, - TotalLinesProcessed: 4, - ExecTime: 5.5, - }, - Store: stats.Store{ - TotalChunksRef: 6, - TotalChunksDownloaded: 7, - ChunksDownloadTime: 8.8, - HeadChunkBytes: 9, - HeadChunkLines: 10, - DecompressedBytes: 11, - DecompressedLines: 12, - CompressedBytes: 13, - TotalDuplicates: 14, - }, - Ingester: stats.Ingester{ - TotalReached: 15, - TotalChunksMatched: 16, - TotalBatches: 17, - TotalLinesSent: 18, - HeadChunkBytes: 19, - HeadChunkLines: 20, - DecompressedBytes: 21, - DecompressedLines: 22, - CompressedBytes: 23, - TotalDuplicates: 24, - }, - } - - expected := []data.QueryStat{ - {FieldConfig: data.FieldConfig{DisplayName: "Summary: bytes processed per second", Unit: "Bps"}, Value: 1}, - {FieldConfig: data.FieldConfig{DisplayName: "Summary: lines processed per second", Unit: ""}, Value: 2}, - {FieldConfig: data.FieldConfig{DisplayName: "Summary: total bytes processed", Unit: "decbytes"}, Value: 3}, - {FieldConfig: data.FieldConfig{DisplayName: "Summary: total lines processed", Unit: ""}, Value: 4}, - {FieldConfig: data.FieldConfig{DisplayName: "Summary: exec time", Unit: "s"}, Value: 5.5}, - - {FieldConfig: data.FieldConfig{DisplayName: "Store: total chunks ref", Unit: ""}, Value: 6}, - {FieldConfig: data.FieldConfig{DisplayName: "Store: total chunks downloaded", Unit: ""}, Value: 7}, - {FieldConfig: data.FieldConfig{DisplayName: "Store: chunks download time", Unit: "s"}, Value: 8.8}, - {FieldConfig: data.FieldConfig{DisplayName: "Store: head chunk bytes", Unit: "decbytes"}, Value: 9}, - {FieldConfig: data.FieldConfig{DisplayName: "Store: head chunk lines", Unit: ""}, Value: 10}, - {FieldConfig: data.FieldConfig{DisplayName: "Store: decompressed bytes", Unit: "decbytes"}, Value: 11}, - {FieldConfig: data.FieldConfig{DisplayName: "Store: decompressed lines", Unit: ""}, Value: 12}, - {FieldConfig: data.FieldConfig{DisplayName: "Store: compressed bytes", Unit: "decbytes"}, Value: 13}, - {FieldConfig: data.FieldConfig{DisplayName: "Store: total duplicates", Unit: ""}, Value: 14}, - - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total reached", Unit: ""}, Value: 15}, - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total chunks matched", Unit: ""}, Value: 16}, - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total batches", Unit: ""}, Value: 17}, - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total lines sent", Unit: ""}, Value: 18}, - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: head chunk bytes", Unit: "decbytes"}, Value: 19}, - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: head chunk lines", Unit: ""}, Value: 20}, - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: decompressed bytes", Unit: "decbytes"}, Value: 21}, - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: decompressed lines", Unit: ""}, Value: 22}, - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: compressed bytes", Unit: "decbytes"}, Value: 23}, - {FieldConfig: data.FieldConfig{DisplayName: "Ingester: total duplicates", Unit: ""}, Value: 24}, - } - - result := parseStats((stats)) - - // NOTE: i compare it item-by-item otherwise the test-fail-error-message is very hard to read - require.Len(t, result, len(expected)) - - for i := 0; i < len(result); i++ { - require.Equal(t, expected[i], result[i]) - } - }) -} diff --git a/pkg/util/converter/prom.go b/pkg/util/converter/prom.go index 43a7ff3e964..dcde552106c 100644 --- a/pkg/util/converter/prom.go +++ b/pkg/util/converter/prom.go @@ -1,6 +1,7 @@ package converter import ( + "encoding/json" "fmt" "strconv" "time" @@ -349,6 +350,7 @@ func readMatrixOrVector(iter *jsoniter.Iterator) *backend.DataResponse { timeField := data.NewFieldFromFieldType(data.FieldTypeTime, 0) timeField.Name = data.TimeSeriesTimeFieldName valueField := data.NewFieldFromFieldType(data.FieldTypeFloat64, 0) + valueField.Name = data.TimeSeriesValueFieldName valueField.Labels = data.Labels{} for l1Field := iter.ReadObject(); l1Field != ""; l1Field = iter.ReadObject() { @@ -375,14 +377,6 @@ func readMatrixOrVector(iter *jsoniter.Iterator) *backend.DataResponse { } } - name, ok := valueField.Labels["__name__"] - if ok { - valueField.Name = name - delete(valueField.Labels, "__name__") - } else { - valueField.Name = data.TimeSeriesValueFieldName - } - frame := data.NewFrame("", timeField, valueField) frame.Meta = &data.FrameMeta{ Type: data.FrameTypeTimeSeriesMany, @@ -408,7 +402,7 @@ func readTimeValuePair(iter *jsoniter.Iterator) (time.Time, float64, error) { func readStream(iter *jsoniter.Iterator) *backend.DataResponse { rsp := &backend.DataResponse{} - labelsField := data.NewFieldFromFieldType(data.FieldTypeString, 0) + labelsField := data.NewFieldFromFieldType(data.FieldTypeJSON, 0) labelsField.Name = "__labels" // avoid automatically spreading this by labels timeField := data.NewFieldFromFieldType(data.FieldTypeTime, 0) @@ -422,14 +416,20 @@ func readStream(iter *jsoniter.Iterator) *backend.DataResponse { tsField.Name = "TS" labels := data.Labels{} - labelString := labels.String() + labelJson, err := labelsToRawJson(labels) + if err != nil { + return &backend.DataResponse{Error: err} + } for iter.ReadArray() { for l1Field := iter.ReadObject(); l1Field != ""; l1Field = iter.ReadObject() { switch l1Field { case "stream": iter.ReadVal(&labels) - labelString = labels.String() + labelJson, err = labelsToRawJson(labels) + if err != nil { + return &backend.DataResponse{Error: err} + } case "values": for iter.ReadArray() { @@ -441,7 +441,7 @@ func readStream(iter *jsoniter.Iterator) *backend.DataResponse { t := timeFromLokiString(ts) - labelsField.Append(labelString) + labelsField.Append(labelJson) timeField.Append(t) lineField.Append(line) tsField.Append(ts) @@ -477,3 +477,13 @@ func timeFromLokiString(str string) time.Time { ns, _ := strconv.ParseInt(str[10:], 10, 64) return time.Unix(ss, ns).UTC() } + +func labelsToRawJson(labels data.Labels) (json.RawMessage, error) { + // data.Labels when converted to JSON keep the fields sorted + bytes, err := jsoniter.Marshal(labels) + if err != nil { + return nil, err + } + + return json.RawMessage(bytes), nil +} diff --git a/pkg/util/converter/testdata/loki-streams-a-frame.json b/pkg/util/converter/testdata/loki-streams-a-frame.json index bd5df94989e..622d46826a1 100644 --- a/pkg/util/converter/testdata/loki-streams-a-frame.json +++ b/pkg/util/converter/testdata/loki-streams-a-frame.json @@ -5,17 +5,28 @@ "meta": { "custom": { "stats": { - "ingester": { - "totalChunksMatched": 0, - "totalBatches": 0, - "totalLinesSent": 0, + "store": { "headChunkBytes": 0, "headChunkLines": 0, + "compressedBytes": 31432, + "decompressedBytes": 7772, + "decompressedLines": 55, + "totalDuplicates": 0, + "totalChunksRef": 2, + "totalChunksDownloaded": 2, + "chunksDownloadTime": 0.000390958 + }, + "ingester": { + "totalReached": 0, + "headChunkBytes": 0, + "totalDuplicates": 0, + "headChunkLines": 0, + "decompressedBytes": 0, "decompressedLines": 0, "compressedBytes": 0, - "totalReached": 0, - "totalDuplicates": 0, - "decompressedBytes": 0 + "totalChunksMatched": 0, + "totalBatches": 0, + "totalLinesSent": 0 }, "summary": { "bytesProcessedPerSecond": 3507022, @@ -23,17 +34,6 @@ "totalBytesProcessed": 7772, "totalLinesProcessed": 55, "execTime": 0.002216125 - }, - "store": { - "totalChunksDownloaded": 2, - "headChunkBytes": 0, - "decompressedLines": 55, - "totalDuplicates": 0, - "totalChunksRef": 2, - "headChunkLines": 0, - "decompressedBytes": 7772, - "compressedBytes": 31432, - "chunksDownloadTime": 0.000390958 } } } @@ -41,9 +41,9 @@ "fields": [ { "name": "__labels", - "type": "string", + "type": "other", "typeInfo": { - "frame": "string" + "frame": "json.RawMessage" } }, { @@ -72,7 +72,7 @@ "data": { "values": [ [ - "level=error, location=moon🌙","level=info, location=moon🌙","level=info, location=moon🌙","level=info, location=moon🌙","level=info, location=moon🌙","level=info, location=moon🌙" + {"level":"error","location":"moon🌙"},{"level":"info","location":"moon🌙"},{"level":"info","location":"moon🌙"},{"level":"info","location":"moon🌙"},{"level":"info","location":"moon🌙"},{"level":"info","location":"moon🌙"} ], [ 1645030244810,1645030247027,1645030246277,1645030246277,1645030245539,1645030244091 diff --git a/pkg/util/converter/testdata/loki-streams-a-golden.txt b/pkg/util/converter/testdata/loki-streams-a-golden.txt index 727f13947ae..9d1fdaa4caf 100644 --- a/pkg/util/converter/testdata/loki-streams-a-golden.txt +++ b/pkg/util/converter/testdata/loki-streams-a-golden.txt @@ -38,19 +38,19 @@ Frame[0] { } Name: Dimensions: 4 Fields by 6 Rows -+------------------------------+-----------------------------------------+------------------+---------------------+ -| Name: __labels | Name: Time | Name: Line | Name: TS | -| Labels: | Labels: | Labels: | Labels: | -| Type: []string | Type: []time.Time | Type: []string | Type: []string | -+------------------------------+-----------------------------------------+------------------+---------------------+ -| level=error, location=moon🌙 | 2022-02-16 16:50:44.81075712 +0000 UTC | log line error 1 | 1645030244810757120 | -| level=info, location=moon🌙 | 2022-02-16 16:50:47.02773504 +0000 UTC | log line info 1 | 1645030247027735040 | -| level=info, location=moon🌙 | 2022-02-16 16:50:46.277587968 +0000 UTC | log line info 2 | 1645030246277587968 | -| level=info, location=moon🌙 | 2022-02-16 16:50:46.277587968 +0000 UTC | log line info 2 | 1645030246277587968 | -| level=info, location=moon🌙 | 2022-02-16 16:50:45.539423744 +0000 UTC | log line info 3 | 1645030245539423744 | -| level=info, location=moon🌙 | 2022-02-16 16:50:44.091700992 +0000 UTC | log line info 4 | 1645030244091700992 | -+------------------------------+-----------------------------------------+------------------+---------------------+ ++---------------------------------------+-----------------------------------------+------------------+---------------------+ +| Name: __labels | Name: Time | Name: Line | Name: TS | +| Labels: | Labels: | Labels: | Labels: | +| Type: []json.RawMessage | Type: []time.Time | Type: []string | Type: []string | ++---------------------------------------+-----------------------------------------+------------------+---------------------+ +| {"level":"error","location":"moon🌙"} | 2022-02-16 16:50:44.81075712 +0000 UTC | log line error 1 | 1645030244810757120 | +| {"level":"info","location":"moon🌙"} | 2022-02-16 16:50:47.02773504 +0000 UTC | log line info 1 | 1645030247027735040 | +| {"level":"info","location":"moon🌙"} | 2022-02-16 16:50:46.277587968 +0000 UTC | log line info 2 | 1645030246277587968 | +| {"level":"info","location":"moon🌙"} | 2022-02-16 16:50:46.277587968 +0000 UTC | log line info 2 | 1645030246277587968 | +| {"level":"info","location":"moon🌙"} | 2022-02-16 16:50:45.539423744 +0000 UTC | log line info 3 | 1645030245539423744 | +| {"level":"info","location":"moon🌙"} | 2022-02-16 16:50:44.091700992 +0000 UTC | log line info 4 | 1645030244091700992 | ++---------------------------------------+-----------------------------------------+------------------+---------------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=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 diff --git a/pkg/util/converter/testdata/loki-streams-b-frame.json b/pkg/util/converter/testdata/loki-streams-b-frame.json index 28ef33d2768..7c8c7dd3610 100644 --- a/pkg/util/converter/testdata/loki-streams-b-frame.json +++ b/pkg/util/converter/testdata/loki-streams-b-frame.json @@ -6,34 +6,34 @@ "custom": { "stats": { "summary": { - "execTime": 0.002216125, "bytesProcessedPerSecond": 3507022, "linesProcessedPerSecond": 24818, "totalBytesProcessed": 7772, - "totalLinesProcessed": 55 + "totalLinesProcessed": 55, + "execTime": 0.002216125 }, "store": { - "headChunkBytes": 0, - "decompressedLines": 55, - "compressedBytes": 31432, - "totalDuplicates": 0, - "totalChunksDownloaded": 2, + "totalChunksRef": 2, "chunksDownloadTime": 0.000390958, "headChunkLines": 0, + "decompressedLines": 55, + "totalDuplicates": 0, + "totalChunksDownloaded": 2, + "headChunkBytes": 0, "decompressedBytes": 7772, - "totalChunksRef": 2 + "compressedBytes": 31432 }, "ingester": { - "headChunkBytes": 0, - "decompressedBytes": 0, - "totalBatches": 0, - "totalLinesSent": 0, - "headChunkLines": 0, - "decompressedLines": 0, - "compressedBytes": 0, - "totalDuplicates": 0, "totalReached": 0, - "totalChunksMatched": 0 + "totalChunksMatched": 0, + "totalLinesSent": 0, + "headChunkBytes": 0, + "decompressedLines": 0, + "totalBatches": 0, + "headChunkLines": 0, + "decompressedBytes": 0, + "compressedBytes": 0, + "totalDuplicates": 0 } } } @@ -41,9 +41,9 @@ "fields": [ { "name": "__labels", - "type": "string", + "type": "other", "typeInfo": { - "frame": "string" + "frame": "json.RawMessage" } }, { @@ -72,7 +72,7 @@ "data": { "values": [ [ - "level=error, location=moon","level=info, location=moon","level=info, location=moon","level=info, location=moon","level=info, location=moon" + {"level":"error","location":"moon"},{"level":"info","location":"moon"},{"level":"info","location":"moon"},{"level":"info","location":"moon"},{"level":"info","location":"moon"} ], [ 1645030244810,1645030247027,1645030246277,1645030245539,1645030244091 diff --git a/pkg/util/converter/testdata/loki-streams-b-golden.txt b/pkg/util/converter/testdata/loki-streams-b-golden.txt index c5291a2e1a5..d8b09e5d00a 100644 --- a/pkg/util/converter/testdata/loki-streams-b-golden.txt +++ b/pkg/util/converter/testdata/loki-streams-b-golden.txt @@ -38,18 +38,18 @@ Frame[0] { } Name: Dimensions: 4 Fields by 5 Rows -+----------------------------+-----------------------------------------+------------------+---------------------+ -| Name: __labels | Name: Time | Name: Line | Name: TS | -| Labels: | Labels: | Labels: | Labels: | -| Type: []string | Type: []time.Time | Type: []string | Type: []string | -+----------------------------+-----------------------------------------+------------------+---------------------+ -| level=error, location=moon | 2022-02-16 16:50:44.81075712 +0000 UTC | log line error 1 | 1645030244810757120 | -| level=info, location=moon | 2022-02-16 16:50:47.02773504 +0000 UTC | log line info 1 | 1645030247027735040 | -| level=info, location=moon | 2022-02-16 16:50:46.277587968 +0000 UTC | log line info 2 | 1645030246277587968 | -| level=info, location=moon | 2022-02-16 16:50:45.539423744 +0000 UTC | log line info 3 | 1645030245539423744 | -| level=info, location=moon | 2022-02-16 16:50:44.091700992 +0000 UTC | log line info 4 | 1645030244091700992 | -+----------------------------+-----------------------------------------+------------------+---------------------+ ++-------------------------------------+-----------------------------------------+------------------+---------------------+ +| Name: __labels | Name: Time | Name: Line | Name: TS | +| Labels: | Labels: | Labels: | Labels: | +| Type: []json.RawMessage | Type: []time.Time | Type: []string | Type: []string | ++-------------------------------------+-----------------------------------------+------------------+---------------------+ +| {"level":"error","location":"moon"} | 2022-02-16 16:50:44.81075712 +0000 UTC | log line error 1 | 1645030244810757120 | +| {"level":"info","location":"moon"} | 2022-02-16 16:50:47.02773504 +0000 UTC | log line info 1 | 1645030247027735040 | +| {"level":"info","location":"moon"} | 2022-02-16 16:50:46.277587968 +0000 UTC | log line info 2 | 1645030246277587968 | +| {"level":"info","location":"moon"} | 2022-02-16 16:50:45.539423744 +0000 UTC | log line info 3 | 1645030245539423744 | +| {"level":"info","location":"moon"} | 2022-02-16 16:50:44.091700992 +0000 UTC | log line info 4 | 1645030244091700992 | ++-------------------------------------+-----------------------------------------+------------------+---------------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=QVJST1cxAAD/////oAQAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAANACAAADAAAATAAAACgAAAAEAAAA9Pv//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAAAU/P//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAADT8//8IAAAAaAIAAFwCAAB7ImN1c3RvbSI6eyJzdGF0cyI6eyJpbmdlc3RlciI6eyJjb21wcmVzc2VkQnl0ZXMiOjAsImRlY29tcHJlc3NlZEJ5dGVzIjowLCJkZWNvbXByZXNzZWRMaW5lcyI6MCwiaGVhZENodW5rQnl0ZXMiOjAsImhlYWRDaHVua0xpbmVzIjowLCJ0b3RhbEJhdGNoZXMiOjAsInRvdGFsQ2h1bmtzTWF0Y2hlZCI6MCwidG90YWxEdXBsaWNhdGVzIjowLCJ0b3RhbExpbmVzU2VudCI6MCwidG90YWxSZWFjaGVkIjowfSwic3RvcmUiOnsiY2h1bmtzRG93bmxvYWRUaW1lIjowLjAwMDM5MDk1OCwiY29tcHJlc3NlZEJ5dGVzIjozMTQzMiwiZGVjb21wcmVzc2VkQnl0ZXMiOjc3NzIsImRlY29tcHJlc3NlZExpbmVzIjo1NSwiaGVhZENodW5rQnl0ZXMiOjAsImhlYWRDaHVua0xpbmVzIjowLCJ0b3RhbENodW5rc0Rvd25sb2FkZWQiOjIsInRvdGFsQ2h1bmtzUmVmIjoyLCJ0b3RhbER1cGxpY2F0ZXMiOjB9LCJzdW1tYXJ5Ijp7ImJ5dGVzUHJvY2Vzc2VkUGVyU2Vjb25kIjozNTA3MDIyLCJleGVjVGltZSI6MC4wMDIyMTYxMjUsImxpbmVzUHJvY2Vzc2VkUGVyU2Vjb25kIjoyNDgxOCwidG90YWxCeXRlc1Byb2Nlc3NlZCI6Nzc3MiwidG90YWxMaW5lc1Byb2Nlc3NlZCI6NTV9fX19AAAAAAQAAABtZXRhAAAAAAQAAAAsAQAAtAAAAFgAAAAEAAAA9v7//xQAAAA4AAAAOAAAAAAAAAU0AAAAAQAAAAQAAADk/v//CAAAAAwAAAACAAAAVFMAAAQAAABuYW1lAAAAAAAAAADU/v//AgAAAFRTAABG////FAAAADwAAAA8AAAAAAAABTgAAAABAAAABAAAADT///8IAAAAEAAAAAQAAABMaW5lAAAAAAQAAABuYW1lAAAAAAAAAAAo////BAAAAExpbmUAAAAAnv///xQAAAA8AAAARAAAAAAAAApEAAAAAQAAAAQAAACM////CAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEgAAABMAAAAAAAABEgAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAUAAAACAAAAF9fbGFiZWxzAAAAAAQAAABuYW1lAAAAAAAAAAAEAAQABAAAAAgAAABfX2xhYmVscwAAAAAAAAAA/////0gBAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAADQAQAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAADIAAAABQAAAAAAAAAAAAAACwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAGAAAAAAAAACrAAAAAAAAAMgAAAAAAAAAAAAAAAAAAADIAAAAAAAAACgAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAGAAAAAAAAAAIAQAAAAAAAEwAAAAAAAAAWAEAAAAAAAAAAAAAAAAAAFgBAAAAAAAAGAAAAAAAAABwAQAAAAAAAF8AAAAAAAAAAAAAAAQAAAAFAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAACMAAABFAAAAZwAAAIkAAACrAAAAeyJsZXZlbCI6ImVycm9yIiwibG9jYXRpb24iOiJtb29uIn17ImxldmVsIjoiaW5mbyIsImxvY2F0aW9uIjoibW9vbiJ9eyJsZXZlbCI6ImluZm8iLCJsb2NhdGlvbiI6Im1vb24ifXsibGV2ZWwiOiJpbmZvIiwibG9jYXRpb24iOiJtb29uIn17ImxldmVsIjoiaW5mbyIsImxvY2F0aW9uIjoibW9vbiJ9AAAAAAAAFLi6SlLUFgB63D5LUtQWACQmEktS1BYApibmSlLUFgAn3I9KUtQWAAAAABAAAAAfAAAALgAAAD0AAABMAAAAbG9nIGxpbmUgZXJyb3IgMWxvZyBsaW5lIGluZm8gMWxvZyBsaW5lIGluZm8gMmxvZyBsaW5lIGluZm8gM2xvZyBsaW5lIGluZm8gNAAAAAAAAAAAEwAAACYAAAA5AAAATAAAAF8AAAAxNjQ1MDMwMjQ0ODEwNzU3MTIwMTY0NTAzMDI0NzAyNzczNTA0MDE2NDUwMzAyNDYyNzc1ODc5NjgxNjQ1MDMwMjQ1NTM5NDIzNzQ0MTY0NTAzMDI0NDA5MTcwMDk5MgAQAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAABAABAAAAsAQAAAAAAABQAQAAAAAAANABAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAANACAAADAAAATAAAACgAAAAEAAAA9Pv//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAAAU/P//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAADT8//8IAAAAaAIAAFwCAAB7ImN1c3RvbSI6eyJzdGF0cyI6eyJpbmdlc3RlciI6eyJjb21wcmVzc2VkQnl0ZXMiOjAsImRlY29tcHJlc3NlZEJ5dGVzIjowLCJkZWNvbXByZXNzZWRMaW5lcyI6MCwiaGVhZENodW5rQnl0ZXMiOjAsImhlYWRDaHVua0xpbmVzIjowLCJ0b3RhbEJhdGNoZXMiOjAsInRvdGFsQ2h1bmtzTWF0Y2hlZCI6MCwidG90YWxEdXBsaWNhdGVzIjowLCJ0b3RhbExpbmVzU2VudCI6MCwidG90YWxSZWFjaGVkIjowfSwic3RvcmUiOnsiY2h1bmtzRG93bmxvYWRUaW1lIjowLjAwMDM5MDk1OCwiY29tcHJlc3NlZEJ5dGVzIjozMTQzMiwiZGVjb21wcmVzc2VkQnl0ZXMiOjc3NzIsImRlY29tcHJlc3NlZExpbmVzIjo1NSwiaGVhZENodW5rQnl0ZXMiOjAsImhlYWRDaHVua0xpbmVzIjowLCJ0b3RhbENodW5rc0Rvd25sb2FkZWQiOjIsInRvdGFsQ2h1bmtzUmVmIjoyLCJ0b3RhbER1cGxpY2F0ZXMiOjB9LCJzdW1tYXJ5Ijp7ImJ5dGVzUHJvY2Vzc2VkUGVyU2Vjb25kIjozNTA3MDIyLCJleGVjVGltZSI6MC4wMDIyMTYxMjUsImxpbmVzUHJvY2Vzc2VkUGVyU2Vjb25kIjoyNDgxOCwidG90YWxCeXRlc1Byb2Nlc3NlZCI6Nzc3MiwidG90YWxMaW5lc1Byb2Nlc3NlZCI6NTV9fX19AAAAAAQAAABtZXRhAAAAAAQAAAAsAQAAtAAAAFgAAAAEAAAA9v7//xQAAAA4AAAAOAAAAAAAAAU0AAAAAQAAAAQAAADk/v//CAAAAAwAAAACAAAAVFMAAAQAAABuYW1lAAAAAAAAAADU/v//AgAAAFRTAABG////FAAAADwAAAA8AAAAAAAABTgAAAABAAAABAAAADT///8IAAAAEAAAAAQAAABMaW5lAAAAAAQAAABuYW1lAAAAAAAAAAAo////BAAAAExpbmUAAAAAnv///xQAAAA8AAAARAAAAAAAAApEAAAAAQAAAAQAAACM////CAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEgAAABMAAAAAAAABEgAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAUAAAACAAAAF9fbGFiZWxzAAAAAAQAAABuYW1lAAAAAAAAAAAEAAQABAAAAAgAAABfX2xhYmVscwAAAADIBAAAQVJST1cx diff --git a/pkg/util/converter/testdata/prom-matrix-frame.json b/pkg/util/converter/testdata/prom-matrix-frame.json index af91613cb20..11484d37e8a 100644 --- a/pkg/util/converter/testdata/prom-matrix-frame.json +++ b/pkg/util/converter/testdata/prom-matrix-frame.json @@ -14,14 +14,15 @@ } }, { - "name": "up", + "name": "Value", "type": "number", "typeInfo": { "frame": "float64" }, "labels": { "job": "prometheus", - "instance": "localhost:9090" + "instance": "localhost:9090", + "__name__": "up" } } ] @@ -51,12 +52,13 @@ } }, { - "name": "up", + "name": "Value", "type": "number", "typeInfo": { "frame": "float64" }, "labels": { + "__name__": "up", "job": "node", "instance": "localhost:9091" } diff --git a/pkg/util/converter/testdata/prom-matrix-golden.txt b/pkg/util/converter/testdata/prom-matrix-golden.txt index 83088450f9d..c9cf2dc5e93 100644 --- a/pkg/util/converter/testdata/prom-matrix-golden.txt +++ b/pkg/util/converter/testdata/prom-matrix-golden.txt @@ -5,15 +5,15 @@ Frame[0] { } Name: Dimensions: 2 Fields by 3 Rows -+-----------------------------------+-------------------------------------------------+ -| Name: Time | Name: up | -| Labels: | Labels: instance=localhost:9090, job=prometheus | -| Type: []time.Time | Type: []float64 | -+-----------------------------------+-------------------------------------------------+ -| 2015-07-01 20:10:30.781 +0000 UTC | 1 | -| 2015-07-01 20:10:45.781 +0000 UTC | 1 | -| 2015-07-01 20:11:00.781 +0000 UTC | 1 | -+-----------------------------------+-------------------------------------------------+ ++-----------------------------------+--------------------------------------------------------------+ +| Name: Time | Name: Value | +| Labels: | Labels: __name__=up, instance=localhost:9090, job=prometheus | +| Type: []time.Time | Type: []float64 | ++-----------------------------------+--------------------------------------------------------------+ +| 2015-07-01 20:10:30.781 +0000 UTC | 1 | +| 2015-07-01 20:10:45.781 +0000 UTC | 1 | +| 2015-07-01 20:11:00.781 +0000 UTC | 1 | ++-----------------------------------+--------------------------------------------------------------+ @@ -22,17 +22,17 @@ Frame[1] { } Name: Dimensions: 2 Fields by 3 Rows -+-----------------------------------+-------------------------------------------+ -| Name: Time | Name: up | -| Labels: | Labels: instance=localhost:9091, job=node | -| Type: []time.Time | Type: []float64 | -+-----------------------------------+-------------------------------------------+ -| 2015-07-01 20:10:30.781 +0000 UTC | 0 | -| 2015-07-01 20:10:45.781 +0000 UTC | 0 | -| 2015-07-01 20:11:00.781 +0000 UTC | 1 | -+-----------------------------------+-------------------------------------------+ ++-----------------------------------+--------------------------------------------------------+ +| Name: Time | Name: Value | +| Labels: | Labels: __name__=up, instance=localhost:9091, job=node | +| Type: []time.Time | Type: []float64 | ++-----------------------------------+--------------------------------------------------------+ +| 2015-07-01 20:10:30.781 +0000 UTC | 0 | +| 2015-07-01 20:10:45.781 +0000 UTC | 0 | +| 2015-07-01 20:11:00.781 +0000 UTC | 1 | ++-----------------------------------+--------------------------------------------------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=QVJST1cxAAD/////8AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAoP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADA/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOD+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAAMQAAAAEAAAAVv///xQAAACMAAAAjAAAAAAAAAOMAAAAAgAAACgAAAAEAAAASP///wgAAAAMAAAAAgAAAHVwAAAEAAAAbmFtZQAAAABo////CAAAADwAAAAwAAAAeyJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MDkwIiwiam9iIjoicHJvbWV0aGV1cyJ9AAAAAAYAAABsYWJlbHMAAAAAAACK////AAACAAIAAAB1cAAAAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAD/////uAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAADAAAAAAAAAAFAAAAAAAAAMEAAoAGAAMAAgABAAKAAAAFAAAAFgAAAADAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAYAAAAAAAAAAAAAAACAAAAAwAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAABAuc2smuvsE0CP3yqe6+wTQGXxqKHr7BMAAAAAAADwPwAAAAAAAPA/AAAAAAAA8D8QAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAPAAAAAAABAABAAAAAAIAAAAAAADAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAwAAAAIAAQACgAAAAgAAACMAAAAAwAAAEwAAAAoAAAABAAAAKD+//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAAwP7//wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAADg/v//CAAAACQAAAAaAAAAeyJ0eXBlIjoidGltZXNlcmllcy1tYW55In0AAAQAAABtZXRhAAAAAAIAAADEAAAABAAAAFb///8UAAAAjAAAAIwAAAAAAAADjAAAAAIAAAAoAAAABAAAAEj///8IAAAADAAAAAIAAAB1cAAABAAAAG5hbWUAAAAAaP///wgAAAA8AAAAMAAAAHsiaW5zdGFuY2UiOiJsb2NhbGhvc3Q6OTA5MCIsImpvYiI6InByb21ldGhldXMifQAAAAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgACAAAAdXAAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAIAIAAEFSUk9XMQ== -FRAME=QVJST1cxAAD/////6AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAqP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADI/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOj+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAALwAAAAEAAAAXv///xQAAACEAAAAhAAAAAAAAAOEAAAAAgAAACgAAAAEAAAAUP///wgAAAAMAAAAAgAAAHVwAAAEAAAAbmFtZQAAAABw////CAAAADQAAAAqAAAAeyJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MDkxIiwiam9iIjoibm9kZSJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgACAAAAdXAAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAwAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAwAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAGAAAAAAAAAAAAAAAAgAAAAMAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAQLnNrJrr7BNAj98qnuvsE0Bl8aih6+wTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPA/EAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADwAAAAAAAQAAQAAAPgBAAAAAAAAwAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAjAAAAAMAAABMAAAAKAAAAAQAAACo/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAMj+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA6P7//wgAAAAkAAAAGgAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSJ9AAAEAAAAbWV0YQAAAAACAAAAvAAAAAQAAABe////FAAAAIQAAACEAAAAAAAAA4QAAAACAAAAKAAAAAQAAABQ////CAAAAAwAAAACAAAAdXAAAAQAAABuYW1lAAAAAHD///8IAAAANAAAACoAAAB7Imluc3RhbmNlIjoibG9jYWxob3N0OjkwOTEiLCJqb2IiOiJub2RlIn0AAAYAAABsYWJlbHMAAAAAAACK////AAACAAIAAAB1cAAAAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAYAgAAQVJST1cx +FRAME=QVJST1cxAAD/////CAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAjP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAMz+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAANgAAAAEAAAAQv///xQAAACgAAAAoAAAAAAAAAOgAAAAAgAAACwAAAAEAAAANP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAWP///wgAAABMAAAAQAAAAHsiX19uYW1lX18iOiJ1cCIsImluc3RhbmNlIjoibG9jYWxob3N0OjkwOTAiLCJqb2IiOiJwcm9tZXRoZXVzIn0AAAAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAAAAAAD/////uAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAADAAAAAAAAAAFAAAAAAAAAMEAAoAGAAMAAgABAAKAAAAFAAAAFgAAAADAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAYAAAAAAAAAAAAAAACAAAAAwAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAABAuc2smuvsE0CP3yqe6+wTQGXxqKHr7BMAAAAAAADwPwAAAAAAAPA/AAAAAAAA8D8QAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAABAABAAAAGAIAAAAAAADAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAjP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAMz+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAANgAAAAEAAAAQv///xQAAACgAAAAoAAAAAAAAAOgAAAAAgAAACwAAAAEAAAANP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAWP///wgAAABMAAAAQAAAAHsiX19uYW1lX18iOiJ1cCIsImluc3RhbmNlIjoibG9jYWxob3N0OjkwOTAiLCJqb2IiOiJwcm9tZXRoZXVzIn0AAAAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAADACAABBUlJPVzE= +FRAME=QVJST1cxAAD/////AAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAlP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAAC0/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAANT+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAANAAAAAEAAAASv///xQAAACYAAAAmAAAAAAAAAOYAAAAAgAAACwAAAAEAAAAPP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAYP///wgAAABEAAAAOgAAAHsiX19uYW1lX18iOiJ1cCIsImluc3RhbmNlIjoibG9jYWxob3N0OjkwOTEiLCJqb2IiOiJub2RlIn0AAAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAwAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAwAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAGAAAAAAAAAAAAAAAAgAAAAMAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAQLnNrJrr7BNAj98qnuvsE0Bl8aih6+wTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPA/EAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADgAAAAAAAQAAQAAABACAAAAAAAAwAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAKAAwAAAAIAAQACgAAAAgAAACMAAAAAwAAAEwAAAAoAAAABAAAAJT+//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAAtP7//wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAADU/v//CAAAACQAAAAaAAAAeyJ0eXBlIjoidGltZXNlcmllcy1tYW55In0AAAQAAABtZXRhAAAAAAIAAADQAAAABAAAAEr///8UAAAAmAAAAJgAAAAAAAADmAAAAAIAAAAsAAAABAAAADz///8IAAAAEAAAAAUAAABWYWx1ZQAAAAQAAABuYW1lAAAAAGD///8IAAAARAAAADoAAAB7Il9fbmFtZV9fIjoidXAiLCJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MDkxIiwiam9iIjoibm9kZSJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAKAIAAEFSUk9XMQ== diff --git a/pkg/util/converter/testdata/prom-vector-frame.json b/pkg/util/converter/testdata/prom-vector-frame.json index 8d81e0ba3ca..eaff88b9a5a 100644 --- a/pkg/util/converter/testdata/prom-vector-frame.json +++ b/pkg/util/converter/testdata/prom-vector-frame.json @@ -14,12 +14,13 @@ } }, { - "name": "up", + "name": "Value", "type": "number", "typeInfo": { "frame": "float64" }, "labels": { + "__name__": "up", "job": "prometheus", "instance": "localhost:9090" } @@ -51,14 +52,15 @@ } }, { - "name": "up", + "name": "Value", "type": "number", "typeInfo": { "frame": "float64" }, "labels": { "job": "node", - "instance": "localhost:9100" + "instance": "localhost:9100", + "__name__": "up" } } ] diff --git a/pkg/util/converter/testdata/prom-vector-golden.txt b/pkg/util/converter/testdata/prom-vector-golden.txt index a1dfe5665b5..0c4c0d3150e 100644 --- a/pkg/util/converter/testdata/prom-vector-golden.txt +++ b/pkg/util/converter/testdata/prom-vector-golden.txt @@ -5,13 +5,13 @@ Frame[0] { } Name: Dimensions: 2 Fields by 1 Rows -+-----------------------------------+-------------------------------------------------+ -| Name: Time | Name: up | -| Labels: | Labels: instance=localhost:9090, job=prometheus | -| Type: []time.Time | Type: []float64 | -+-----------------------------------+-------------------------------------------------+ -| 2015-07-01 20:10:51.781 +0000 UTC | 1 | -+-----------------------------------+-------------------------------------------------+ ++-----------------------------------+--------------------------------------------------------------+ +| Name: Time | Name: Value | +| Labels: | Labels: __name__=up, instance=localhost:9090, job=prometheus | +| Type: []time.Time | Type: []float64 | ++-----------------------------------+--------------------------------------------------------------+ +| 2015-07-01 20:10:51.781 +0000 UTC | 1 | ++-----------------------------------+--------------------------------------------------------------+ @@ -20,13 +20,13 @@ Frame[1] { } Name: Dimensions: 2 Fields by 1 Rows -+-----------------------------------+-------------------------------------------+ -| Name: Time | Name: up | -| Labels: | Labels: instance=localhost:9100, job=node | -| Type: []time.Time | Type: []float64 | -+-----------------------------------+-------------------------------------------+ -| 2015-07-01 20:10:51.781 +0000 UTC | 0 | -+-----------------------------------+-------------------------------------------+ ++-----------------------------------+--------------------------------------------------------+ +| Name: Time | Name: Value | +| Labels: | Labels: __name__=up, instance=localhost:9100, job=node | +| Type: []time.Time | Type: []float64 | ++-----------------------------------+--------------------------------------------------------+ +| 2015-07-01 20:10:51.781 +0000 UTC | 0 | ++-----------------------------------+--------------------------------------------------------+ @@ -75,8 +75,8 @@ Dimensions: 2 Fields by 1 Rows ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=QVJST1cxAAD/////8AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAoP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADA/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOD+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAAMQAAAAEAAAAVv///xQAAACMAAAAjAAAAAAAAAOMAAAAAgAAACgAAAAEAAAASP///wgAAAAMAAAAAgAAAHVwAAAEAAAAbmFtZQAAAABo////CAAAADwAAAAwAAAAeyJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MDkwIiwiam9iIjoicHJvbWV0aGV1cyJ9AAAAAAYAAABsYWJlbHMAAAAAAACK////AAACAAIAAAB1cAAAAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAD/////uAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAABAAAAAAAAAAFAAAAAAAAAMEAAoAGAAMAAgABAAKAAAAFAAAAFgAAAABAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAACAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAABAS4CQn+vsEwAAAAAAAPA/EAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADwAAAAAAAQAAQAAAAACAAAAAAAAwAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAjAAAAAMAAABMAAAAKAAAAAQAAACg/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAMD+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA4P7//wgAAAAkAAAAGgAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSJ9AAAEAAAAbWV0YQAAAAACAAAAxAAAAAQAAABW////FAAAAIwAAACMAAAAAAAAA4wAAAACAAAAKAAAAAQAAABI////CAAAAAwAAAACAAAAdXAAAAQAAABuYW1lAAAAAGj///8IAAAAPAAAADAAAAB7Imluc3RhbmNlIjoibG9jYWxob3N0OjkwOTAiLCJqb2IiOiJwcm9tZXRoZXVzIn0AAAAABgAAAGxhYmVscwAAAAAAAIr///8AAAIAAgAAAHVwAAAAABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAACACAABBUlJPVzE= -FRAME=QVJST1cxAAD/////6AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAqP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADI/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOj+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAALwAAAAEAAAAXv///xQAAACEAAAAhAAAAAAAAAOEAAAAAgAAACgAAAAEAAAAUP///wgAAAAMAAAAAgAAAHVwAAAEAAAAbmFtZQAAAABw////CAAAADQAAAAqAAAAeyJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MTAwIiwiam9iIjoibm9kZSJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgACAAAAdXAAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAQEuAkJ/r7BMAAAAAAAAAABAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA8AAAAAAAEAAEAAAD4AQAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAqP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADI/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOj+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAALwAAAAEAAAAXv///xQAAACEAAAAhAAAAAAAAAOEAAAAAgAAACgAAAAEAAAAUP///wgAAAAMAAAAAgAAAHVwAAAEAAAAbmFtZQAAAABw////CAAAADQAAAAqAAAAeyJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MTAwIiwiam9iIjoibm9kZSJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgACAAAAdXAAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAGAIAAEFSUk9XMQ== +FRAME=QVJST1cxAAD/////CAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAjP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAMz+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAANgAAAAEAAAAQv///xQAAACgAAAAoAAAAAAAAAOgAAAAAgAAACwAAAAEAAAANP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAWP///wgAAABMAAAAQAAAAHsiX19uYW1lX18iOiJ1cCIsImluc3RhbmNlIjoibG9jYWxob3N0OjkwOTAiLCJqb2IiOiJwcm9tZXRoZXVzIn0AAAAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAAAAAAD/////uAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAABAAAAAAAAAAFAAAAAAAAAMEAAoAGAAMAAgABAAKAAAAFAAAAFgAAAABAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAACAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAABAS4CQn+vsEwAAAAAAAPA/EAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADgAAAAAAAQAAQAAABgCAAAAAAAAwAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAKAAwAAAAIAAQACgAAAAgAAACMAAAAAwAAAEwAAAAoAAAABAAAAIz+//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAArP7//wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAADM/v//CAAAACQAAAAaAAAAeyJ0eXBlIjoidGltZXNlcmllcy1tYW55In0AAAQAAABtZXRhAAAAAAIAAADYAAAABAAAAEL///8UAAAAoAAAAKAAAAAAAAADoAAAAAIAAAAsAAAABAAAADT///8IAAAAEAAAAAUAAABWYWx1ZQAAAAQAAABuYW1lAAAAAFj///8IAAAATAAAAEAAAAB7Il9fbmFtZV9fIjoidXAiLCJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MDkwIiwiam9iIjoicHJvbWV0aGV1cyJ9AAAAAAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAwAgAAQVJST1cx +FRAME=QVJST1cxAAD/////AAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAlP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAAC0/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAANT+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAANAAAAAEAAAASv///xQAAACYAAAAmAAAAAAAAAOYAAAAAgAAACwAAAAEAAAAPP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAYP///wgAAABEAAAAOgAAAHsiX19uYW1lX18iOiJ1cCIsImluc3RhbmNlIjoibG9jYWxob3N0OjkxMDAiLCJqb2IiOiJub2RlIn0AAAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAQEuAkJ/r7BMAAAAAAAAAABAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAAAQAgAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAjAAAAAMAAABMAAAAKAAAAAQAAACU/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAALT+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA1P7//wgAAAAkAAAAGgAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSJ9AAAEAAAAbWV0YQAAAAACAAAA0AAAAAQAAABK////FAAAAJgAAACYAAAAAAAAA5gAAAACAAAALAAAAAQAAAA8////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAABg////CAAAAEQAAAA6AAAAeyJfX25hbWVfXyI6InVwIiwiaW5zdGFuY2UiOiJsb2NhbGhvc3Q6OTEwMCIsImpvYiI6Im5vZGUifQAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAACgCAABBUlJPVzE= FRAME=QVJST1cxAAD/////6AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAArP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADM/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOz+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAALgAAAAEAAAAYv///xQAAACAAAAAgAAAAAAAAAOAAAAAAgAAACwAAAAEAAAAVP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAeP///wgAAAAsAAAAIwAAAHsibGV2ZWwiOiJlcnJvciIsImxvY2F0aW9uIjoibW9vbiJ9AAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAN7cpctR1BYAAAAAAADwfxAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAAD4AQAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAjAAAAAMAAABMAAAAKAAAAAQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAMz+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA7P7//wgAAAAkAAAAGgAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSJ9AAAEAAAAbWV0YQAAAAACAAAAuAAAAAQAAABi////FAAAAIAAAACAAAAAAAAAA4AAAAACAAAALAAAAAQAAABU////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAAB4////CAAAACwAAAAjAAAAeyJsZXZlbCI6ImVycm9yIiwibG9jYXRpb24iOiJtb29uIn0ABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAABACAABBUlJPVzE= FRAME=QVJST1cxAAD/////6AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAArP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADM/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOz+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAALgAAAAEAAAAYv///xQAAACAAAAAgAAAAAAAAAOAAAAAAgAAACwAAAAEAAAAVP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAeP///wgAAAAsAAAAIgAAAHsibGV2ZWwiOiJpbmZvIiwibG9jYXRpb24iOiJtb29uIn0AAAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAN7cpctR1BYAAAAAAADw/xAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAAD4AQAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAjAAAAAMAAABMAAAAKAAAAAQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAMz+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA7P7//wgAAAAkAAAAGgAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSJ9AAAEAAAAbWV0YQAAAAACAAAAuAAAAAQAAABi////FAAAAIAAAACAAAAAAAAAA4AAAAACAAAALAAAAAQAAABU////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAAB4////CAAAACwAAAAiAAAAeyJsZXZlbCI6ImluZm8iLCJsb2NhdGlvbiI6Im1vb24ifQAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAABACAABBUlJPVzE= FRAME=QVJST1cxAAD/////6AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAArP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADM/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOz+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAALgAAAAEAAAAYv///xQAAACAAAAAgAAAAAAAAAOAAAAAAgAAACwAAAAEAAAAVP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAeP///wgAAAAsAAAAIwAAAHsibGV2ZWwiOiJkZWJ1ZyIsImxvY2F0aW9uIjoibW9vbiJ9AAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAN7cpctR1BYBAAAAAAD4fxAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAAD4AQAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAjAAAAAMAAABMAAAAKAAAAAQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAMz+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA7P7//wgAAAAkAAAAGgAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSJ9AAAEAAAAbWV0YQAAAAACAAAAuAAAAAQAAABi////FAAAAIAAAACAAAAAAAAAA4AAAAACAAAALAAAAAQAAABU////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAAB4////CAAAACwAAAAjAAAAeyJsZXZlbCI6ImRlYnVnIiwibG9jYXRpb24iOiJtb29uIn0ABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAABACAABBUlJPVzE= diff --git a/pkg/util/converter/testdata/prom-warnings-frame.json b/pkg/util/converter/testdata/prom-warnings-frame.json index 6d28c2f2c85..0fb7dde53f6 100644 --- a/pkg/util/converter/testdata/prom-warnings-frame.json +++ b/pkg/util/converter/testdata/prom-warnings-frame.json @@ -24,12 +24,13 @@ } }, { - "name": "up", + "name": "Value", "type": "number", "typeInfo": { "frame": "float64" }, "labels": { + "__name__": "up", "instance": "localhost:9090", "job": "prometheus" } @@ -71,12 +72,13 @@ } }, { - "name": "up", + "name": "Value", "type": "number", "typeInfo": { "frame": "float64" }, "labels": { + "__name__": "up", "instance": "localhost:9100", "job": "node" } diff --git a/pkg/util/converter/testdata/prom-warnings-golden.txt b/pkg/util/converter/testdata/prom-warnings-golden.txt index 83c40ec9fcd..81afb1b6749 100644 --- a/pkg/util/converter/testdata/prom-warnings-golden.txt +++ b/pkg/util/converter/testdata/prom-warnings-golden.txt @@ -15,13 +15,13 @@ Frame[0] { } Name: Dimensions: 2 Fields by 1 Rows -+-----------------------------------+-------------------------------------------------+ -| Name: Time | Name: up | -| Labels: | Labels: instance=localhost:9090, job=prometheus | -| Type: []time.Time | Type: []float64 | -+-----------------------------------+-------------------------------------------------+ -| 2015-07-01 20:10:51.781 +0000 UTC | 1 | -+-----------------------------------+-------------------------------------------------+ ++-----------------------------------+--------------------------------------------------------------+ +| Name: Time | Name: Value | +| Labels: | Labels: __name__=up, instance=localhost:9090, job=prometheus | +| Type: []time.Time | Type: []float64 | ++-----------------------------------+--------------------------------------------------------------+ +| 2015-07-01 20:10:51.781 +0000 UTC | 1 | ++-----------------------------------+--------------------------------------------------------------+ @@ -40,13 +40,13 @@ Frame[1] { } Name: Dimensions: 2 Fields by 1 Rows -+-----------------------------------+-------------------------------------------+ -| Name: Time | Name: up | -| Labels: | Labels: instance=localhost:9100, job=node | -| Type: []time.Time | Type: []float64 | -+-----------------------------------+-------------------------------------------+ -| 2015-07-01 20:10:51.781 +0000 UTC | 0 | -+-----------------------------------+-------------------------------------------+ ++-----------------------------------+--------------------------------------------------------+ +| Name: Time | Name: Value | +| Labels: | Labels: __name__=up, instance=localhost:9100, job=node | +| Type: []time.Time | Type: []float64 | ++-----------------------------------+--------------------------------------------------------+ +| 2015-07-01 20:10:51.781 +0000 UTC | 0 | ++-----------------------------------+--------------------------------------------------------+ @@ -125,8 +125,8 @@ Dimensions: 2 Fields by 1 Rows ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=QVJST1cxAAD/////UAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAOwAAAADAAAATAAAACgAAAAEAAAAQP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAABg/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAID+//8IAAAAhAAAAHoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJub3RpY2VzIjpbeyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAxIn0seyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAyIn1dfQAABAAAAG1ldGEAAAAAAgAAAMQAAAAEAAAAVv///xQAAACMAAAAjAAAAAAAAAOMAAAAAgAAACgAAAAEAAAASP///wgAAAAMAAAAAgAAAHVwAAAEAAAAbmFtZQAAAABo////CAAAADwAAAAwAAAAeyJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MDkwIiwiam9iIjoicHJvbWV0aGV1cyJ9AAAAAAYAAABsYWJlbHMAAAAAAACK////AAACAAIAAAB1cAAAAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAD/////uAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAABAAAAAAAAAAFAAAAAAAAAMEAAoAGAAMAAgABAAKAAAAFAAAAFgAAAABAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAACAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAABAS4CQn+vsEwAAAAAAAPA/EAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADwAAAAAAAQAAQAAAGACAAAAAAAAwAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAA7AAAAAMAAABMAAAAKAAAAAQAAABA/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAGD+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAgP7//wgAAACEAAAAegAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSIsIm5vdGljZXMiOlt7InNldmVyaXR5Ijoid2FybmluZyIsInRleHQiOiJ3YXJuaW5nIDEifSx7InNldmVyaXR5Ijoid2FybmluZyIsInRleHQiOiJ3YXJuaW5nIDIifV19AAAEAAAAbWV0YQAAAAACAAAAxAAAAAQAAABW////FAAAAIwAAACMAAAAAAAAA4wAAAACAAAAKAAAAAQAAABI////CAAAAAwAAAACAAAAdXAAAAQAAABuYW1lAAAAAGj///8IAAAAPAAAADAAAAB7Imluc3RhbmNlIjoibG9jYWxob3N0OjkwOTAiLCJqb2IiOiJwcm9tZXRoZXVzIn0AAAAABgAAAGxhYmVscwAAAAAAAIr///8AAAIAAgAAAHVwAAAAABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAIACAABBUlJPVzE= -FRAME=QVJST1cxAAD/////SAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAOwAAAADAAAATAAAACgAAAAEAAAASP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAABo/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAIj+//8IAAAAhAAAAHoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJub3RpY2VzIjpbeyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAxIn0seyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAyIn1dfQAABAAAAG1ldGEAAAAAAgAAALwAAAAEAAAAXv///xQAAACEAAAAhAAAAAAAAAOEAAAAAgAAACgAAAAEAAAAUP///wgAAAAMAAAAAgAAAHVwAAAEAAAAbmFtZQAAAABw////CAAAADQAAAAqAAAAeyJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MTAwIiwiam9iIjoibm9kZSJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgACAAAAdXAAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAQEuAkJ/r7BMAAAAAAAAAABAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA8AAAAAAAEAAEAAABYAgAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAOwAAAADAAAATAAAACgAAAAEAAAASP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAABo/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAIj+//8IAAAAhAAAAHoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJub3RpY2VzIjpbeyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAxIn0seyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAyIn1dfQAABAAAAG1ldGEAAAAAAgAAALwAAAAEAAAAXv///xQAAACEAAAAhAAAAAAAAAOEAAAAAgAAACgAAAAEAAAAUP///wgAAAAMAAAAAgAAAHVwAAAEAAAAbmFtZQAAAABw////CAAAADQAAAAqAAAAeyJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MTAwIiwiam9iIjoibm9kZSJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgACAAAAdXAAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAeAIAAEFSUk9XMQ== +FRAME=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 +FRAME=QVJST1cxAAD/////YAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAOwAAAADAAAATAAAACgAAAAEAAAANP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAABU/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAHT+//8IAAAAhAAAAHoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJub3RpY2VzIjpbeyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAxIn0seyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAyIn1dfQAABAAAAG1ldGEAAAAAAgAAANAAAAAEAAAASv///xQAAACYAAAAmAAAAAAAAAOYAAAAAgAAACwAAAAEAAAAPP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAYP///wgAAABEAAAAOgAAAHsiX19uYW1lX18iOiJ1cCIsImluc3RhbmNlIjoibG9jYWxob3N0OjkxMDAiLCJqb2IiOiJub2RlIn0AAAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAQEuAkJ/r7BMAAAAAAAAAABAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAABwAgAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAA7AAAAAMAAABMAAAAKAAAAAQAAAA0/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAFT+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAdP7//wgAAACEAAAAegAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSIsIm5vdGljZXMiOlt7InNldmVyaXR5Ijoid2FybmluZyIsInRleHQiOiJ3YXJuaW5nIDEifSx7InNldmVyaXR5Ijoid2FybmluZyIsInRleHQiOiJ3YXJuaW5nIDIifV19AAAEAAAAbWV0YQAAAAACAAAA0AAAAAQAAABK////FAAAAJgAAACYAAAAAAAAA5gAAAACAAAALAAAAAQAAAA8////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAABg////CAAAAEQAAAA6AAAAeyJfX25hbWVfXyI6InVwIiwiaW5zdGFuY2UiOiJsb2NhbGhvc3Q6OTEwMCIsImpvYiI6Im5vZGUifQAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAIgCAABBUlJPVzE= FRAME=QVJST1cxAAD/////SAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAOwAAAADAAAATAAAACgAAAAEAAAATP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAABs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAIz+//8IAAAAhAAAAHoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJub3RpY2VzIjpbeyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAxIn0seyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAyIn1dfQAABAAAAG1ldGEAAAAAAgAAALgAAAAEAAAAYv///xQAAACAAAAAgAAAAAAAAAOAAAAAAgAAACwAAAAEAAAAVP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAeP///wgAAAAsAAAAIwAAAHsibGV2ZWwiOiJlcnJvciIsImxvY2F0aW9uIjoibW9vbiJ9AAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAN7cpctR1BYAAAAAAADwfxAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAABYAgAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAA7AAAAAMAAABMAAAAKAAAAAQAAABM/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAGz+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAjP7//wgAAACEAAAAegAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSIsIm5vdGljZXMiOlt7InNldmVyaXR5Ijoid2FybmluZyIsInRleHQiOiJ3YXJuaW5nIDEifSx7InNldmVyaXR5Ijoid2FybmluZyIsInRleHQiOiJ3YXJuaW5nIDIifV19AAAEAAAAbWV0YQAAAAACAAAAuAAAAAQAAABi////FAAAAIAAAACAAAAAAAAAA4AAAAACAAAALAAAAAQAAABU////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAAB4////CAAAACwAAAAjAAAAeyJsZXZlbCI6ImVycm9yIiwibG9jYXRpb24iOiJtb29uIn0ABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAHACAABBUlJPVzE= FRAME=QVJST1cxAAD/////SAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAOwAAAADAAAATAAAACgAAAAEAAAATP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAABs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAIz+//8IAAAAhAAAAHoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJub3RpY2VzIjpbeyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAxIn0seyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAyIn1dfQAABAAAAG1ldGEAAAAAAgAAALgAAAAEAAAAYv///xQAAACAAAAAgAAAAAAAAAOAAAAAAgAAACwAAAAEAAAAVP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAeP///wgAAAAsAAAAIgAAAHsibGV2ZWwiOiJpbmZvIiwibG9jYXRpb24iOiJtb29uIn0AAAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAN7cpctR1BYAAAAAAADw/xAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAABYAgAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAA7AAAAAMAAABMAAAAKAAAAAQAAABM/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAGz+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAjP7//wgAAACEAAAAegAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSIsIm5vdGljZXMiOlt7InNldmVyaXR5Ijoid2FybmluZyIsInRleHQiOiJ3YXJuaW5nIDEifSx7InNldmVyaXR5Ijoid2FybmluZyIsInRleHQiOiJ3YXJuaW5nIDIifV19AAAEAAAAbWV0YQAAAAACAAAAuAAAAAQAAABi////FAAAAIAAAACAAAAAAAAAA4AAAAACAAAALAAAAAQAAABU////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAAB4////CAAAACwAAAAiAAAAeyJsZXZlbCI6ImluZm8iLCJsb2NhdGlvbiI6Im1vb24ifQAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAHACAABBUlJPVzE= FRAME=QVJST1cxAAD/////SAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAOwAAAADAAAATAAAACgAAAAEAAAATP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAABs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAIz+//8IAAAAhAAAAHoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJub3RpY2VzIjpbeyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAxIn0seyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAyIn1dfQAABAAAAG1ldGEAAAAAAgAAALgAAAAEAAAAYv///xQAAACAAAAAgAAAAAAAAAOAAAAAAgAAACwAAAAEAAAAVP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAeP///wgAAAAsAAAAIwAAAHsibGV2ZWwiOiJkZWJ1ZyIsImxvY2F0aW9uIjoibW9vbiJ9AAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAN7cpctR1BYBAAAAAAD4fxAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAABYAgAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAA7AAAAAMAAABMAAAAKAAAAAQAAABM/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAGz+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAjP7//wgAAACEAAAAegAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSIsIm5vdGljZXMiOlt7InNldmVyaXR5Ijoid2FybmluZyIsInRleHQiOiJ3YXJuaW5nIDEifSx7InNldmVyaXR5Ijoid2FybmluZyIsInRleHQiOiJ3YXJuaW5nIDIifV19AAAEAAAAbWV0YQAAAAACAAAAuAAAAAQAAABi////FAAAAIAAAACAAAAAAAAAA4AAAAACAAAALAAAAAQAAABU////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAAB4////CAAAACwAAAAjAAAAeyJsZXZlbCI6ImRlYnVnIiwibG9jYXRpb24iOiJtb29uIn0ABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAHACAABBUlJPVzE= diff --git a/public/app/plugins/datasource/loki/backendResultTransformer.test.ts b/public/app/plugins/datasource/loki/backendResultTransformer.test.ts index 6179df79462..2d01b584b63 100644 --- a/public/app/plugins/datasource/loki/backendResultTransformer.test.ts +++ b/public/app/plugins/datasource/loki/backendResultTransformer.test.ts @@ -31,7 +31,7 @@ const inputFrame: DataFrame = { json: true, }, }, - values: new ArrayVector([`[["level", "info"],["code", "41🌙"]]`, `[["level", "error"],["code", "41🌙"]]`]), + values: new ArrayVector(['{ "level": "info", "code": "41🌙" }', '{ "level": "error", "code": "41🌙" }']), }, { name: 'tsNs', From 696405ba7be7ac8ff70a1e597071e027a3dd199a Mon Sep 17 00:00:00 2001 From: Dimitris Sotirakis Date: Thu, 5 May 2022 12:19:35 +0100 Subject: [PATCH 061/440] Remove folder (#48617) --- packaging/publish/publish_both.sh | 22 ---------------------- packaging/publish/publish_testing.sh | 16 ---------------- 2 files changed, 38 deletions(-) delete mode 100755 packaging/publish/publish_both.sh delete mode 100755 packaging/publish/publish_testing.sh diff --git a/packaging/publish/publish_both.sh b/packaging/publish/publish_both.sh deleted file mode 100755 index b86c20011e0..00000000000 --- a/packaging/publish/publish_both.sh +++ /dev/null @@ -1,22 +0,0 @@ -#! /usr/bin/env bash -version=5.4.2 - -# wget https://dl.grafana.com/oss/release/grafana_${version}_amd64.deb -# -# package_cloud push grafana/stable/debian/jessie grafana_${version}_amd64.deb -# package_cloud push grafana/stable/debian/wheezy grafana_${version}_amd64.deb -# package_cloud push grafana/stable/debian/stretch grafana_${version}_amd64.deb -# -# package_cloud push grafana/testing/debian/jessie grafana_${version}_amd64.deb -# package_cloud push grafana/testing/debian/wheezy grafana_${version}_amd64.deb --verbose -# package_cloud push grafana/testing/debian/stretch grafana_${version}_amd64.deb --verbose - -wget https://dl.grafana.com/oss/release/grafana-${version}-1.x86_64.rpm - -package_cloud push grafana/testing/el/6 grafana-${version}-1.x86_64.rpm --verbose -package_cloud push grafana/testing/el/7 grafana-${version}-1.x86_64.rpm --verbose - -package_cloud push grafana/stable/el/7 grafana-${version}-1.x86_64.rpm --verbose -package_cloud push grafana/stable/el/6 grafana-${version}-1.x86_64.rpm --verbose - -rm grafana*.{deb,rpm} diff --git a/packaging/publish/publish_testing.sh b/packaging/publish/publish_testing.sh deleted file mode 100755 index 9fd4e1f93b9..00000000000 --- a/packaging/publish/publish_testing.sh +++ /dev/null @@ -1,16 +0,0 @@ -#! /usr/bin/env bash -deb_ver=5.1.0-beta1 -rpm_ver=5.1.0-beta1 - -wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_${deb_ver}_amd64.deb - -package_cloud push grafana/testing/debian/jessie grafana_${deb_ver}_amd64.deb -package_cloud push grafana/testing/debian/wheezy grafana_${deb_ver}_amd64.deb -package_cloud push grafana/testing/debian/stretch grafana_${deb_ver}_amd64.deb - -wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-${rpm_ver}.x86_64.rpm - -package_cloud push grafana/testing/el/6 grafana-${rpm_ver}.x86_64.rpm -package_cloud push grafana/testing/el/7 grafana-${rpm_ver}.x86_64.rpm - -rm grafana*.{deb,rpm} From 65d7d466d781e2183041a46b7881e1e574ab50e0 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Thu, 5 May 2022 13:34:58 +0200 Subject: [PATCH 062/440] Alerting: Improved RBAC for Alert managers (#48344) * Initial support for grafana or cloud only alert managers * Handle missing alert manager * Refactor code, fix tests * Fix redirect url * Bring back the test * Improve missing alert manager warning, add useAlertManagerSourceName tests * Fix lint errors * Rename alert manager hook * Refactor alert manager label creation * Improve warnings' messages * Fix linter * Fix warning condition in RuleEditor --- public/app/features/alerting/routes.tsx | 5 +- .../features/alerting/unified/AlertGroups.tsx | 13 ++- .../features/alerting/unified/AmRoutes.tsx | 18 ++- .../features/alerting/unified/MuteTimings.tsx | 4 +- .../features/alerting/unified/Receivers.tsx | 14 ++- .../features/alerting/unified/RuleEditor.tsx | 2 +- .../features/alerting/unified/Silences.tsx | 27 +++-- .../unified/components/AlertManagerPicker.tsx | 36 +++--- .../components/NoAlertManagerWarning.tsx | 42 +++++++ .../components/admin/AlertmanagerConfig.tsx | 11 +- .../alert-groups/AlertGroupFilter.tsx | 10 +- .../components/amroutes/MuteTimingForm.tsx | 11 +- .../receivers/form/CloudReceiverForm.tsx | 3 +- .../hooks/useAlertManagerSourceName.test.tsx | 103 ++++++++++++++++++ .../hooks/useAlertManagerSourceName.ts | 39 ++++--- .../unified/hooks/useAlertManagerSources.ts | 7 ++ .../unified/hooks/useMuteTimingOptions.ts | 4 +- .../alerting/unified/utils/access-control.ts | 4 +- .../alerting/unified/utils/datasource.ts | 53 ++++++++- .../app/plugins/panel/alertGroups/module.tsx | 10 +- 20 files changed, 349 insertions(+), 67 deletions(-) create mode 100644 public/app/features/alerting/unified/components/NoAlertManagerWarning.tsx create mode 100644 public/app/features/alerting/unified/hooks/useAlertManagerSourceName.test.tsx create mode 100644 public/app/features/alerting/unified/hooks/useAlertManagerSources.ts diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx index 333a5cbce42..21759efc298 100644 --- a/public/app/features/alerting/routes.tsx +++ b/public/app/features/alerting/routes.tsx @@ -132,7 +132,10 @@ const unifiedRoutes: RouteDescriptor[] = [ }, { path: '/alerting/silences', - roles: evaluateAccess([AccessControlAction.AlertingInstanceRead], ['Editor', 'Admin']), + roles: evaluateAccess( + [AccessControlAction.AlertingInstanceRead, AccessControlAction.AlertingInstancesExternalRead], + ['Editor', 'Admin'] + ), component: SafeDynamicImport( () => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences') ), diff --git a/public/app/features/alerting/unified/AlertGroups.tsx b/public/app/features/alerting/unified/AlertGroups.tsx index 2513f9788eb..9aee9d3c81b 100644 --- a/public/app/features/alerting/unified/AlertGroups.tsx +++ b/public/app/features/alerting/unified/AlertGroups.tsx @@ -7,9 +7,11 @@ import { Alert, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { AlertingPageWrapper } from './components/AlertingPageWrapper'; +import { NoAlertManagerWarning } from './components/NoAlertManagerWarning'; import { AlertGroup } from './components/alert-groups/AlertGroup'; import { AlertGroupFilter } from './components/alert-groups/AlertGroupFilter'; import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName'; +import { useAlertManagersByPermission } from './hooks/useAlertManagerSources'; import { useFilteredAmGroups } from './hooks/useFilteredAmGroups'; import { useGroupedAlerts } from './hooks/useGroupedAlerts'; import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector'; @@ -19,7 +21,8 @@ import { getFiltersFromUrlParams } from './utils/misc'; import { initialAsyncRequestState } from './utils/redux'; const AlertGroups = () => { - const [alertManagerSourceName] = useAlertManagerSourceName(); + const alertManagers = useAlertManagersByPermission('instance'); + const [alertManagerSourceName] = useAlertManagerSourceName(alertManagers); const dispatch = useDispatch(); const [queryParams] = useQueryParams(); const { groupBy = [] } = getFiltersFromUrlParams(queryParams); @@ -48,6 +51,14 @@ const AlertGroups = () => { }; }, [dispatch, alertManagerSourceName]); + if (!alertManagerSourceName) { + return ( + + + + ); + } + return ( diff --git a/public/app/features/alerting/unified/AmRoutes.tsx b/public/app/features/alerting/unified/AmRoutes.tsx index 489de5941dc..eddb4012644 100644 --- a/public/app/features/alerting/unified/AmRoutes.tsx +++ b/public/app/features/alerting/unified/AmRoutes.tsx @@ -1,7 +1,6 @@ import { css } from '@emotion/css'; import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; -import { Redirect } from 'react-router-dom'; import { GrafanaTheme2 } from '@grafana/data'; import { Alert, LoadingPlaceholder, useStyles2, withErrorBoundary } from '@grafana/ui'; @@ -11,10 +10,12 @@ import { useCleanup } from '../../../core/hooks/useCleanup'; import { AlertManagerPicker } from './components/AlertManagerPicker'; import { AlertingPageWrapper } from './components/AlertingPageWrapper'; +import { NoAlertManagerWarning } from './components/NoAlertManagerWarning'; import { AmRootRoute } from './components/amroutes/AmRootRoute'; import { AmSpecificRouting } from './components/amroutes/AmSpecificRouting'; import { MuteTimingsTable } from './components/amroutes/MuteTimingsTable'; import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName'; +import { useAlertManagersByPermission } from './hooks/useAlertManagerSources'; import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector'; import { fetchAlertManagerConfigAction, updateAlertManagerConfigAction } from './state/actions'; import { AmRouteReceiver, FormAmRoute } from './types/amroutes'; @@ -26,7 +27,8 @@ const AmRoutes: FC = () => { const dispatch = useDispatch(); const styles = useStyles2(getStyles); const [isRootRouteEditMode, setIsRootRouteEditMode] = useState(false); - const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(); + const alertManagers = useAlertManagersByPermission('notification'); + const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers); const readOnly = alertManagerSourceName ? isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName) : true; @@ -100,12 +102,20 @@ const AmRoutes: FC = () => { }; if (!alertManagerSourceName) { - return ; + return ( + + + + ); } return ( - + {resultError && !resultLoading && ( {resultError.message || 'Unknown error.'} diff --git a/public/app/features/alerting/unified/MuteTimings.tsx b/public/app/features/alerting/unified/MuteTimings.tsx index 97221db287e..3bee4439115 100644 --- a/public/app/features/alerting/unified/MuteTimings.tsx +++ b/public/app/features/alerting/unified/MuteTimings.tsx @@ -8,6 +8,7 @@ import { MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types'; import MuteTimingForm from './components/amroutes/MuteTimingForm'; import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName'; +import { useAlertManagersByPermission } from './hooks/useAlertManagerSources'; import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector'; import { fetchAlertManagerConfigAction } from './state/actions'; import { initialAsyncRequestState } from './utils/redux'; @@ -15,7 +16,8 @@ import { initialAsyncRequestState } from './utils/redux'; const MuteTimings = () => { const [queryParams] = useQueryParams(); const dispatch = useDispatch(); - const [alertManagerSourceName] = useAlertManagerSourceName(); + const alertManagers = useAlertManagersByPermission('notification'); + const [alertManagerSourceName] = useAlertManagerSourceName(alertManagers); const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs); diff --git a/public/app/features/alerting/unified/Receivers.tsx b/public/app/features/alerting/unified/Receivers.tsx index 3335aa988ee..f06fdfb57a7 100644 --- a/public/app/features/alerting/unified/Receivers.tsx +++ b/public/app/features/alerting/unified/Receivers.tsx @@ -6,6 +6,7 @@ import { Alert, LoadingPlaceholder, withErrorBoundary } from '@grafana/ui'; import { AlertManagerPicker } from './components/AlertManagerPicker'; import { AlertingPageWrapper } from './components/AlertingPageWrapper'; +import { NoAlertManagerWarning } from './components/NoAlertManagerWarning'; import { EditReceiverView } from './components/receivers/EditReceiverView'; import { EditTemplateView } from './components/receivers/EditTemplateView'; import { GlobalConfigForm } from './components/receivers/GlobalConfigForm'; @@ -13,13 +14,15 @@ import { NewReceiverView } from './components/receivers/NewReceiverView'; import { NewTemplateView } from './components/receivers/NewTemplateView'; import { ReceiversAndTemplatesView } from './components/receivers/ReceiversAndTemplatesView'; import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName'; +import { useAlertManagersByPermission } from './hooks/useAlertManagerSources'; import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector'; import { fetchAlertManagerConfigAction, fetchGrafanaNotifiersAction } from './state/actions'; import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; import { initialAsyncRequestState } from './utils/redux'; const Receivers: FC = () => { - const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(); + const alertManagers = useAlertManagersByPermission('notification'); + const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers); const dispatch = useDispatch(); const location = useLocation(); @@ -54,7 +57,13 @@ const Receivers: FC = () => { const disableAmSelect = !isRoot; if (!alertManagerSourceName) { - return ; + return isRoot ? ( + + + + ) : ( + + ); } return ( @@ -63,6 +72,7 @@ const Receivers: FC = () => { current={alertManagerSourceName} disabled={disableAmSelect} onChange={setAlertManagerSourceName} + dataSources={alertManagers} /> {error && !loading && ( diff --git a/public/app/features/alerting/unified/RuleEditor.tsx b/public/app/features/alerting/unified/RuleEditor.tsx index 3b29811a926..57d66fc319f 100644 --- a/public/app/features/alerting/unified/RuleEditor.tsx +++ b/public/app/features/alerting/unified/RuleEditor.tsx @@ -75,7 +75,7 @@ const RuleEditor: FC = ({ match }) => { const { canCreateGrafanaRules, canCreateCloudRules, canEditRules } = useRulesAccess(); - if (!canCreateGrafanaRules && !canCreateCloudRules) { + if (!identifier && !canCreateGrafanaRules && !canCreateCloudRules) { return Sorry! You are not allowed to create rules.; } diff --git a/public/app/features/alerting/unified/Silences.tsx b/public/app/features/alerting/unified/Silences.tsx index 828a26a23df..c0c1eda8135 100644 --- a/public/app/features/alerting/unified/Silences.tsx +++ b/public/app/features/alerting/unified/Silences.tsx @@ -1,24 +1,26 @@ -import React, { FC, useEffect, useCallback } from 'react'; +import React, { FC, useCallback, useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { Redirect, Route, RouteChildrenProps, Switch, useLocation } from 'react-router-dom'; import { Alert, LoadingPlaceholder, withErrorBoundary } from '@grafana/ui'; import { Silence } from 'app/plugins/datasource/alertmanager/types'; -import { AccessControlAction } from 'app/types'; import { AlertManagerPicker } from './components/AlertManagerPicker'; import { AlertingPageWrapper } from './components/AlertingPageWrapper'; -import { Authorize } from './components/Authorize'; +import { NoAlertManagerWarning } from './components/NoAlertManagerWarning'; import SilencesEditor from './components/silences/SilencesEditor'; import SilencesTable from './components/silences/SilencesTable'; import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName'; +import { useAlertManagersByPermission } from './hooks/useAlertManagerSources'; import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector'; import { fetchAmAlertsAction, fetchSilencesAction } from './state/actions'; import { SILENCES_POLL_INTERVAL_MS } from './utils/constants'; import { AsyncRequestState, initialAsyncRequestState } from './utils/redux'; const Silences: FC = () => { - const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(); + const alertManagers = useAlertManagersByPermission('instance'); + const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers); + const dispatch = useDispatch(); const silences = useUnifiedAlertingSelector((state) => state.silences); const alertsRequests = useUnifiedAlertingSelector((state) => state.amAlerts); @@ -49,14 +51,23 @@ const Silences: FC = () => { const getSilenceById = useCallback((id: string) => result && result.find((silence) => silence.id === id), [result]); if (!alertManagerSourceName) { - return ; + return isRoot ? ( + + + + ) : ( + + ); } return ( - - - + {error && !loading && ( {error.message || 'Unknown error.'} diff --git a/public/app/features/alerting/unified/components/AlertManagerPicker.tsx b/public/app/features/alerting/unified/components/AlertManagerPicker.tsx index e775215be28..94f30bcc38c 100644 --- a/public/app/features/alerting/unified/components/AlertManagerPicker.tsx +++ b/public/app/features/alerting/unified/components/AlertManagerPicker.tsx @@ -1,39 +1,33 @@ import { css } from '@emotion/css'; import React, { FC, useMemo } from 'react'; -import { SelectableValue, GrafanaTheme2 } from '@grafana/data'; +import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { Field, Select, useStyles2 } from '@grafana/ui'; -import { getAllDataSources } from '../utils/config'; -import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; +import { AlertManagerDataSource, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; interface Props { onChange: (alertManagerSourceName: string) => void; current?: string; disabled?: boolean; + dataSources: AlertManagerDataSource[]; } -export const AlertManagerPicker: FC = ({ onChange, current, disabled = false }) => { +function getAlertManagerLabel(alertManager: AlertManagerDataSource) { + return alertManager.name === GRAFANA_RULES_SOURCE_NAME ? 'Grafana' : alertManager.name.slice(0, 37); +} + +export const AlertManagerPicker: FC = ({ onChange, current, dataSources, disabled = false }) => { const styles = useStyles2(getStyles); const options: Array> = useMemo(() => { - return [ - { - label: 'Grafana', - value: GRAFANA_RULES_SOURCE_NAME, - imgUrl: 'public/img/grafana_icon.svg', - meta: {}, - }, - ...getAllDataSources() - .filter((ds) => ds.type === DataSourceType.Alertmanager) - .map((ds) => ({ - label: ds.name.slice(0, 37), - value: ds.name, - imgUrl: ds.meta.info.logos.small, - meta: ds.meta, - })), - ]; - }, []); + return dataSources.map((ds) => ({ + label: getAlertManagerLabel(ds), + value: ds.name, + imgUrl: ds.imgUrl, + meta: ds.meta, + })); + }, [dataSources]); return ( ( + + We could not find any external Alertmanagers and you may not have access to the built-in Grafana Alertmanager. + +); + +const OtherAlertManagersAvailable = () => ( + + Selected Alertmanager no longer exists or you may not have permission to access it. + +); + +export const NoAlertManagerWarning = ({ availableAlertManagers }: Props) => { + const [_, setAlertManagerSourceName] = useAlertManagerSourceName(availableAlertManagers); + const hasOtherAMs = availableAlertManagers.length > 0; + + return ( +
    + {hasOtherAMs ? ( + <> + + + + ) : ( + + )} +
    + ); +}; diff --git a/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.tsx b/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.tsx index aaa09860c15..fdf286df090 100644 --- a/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.tsx +++ b/public/app/features/alerting/unified/components/admin/AlertmanagerConfig.tsx @@ -6,6 +6,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Alert, Button, ConfirmModal, TextArea, HorizontalGroup, Field, Form, useStyles2 } from '@grafana/ui'; import { useAlertManagerSourceName } from '../../hooks/useAlertManagerSourceName'; +import { useAlertManagersByPermission } from '../../hooks/useAlertManagerSources'; import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; import { deleteAlertManagerConfigAction, @@ -22,7 +23,9 @@ interface FormValues { export default function AlertmanagerConfig(): JSX.Element { const dispatch = useDispatch(); - const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(); + const alertManagers = useAlertManagersByPermission('notification'); + const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers); + const [showConfirmDeleteAMConfig, setShowConfirmDeleteAMConfig] = useState(false); const { loading: isDeleting } = useUnifiedAlertingSelector((state) => state.deleteAMConfig); const { loading: isSaving } = useUnifiedAlertingSelector((state) => state.saveAMConfig); @@ -75,7 +78,11 @@ export default function AlertmanagerConfig(): JSX.Element { return (
    - + {loadingError && !loading && ( {loadingError.message || 'Unknown error.'} diff --git a/public/app/features/alerting/unified/components/alert-groups/AlertGroupFilter.tsx b/public/app/features/alerting/unified/components/alert-groups/AlertGroupFilter.tsx index 7af9efe0958..2950864ac2b 100644 --- a/public/app/features/alerting/unified/components/alert-groups/AlertGroupFilter.tsx +++ b/public/app/features/alerting/unified/components/alert-groups/AlertGroupFilter.tsx @@ -7,6 +7,7 @@ import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { AlertmanagerGroup, AlertState } from 'app/plugins/datasource/alertmanager/types'; import { useAlertManagerSourceName } from '../../hooks/useAlertManagerSourceName'; +import { useAlertManagersByPermission } from '../../hooks/useAlertManagerSources'; import { getFiltersFromUrlParams } from '../../utils/misc'; import { AlertManagerPicker } from '../AlertManagerPicker'; @@ -24,7 +25,8 @@ export const AlertGroupFilter = ({ groups }: Props) => { const { groupBy = [], queryString, alertState } = getFiltersFromUrlParams(queryParams); const matcherFilterKey = `matcher-${filterKey}`; - const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(); + const alertManagers = useAlertManagersByPermission('instance'); + const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers); const styles = useStyles2(getStyles); const clearFilters = () => { @@ -40,7 +42,11 @@ export const AlertGroupFilter = ({ groups }: Props) => { return (
    - +
    { const MuteTimingForm = ({ muteTiming, showError }: Props) => { const dispatch = useDispatch(); - const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(); + const alertManagers = useAlertManagersByPermission('notification'); + const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName(alertManagers); const styles = useStyles2(getStyles); const defaultAmCortexConfig = { alertmanager_config: {}, template_files: {} }; @@ -101,7 +103,12 @@ const MuteTimingForm = ({ muteTiming, showError }: Props) => { return ( - + {result && !loading && (
    diff --git a/public/app/features/alerting/unified/components/receivers/form/CloudReceiverForm.tsx b/public/app/features/alerting/unified/components/receivers/form/CloudReceiverForm.tsx index 1b9bebabbb3..2f07e7dd0e8 100644 --- a/public/app/features/alerting/unified/components/receivers/form/CloudReceiverForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/CloudReceiverForm.tsx @@ -8,7 +8,6 @@ import { updateAlertManagerConfigAction } from '../../../state/actions'; import { CloudChannelValues, ReceiverFormValues, CloudChannelMap } from '../../../types/receiver-form'; import { cloudNotifierTypes } from '../../../utils/cloud-alertmanager-notifier-types'; import { isVanillaPrometheusAlertManagerDataSource } from '../../../utils/datasource'; -import { makeAMLink } from '../../../utils/misc'; import { cloudReceiverToFormValues, formValuesToCloudReceiver, @@ -53,7 +52,7 @@ export const CloudReceiverForm: FC = ({ existing, alertManagerSourceName, oldConfig: config, alertManagerSourceName, successMessage: existing ? 'Contact point updated.' : 'Contact point created.', - redirectPath: makeAMLink('/alerting/notifications', alertManagerSourceName), + redirectPath: '/alerting/notifications', }) ); }; diff --git a/public/app/features/alerting/unified/hooks/useAlertManagerSourceName.test.tsx b/public/app/features/alerting/unified/hooks/useAlertManagerSourceName.test.tsx new file mode 100644 index 00000000000..aaf5a8a774c --- /dev/null +++ b/public/app/features/alerting/unified/hooks/useAlertManagerSourceName.test.tsx @@ -0,0 +1,103 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { createMemoryHistory } from 'history'; +import React from 'react'; +import { MemoryRouter, Router } from 'react-router-dom'; + +import store from 'app/core/store'; + +import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY } from '../utils/constants'; +import { AlertManagerDataSource, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; + +import { useAlertManagerSourceName } from './useAlertManagerSourceName'; + +const grafanaAm: AlertManagerDataSource = { + name: GRAFANA_RULES_SOURCE_NAME, + imgUrl: '', +}; + +const externalAmProm: AlertManagerDataSource = { + name: 'PrometheusAm', + imgUrl: '', +}; + +const externalAmMimir: AlertManagerDataSource = { + name: 'MimirAm', + imgUrl: '', +}; + +describe('useAlertManagerSourceName', () => { + it('Should return undefined alert manager name when there are no available alert managers', () => { + const wrapper: React.FC = ({ children }) => {children}; + const { result } = renderHook(() => useAlertManagerSourceName([]), { wrapper }); + + const [alertManager] = result.current; + + expect(alertManager).toBeUndefined(); + }); + + it('Should return Grafana AM when it is available and no alert manager query param exists', () => { + const wrapper: React.FC = ({ children }) => {children}; + + const availableAMs = [grafanaAm, externalAmProm, externalAmMimir]; + const { result } = renderHook(() => useAlertManagerSourceName(availableAMs), { wrapper }); + + const [alertManager] = result.current; + + expect(alertManager).toBe(grafanaAm.name); + }); + + it('Should return alert manager included in the query param when available', () => { + const history = createMemoryHistory(); + history.push({ search: `alertmanager=${externalAmProm.name}` }); + const wrapper: React.FC = ({ children }) => {children}; + + const availableAMs = [grafanaAm, externalAmProm, externalAmMimir]; + const { result } = renderHook(() => useAlertManagerSourceName(availableAMs), { wrapper }); + + const [alertManager] = result.current; + + expect(alertManager).toBe(externalAmProm.name); + }); + + it('Should return undefined if alert manager included in the query is not available', () => { + const history = createMemoryHistory(); + history.push({ search: `alertmanager=Not available external AM` }); + const wrapper: React.FC = ({ children }) => {children}; + + const availableAMs = [grafanaAm, externalAmProm, externalAmMimir]; + + const { result } = renderHook(() => useAlertManagerSourceName(availableAMs), { wrapper }); + + const [alertManager] = result.current; + + expect(alertManager).toBe(undefined); + }); + + it('Should return alert manager from store if available and query is empty', () => { + const wrapper: React.FC = ({ children }) => {children}; + + const availableAMs = [grafanaAm, externalAmProm, externalAmMimir]; + store.set(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, externalAmProm.name); + + const { result } = renderHook(() => useAlertManagerSourceName(availableAMs), { wrapper }); + + const [alertManager] = result.current; + + expect(alertManager).toBe(externalAmProm.name); + }); + + it('Should prioritize the alert manager from query over store', () => { + const history = createMemoryHistory(); + history.push({ search: `alertmanager=${externalAmProm.name}` }); + const wrapper: React.FC = ({ children }) => {children}; + + const availableAMs = [grafanaAm, externalAmProm, externalAmMimir]; + store.set(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, externalAmMimir.name); + + const { result } = renderHook(() => useAlertManagerSourceName(availableAMs), { wrapper }); + + const [alertManager] = result.current; + + expect(alertManager).toBe(externalAmProm.name); + }); +}); diff --git a/public/app/features/alerting/unified/hooks/useAlertManagerSourceName.ts b/public/app/features/alerting/unified/hooks/useAlertManagerSourceName.ts index 8fe2c34a600..e453588911c 100644 --- a/public/app/features/alerting/unified/hooks/useAlertManagerSourceName.ts +++ b/public/app/features/alerting/unified/hooks/useAlertManagerSourceName.ts @@ -4,25 +4,31 @@ import { useQueryParams } from 'app/core/hooks/useQueryParams'; import store from 'app/core/store'; import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from '../utils/constants'; -import { getAlertManagerDataSources, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; +import { AlertManagerDataSource, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; -function isAlertManagerSource(alertManagerSourceName: string): boolean { - return ( - alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME || - !!getAlertManagerDataSources().find((ds) => ds.name === alertManagerSourceName) +function useIsAlertManagerAvailable(availableAlertManagers: AlertManagerDataSource[]) { + return useCallback( + (alertManagerName: string) => { + const availableAlertManagersNames = availableAlertManagers.map((am) => am.name); + return availableAlertManagersNames.includes(alertManagerName); + }, + [availableAlertManagers] ); } -/* this will return am name either from query params or from local storage or a default (grafana). - * - * fallbackUrl - if provided, will redirect to this url if alertmanager provided in query no longer +/* This will return am name either from query params or from local storage or a default (grafana). + * Due to RBAC permissions Grafana Managed Alert manager or external alert managers may not be available + * In the worst case neihter GMA nor external alert manager is available */ -export function useAlertManagerSourceName(): [string | undefined, (alertManagerSourceName: string) => void] { +export function useAlertManagerSourceName( + availableAlertManagers: AlertManagerDataSource[] +): [string | undefined, (alertManagerSourceName: string) => void] { const [queryParams, updateQueryParams] = useQueryParams(); + const isAlertManagerAvailable = useIsAlertManagerAvailable(availableAlertManagers); const update = useCallback( (alertManagerSourceName: string) => { - if (!isAlertManagerSource(alertManagerSourceName)) { + if (!isAlertManagerAvailable(alertManagerSourceName)) { return; } if (alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME) { @@ -33,24 +39,29 @@ export function useAlertManagerSourceName(): [string | undefined, (alertManagerS updateQueryParams({ [ALERTMANAGER_NAME_QUERY_KEY]: alertManagerSourceName }); } }, - [updateQueryParams] + [updateQueryParams, isAlertManagerAvailable] ); const querySource = queryParams[ALERTMANAGER_NAME_QUERY_KEY]; if (querySource && typeof querySource === 'string') { - if (isAlertManagerSource(querySource)) { + if (isAlertManagerAvailable(querySource)) { return [querySource, update]; } else { // non existing alertmanager return [undefined, update]; } } + const storeSource = store.get(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY); - if (storeSource && typeof storeSource === 'string' && isAlertManagerSource(storeSource)) { + if (storeSource && typeof storeSource === 'string' && isAlertManagerAvailable(storeSource)) { update(storeSource); return [storeSource, update]; } - return [GRAFANA_RULES_SOURCE_NAME, update]; + if (isAlertManagerAvailable(GRAFANA_RULES_SOURCE_NAME)) { + return [GRAFANA_RULES_SOURCE_NAME, update]; + } + + return [undefined, update]; } diff --git a/public/app/features/alerting/unified/hooks/useAlertManagerSources.ts b/public/app/features/alerting/unified/hooks/useAlertManagerSources.ts new file mode 100644 index 00000000000..25177b0c1ae --- /dev/null +++ b/public/app/features/alerting/unified/hooks/useAlertManagerSources.ts @@ -0,0 +1,7 @@ +import { useMemo } from 'react'; + +import { getAlertManagerDataSourcesByPermission } from '../utils/datasource'; + +export function useAlertManagersByPermission(accessType: 'instance' | 'notification') { + return useMemo(() => getAlertManagerDataSourcesByPermission(accessType), [accessType]); +} diff --git a/public/app/features/alerting/unified/hooks/useMuteTimingOptions.ts b/public/app/features/alerting/unified/hooks/useMuteTimingOptions.ts index 843a7ec3197..2f702ad3836 100644 --- a/public/app/features/alerting/unified/hooks/useMuteTimingOptions.ts +++ b/public/app/features/alerting/unified/hooks/useMuteTimingOptions.ts @@ -7,10 +7,12 @@ import { timeIntervalToString } from '../utils/alertmanager'; import { initialAsyncRequestState } from '../utils/redux'; import { useAlertManagerSourceName } from './useAlertManagerSourceName'; +import { useAlertManagersByPermission } from './useAlertManagerSources'; import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector'; export function useMuteTimingOptions(): Array> { - const [alertManagerSourceName] = useAlertManagerSourceName(); + const alertManagers = useAlertManagersByPermission('notification'); + const [alertManagerSourceName] = useAlertManagerSourceName(alertManagers); const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs); return useMemo(() => { diff --git a/public/app/features/alerting/unified/utils/access-control.ts b/public/app/features/alerting/unified/utils/access-control.ts index 9b736179118..57d785df5ec 100644 --- a/public/app/features/alerting/unified/utils/access-control.ts +++ b/public/app/features/alerting/unified/utils/access-control.ts @@ -9,7 +9,7 @@ function getRulesSourceType(alertManagerSourceName: string): RulesSourceType { return isGrafanaRulesSource(alertManagerSourceName) ? 'grafana' : 'external'; } -const instancesPermissions = { +export const instancesPermissions = { read: { grafana: AccessControlAction.AlertingInstanceRead, external: AccessControlAction.AlertingInstancesExternalRead, @@ -28,7 +28,7 @@ const instancesPermissions = { }, }; -const notificationsPermissions = { +export const notificationsPermissions = { read: { grafana: AccessControlAction.AlertingNotificationsRead, external: AccessControlAction.AlertingNotificationsExternalRead, diff --git a/public/app/features/alerting/unified/utils/datasource.ts b/public/app/features/alerting/unified/utils/datasource.ts index b13997c4d78..216105023c5 100644 --- a/public/app/features/alerting/unified/utils/datasource.ts +++ b/public/app/features/alerting/unified/utils/datasource.ts @@ -1,9 +1,10 @@ -import { DataSourceJsonData, DataSourceInstanceSettings } from '@grafana/data'; +import { DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data'; import { contextSrv } from 'app/core/services/context_srv'; import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types'; import { AccessControlAction } from 'app/types'; import { RulesSource } from 'app/types/unified-alerting'; +import { instancesPermissions, notificationsPermissions } from './access-control'; import { getAllDataSources } from './config'; export const GRAFANA_RULES_SOURCE_NAME = 'grafana'; @@ -15,6 +16,12 @@ export enum DataSourceType { Prometheus = 'prometheus', } +export interface AlertManagerDataSource { + name: string; + imgUrl: string; + meta?: DataSourceInstanceSettings['meta']; +} + export const RulesDataSourceTypes: string[] = [DataSourceType.Loki, DataSourceType.Prometheus]; export function getRulesDataSources() { @@ -37,6 +44,50 @@ export function getAlertManagerDataSources() { .sort((a, b) => a.name.localeCompare(b.name)); } +const grafanaAlertManagerDataSource: AlertManagerDataSource = { + name: GRAFANA_RULES_SOURCE_NAME, + imgUrl: 'public/img/grafana_icon.svg', +}; + +// Used only as a fallback for Alert Group plugin +export function getAllAlertManagerDataSources(): AlertManagerDataSource[] { + return [ + grafanaAlertManagerDataSource, + ...getAlertManagerDataSources().map((ds) => ({ + name: ds.name, + displayName: ds.name, + imgUrl: ds.meta.info.logos.small, + meta: ds.meta, + })), + ]; +} + +export function getAlertManagerDataSourcesByPermission( + permission: 'instance' | 'notification' +): AlertManagerDataSource[] { + const availableDataSources: AlertManagerDataSource[] = []; + const permissions = { + instance: instancesPermissions.read, + notification: notificationsPermissions.read, + }; + + if (contextSrv.hasPermission(permissions[permission].grafana)) { + availableDataSources.push(grafanaAlertManagerDataSource); + } + + if (contextSrv.hasPermission(permissions[permission].external)) { + const cloudSources = getAlertManagerDataSources().map((ds) => ({ + name: ds.name, + displayName: ds.name, + imgUrl: ds.meta.info.logos.small, + meta: ds.meta, + })); + availableDataSources.push(...cloudSources); + } + + return availableDataSources; +} + export function getLotexDataSourceByName(dataSourceName: string): DataSourceInstanceSettings { const dataSource = getDataSourceByName(dataSourceName); if (!dataSource) { diff --git a/public/app/plugins/panel/alertGroups/module.tsx b/public/app/plugins/panel/alertGroups/module.tsx index 94ccb5c7e26..b2e37a74ff9 100644 --- a/public/app/plugins/panel/alertGroups/module.tsx +++ b/public/app/plugins/panel/alertGroups/module.tsx @@ -1,8 +1,11 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { PanelPlugin } from '@grafana/data'; import { AlertManagerPicker } from 'app/features/alerting/unified/components/AlertManagerPicker'; -import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; +import { + getAllAlertManagerDataSources, + GRAFANA_RULES_SOURCE_NAME, +} from 'app/features/alerting/unified/utils/datasource'; import { AlertGroupsPanel } from './AlertGroupsPanel'; import { AlertGroupPanelOptions } from './types'; @@ -16,12 +19,15 @@ export const plugin = new PanelPlugin(AlertGroupsPanel). defaultValue: GRAFANA_RULES_SOURCE_NAME, category: ['Options'], editor: function RenderAlertmanagerPicker(props) { + const alertManagers = useMemo(getAllAlertManagerDataSources, []); + return ( { return props.onChange(alertManagerSourceName); }} + dataSources={alertManagers} /> ); }, From 610247d52a971d66a0130043e7f240e70819b67c Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> Date: Thu, 5 May 2022 14:58:32 +0300 Subject: [PATCH 063/440] Alerting: modify ruler endpoints for proxying using the datasource UID (#48046) * Modify ruler endpoints to expect the data source UID * Update frontend * Apply suggestion from code review --- pkg/services/ngalert/api/authorization.go | 24 +++---- pkg/services/ngalert/api/fork_ruler.go | 12 ++-- .../ngalert/api/generated_base_api_ruler.go | 36 +++++----- pkg/services/ngalert/api/lotex_ruler.go | 9 ++- pkg/services/ngalert/api/lotex_ruler_test.go | 24 +++---- .../api/test-data/ruler-cortex-recipient.http | 27 +++---- .../api/test-data/ruler-loki-recipient.http | 26 +++---- .../api/tooling/definitions/alertmanager.go | 6 +- .../api/tooling/definitions/cortex-ruler.go | 12 ++-- pkg/services/ngalert/api/tooling/post.json | 71 ++++++++++--------- pkg/services/ngalert/api/tooling/spec.json | 63 ++++++++-------- public/api-merged.json | 71 ++++++++++--------- .../alerting/unified/api/ruler.test.ts | 12 ++-- .../features/alerting/unified/api/ruler.ts | 6 +- 14 files changed, 200 insertions(+), 199 deletions(-) diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index 560c10ca168..74c2d9fdb21 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -74,18 +74,18 @@ func (api *API) authorize(method, path string) web.Handler { eval = ac.EvalPermission(ac.ActionAlertingRuleRead) // Lotex Paths - case http.MethodDelete + "/api/ruler/{DatasourceID}/api/v1/rules/{Namespace}": - eval = ac.EvalPermission(ac.ActionAlertingRuleExternalWrite, datasources.ScopeProvider.GetResourceScope(ac.Parameter(":DatasourceID"))) - case http.MethodDelete + "/api/ruler/{DatasourceID}/api/v1/rules/{Namespace}/{Groupname}": - eval = ac.EvalPermission(ac.ActionAlertingRuleExternalWrite, datasources.ScopeProvider.GetResourceScope(ac.Parameter(":DatasourceID"))) - case http.MethodGet + "/api/ruler/{DatasourceID}/api/v1/rules/{Namespace}": - eval = ac.EvalPermission(ac.ActionAlertingRuleExternalRead, datasources.ScopeProvider.GetResourceScope(ac.Parameter(":DatasourceID"))) - case http.MethodGet + "/api/ruler/{DatasourceID}/api/v1/rules/{Namespace}/{Groupname}": - eval = ac.EvalPermission(ac.ActionAlertingRuleExternalRead, datasources.ScopeProvider.GetResourceScope(ac.Parameter(":DatasourceID"))) - case http.MethodGet + "/api/ruler/{DatasourceID}/api/v1/rules": - eval = ac.EvalPermission(ac.ActionAlertingRuleExternalRead, datasources.ScopeProvider.GetResourceScope(ac.Parameter(":DatasourceID"))) - case http.MethodPost + "/api/ruler/{DatasourceID}/api/v1/rules/{Namespace}": - eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalWrite, datasources.ScopeProvider.GetResourceScope(ac.Parameter(":DatasourceID"))) + case http.MethodDelete + "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}": + eval = ac.EvalPermission(ac.ActionAlertingRuleExternalWrite, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID"))) + case http.MethodDelete + "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}/{Groupname}": + eval = ac.EvalPermission(ac.ActionAlertingRuleExternalWrite, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID"))) + case http.MethodGet + "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}": + eval = ac.EvalPermission(ac.ActionAlertingRuleExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID"))) + case http.MethodGet + "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}/{Groupname}": + eval = ac.EvalPermission(ac.ActionAlertingRuleExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID"))) + case http.MethodGet + "/api/ruler/{DatasourceUID}/api/v1/rules": + eval = ac.EvalPermission(ac.ActionAlertingRuleExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID"))) + case http.MethodPost + "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}": + eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalWrite, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID"))) // Lotex Prometheus-compatible Paths case http.MethodGet + "/api/prometheus/{DatasourceID}/api/v1/rules": diff --git a/pkg/services/ngalert/api/fork_ruler.go b/pkg/services/ngalert/api/fork_ruler.go index d43d31c632c..6b6ffb8919c 100644 --- a/pkg/services/ngalert/api/fork_ruler.go +++ b/pkg/services/ngalert/api/fork_ruler.go @@ -26,7 +26,7 @@ func NewForkedRuler(datasourceCache datasources.CacheService, lotex *LotexRuler, } func (f *ForkedRulerApi) forkRouteDeleteNamespaceRulesConfig(ctx *models.ReqContext) response.Response { - t, err := backendType(ctx, f.DatasourceCache) + t, err := backendTypeByUID(ctx, f.DatasourceCache) if err != nil { return ErrResp(400, err, "") } @@ -39,7 +39,7 @@ func (f *ForkedRulerApi) forkRouteDeleteNamespaceRulesConfig(ctx *models.ReqCont } func (f *ForkedRulerApi) forkRouteDeleteRuleGroupConfig(ctx *models.ReqContext) response.Response { - t, err := backendType(ctx, f.DatasourceCache) + t, err := backendTypeByUID(ctx, f.DatasourceCache) if err != nil { return ErrResp(400, err, "") } @@ -52,7 +52,7 @@ func (f *ForkedRulerApi) forkRouteDeleteRuleGroupConfig(ctx *models.ReqContext) } func (f *ForkedRulerApi) forkRouteGetNamespaceRulesConfig(ctx *models.ReqContext) response.Response { - t, err := backendType(ctx, f.DatasourceCache) + t, err := backendTypeByUID(ctx, f.DatasourceCache) if err != nil { return ErrResp(400, err, "") } @@ -65,7 +65,7 @@ func (f *ForkedRulerApi) forkRouteGetNamespaceRulesConfig(ctx *models.ReqContext } func (f *ForkedRulerApi) forkRouteGetRulegGroupConfig(ctx *models.ReqContext) response.Response { - t, err := backendType(ctx, f.DatasourceCache) + t, err := backendTypeByUID(ctx, f.DatasourceCache) if err != nil { return ErrResp(400, err, "") } @@ -78,7 +78,7 @@ func (f *ForkedRulerApi) forkRouteGetRulegGroupConfig(ctx *models.ReqContext) re } func (f *ForkedRulerApi) forkRouteGetRulesConfig(ctx *models.ReqContext) response.Response { - t, err := backendType(ctx, f.DatasourceCache) + t, err := backendTypeByUID(ctx, f.DatasourceCache) if err != nil { return ErrResp(400, err, "") } @@ -91,7 +91,7 @@ func (f *ForkedRulerApi) forkRouteGetRulesConfig(ctx *models.ReqContext) respons } func (f *ForkedRulerApi) forkRoutePostNameRulesConfig(ctx *models.ReqContext, conf apimodels.PostableRuleGroupConfig) response.Response { - backendType, err := backendType(ctx, f.DatasourceCache) + backendType, err := backendTypeByUID(ctx, f.DatasourceCache) if err != nil { return ErrResp(400, err, "") } diff --git a/pkg/services/ngalert/api/generated_base_api_ruler.go b/pkg/services/ngalert/api/generated_base_api_ruler.go index 751646342fc..3d2979b2ca3 100644 --- a/pkg/services/ngalert/api/generated_base_api_ruler.go +++ b/pkg/services/ngalert/api/generated_base_api_ruler.go @@ -113,21 +113,21 @@ func (api *API) RegisterRulerApiEndpoints(srv RulerApiForkingService, m *metrics ), ) group.Delete( - toMacaronPath("/api/ruler/{DatasourceID}/api/v1/rules/{Namespace}"), - api.authorize(http.MethodDelete, "/api/ruler/{DatasourceID}/api/v1/rules/{Namespace}"), + toMacaronPath("/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}"), + api.authorize(http.MethodDelete, "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}"), metrics.Instrument( http.MethodDelete, - "/api/ruler/{DatasourceID}/api/v1/rules/{Namespace}", + "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}", srv.RouteDeleteNamespaceRulesConfig, m, ), ) group.Delete( - toMacaronPath("/api/ruler/{DatasourceID}/api/v1/rules/{Namespace}/{Groupname}"), - api.authorize(http.MethodDelete, "/api/ruler/{DatasourceID}/api/v1/rules/{Namespace}/{Groupname}"), + toMacaronPath("/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}/{Groupname}"), + api.authorize(http.MethodDelete, "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}/{Groupname}"), metrics.Instrument( http.MethodDelete, - "/api/ruler/{DatasourceID}/api/v1/rules/{Namespace}/{Groupname}", + "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}/{Groupname}", srv.RouteDeleteRuleGroupConfig, m, ), @@ -163,31 +163,31 @@ func (api *API) RegisterRulerApiEndpoints(srv RulerApiForkingService, m *metrics ), ) group.Get( - toMacaronPath("/api/ruler/{DatasourceID}/api/v1/rules/{Namespace}"), - api.authorize(http.MethodGet, "/api/ruler/{DatasourceID}/api/v1/rules/{Namespace}"), + toMacaronPath("/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}"), + api.authorize(http.MethodGet, "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}"), metrics.Instrument( http.MethodGet, - "/api/ruler/{DatasourceID}/api/v1/rules/{Namespace}", + "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}", srv.RouteGetNamespaceRulesConfig, m, ), ) group.Get( - toMacaronPath("/api/ruler/{DatasourceID}/api/v1/rules/{Namespace}/{Groupname}"), - api.authorize(http.MethodGet, "/api/ruler/{DatasourceID}/api/v1/rules/{Namespace}/{Groupname}"), + toMacaronPath("/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}/{Groupname}"), + api.authorize(http.MethodGet, "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}/{Groupname}"), metrics.Instrument( http.MethodGet, - "/api/ruler/{DatasourceID}/api/v1/rules/{Namespace}/{Groupname}", + "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}/{Groupname}", srv.RouteGetRulegGroupConfig, m, ), ) group.Get( - toMacaronPath("/api/ruler/{DatasourceID}/api/v1/rules"), - api.authorize(http.MethodGet, "/api/ruler/{DatasourceID}/api/v1/rules"), + toMacaronPath("/api/ruler/{DatasourceUID}/api/v1/rules"), + api.authorize(http.MethodGet, "/api/ruler/{DatasourceUID}/api/v1/rules"), metrics.Instrument( http.MethodGet, - "/api/ruler/{DatasourceID}/api/v1/rules", + "/api/ruler/{DatasourceUID}/api/v1/rules", srv.RouteGetRulesConfig, m, ), @@ -203,11 +203,11 @@ func (api *API) RegisterRulerApiEndpoints(srv RulerApiForkingService, m *metrics ), ) group.Post( - toMacaronPath("/api/ruler/{DatasourceID}/api/v1/rules/{Namespace}"), - api.authorize(http.MethodPost, "/api/ruler/{DatasourceID}/api/v1/rules/{Namespace}"), + toMacaronPath("/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}"), + api.authorize(http.MethodPost, "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}"), metrics.Instrument( http.MethodPost, - "/api/ruler/{DatasourceID}/api/v1/rules/{Namespace}", + "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}", srv.RoutePostNameRulesConfig, m, ), diff --git a/pkg/services/ngalert/api/lotex_ruler.go b/pkg/services/ngalert/api/lotex_ruler.go index efdfbd564b7..bf0e643fb79 100644 --- a/pkg/services/ngalert/api/lotex_ruler.go +++ b/pkg/services/ngalert/api/lotex_ruler.go @@ -5,7 +5,6 @@ import ( "fmt" "net/http" "net/url" - "strconv" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/web" @@ -176,12 +175,12 @@ func (r *LotexRuler) RoutePostNameRulesConfig(ctx *models.ReqContext, conf apimo } func (r *LotexRuler) validateAndGetPrefix(ctx *models.ReqContext) (string, error) { - datasourceID, err := strconv.ParseInt(web.Params(ctx.Req)[":DatasourceID"], 10, 64) - if err != nil { - return "", fmt.Errorf("datasource ID is invalid") + datasourceUID := web.Params(ctx.Req)[":DatasourceUID"] + if datasourceUID == "" { + return "", fmt.Errorf("datasource UID is invalid") } - ds, err := r.DataProxy.DataSourceCache.GetDatasource(ctx.Req.Context(), datasourceID, ctx.SignedInUser, ctx.SkipCache) + ds, err := r.DataProxy.DataSourceCache.GetDatasourceByUID(ctx.Req.Context(), datasourceUID, ctx.SignedInUser, ctx.SkipCache) if err != nil { return "", err } diff --git a/pkg/services/ngalert/api/lotex_ruler_test.go b/pkg/services/ngalert/api/lotex_ruler_test.go index d1afa5a946a..0299aef846a 100644 --- a/pkg/services/ngalert/api/lotex_ruler_test.go +++ b/pkg/services/ngalert/api/lotex_ruler_test.go @@ -25,64 +25,64 @@ func TestLotexRuler_ValidateAndGetPrefix(t *testing.T) { err error }{ { - name: "with an invalid datasource ID", - namedParams: map[string]string{":DatasourceID": "AAABBB"}, - err: errors.New("datasource ID is invalid"), + name: "with an empty datasource UID", + namedParams: map[string]string{":DatasourceUID": ""}, + err: errors.New("datasource UID is invalid"), }, { name: "with an error while trying to fetch the datasource", - namedParams: map[string]string{":DatasourceID": "164"}, + namedParams: map[string]string{":DatasourceUID": "d164"}, datasourceCache: fakeCacheService{err: models.ErrDataSourceNotFound}, err: errors.New("data source not found"), }, { name: "with an empty datasource URL", - namedParams: map[string]string{":DatasourceID": "164"}, + namedParams: map[string]string{":DatasourceUID": "d164"}, datasourceCache: fakeCacheService{datasource: &models.DataSource{}}, err: errors.New("URL for this data source is empty"), }, { name: "with an unsupported datasource type", - namedParams: map[string]string{":DatasourceID": "164"}, + namedParams: map[string]string{":DatasourceUID": "d164"}, datasourceCache: fakeCacheService{datasource: &models.DataSource{Url: "http://loki.com"}}, err: errors.New("unexpected datasource type. expecting loki or prometheus"), }, { name: "with a Loki datasource", - namedParams: map[string]string{":DatasourceID": "164"}, + namedParams: map[string]string{":DatasourceUID": "d164"}, datasourceCache: fakeCacheService{datasource: &models.DataSource{Url: "http://loki.com", Type: LokiDatasourceType}}, expected: "/api/prom/rules", }, { name: "with a Prometheus datasource", - namedParams: map[string]string{":DatasourceID": "164"}, + namedParams: map[string]string{":DatasourceUID": "d164"}, datasourceCache: fakeCacheService{datasource: &models.DataSource{Url: "http://loki.com", Type: PrometheusDatasourceType}}, expected: "/rules", }, { name: "with a Prometheus datasource and subtype of Cortex", - namedParams: map[string]string{":DatasourceID": "164"}, + namedParams: map[string]string{":DatasourceUID": "d164"}, urlParams: "?subtype=cortex", datasourceCache: fakeCacheService{datasource: &models.DataSource{Url: "http://loki.com", Type: PrometheusDatasourceType}}, expected: "/rules", }, { name: "with a Prometheus datasource and subtype of Mimir", - namedParams: map[string]string{":DatasourceID": "164"}, + namedParams: map[string]string{":DatasourceUID": "d164"}, urlParams: "?subtype=mimir", datasourceCache: fakeCacheService{datasource: &models.DataSource{Url: "http://loki.com", Type: PrometheusDatasourceType}}, expected: "/config/v1/rules", }, { name: "with a Prometheus datasource and subtype of Prometheus", - namedParams: map[string]string{":DatasourceID": "164"}, + namedParams: map[string]string{":DatasourceUID": "d164"}, urlParams: "?subtype=prometheus", datasourceCache: fakeCacheService{datasource: &models.DataSource{Url: "http://loki.com", Type: PrometheusDatasourceType}}, expected: "/rules", }, { name: "with a Prometheus datasource and no subtype", - namedParams: map[string]string{":DatasourceID": "164"}, + namedParams: map[string]string{":DatasourceUID": "d164"}, datasourceCache: fakeCacheService{datasource: &models.DataSource{Url: "http://loki.com", Type: PrometheusDatasourceType}}, expected: "/rules", }, diff --git a/pkg/services/ngalert/api/test-data/ruler-cortex-recipient.http b/pkg/services/ngalert/api/test-data/ruler-cortex-recipient.http index f0dda6c2f67..dc4ddf9ef49 100644 --- a/pkg/services/ngalert/api/test-data/ruler-cortex-recipient.http +++ b/pkg/services/ngalert/api/test-data/ruler-cortex-recipient.http @@ -1,10 +1,10 @@ -@prometheusDatasourceID = 35 +@prometheusDatasourceUID = 7DEsN5_Mk // should point to an existing folder named alerting @namespace1 = test // create/update test namespace group42 rulegroup -POST http://admin:admin@localhost:3000/api/ruler/{{prometheusDatasourceID}}/api/v1/rules/{{namespace1}} +POST http://admin:admin@localhost:3000/api/ruler/{{prometheusDatasourceUID}}/api/v1/rules/{{namespace1}} content-type: application/json { @@ -20,7 +20,7 @@ content-type: application/json ### // create group101 -POST http://admin:admin@localhost:3000/api/ruler/{{prometheusDatasourceID}}/api/v1/rules/{{namespace1}} +POST http://admin:admin@localhost:3000/api/ruler/{{prometheusDatasourceUID}}/api/v1/rules/{{namespace1}} content-type: application/json { @@ -36,40 +36,41 @@ content-type: application/json ### // get group42 rules -GET http://admin:admin@localhost:3000/api/ruler/{{prometheusDatasourceID}}/api/v1/rules/{{namespace1}}/group42 +GET http://admin:admin@localhost:3000/api/ruler/{{prometheusDatasourceUID}}/api/v1/rules/{{namespace1}}/group42 ### // get group101 rules -GET http://admin:admin@localhost:3000/api/ruler/{{prometheusDatasourceID}}/api/v1/rules/{{namespace1}}/group101 +GET http://admin:admin@localhost:3000/api/ruler/{{prometheusDatasourceUID}}/api/v1/rules/{{namespace1}}/group101 ### // get namespace rules -GET http://admin:admin@localhost:3000/api/ruler/{{prometheusDatasourceID}}/api/v1/rules/{{namespace1}} +GET http://admin:admin@localhost:3000/api/ruler/{{prometheusDatasourceUID}}/api/v1/rules/{{namespace1}} ### // get org rules -GET http://admin:admin@localhost:3000/api/ruler/{{prometheusDatasourceID}}/api/v1/rules +GET http://admin:admin@localhost:3000/api/ruler/{{prometheusDatasourceUID}}/api/v1/rules ### // delete group42 rules -DELETE http://admin:admin@localhost:3000/api/ruler/{{prometheusDatasourceID}}/api/v1/rules/{{namespace1}}/group42 +DELETE http://admin:admin@localhost:3000/api/ruler/{{prometheusDatasourceUID}}/api/v1/rules/{{namespace1}}/group42 ### // get namespace rules - only group101 should be listed -GET http://admin:admin@localhost:3000/api/ruler/{{prometheusDatasourceID}}/api/v1/rules/{{namespace1}} +GET http://admin:admin@localhost:3000/api/ruler/{{prometheusDatasourceUID}}/api/v1/rules/{{namespace1}} ### // delete namespace rules -DELETE http://admin:admin@localhost:3000/api/ruler/{{prometheusDatasourceID}}/api/v1/rules/{{namespace1}} +DELETE http://admin:admin@localhost:3000/api/ruler/{{prometheusDatasourceUID}}/api/v1/rules/{{namespace1}} ### // get namespace rules - no rules -GET http://admin:admin@localhost:3000/api/ruler/{{prometheusDatasourceID}}/api/v1/rules/{{namespace1}} +GET http://admin:admin@localhost:3000/api/ruler/{{prometheusDatasourceUID}}/api/v1/rules/{{namespace1}} ### // get group42 rules -GET http://admin:admin@localhost:3000/api/ruler/{{prometheusDatasourceID}}/api/v1/rules/{{namespace1}}/group42 +GET http://admin:admin@localhost:3000/api/ruler/{{prometheusDatasourceUID}}/api/v1/rules/{{namespace1}}/group42 ### // get namespace rules -GET http://admin:admin@localhost:3000/api/ruler/{{prometheusDatasourceID}}/api/v1/rules/{{namespace1}} +GET http://admin:admin@localhost:3000/api/ruler/{{prometheusDatasourceUID}}/api/v1/rules/{{namespace1}} +U \ No newline at end of file diff --git a/pkg/services/ngalert/api/test-data/ruler-loki-recipient.http b/pkg/services/ngalert/api/test-data/ruler-loki-recipient.http index 8fa3ef1c8f4..60380e8ba82 100644 --- a/pkg/services/ngalert/api/test-data/ruler-loki-recipient.http +++ b/pkg/services/ngalert/api/test-data/ruler-loki-recipient.http @@ -1,10 +1,10 @@ -@lokiDatasourceID = 32 +@lokiDatasourceUID = 9w8X2zlMz // should point to an existing folder named alerting @namespace1 = test // create/update test namespace group42 rulegroup -POST http://admin:admin@localhost:3000/api/ruler/{{lokiDatasourceID}}/api/v1/rules/{{namespace1}} +POST http://admin:admin@localhost:3000/api/ruler/{{lokiDatasourceUID}}/api/v1/rules/{{namespace1}} content-type: application/json { @@ -20,7 +20,7 @@ content-type: application/json ### // create group101 -POST http://admin:admin@localhost:3000/api/ruler/{{lokiDatasourceID}}/api/v1/rules/{{namespace1}} +POST http://admin:admin@localhost:3000/api/ruler/{{lokiDatasourceUID}}/api/v1/rules/{{namespace1}} content-type: application/json { @@ -36,40 +36,40 @@ content-type: application/json ### // get group42 rules -GET http://admin:admin@localhost:3000/api/ruler/{{lokiDatasourceID}}/api/v1/rules/{{namespace1}}/group42 +GET http://admin:admin@localhost:3000/api/ruler/{{lokiDatasourceUID}}/api/v1/rules/{{namespace1}}/group42 ### // get group101 rules -GET http://admin:admin@localhost:3000/api/ruler/{{lokiDatasourceID}}/api/v1/rules/{{namespace1}}/group101 +GET http://admin:admin@localhost:3000/api/ruler/{{lokiDatasourceUID}}/api/v1/rules/{{namespace1}}/group101 ### // get namespace rules -GET http://admin:admin@localhost:3000/api/ruler/{{lokiDatasourceID}}/api/v1/rules/{{namespace1}} +GET http://admin:admin@localhost:3000/api/ruler/{{lokiDatasourceUID}}/api/v1/rules/{{namespace1}} ### // get org rules -GET http://admin:admin@localhost:3000/api/ruler/{{lokiDatasourceID}}/api/v1/rules +GET http://admin:admin@localhost:3000/api/ruler/{{lokiDatasourceUID}}/api/v1/rules ### // delete group42 rules -DELETE http://admin:admin@localhost:3000/api/ruler/{{lokiDatasourceID}}/api/v1/rules/{{namespace1}}/group42 +DELETE http://admin:admin@localhost:3000/api/ruler/{{lokiDatasourceUID}}/api/v1/rules/{{namespace1}}/group42 ### // get namespace rules - only group101 should be listed -GET http://admin:admin@localhost:3000/api/ruler/{{lokiDatasourceID}}/api/v1/rules/{{namespace1}} +GET http://admin:admin@localhost:3000/api/ruler/{{lokiDatasourceUID}}/api/v1/rules/{{namespace1}} ### // delete namespace rules -DELETE http://admin:admin@localhost:3000/api/ruler/{{lokiDatasourceID}}/api/v1/rules/{{namespace1}} +DELETE http://admin:admin@localhost:3000/api/ruler/{{lokiDatasourceUID}}/api/v1/rules/{{namespace1}} ### // get namespace rules - no rules -GET http://admin:admin@localhost:3000/api/ruler/{{lokiDatasourceID}}/api/v1/rules/{{namespace1}} +GET http://admin:admin@localhost:3000/api/ruler/{{lokiDatasourceUID}}/api/v1/rules/{{namespace1}} ### // get group42 rules -GET http://admin:admin@localhost:3000/api/ruler/{{lokiDatasourceID}}/api/v1/rules/{{namespace1}}/group42 +GET http://admin:admin@localhost:3000/api/ruler/{{lokiDatasourceUID}}/api/v1/rules/{{namespace1}}/group42 ### // get namespace rules -GET http://admin:admin@localhost:3000/api/ruler/{{lokiDatasourceID}}/api/v1/rules/{{namespace1}} +GET http://admin:admin@localhost:3000/api/ruler/{{lokiDatasourceUID}}/api/v1/rules/{{namespace1}} diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go index c7476c5ca79..915e601c303 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go @@ -427,21 +427,17 @@ type AlertsParams struct { } // swagger:parameters RoutePostAMAlerts RoutePostGrafanaAMAlerts -// swagger:parameters RoutePostAMAlerts type PostableAlerts struct { // in:body PostableAlerts []amv2.PostableAlert `yaml:"" json:""` } // swagger:parameters RoutePostAlertingConfig RoutePostGrafanaAlertingConfig -// swagger:parameters RoutePostAlertingConfig type BodyAlertingConfig struct { // in:body Body PostableUserConfig } -// ruler routes -// swagger:parameters RouteGetRulesConfig RoutePostNameRulesConfig RouteGetNamespaceRulesConfig RouteDeleteNamespaceRulesConfig RouteGetRulegGroupConfig RouteDeleteRuleGroupConfig // prom routes // swagger:parameters RouteGetRuleStatuses RouteGetAlertStatuses // testing routes @@ -454,6 +450,8 @@ type DatasourceIDReference struct { // alertmanager routes // swagger:parameters RoutePostAlertingConfig RouteGetAlertingConfig RouteDeleteAlertingConfig RouteGetAMStatus RouteGetAMAlerts RoutePostAMAlerts RouteGetAMAlertGroups RouteGetSilences RouteCreateSilence RouteGetSilence RouteDeleteSilence RoutePostAlertingConfig RoutePostTestReceivers +// ruler routes +// swagger:parameters RouteGetRulesConfig RoutePostNameRulesConfig RouteGetNamespaceRulesConfig RouteDeleteNamespaceRulesConfig RouteGetRulegGroupConfig RouteDeleteRuleGroupConfig type DatasourceUIDReference struct { // DatasoureUID should be the datasource UID identifier // in:path diff --git a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go index 3e913750012..3c93ef4f635 100644 --- a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go +++ b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go @@ -21,7 +21,7 @@ import ( // 202: NamespaceConfigResponse // -// swagger:route Get /api/ruler/{DatasourceID}/api/v1/rules ruler RouteGetRulesConfig +// swagger:route Get /api/ruler/{DatasourceUID}/api/v1/rules ruler RouteGetRulesConfig // // List rule groups // @@ -43,7 +43,7 @@ import ( // 202: Ack // -// swagger:route POST /api/ruler/{DatasourceID}/api/v1/rules/{Namespace} ruler RoutePostNameRulesConfig +// swagger:route POST /api/ruler/{DatasourceUID}/api/v1/rules/{Namespace} ruler RoutePostNameRulesConfig // // Creates or updates a rule group // @@ -64,7 +64,7 @@ import ( // Responses: // 202: NamespaceConfigResponse -// swagger:route Get /api/ruler/{DatasourceID}/api/v1/rules/{Namespace} ruler RouteGetNamespaceRulesConfig +// swagger:route Get /api/ruler/{DatasourceUID}/api/v1/rules/{Namespace} ruler RouteGetNamespaceRulesConfig // // Get rule groups by namespace // @@ -81,7 +81,7 @@ import ( // Responses: // 202: Ack -// swagger:route Delete /api/ruler/{DatasourceID}/api/v1/rules/{Namespace} ruler RouteDeleteNamespaceRulesConfig +// swagger:route Delete /api/ruler/{DatasourceUID}/api/v1/rules/{Namespace} ruler RouteDeleteNamespaceRulesConfig // // Delete namespace // @@ -98,7 +98,7 @@ import ( // Responses: // 202: RuleGroupConfigResponse -// swagger:route Get /api/ruler/{DatasourceID}/api/v1/rules/{Namespace}/{Groupname} ruler RouteGetRulegGroupConfig +// swagger:route Get /api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}/{Groupname} ruler RouteGetRulegGroupConfig // // Get rule group // @@ -115,7 +115,7 @@ import ( // Responses: // 202: Ack -// swagger:route Delete /api/ruler/{DatasourceID}/api/v1/rules/{Namespace}/{Groupname} ruler RouteDeleteRuleGroupConfig +// swagger:route Delete /api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}/{Groupname} ruler RouteDeleteRuleGroupConfig // // Delete rule group // diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index 80dc5b9489a..a901d74ab9c 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -977,6 +977,9 @@ "type": "integer", "x-go-name": "OrgID" }, + "provenance": { + "$ref": "#/definitions/Provenance" + }, "rule_group": { "type": "string", "x-go-name": "RuleGroup" @@ -3103,7 +3106,6 @@ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "alertGroup": { - "description": "AlertGroup alert group", "properties": { "alerts": { "description": "alerts", @@ -3125,14 +3127,17 @@ "labels", "receiver" ], - "type": "object" + "type": "object", + "x-go-name": "AlertGroup", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "alertGroups": { - "description": "AlertGroups alert groups", "items": { "$ref": "#/definitions/alertGroup" }, - "type": "array" + "type": "array", + "x-go-name": "AlertGroups", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "alertStatus": { "description": "AlertStatus alert status", @@ -3252,7 +3257,6 @@ "$ref": "#/definitions/Duration" }, "gettableAlert": { - "description": "GettableAlert gettable alert", "properties": { "annotations": { "$ref": "#/definitions/labelSet" @@ -3311,7 +3315,9 @@ "status", "updatedAt" ], - "type": "object" + "type": "object", + "x-go-name": "GettableAlert", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "gettableAlerts": { "description": "GettableAlerts gettable alerts", @@ -3376,11 +3382,12 @@ "type": "object" }, "gettableSilences": { - "description": "GettableSilences gettable silences", "items": { "$ref": "#/definitions/gettableSilence" }, - "type": "array" + "type": "array", + "x-go-name": "GettableSilences", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "labelSet": { "additionalProperties": { @@ -5162,18 +5169,17 @@ ] } }, - "/api/ruler/{DatasourceID}/api/v1/rules": { + "/api/ruler/{DatasourceUID}/api/v1/rules": { "get": { "description": "List rule groups", "operationId": "RouteGetRulesConfig", "parameters": [ { - "description": "DatasourceID should be the numeric datasource identifier", - "format": "int64", + "description": "DatasoureUID should be the datasource UID identifier", "in": "path", - "name": "DatasourceID", + "name": "DatasourceUID", "required": true, - "type": "integer" + "type": "string" }, { "in": "query", @@ -5203,18 +5209,17 @@ ] } }, - "/api/ruler/{DatasourceID}/api/v1/rules/{Namespace}": { + "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}": { "delete": { "description": "Delete namespace", "operationId": "RouteDeleteNamespaceRulesConfig", "parameters": [ { - "description": "DatasourceID should be the numeric datasource identifier", - "format": "int64", + "description": "DatasoureUID should be the datasource UID identifier", "in": "path", - "name": "DatasourceID", + "name": "DatasourceUID", "required": true, - "type": "integer" + "type": "string" }, { "in": "path", @@ -5240,12 +5245,11 @@ "operationId": "RouteGetNamespaceRulesConfig", "parameters": [ { - "description": "DatasourceID should be the numeric datasource identifier", - "format": "int64", + "description": "DatasoureUID should be the datasource UID identifier", "in": "path", - "name": "DatasourceID", + "name": "DatasourceUID", "required": true, - "type": "integer" + "type": "string" }, { "in": "path", @@ -5278,12 +5282,11 @@ "operationId": "RoutePostNameRulesConfig", "parameters": [ { - "description": "DatasourceID should be the numeric datasource identifier", - "format": "int64", + "description": "DatasoureUID should be the datasource UID identifier", "in": "path", - "name": "DatasourceID", + "name": "DatasourceUID", "required": true, - "type": "integer" + "type": "string" }, { "in": "path", @@ -5312,18 +5315,17 @@ ] } }, - "/api/ruler/{DatasourceID}/api/v1/rules/{Namespace}/{Groupname}": { + "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}/{Groupname}": { "delete": { "description": "Delete rule group", "operationId": "RouteDeleteRuleGroupConfig", "parameters": [ { - "description": "DatasourceID should be the numeric datasource identifier", - "format": "int64", + "description": "DatasoureUID should be the datasource UID identifier", "in": "path", - "name": "DatasourceID", + "name": "DatasourceUID", "required": true, - "type": "integer" + "type": "string" }, { "in": "path", @@ -5355,12 +5357,11 @@ "operationId": "RouteGetRulegGroupConfig", "parameters": [ { - "description": "DatasourceID should be the numeric datasource identifier", - "format": "int64", + "description": "DatasoureUID should be the datasource UID identifier", "in": "path", - "name": "DatasourceID", + "name": "DatasourceUID", "required": true, - "type": "integer" + "type": "string" }, { "in": "path", diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 138349e2462..f8a75f30421 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -1501,7 +1501,7 @@ } } }, - "/api/ruler/{DatasourceID}/api/v1/rules": { + "/api/ruler/{DatasourceUID}/api/v1/rules": { "get": { "description": "List rule groups", "produces": [ @@ -1513,10 +1513,9 @@ "operationId": "RouteGetRulesConfig", "parameters": [ { - "type": "integer", - "format": "int64", - "description": "DatasourceID should be the numeric datasource identifier", - "name": "DatasourceID", + "type": "string", + "description": "DatasoureUID should be the datasource UID identifier", + "name": "DatasourceUID", "in": "path", "required": true }, @@ -1542,7 +1541,7 @@ } } }, - "/api/ruler/{DatasourceID}/api/v1/rules/{Namespace}": { + "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}": { "get": { "description": "Get rule groups by namespace", "produces": [ @@ -1554,10 +1553,9 @@ "operationId": "RouteGetNamespaceRulesConfig", "parameters": [ { - "type": "integer", - "format": "int64", - "description": "DatasourceID should be the numeric datasource identifier", - "name": "DatasourceID", + "type": "string", + "description": "DatasoureUID should be the datasource UID identifier", + "name": "DatasourceUID", "in": "path", "required": true }, @@ -1589,10 +1587,9 @@ "operationId": "RoutePostNameRulesConfig", "parameters": [ { - "type": "integer", - "format": "int64", - "description": "DatasourceID should be the numeric datasource identifier", - "name": "DatasourceID", + "type": "string", + "description": "DatasoureUID should be the datasource UID identifier", + "name": "DatasourceUID", "in": "path", "required": true }, @@ -1627,10 +1624,9 @@ "operationId": "RouteDeleteNamespaceRulesConfig", "parameters": [ { - "type": "integer", - "format": "int64", - "description": "DatasourceID should be the numeric datasource identifier", - "name": "DatasourceID", + "type": "string", + "description": "DatasoureUID should be the datasource UID identifier", + "name": "DatasourceUID", "in": "path", "required": true }, @@ -1651,7 +1647,7 @@ } } }, - "/api/ruler/{DatasourceID}/api/v1/rules/{Namespace}/{Groupname}": { + "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}/{Groupname}": { "get": { "description": "Get rule group", "produces": [ @@ -1663,10 +1659,9 @@ "operationId": "RouteGetRulegGroupConfig", "parameters": [ { - "type": "integer", - "format": "int64", - "description": "DatasourceID should be the numeric datasource identifier", - "name": "DatasourceID", + "type": "string", + "description": "DatasoureUID should be the datasource UID identifier", + "name": "DatasourceUID", "in": "path", "required": true }, @@ -1700,10 +1695,9 @@ "operationId": "RouteDeleteRuleGroupConfig", "parameters": [ { - "type": "integer", - "format": "int64", - "description": "DatasourceID should be the numeric datasource identifier", - "name": "DatasourceID", + "type": "string", + "description": "DatasoureUID should be the datasource UID identifier", + "name": "DatasourceUID", "in": "path", "required": true }, @@ -2922,6 +2916,9 @@ "format": "int64", "x-go-name": "OrgID" }, + "provenance": { + "$ref": "#/definitions/Provenance" + }, "rule_group": { "type": "string", "x-go-name": "RuleGroup" @@ -5048,7 +5045,6 @@ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "alertGroup": { - "description": "AlertGroup alert group", "type": "object", "required": [ "alerts", @@ -5071,14 +5067,17 @@ "$ref": "#/definitions/receiver" } }, + "x-go-name": "AlertGroup", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/alertGroup" }, "alertGroups": { - "description": "AlertGroups alert groups", "type": "array", "items": { "$ref": "#/definitions/alertGroup" }, + "x-go-name": "AlertGroups", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/alertGroups" }, "alertStatus": { @@ -5199,7 +5198,6 @@ "$ref": "#/definitions/Duration" }, "gettableAlert": { - "description": "GettableAlert gettable alert", "type": "object", "required": [ "labels", @@ -5259,6 +5257,8 @@ "x-go-name": "UpdatedAt" } }, + "x-go-name": "GettableAlert", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/gettableAlert" }, "gettableAlerts": { @@ -5326,11 +5326,12 @@ "$ref": "#/definitions/gettableSilence" }, "gettableSilences": { - "description": "GettableSilences gettable silences", "type": "array", "items": { "$ref": "#/definitions/gettableSilence" }, + "x-go-name": "GettableSilences", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/gettableSilences" }, "labelSet": { diff --git a/public/api-merged.json b/public/api-merged.json index 876ebc32d7c..11c1856d659 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -7590,7 +7590,7 @@ } } }, - "/ruler/{DatasourceID}/api/v1/rules": { + "/ruler/{DatasourceUID}/api/v1/rules": { "get": { "description": "List rule groups", "produces": ["application/json"], @@ -7598,10 +7598,9 @@ "operationId": "RouteGetRulesConfig", "parameters": [ { - "type": "integer", - "format": "int64", - "description": "DatasourceID should be the numeric datasource identifier", - "name": "DatasourceID", + "type": "string", + "description": "DatasoureUID should be the datasource UID identifier", + "name": "DatasourceUID", "in": "path", "required": true }, @@ -7627,7 +7626,7 @@ } } }, - "/ruler/{DatasourceID}/api/v1/rules/{Namespace}": { + "/ruler/{DatasourceUID}/api/v1/rules/{Namespace}": { "get": { "description": "Get rule groups by namespace", "produces": ["application/json"], @@ -7635,10 +7634,9 @@ "operationId": "RouteGetNamespaceRulesConfig", "parameters": [ { - "type": "integer", - "format": "int64", - "description": "DatasourceID should be the numeric datasource identifier", - "name": "DatasourceID", + "type": "string", + "description": "DatasoureUID should be the datasource UID identifier", + "name": "DatasourceUID", "in": "path", "required": true }, @@ -7665,10 +7663,9 @@ "operationId": "RoutePostNameRulesConfig", "parameters": [ { - "type": "integer", - "format": "int64", - "description": "DatasourceID should be the numeric datasource identifier", - "name": "DatasourceID", + "type": "string", + "description": "DatasoureUID should be the datasource UID identifier", + "name": "DatasourceUID", "in": "path", "required": true }, @@ -7701,10 +7698,9 @@ "operationId": "RouteDeleteNamespaceRulesConfig", "parameters": [ { - "type": "integer", - "format": "int64", - "description": "DatasourceID should be the numeric datasource identifier", - "name": "DatasourceID", + "type": "string", + "description": "DatasoureUID should be the datasource UID identifier", + "name": "DatasourceUID", "in": "path", "required": true }, @@ -7725,7 +7721,7 @@ } } }, - "/ruler/{DatasourceID}/api/v1/rules/{Namespace}/{Groupname}": { + "/ruler/{DatasourceUID}/api/v1/rules/{Namespace}/{Groupname}": { "get": { "description": "Get rule group", "produces": ["application/json"], @@ -7733,10 +7729,9 @@ "operationId": "RouteGetRulegGroupConfig", "parameters": [ { - "type": "integer", - "format": "int64", - "description": "DatasourceID should be the numeric datasource identifier", - "name": "DatasourceID", + "type": "string", + "description": "DatasoureUID should be the datasource UID identifier", + "name": "DatasourceUID", "in": "path", "required": true }, @@ -7768,10 +7763,9 @@ "operationId": "RouteDeleteRuleGroupConfig", "parameters": [ { - "type": "integer", - "format": "int64", - "description": "DatasourceID should be the numeric datasource identifier", - "name": "DatasourceID", + "type": "string", + "description": "DatasoureUID should be the datasource UID identifier", + "name": "DatasourceUID", "in": "path", "required": true }, @@ -12629,6 +12623,9 @@ "format": "int64", "x-go-name": "OrgID" }, + "provenance": { + "$ref": "#/definitions/Provenance" + }, "rule_group": { "type": "string", "x-go-name": "RuleGroup" @@ -17306,7 +17303,6 @@ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "alertGroup": { - "description": "AlertGroup alert group", "type": "object", "required": ["alerts", "labels", "receiver"], "properties": { @@ -17324,14 +17320,17 @@ "receiver": { "$ref": "#/definitions/receiver" } - } + }, + "x-go-name": "AlertGroup", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "alertGroups": { - "description": "AlertGroups alert groups", "type": "array", "items": { "$ref": "#/definitions/alertGroup" - } + }, + "x-go-name": "AlertGroups", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "alertStatus": { "description": "AlertStatus alert status", @@ -17434,7 +17433,6 @@ "$ref": "#/definitions/Duration" }, "gettableAlert": { - "description": "GettableAlert gettable alert", "type": "object", "required": ["labels", "annotations", "endsAt", "fingerprint", "receivers", "startsAt", "status", "updatedAt"], "properties": { @@ -17484,7 +17482,9 @@ "format": "date-time", "x-go-name": "UpdatedAt" } - } + }, + "x-go-name": "GettableAlert", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "gettableAlerts": { "description": "GettableAlerts gettable alerts", @@ -17540,11 +17540,12 @@ } }, "gettableSilences": { - "description": "GettableSilences gettable silences", "type": "array", "items": { "$ref": "#/definitions/gettableSilence" - } + }, + "x-go-name": "GettableSilences", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "labelSet": { "description": "LabelSet label set", diff --git a/public/app/features/alerting/unified/api/ruler.test.ts b/public/app/features/alerting/unified/api/ruler.test.ts index 2713d0e975b..b31cdd3efd2 100644 --- a/public/app/features/alerting/unified/api/ruler.test.ts +++ b/public/app/features/alerting/unified/api/ruler.test.ts @@ -1,13 +1,13 @@ import { RulerDataSourceConfig } from 'app/types/unified-alerting'; -import { getDatasourceAPIId } from '../utils/datasource'; +import { getDatasourceAPIUid } from '../utils/datasource'; import { rulerUrlBuilder } from './ruler'; jest.mock('../utils/datasource'); const mocks = { - getDatasourceAPIId: jest.mocked(getDatasourceAPIId), + getDatasourceAPIUId: jest.mocked(getDatasourceAPIUid), }; describe('rulerUrlBuilder', () => { @@ -18,7 +18,7 @@ describe('rulerUrlBuilder', () => { apiVersion: 'legacy', }; - mocks.getDatasourceAPIId.mockReturnValue('ds-uid'); + mocks.getDatasourceAPIUId.mockReturnValue('ds-uid'); // Act const builder = rulerUrlBuilder(config); @@ -45,7 +45,7 @@ describe('rulerUrlBuilder', () => { apiVersion: 'config', }; - mocks.getDatasourceAPIId.mockReturnValue('ds-uid'); + mocks.getDatasourceAPIUId.mockReturnValue('ds-uid'); // Act const builder = rulerUrlBuilder(config); @@ -72,7 +72,7 @@ describe('rulerUrlBuilder', () => { apiVersion: 'config', }; - mocks.getDatasourceAPIId.mockReturnValue('ds-uid'); + mocks.getDatasourceAPIUId.mockReturnValue('ds-uid'); // Act const builder = rulerUrlBuilder(config); @@ -94,7 +94,7 @@ describe('rulerUrlBuilder', () => { apiVersion: 'config', }; - mocks.getDatasourceAPIId.mockReturnValue('ds-uid'); + mocks.getDatasourceAPIUId.mockReturnValue('ds-uid'); // Act const builder = rulerUrlBuilder(config); diff --git a/public/app/features/alerting/unified/api/ruler.ts b/public/app/features/alerting/unified/api/ruler.ts index 51af85a77e1..87187461312 100644 --- a/public/app/features/alerting/unified/api/ruler.ts +++ b/public/app/features/alerting/unified/api/ruler.ts @@ -5,7 +5,7 @@ import { RulerDataSourceConfig } from 'app/types/unified-alerting'; import { PostableRulerRuleGroupDTO, RulerRuleGroupDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto'; import { RULER_NOT_SUPPORTED_MSG } from '../utils/constants'; -import { getDatasourceAPIId, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; +import { getDatasourceAPIUid, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; import { prepareRulesFilterQueryParams } from './prometheus'; @@ -20,7 +20,7 @@ export interface RulerRequestUrl { } export function rulerUrlBuilder(rulerConfig: RulerDataSourceConfig) { - const grafanaServerPath = `/api/ruler/${getDatasourceAPIId(rulerConfig.dataSourceName)}`; + const grafanaServerPath = `/api/ruler/${getDatasourceAPIUid(rulerConfig.dataSourceName)}`; const rulerPath = `${grafanaServerPath}/api/v1/rules`; const rulerSearchParams = new URLSearchParams(); @@ -94,7 +94,7 @@ export async function fetchRulerRulesNamespace(rulerConfig: RulerDataSourceConfi // will throw with { status: 404 } if rule group does not exist export async function fetchTestRulerRulesGroup(dataSourceName: string): Promise { return rulerGetRequest( - `/api/ruler/${getDatasourceAPIId(dataSourceName)}/api/v1/rules/test/test`, + `/api/ruler/${getDatasourceAPIUid(dataSourceName)}/api/v1/rules/test/test`, null ); } From b35ca8c08dc0cdebfd62ffc300e041bfea867447 Mon Sep 17 00:00:00 2001 From: Shirley <4163034+fridgepoet@users.noreply.github.com> Date: Thu, 5 May 2022 13:59:23 +0200 Subject: [PATCH 064/440] CloudWatch: Pass label in GetMetricData API request when dynamic label feature toggle is enabled (#48574) --- pkg/tsdb/cloudwatch/cloudwatch_query.go | 1 + .../cloudwatch/metric_data_query_builder.go | 5 + .../metric_data_query_builder_test.go | 39 +++++++ pkg/tsdb/cloudwatch/request_parser.go | 2 + pkg/tsdb/cloudwatch/request_parser_test.go | 12 ++ pkg/tsdb/cloudwatch/response_parser.go | 15 ++- pkg/tsdb/cloudwatch/response_parser_test.go | 33 ++++-- pkg/tsdb/cloudwatch/time_series_query_test.go | 107 ++++++++++++++++-- pkg/tsdb/cloudwatch/utils_test.go | 9 +- 9 files changed, 201 insertions(+), 22 deletions(-) diff --git a/pkg/tsdb/cloudwatch/cloudwatch_query.go b/pkg/tsdb/cloudwatch/cloudwatch_query.go index 229e5661244..064f54a4926 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch_query.go +++ b/pkg/tsdb/cloudwatch/cloudwatch_query.go @@ -21,6 +21,7 @@ type cloudWatchQuery struct { Dimensions map[string][]string Period int Alias string + Label string MatchExact bool UsedExpression string MetricQueryType metricQueryType diff --git a/pkg/tsdb/cloudwatch/metric_data_query_builder.go b/pkg/tsdb/cloudwatch/metric_data_query_builder.go index ff97deb412b..1cff741d788 100644 --- a/pkg/tsdb/cloudwatch/metric_data_query_builder.go +++ b/pkg/tsdb/cloudwatch/metric_data_query_builder.go @@ -8,6 +8,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/cloudwatch" + "github.com/grafana/grafana/pkg/services/featuremgmt" ) func (e *cloudWatchExecutor) buildMetricDataQuery(query *cloudWatchQuery) (*cloudwatch.MetricDataQuery, error) { @@ -16,6 +17,10 @@ func (e *cloudWatchExecutor) buildMetricDataQuery(query *cloudWatchQuery) (*clou ReturnData: aws.Bool(query.ReturnData), } + if e.features.IsEnabled(featuremgmt.FlagCloudWatchDynamicLabels) && len(query.Label) > 0 { + mdq.Label = &query.Label + } + switch query.getGMDAPIMode() { case GMDApiModeMathExpression: mdq.Period = aws.Int64(int64(query.Period)) diff --git a/pkg/tsdb/cloudwatch/metric_data_query_builder_test.go b/pkg/tsdb/cloudwatch/metric_data_query_builder_test.go index 8dd810bb0e8..4b8c5cd92da 100644 --- a/pkg/tsdb/cloudwatch/metric_data_query_builder_test.go +++ b/pkg/tsdb/cloudwatch/metric_data_query_builder_test.go @@ -71,6 +71,45 @@ func TestMetricDataQueryBuilder(t *testing.T) { assert.Equal(t, int64(300), *mdq.Period) assert.Equal(t, `SUM([a,b])`, *mdq.Expression) }) + + t.Run("should set label when dynamic labels feature toggle is enabled", func(t *testing.T) { + executor := newExecutor(nil, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures(featuremgmt.FlagCloudWatchDynamicLabels)) + query := getBaseQuery() + query.Label = "some label" + + mdq, err := executor.buildMetricDataQuery(query) + + assert.NoError(t, err) + require.NotNil(t, mdq.Label) + assert.Equal(t, "some label", *mdq.Label) + }) + + testCases := map[string]struct { + feature *featuremgmt.FeatureManager + label string + }{ + "should not set label when dynamic labels feature toggle is disabled": { + feature: featuremgmt.WithFeatures(), + label: "some label", + }, + "should not set label for empty string query label": { + feature: featuremgmt.WithFeatures(featuremgmt.FlagCloudWatchDynamicLabels), + label: "", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + executor := newExecutor(nil, newTestConfig(), &fakeSessionCache{}, tc.feature) + query := getBaseQuery() + query.Label = tc.label + + mdq, err := executor.buildMetricDataQuery(query) + + assert.NoError(t, err) + assert.Nil(t, mdq.Label) + }) + } }) t.Run("Query should be matched exact", func(t *testing.T) { diff --git a/pkg/tsdb/cloudwatch/request_parser.go b/pkg/tsdb/cloudwatch/request_parser.go index 0b6be76c1bb..f16b63608a4 100644 --- a/pkg/tsdb/cloudwatch/request_parser.go +++ b/pkg/tsdb/cloudwatch/request_parser.go @@ -198,6 +198,7 @@ func parseRequestQuery(model *simplejson.Json, refId string, startTime time.Time expression := model.Get("expression").MustString("") sqlExpression := model.Get("sqlExpression").MustString("") alias := model.Get("alias").MustString() + label := model.Get("label").MustString() returnData := !model.Get("hide").MustBool(false) queryType := model.Get("type").MustString() if queryType == "" { @@ -231,6 +232,7 @@ func parseRequestQuery(model *simplejson.Json, refId string, startTime time.Time Dimensions: dimensions, Period: period, Alias: alias, + Label: label, MatchExact: matchExact, UsedExpression: "", MetricQueryType: metricQueryType, diff --git a/pkg/tsdb/cloudwatch/request_parser_test.go b/pkg/tsdb/cloudwatch/request_parser_test.go index 1000c98b75f..d30d47a28de 100644 --- a/pkg/tsdb/cloudwatch/request_parser_test.go +++ b/pkg/tsdb/cloudwatch/request_parser_test.go @@ -311,6 +311,18 @@ func TestRequestParser(t *testing.T) { assert.Equal(t, "$$", res.RefId) assert.Regexp(t, validMetricDataID, res.Id) }) + + t.Run("parseRequestQuery sets label when label is present in json query", func(t *testing.T) { + query := getBaseJsonQuery() + query.Set("alias", "some alias") + query.Set("label", "some label") + + res, err := parseRequestQuery(query, "ref1", time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour)) + + assert.NoError(t, err) + assert.Equal(t, "some alias", res.Alias) // alias is unmodified + assert.Equal(t, "some label", res.Label) + }) } func getBaseJsonQuery() *simplejson.Json { diff --git a/pkg/tsdb/cloudwatch/response_parser.go b/pkg/tsdb/cloudwatch/response_parser.go index 9b0defde3dc..2d58551b6af 100644 --- a/pkg/tsdb/cloudwatch/response_parser.go +++ b/pkg/tsdb/cloudwatch/response_parser.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/services/featuremgmt" ) func (e *cloudWatchExecutor) parseResponse(startTime time.Time, endTime time.Time, metricDataOutputs []*cloudwatch.GetMetricDataOutput, @@ -31,7 +32,7 @@ func (e *cloudWatchExecutor) parseResponse(startTime time.Time, endTime time.Tim } var err error - dataRes.Frames, err = buildDataFrames(startTime, endTime, response, queryRow) + dataRes.Frames, err = buildDataFrames(startTime, endTime, response, queryRow, e.features.IsEnabled(featuremgmt.FlagCloudWatchDynamicLabels)) if err != nil { return nil, err } @@ -117,7 +118,7 @@ func getLabels(cloudwatchLabel string, query *cloudWatchQuery) data.Labels { } func buildDataFrames(startTime time.Time, endTime time.Time, aggregatedResponse queryRowResponse, - query *cloudWatchQuery) (data.Frames, error) { + query *cloudWatchQuery, dynamicLabelEnabled bool) (data.Frames, error) { frames := data.Frames{} for _, label := range aggregatedResponse.Labels { metric := aggregatedResponse.Metrics[label] @@ -150,7 +151,10 @@ func buildDataFrames(startTime time.Time, endTime time.Time, aggregatedResponse timeField := data.NewField(data.TimeSeriesTimeFieldName, nil, []*time.Time{}) valueField := data.NewField(data.TimeSeriesValueFieldName, labels, []*float64{}) - frameName := formatAlias(query, query.Statistic, labels, label) + frameName := label + if !dynamicLabelEnabled { + frameName = formatAlias(query, query.Statistic, labels, label) + } valueField.SetConfig(&data.FieldConfig{DisplayNameFromDS: frameName, Links: createDataLinks(deepLink)}) emptyFrame := data.Frame{ @@ -179,7 +183,10 @@ func buildDataFrames(startTime time.Time, endTime time.Time, aggregatedResponse timeField := data.NewField(data.TimeSeriesTimeFieldName, nil, timestamps) valueField := data.NewField(data.TimeSeriesValueFieldName, labels, points) - frameName := formatAlias(query, query.Statistic, labels, label) + frameName := label + if !dynamicLabelEnabled { + frameName = formatAlias(query, query.Statistic, labels, label) + } valueField.SetConfig(&data.FieldConfig{DisplayNameFromDS: frameName, Links: createDataLinks(deepLink)}) frame := data.Frame{ diff --git a/pkg/tsdb/cloudwatch/response_parser_test.go b/pkg/tsdb/cloudwatch/response_parser_test.go index b1c3de6db8c..1e591c42339 100644 --- a/pkg/tsdb/cloudwatch/response_parser_test.go +++ b/pkg/tsdb/cloudwatch/response_parser_test.go @@ -143,7 +143,7 @@ func TestCloudWatchResponseParser(t *testing.T) { MetricQueryType: MetricQueryTypeSearch, MetricEditorMode: MetricEditorModeBuilder, } - frames, err := buildDataFrames(startTime, endTime, *response, query) + frames, err := buildDataFrames(startTime, endTime, *response, query, false) require.NoError(t, err) frame1 := frames[0] @@ -207,7 +207,7 @@ func TestCloudWatchResponseParser(t *testing.T) { MetricQueryType: MetricQueryTypeSearch, MetricEditorMode: MetricEditorModeBuilder, } - frames, err := buildDataFrames(startTime, endTime, *response, query) + frames, err := buildDataFrames(startTime, endTime, *response, query, false) require.NoError(t, err) frame1 := frames[0] @@ -272,7 +272,7 @@ func TestCloudWatchResponseParser(t *testing.T) { MetricQueryType: MetricQueryTypeSearch, MetricEditorMode: MetricEditorModeBuilder, } - frames, err := buildDataFrames(startTime, endTime, *response, query) + frames, err := buildDataFrames(startTime, endTime, *response, query, false) require.NoError(t, err) assert.Equal(t, "lb3 Expanded", frames[0].Name) @@ -311,7 +311,7 @@ func TestCloudWatchResponseParser(t *testing.T) { MetricQueryType: MetricQueryTypeSearch, MetricEditorMode: MetricEditorModeBuilder, } - frames, err := buildDataFrames(startTime, endTime, *response, query) + frames, err := buildDataFrames(startTime, endTime, *response, query, false) require.NoError(t, err) assert.Len(t, frames, 2) @@ -354,7 +354,7 @@ func TestCloudWatchResponseParser(t *testing.T) { MetricQueryType: MetricQueryTypeSearch, MetricEditorMode: MetricEditorModeBuilder, } - frames, err := buildDataFrames(startTime, endTime, *response, query) + frames, err := buildDataFrames(startTime, endTime, *response, query, false) require.NoError(t, err) assert.Len(t, frames, 2) @@ -395,7 +395,7 @@ func TestCloudWatchResponseParser(t *testing.T) { MetricQueryType: MetricQueryTypeQuery, MetricEditorMode: MetricEditorModeRaw, } - frames, err := buildDataFrames(startTime, endTime, *response, query) + frames, err := buildDataFrames(startTime, endTime, *response, query, false) require.NoError(t, err) assert.False(t, strings.Contains(frames[0].Name, "AWS/ApplicationELB")) @@ -445,7 +445,7 @@ func TestCloudWatchResponseParser(t *testing.T) { MetricQueryType: MetricQueryTypeSearch, MetricEditorMode: MetricEditorModeBuilder, } - frames, err := buildDataFrames(startTime, endTime, *response, query) + frames, err := buildDataFrames(startTime, endTime, *response, query, false) require.NoError(t, err) frame := frames[0] @@ -458,4 +458,23 @@ func TestCloudWatchResponseParser(t *testing.T) { assert.Equal(t, "Value", frame.Fields[1].Name) assert.Equal(t, "", frame.Fields[1].Config.DisplayName) }) + + t.Run("buildDataFrames should use response label as frame name when dynamic label is enabled", func(t *testing.T) { + response := &queryRowResponse{ + Labels: []string{"some response label"}, + Metrics: map[string]*cloudwatch.MetricDataResult{ + "some response label": { + Timestamps: []*time.Time{}, + Values: []*float64{aws.Float64(10)}, + StatusCode: aws.String("Complete"), + }, + }, + } + + frames, err := buildDataFrames(startTime, endTime, *response, &cloudWatchQuery{}, true) + + assert.NoError(t, err) + require.Len(t, frames, 1) + assert.Equal(t, "some response label", frames[0].Name) + }) } diff --git a/pkg/tsdb/cloudwatch/time_series_query_test.go b/pkg/tsdb/cloudwatch/time_series_query_test.go index cce63f8aa39..c1863773507 100644 --- a/pkg/tsdb/cloudwatch/time_series_query_test.go +++ b/pkg/tsdb/cloudwatch/time_series_query_test.go @@ -148,6 +148,7 @@ type queryParameters struct { Dimensions queryDimensions `json:"dimensions"` Expression string `json:"expression"` Alias string `json:"alias"` + Label *string `json:"label"` Statistic string `json:"statistic"` Period string `json:"period"` MatchExact bool `json:"matchExact"` @@ -168,14 +169,15 @@ func newTestQuery(t testing.TB, p queryParameters) json.RawMessage { Dimensions struct { InstanceID []string `json:"InstanceId,omitempty"` } `json:"dimensions"` - Expression string `json:"expression"` - Region string `json:"region"` - ID string `json:"id"` - Alias string `json:"alias"` - Statistic string `json:"statistic"` - Period string `json:"period"` - MatchExact bool `json:"matchExact"` - RefID string `json:"refId"` + Expression string `json:"expression"` + Region string `json:"region"` + ID string `json:"id"` + Alias string `json:"alias"` + Label *string `json:"label"` + Statistic string `json:"statistic"` + Period string `json:"period"` + MatchExact bool `json:"matchExact"` + RefID string `json:"refId"` }{ Type: "timeSeriesQuery", Region: "us-east-2", @@ -188,6 +190,7 @@ func newTestQuery(t testing.TB, p queryParameters) json.RawMessage { Dimensions: p.Dimensions, Expression: p.Expression, Alias: p.Alias, + Label: p.Label, Statistic: p.Statistic, Period: p.Period, MetricName: p.MetricName, @@ -199,6 +202,94 @@ func newTestQuery(t testing.TB, p queryParameters) json.RawMessage { return marshalled } +func Test_QueryData_timeSeriesQuery_GetMetricDataWithContext(t *testing.T) { + origNewCWClient := NewCWClient + t.Cleanup(func() { + NewCWClient = origNewCWClient + }) + var cwClient fakeCWClient + NewCWClient = func(sess *session.Session) cloudwatchiface.CloudWatchAPI { + return &cwClient + } + + im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + return datasourceInfo{}, nil + }) + + t.Run("passes query label as GetMetricData label when dynamic labels feature toggle is enabled", func(t *testing.T) { + cwClient = fakeCWClient{} + executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures(featuremgmt.FlagCloudWatchDynamicLabels)) + query := newTestQuery(t, queryParameters{ + Label: aws.String("${PROP('Period')} some words ${PROP('Dim.InstanceId')}"), + }) + + _, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ + PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, + Queries: []backend.DataQuery{ + { + RefID: "A", + TimeRange: backend.TimeRange{ + From: time.Now().Add(time.Hour * -2), + To: time.Now().Add(time.Hour * -1), + }, + JSON: query, + }, + }, + }) + + assert.NoError(t, err) + require.Len(t, cwClient.callsGetMetricDataWithContext, 1) + require.Len(t, cwClient.callsGetMetricDataWithContext[0].MetricDataQueries, 1) + require.NotNil(t, cwClient.callsGetMetricDataWithContext[0].MetricDataQueries[0].Label) + + assert.Equal(t, "${PROP('Period')} some words ${PROP('Dim.InstanceId')}", *cwClient.callsGetMetricDataWithContext[0].MetricDataQueries[0].Label) + }) + + testCases := map[string]struct { + feature *featuremgmt.FeatureManager + parameters queryParameters + }{ + "should not pass GetMetricData label when query label is empty, dynamic labels is enabled": { + feature: featuremgmt.WithFeatures(featuremgmt.FlagCloudWatchDynamicLabels), + }, + "should not pass GetMetricData label when query label is empty string, dynamic labels is enabled": { + feature: featuremgmt.WithFeatures(featuremgmt.FlagCloudWatchDynamicLabels), + parameters: queryParameters{Label: aws.String("")}, + }, + "should not pass GetMetricData label when dynamic labels is disabled": { + feature: featuremgmt.WithFeatures(), + parameters: queryParameters{Label: aws.String("${PROP('Period')} some words ${PROP('Dim.InstanceId')}")}, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + cwClient = fakeCWClient{} + executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}, tc.feature) + + _, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ + PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, + Queries: []backend.DataQuery{ + { + RefID: "A", + TimeRange: backend.TimeRange{ + From: time.Now().Add(time.Hour * -2), + To: time.Now().Add(time.Hour * -1), + }, + JSON: newTestQuery(t, tc.parameters), + }, + }, + }) + + assert.NoError(t, err) + require.Len(t, cwClient.callsGetMetricDataWithContext, 1) + require.Len(t, cwClient.callsGetMetricDataWithContext[0].MetricDataQueries, 1) + + assert.Nil(t, cwClient.callsGetMetricDataWithContext[0].MetricDataQueries[0].Label) + }) + } +} + func Test_QueryData_response_data_frame_names(t *testing.T) { origNewCWClient := NewCWClient t.Cleanup(func() { diff --git a/pkg/tsdb/cloudwatch/utils_test.go b/pkg/tsdb/cloudwatch/utils_test.go index 9f1213ed435..ce00c5f4a78 100644 --- a/pkg/tsdb/cloudwatch/utils_test.go +++ b/pkg/tsdb/cloudwatch/utils_test.go @@ -62,12 +62,15 @@ type fakeCWClient struct { cloudwatchiface.CloudWatchAPI cloudwatch.GetMetricDataOutput - Metrics []*cloudwatch.Metric - + Metrics []*cloudwatch.Metric MetricsPerPage int + + callsGetMetricDataWithContext []*cloudwatch.GetMetricDataInput } -func (c *fakeCWClient) GetMetricDataWithContext(aws.Context, *cloudwatch.GetMetricDataInput, ...request.Option) (*cloudwatch.GetMetricDataOutput, error) { +func (c *fakeCWClient) GetMetricDataWithContext(ctx aws.Context, input *cloudwatch.GetMetricDataInput, opts ...request.Option) (*cloudwatch.GetMetricDataOutput, error) { + c.callsGetMetricDataWithContext = append(c.callsGetMetricDataWithContext, input) + return &c.GetMetricDataOutput, nil } From 08bee1e682c4d09b3fddbcfb935ab83b8946d983 Mon Sep 17 00:00:00 2001 From: Connor Lindsey Date: Thu, 5 May 2022 06:28:28 -0600 Subject: [PATCH 065/440] Trace to logs: Only show loki and splunk datasources in settings (#48723) --- .../app/core/components/TraceToLogs/TraceToLogsSettings.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/public/app/core/components/TraceToLogs/TraceToLogsSettings.tsx b/public/app/core/components/TraceToLogs/TraceToLogsSettings.tsx index 9df3adefe7a..bfafdfee442 100644 --- a/public/app/core/components/TraceToLogs/TraceToLogsSettings.tsx +++ b/public/app/core/components/TraceToLogs/TraceToLogsSettings.tsx @@ -47,7 +47,10 @@ export function TraceToLogsSettings({ options, onOptionsChange }: Props) { { + // Trace to logs only supports loki and splunk at the moment + return ds.type === 'loki' || ds.type === 'grafana-splunk-datasource'; + }} current={options.jsonData.tracesToLogs?.datasourceUid} noDefault={true} width={40} From af578045136a76c2e59ae308c3045abf5efaa8ad Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Thu, 5 May 2022 14:28:45 +0100 Subject: [PATCH 066/440] Tooltip: Make tooltip use secondary background color for legible links (#48748) --- packages/grafana-data/src/themes/createComponents.ts | 4 ++-- packages/grafana-ui/src/components/Tooltip/Tooltip.tsx | 3 +++ public/sass/_variables.dark.generated.scss | 4 ++-- public/sass/_variables.light.generated.scss | 6 +++--- public/sass/components/_panel_header.scss | 8 -------- 5 files changed, 10 insertions(+), 15 deletions(-) diff --git a/packages/grafana-data/src/themes/createComponents.ts b/packages/grafana-data/src/themes/createComponents.ts index 51603f036c7..54af55d55bc 100644 --- a/packages/grafana-data/src/themes/createComponents.ts +++ b/packages/grafana-data/src/themes/createComponents.ts @@ -75,8 +75,8 @@ export function createComponents(colors: ThemeColors, shadows: ThemeShadows): Th background: input.background, }, tooltip: { - background: colors.mode === 'light' ? '#555' : '#35383e', - text: colors.mode === 'light' ? '#FFF' : colors.text.primary, + background: colors.background.secondary, + text: colors.text.primary, }, dashboard: { background: colors.background.canvas, diff --git a/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx b/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx index 08647671b66..a0cd2ef0551 100644 --- a/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx +++ b/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx @@ -188,6 +188,9 @@ function getStyles(theme: GrafanaTheme2) { a { color: ${theme.colors.text.link}; + } + + a:hover { text-decoration: underline; } `; diff --git a/public/sass/_variables.dark.generated.scss b/public/sass/_variables.dark.generated.scss index 1de1fd75353..474f9c119de 100644 --- a/public/sass/_variables.dark.generated.scss +++ b/public/sass/_variables.dark.generated.scss @@ -291,9 +291,9 @@ $tooltipLinkColor: $link-color; $tooltipExternalLinkColor: $external-link-color; $graph-tooltip-bg: $dark-1; -$tooltipBackground: #35383e; +$tooltipBackground: #22252b; $tooltipColor: rgb(204, 204, 220); -$tooltipArrowColor: #35383e; +$tooltipArrowColor: #22252b; $tooltipBackgroundError: #D10E5C; $tooltipShadow: 0px 4px 8px rgba(24, 26, 27, 0.75); diff --git a/public/sass/_variables.light.generated.scss b/public/sass/_variables.light.generated.scss index e12d2876870..e0bbb8a6371 100644 --- a/public/sass/_variables.light.generated.scss +++ b/public/sass/_variables.light.generated.scss @@ -282,9 +282,9 @@ $alert-warning-bg: #FAD34A; $alert-info-bg: #FAD34A; // Tooltips and popovers -$tooltipBackground: #555; -$tooltipColor: #FFF; -$tooltipArrowColor: #555; +$tooltipBackground: #F4F5F5; +$tooltipColor: rgba(36, 41, 46, 1); +$tooltipArrowColor: #F4F5F5; $tooltipBackgroundError: #E0226E; $tooltipShadow: 0px 4px 8px rgba(24, 26, 27, 0.2); diff --git a/public/sass/components/_panel_header.scss b/public/sass/components/_panel_header.scss index c6f616d5b18..9c5499c0e5f 100644 --- a/public/sass/components/_panel_header.scss +++ b/public/sass/components/_panel_header.scss @@ -161,14 +161,6 @@ $panel-header-no-title-zindex: 1; } .panel-info-content { - a { - color: $gray-6; - - &:hover { - color: darken($white, 10%); - } - } - code { white-space: normal; word-wrap: break-word; From 32a26d87a47c1b6fe408310297d272ca3ad67e1b Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Thu, 5 May 2022 14:35:08 +0100 Subject: [PATCH 067/440] Chore: Generate JSON theme files (#48762) --- public/sass/theme.dark.generated.json | 946 +++++++++++++++++++++++ public/sass/theme.light.generated.json | 946 +++++++++++++++++++++++ scripts/cli/generateSassVariableFiles.ts | 13 + 3 files changed, 1905 insertions(+) create mode 100644 public/sass/theme.dark.generated.json create mode 100644 public/sass/theme.light.generated.json diff --git a/public/sass/theme.dark.generated.json b/public/sass/theme.dark.generated.json new file mode 100644 index 00000000000..fc5311eaa6d --- /dev/null +++ b/public/sass/theme.dark.generated.json @@ -0,0 +1,946 @@ +{ + "name": "Dark", + "isDark": true, + "isLight": false, + "colors": { + "mode": "dark", + "whiteBase": "204, 204, 220", + "border": { + "weak": "rgba(204, 204, 220, 0.07)", + "medium": "rgba(204, 204, 220, 0.15)", + "strong": "rgba(204, 204, 220, 0.25)" + }, + "text": { + "primary": "rgb(204, 204, 220)", + "secondary": "rgba(204, 204, 220, 0.65)", + "disabled": "rgba(204, 204, 220, 0.6)", + "link": "#6E9FFF", + "maxContrast": "#FFFFFF" + }, + "primary": { + "main": "#3D71D9", + "text": "#6E9FFF", + "border": "#6E9FFF", + "name": "primary", + "shade": "rgb(90, 134, 222)", + "transparent": "#3D71D926", + "contrastText": "#FFFFFF" + }, + "secondary": { + "main": "rgba(204, 204, 220, 0.16)", + "shade": "rgba(204, 204, 220, 0.20)", + "text": "rgb(204, 204, 220)", + "contrastText": "rgb(204, 204, 220)", + "border": "rgba(204, 204, 220, 0.25)", + "name": "secondary", + "transparent": "rgba(204, 204, 220, 0.15)" + }, + "info": { + "main": "#3D71D9", + "text": "#6E9FFF", + "border": "#6E9FFF", + "name": "info", + "shade": "rgb(90, 134, 222)", + "transparent": "#3D71D926", + "contrastText": "#FFFFFF" + }, + "error": { + "main": "#D10E5C", + "text": "#FF5286", + "name": "error", + "border": "#FF5286", + "shade": "rgb(215, 50, 116)", + "transparent": "#D10E5C26", + "contrastText": "#FFFFFF" + }, + "success": { + "main": "#1A7F4B", + "text": "#6CCF8E", + "name": "success", + "border": "#6CCF8E", + "shade": "rgb(60, 146, 102)", + "transparent": "#1A7F4B26", + "contrastText": "#FFFFFF" + }, + "warning": { + "main": "#F5B73D", + "text": "#F8D06B", + "name": "warning", + "border": "#F8D06B", + "shade": "rgb(246, 193, 90)", + "transparent": "#F5B73D26", + "contrastText": "#000000" + }, + "background": { + "canvas": "#111217", + "primary": "#181b1f", + "secondary": "#22252b" + }, + "action": { + "hover": "rgba(204, 204, 220, 0.16)", + "selected": "rgba(204, 204, 220, 0.12)", + "focus": "rgba(204, 204, 220, 0.16)", + "hoverOpacity": 0.08, + "disabledText": "rgba(204, 204, 220, 0.6)", + "disabledBackground": "rgba(204, 204, 220, 0.04)", + "disabledOpacity": 0.38 + }, + "gradients": { + "brandHorizontal": " linear-gradient(270deg, #F55F3E 0%, #FF8833 100%);", + "brandVertical": "linear-gradient(0.01deg, #F55F3E 0.01%, #FF8833 99.99%);" + }, + "contrastThreshold": 3, + "hoverFactor": 0.03, + "tonalOffset": 0.15 + }, + "breakpoints": { + "values": { + "xs": 0, + "sm": 544, + "md": 769, + "lg": 992, + "xl": 1200, + "xxl": 1440 + }, + "keys": ["xs", "sm", "md", "lg", "xl", "xxl"], + "unit": "px" + }, + "shape": {}, + "components": { + "height": { + "sm": 3, + "md": 4, + "lg": 6 + }, + "input": { + "borderColor": "rgba(204, 204, 220, 0.15)", + "borderHover": "rgba(204, 204, 220, 0.25)", + "text": "rgb(204, 204, 220)", + "background": "#111217" + }, + "panel": { + "padding": 1, + "headerHeight": 4, + "background": "#181b1f", + "borderColor": "rgba(204, 204, 220, 0.07)", + "boxShadow": "none" + }, + "dropdown": { + "background": "#111217" + }, + "tooltip": { + "background": "#35383e", + "text": "rgb(204, 204, 220)" + }, + "dashboard": { + "background": "#111217", + "padding": 1 + }, + "overlay": { + "background": "rgba(63, 62, 62, 0.45)" + }, + "sidemenu": { + "width": 48 + } + }, + "typography": { + "htmlFontSize": 14, + "fontFamily": "\"Roboto\", \"Helvetica\", \"Arial\", sans-serif", + "fontFamilyMonospace": "'Roboto Mono', monospace", + "fontSize": 14, + "fontWeightLight": 300, + "fontWeightRegular": 400, + "fontWeightMedium": 500, + "fontWeightBold": 500, + "size": { + "base": "14px", + "xs": "10px", + "sm": "12px", + "md": "14px", + "lg": "18px" + }, + "h1": { + "fontFamily": "\"Roboto\", \"Helvetica\", \"Arial\", sans-serif", + "fontWeight": 300, + "fontSize": "2rem", + "lineHeight": 1.167, + "letterSpacing": "-0.00893em" + }, + "h2": { + "fontFamily": "\"Roboto\", \"Helvetica\", \"Arial\", sans-serif", + "fontWeight": 300, + "fontSize": "1.7142857142857142rem", + "lineHeight": 1.2, + "letterSpacing": "0em" + }, + "h3": { + "fontFamily": "\"Roboto\", \"Helvetica\", \"Arial\", sans-serif", + "fontWeight": 400, + "fontSize": "1.5rem", + "lineHeight": 1.167, + "letterSpacing": "0em" + }, + "h4": { + "fontFamily": "\"Roboto\", \"Helvetica\", \"Arial\", sans-serif", + "fontWeight": 400, + "fontSize": "1.2857142857142858rem", + "lineHeight": 1.235, + "letterSpacing": "0.01389em" + }, + "h5": { + "fontFamily": "\"Roboto\", \"Helvetica\", \"Arial\", sans-serif", + "fontWeight": 400, + "fontSize": "1.1428571428571428rem", + "lineHeight": 1.334, + "letterSpacing": "0em" + }, + "h6": { + "fontFamily": "\"Roboto\", \"Helvetica\", \"Arial\", sans-serif", + "fontWeight": 500, + "fontSize": "1rem", + "lineHeight": 1.6, + "letterSpacing": "0.01071em" + }, + "body": { + "fontFamily": "\"Roboto\", \"Helvetica\", \"Arial\", sans-serif", + "fontWeight": 400, + "fontSize": "1rem", + "lineHeight": 1.5, + "letterSpacing": "0.01071em" + }, + "bodySmall": { + "fontFamily": "\"Roboto\", \"Helvetica\", \"Arial\", sans-serif", + "fontWeight": 400, + "fontSize": "0.8571428571428571rem", + "lineHeight": 1.5, + "letterSpacing": "0.0125em" + } + }, + "shadows": { + "z1": "0px 1px 2px rgba(24, 26, 27, 0.75)", + "z2": "0px 4px 8px rgba(24, 26, 27, 0.75)", + "z3": "0px 8px 24px rgb(1,4,9)" + }, + "transitions": { + "duration": { + "shortest": 150, + "shorter": 200, + "short": 250, + "standard": 300, + "complex": 375, + "enteringScreen": 225, + "leavingScreen": 195 + }, + "easing": { + "easeInOut": "cubic-bezier(0.4, 0, 0.2, 1)", + "easeOut": "cubic-bezier(0.0, 0, 0.2, 1)", + "easeIn": "cubic-bezier(0.4, 0, 1, 1)", + "sharp": "cubic-bezier(0.4, 0, 0.6, 1)" + } + }, + "visualization": { + "hues": [ + { + "name": "red", + "shades": [ + { + "color": "#FFA6B0", + "name": "super-light-red" + }, + { + "color": "#FF7383", + "name": "light-red" + }, + { + "color": "#F2495C", + "name": "red", + "primary": true + }, + { + "color": "#E02F44", + "name": "semi-dark-red" + }, + { + "color": "#C4162A", + "name": "dark-red" + } + ] + }, + { + "name": "orange", + "shades": [ + { + "color": "#FFCB7D", + "name": "super-light-orange", + "aliases": [] + }, + { + "color": "#FFB357", + "name": "light-orange", + "aliases": [] + }, + { + "color": "#FF9830", + "name": "orange", + "aliases": [], + "primary": true + }, + { + "color": "#FF780A", + "name": "semi-dark-orange", + "aliases": [] + }, + { + "color": "#FA6400", + "name": "dark-orange", + "aliases": [] + } + ] + }, + { + "name": "yellow", + "shades": [ + { + "color": "#FFF899", + "name": "super-light-yellow", + "aliases": [] + }, + { + "color": "#FFEE52", + "name": "light-yellow", + "aliases": [] + }, + { + "color": "#FADE2A", + "name": "yellow", + "aliases": [], + "primary": true + }, + { + "color": "#F2CC0C", + "name": "semi-dark-yellow", + "aliases": [] + }, + { + "color": "#E0B400", + "name": "dark-yellow", + "aliases": [] + } + ] + }, + { + "name": "green", + "shades": [ + { + "color": "#C8F2C2", + "name": "super-light-green", + "aliases": [] + }, + { + "color": "#96D98D", + "name": "light-green", + "aliases": [] + }, + { + "color": "#73BF69", + "name": "green", + "aliases": [], + "primary": true + }, + { + "color": "#56A64B", + "name": "semi-dark-green", + "aliases": [] + }, + { + "color": "#37872D", + "name": "dark-green", + "aliases": [] + } + ] + }, + { + "name": "blue", + "shades": [ + { + "color": "#C0D8FF", + "name": "super-light-blue", + "aliases": [] + }, + { + "color": "#8AB8FF", + "name": "light-blue", + "aliases": [] + }, + { + "color": "#5794F2", + "name": "blue", + "aliases": [], + "primary": true + }, + { + "color": "#3274D9", + "name": "semi-dark-blue", + "aliases": [] + }, + { + "color": "#1F60C4", + "name": "dark-blue", + "aliases": [] + } + ] + }, + { + "name": "purple", + "shades": [ + { + "color": "#DEB6F2", + "name": "super-light-purple", + "aliases": [] + }, + { + "color": "#CA95E5", + "name": "light-purple", + "aliases": [] + }, + { + "color": "#B877D9", + "name": "purple", + "aliases": [], + "primary": true + }, + { + "color": "#A352CC", + "name": "semi-dark-purple", + "aliases": [] + }, + { + "color": "#8F3BB8", + "name": "dark-purple", + "aliases": [] + } + ] + } + ], + "palette": [ + "green", + "semi-dark-yellow", + "light-blue", + "semi-dark-orange", + "red", + "blue", + "purple", + "#705DA0", + "dark-green", + "yellow", + "#447EBC", + "#C15C17", + "#890F02", + "#0A437C", + "#6D1F62", + "#584477", + "#B7DBAB", + "#F4D598", + "#70DBED", + "#F9BA8F", + "#F29191", + "#82B5D8", + "#E5A8E2", + "#AEA2E0", + "#629E51", + "#E5AC0E", + "#64B0C8", + "#E0752D", + "#BF1B00", + "#0A50A1", + "#962D82", + "#614D93", + "#9AC48A", + "#F2C96D", + "#65C5DB", + "#F9934E", + "#EA6460", + "#5195CE", + "#D683CE", + "#806EB7", + "#3F6833", + "#967302", + "#2F575E", + "#99440A", + "#58140C", + "#052B51", + "#511749", + "#3F2B5B", + "#E0F9D7", + "#FCEACA", + "#CFFAFF", + "#F9E2D2", + "#FCE2DE", + "#BADFF4", + "#F9D9F9", + "#DEDAF7" + ] + }, + "zIndex": { + "navbarFixed": 1000, + "sidemenu": 1020, + "dropdown": 1030, + "typeahead": 1030, + "tooltip": 1040, + "modalBackdrop": 1050, + "modal": 1060, + "portal": 1061 + }, + "v1": { + "name": "Dark", + "typography": { + "fontFamily": { + "sansSerif": "\"Roboto\", \"Helvetica\", \"Arial\", sans-serif", + "monospace": "'Roboto Mono', monospace" + }, + "size": { + "base": "14px", + "xs": "10px", + "sm": "12px", + "md": "14px", + "lg": "18px" + }, + "heading": { + "h1": "2rem", + "h2": "1.7142857142857142rem", + "h3": "1.5rem", + "h4": "1.2857142857142858rem", + "h5": "1.1428571428571428rem", + "h6": "1rem" + }, + "weight": { + "light": 300, + "regular": 400, + "semibold": 500, + "bold": 500 + }, + "lineHeight": { + "xs": 1.5, + "sm": 1.5, + "md": 1.5, + "lg": 1.2 + }, + "link": { + "decoration": "none", + "hoverDecoration": "none" + } + }, + "breakpoints": { + "xs": "0px", + "sm": "544px", + "md": "769px", + "lg": "992px", + "xl": "1200px", + "xxl": "1440px" + }, + "spacing": { + "base": 8, + "insetSquishMd": "4px 8px", + "d": "16px", + "xxs": "2px", + "xs": "4px", + "sm": "8px", + "md": "16px", + "lg": "24px", + "xl": "32px", + "gutter": "32px", + "formSpacingBase": 8, + "formMargin": "32px", + "formFieldsetMargin": "16px", + "formInputHeight": 32, + "formButtonHeight": 32, + "formInputPaddingHorizontal": "8px", + "formInputAffixPaddingHorizontal": "4px", + "formInputMargin": "16px", + "formLabelPadding": "0 0 0 2px", + "formLabelMargin": "0 0 4px 0", + "formValidationMessagePadding": "4px 8px", + "formValidationMessageMargin": "4px 0 0 0", + "inlineFormMargin": "4px" + }, + "border": { + "radius": { + "sm": "2px", + "md": "4px", + "lg": "6px" + }, + "width": { + "sm": "1px" + } + }, + "height": { + "sm": 24, + "md": 32, + "lg": 48 + }, + "panelPadding": 8, + "panelHeaderHeight": 32, + "zIndex": { + "navbarFixed": 1000, + "sidemenu": 1020, + "dropdown": 1030, + "typeahead": 1030, + "tooltip": 1040, + "modalBackdrop": 1050, + "modal": 1060, + "portal": 1061 + }, + "type": "dark", + "isDark": true, + "isLight": false, + "palette": { + "gray98": "#f7f8fa", + "gray97": "#f1f5f9", + "gray95": "#e9edf2", + "gray90": "#dce1e6", + "gray85": "#c7d0d9", + "gray70": "#9fa7b3", + "gray60": "#7b8087", + "gray33": "#464c54", + "gray25": "#2c3235", + "gray15": "#202226", + "gray10": "#141619", + "gray05": "#0b0c0e", + "blue95": "#5794f2", + "blue85": "#33a2e5", + "blue80": "#3274d9", + "blue77": "#1f60c4", + "red88": "#e02f44", + "black": "#000000", + "white": "#ffffff", + "dark1": "#141414", + "dark2": "#161719", + "dark3": "#1f1f20", + "dark4": "#212124", + "dark5": "#222426", + "dark6": "#262628", + "dark7": "#292a2d", + "dark8": "#2f2f32", + "dark9": "#343436", + "dark10": "#424345", + "gray1": "#555555", + "gray2": "#8e8e8e", + "gray3": "#b3b3b3", + "gray4": "#d8d9da", + "gray5": "#ececec", + "gray6": "#f4f5f8", + "gray7": "#fbfbfb", + "redBase": "#e02f44", + "redShade": "#c4162a", + "greenBase": "#299c46", + "greenShade": "#23843b", + "red": "#d44a3a", + "yellow": "#ecbb13", + "purple": "#9933cc", + "variable": "#32d1df", + "orange": "#eb7b18", + "orangeDark": "#ff780a", + "brandPrimary": "#eb7b18", + "brandSuccess": "#1A7F4B", + "brandWarning": "#F5B73D", + "brandDanger": "#D10E5C", + "queryRed": "#FF5286", + "queryGreen": "#6CCF8E", + "queryPurple": "#fe85fc", + "queryOrange": "#eb7b18", + "online": "#1A7F4B", + "warn": "#1A7F4B", + "critical": "#1A7F4B" + }, + "colors": { + "bg1": "#181b1f", + "bg2": "#22252b", + "bg3": "rgba(204, 204, 220, 0.16)", + "dashboardBg": "#111217", + "bgBlue1": "#3D71D9", + "bgBlue2": "rgb(90, 134, 222)", + "border1": "rgba(204, 204, 220, 0.07)", + "border2": "rgba(204, 204, 220, 0.15)", + "border3": "rgba(204, 204, 220, 0.25)", + "formLabel": "rgb(204, 204, 220)", + "formDescription": "rgba(204, 204, 220, 0.65)", + "formInputBg": "#111217", + "formInputBgDisabled": "rgba(204, 204, 220, 0.04)", + "formInputBorder": "rgba(204, 204, 220, 0.15)", + "formInputBorderHover": "rgba(204, 204, 220, 0.25)", + "formInputBorderActive": "#6E9FFF", + "formInputBorderInvalid": "#FF5286", + "formInputPlaceholderText": "rgba(204, 204, 220, 0.6)", + "formInputText": "rgb(204, 204, 220)", + "formInputDisabledText": "rgba(204, 204, 220, 0.6)", + "formFocusOutline": "#3D71D9", + "formValidationMessageText": "#FFFFFF", + "formValidationMessageBg": "#D10E5C", + "textStrong": "#FFFFFF", + "textHeading": "rgb(204, 204, 220)", + "text": "rgb(204, 204, 220)", + "textSemiWeak": "rgba(204, 204, 220, 0.65)", + "textWeak": "rgba(204, 204, 220, 0.65)", + "textFaint": "rgba(204, 204, 220, 0.6)", + "textBlue": "#6E9FFF", + "bodyBg": "#111217", + "panelBg": "#181b1f", + "panelBorder": "rgba(204, 204, 220, 0.07)", + "pageHeaderBg": "#111217", + "pageHeaderBorder": "#111217", + "dropdownBg": "#111217", + "dropdownShadow": "#000000", + "dropdownOptionHoverBg": "#22252b", + "link": "rgb(204, 204, 220)", + "linkDisabled": "rgba(204, 204, 220, 0.6)", + "linkHover": "#FFFFFF", + "linkExternal": "#6E9FFF" + }, + "shadows": { + "listItem": "none" + }, + "visualization": { + "hues": [ + { + "name": "red", + "shades": [ + { + "color": "#FFA6B0", + "name": "super-light-red" + }, + { + "color": "#FF7383", + "name": "light-red" + }, + { + "color": "#F2495C", + "name": "red", + "primary": true + }, + { + "color": "#E02F44", + "name": "semi-dark-red" + }, + { + "color": "#C4162A", + "name": "dark-red" + } + ] + }, + { + "name": "orange", + "shades": [ + { + "color": "#FFCB7D", + "name": "super-light-orange", + "aliases": [] + }, + { + "color": "#FFB357", + "name": "light-orange", + "aliases": [] + }, + { + "color": "#FF9830", + "name": "orange", + "aliases": [], + "primary": true + }, + { + "color": "#FF780A", + "name": "semi-dark-orange", + "aliases": [] + }, + { + "color": "#FA6400", + "name": "dark-orange", + "aliases": [] + } + ] + }, + { + "name": "yellow", + "shades": [ + { + "color": "#FFF899", + "name": "super-light-yellow", + "aliases": [] + }, + { + "color": "#FFEE52", + "name": "light-yellow", + "aliases": [] + }, + { + "color": "#FADE2A", + "name": "yellow", + "aliases": [], + "primary": true + }, + { + "color": "#F2CC0C", + "name": "semi-dark-yellow", + "aliases": [] + }, + { + "color": "#E0B400", + "name": "dark-yellow", + "aliases": [] + } + ] + }, + { + "name": "green", + "shades": [ + { + "color": "#C8F2C2", + "name": "super-light-green", + "aliases": [] + }, + { + "color": "#96D98D", + "name": "light-green", + "aliases": [] + }, + { + "color": "#73BF69", + "name": "green", + "aliases": [], + "primary": true + }, + { + "color": "#56A64B", + "name": "semi-dark-green", + "aliases": [] + }, + { + "color": "#37872D", + "name": "dark-green", + "aliases": [] + } + ] + }, + { + "name": "blue", + "shades": [ + { + "color": "#C0D8FF", + "name": "super-light-blue", + "aliases": [] + }, + { + "color": "#8AB8FF", + "name": "light-blue", + "aliases": [] + }, + { + "color": "#5794F2", + "name": "blue", + "aliases": [], + "primary": true + }, + { + "color": "#3274D9", + "name": "semi-dark-blue", + "aliases": [] + }, + { + "color": "#1F60C4", + "name": "dark-blue", + "aliases": [] + } + ] + }, + { + "name": "purple", + "shades": [ + { + "color": "#DEB6F2", + "name": "super-light-purple", + "aliases": [] + }, + { + "color": "#CA95E5", + "name": "light-purple", + "aliases": [] + }, + { + "color": "#B877D9", + "name": "purple", + "aliases": [], + "primary": true + }, + { + "color": "#A352CC", + "name": "semi-dark-purple", + "aliases": [] + }, + { + "color": "#8F3BB8", + "name": "dark-purple", + "aliases": [] + } + ] + } + ], + "palette": [ + "green", + "semi-dark-yellow", + "light-blue", + "semi-dark-orange", + "red", + "blue", + "purple", + "#705DA0", + "dark-green", + "yellow", + "#447EBC", + "#C15C17", + "#890F02", + "#0A437C", + "#6D1F62", + "#584477", + "#B7DBAB", + "#F4D598", + "#70DBED", + "#F9BA8F", + "#F29191", + "#82B5D8", + "#E5A8E2", + "#AEA2E0", + "#629E51", + "#E5AC0E", + "#64B0C8", + "#E0752D", + "#BF1B00", + "#0A50A1", + "#962D82", + "#614D93", + "#9AC48A", + "#F2C96D", + "#65C5DB", + "#F9934E", + "#EA6460", + "#5195CE", + "#D683CE", + "#806EB7", + "#3F6833", + "#967302", + "#2F575E", + "#99440A", + "#58140C", + "#052B51", + "#511749", + "#3F2B5B", + "#E0F9D7", + "#FCEACA", + "#CFFAFF", + "#F9E2D2", + "#FCE2DE", + "#BADFF4", + "#F9D9F9", + "#DEDAF7" + ] + } + } +} diff --git a/public/sass/theme.light.generated.json b/public/sass/theme.light.generated.json new file mode 100644 index 00000000000..1cdea276243 --- /dev/null +++ b/public/sass/theme.light.generated.json @@ -0,0 +1,946 @@ +{ + "name": "Dark", + "isDark": false, + "isLight": true, + "colors": { + "mode": "light", + "blackBase": "36, 41, 46", + "primary": { + "main": "#3871DC", + "border": "#1F62E0", + "text": "#1F62E0", + "name": "primary", + "shade": "rgb(44, 90, 176)", + "transparent": "#3871DC14", + "contrastText": "#FFFFFF" + }, + "text": { + "primary": "rgba(36, 41, 46, 1)", + "secondary": "rgba(36, 41, 46, 0.75)", + "disabled": "rgba(36, 41, 46, 0.50)", + "link": "#1F62E0", + "maxContrast": "#000000" + }, + "border": { + "weak": "rgba(36, 41, 46, 0.12)", + "medium": "rgba(36, 41, 46, 0.30)", + "strong": "rgba(36, 41, 46, 0.40)" + }, + "secondary": { + "main": "rgba(36, 41, 46, 0.16)", + "shade": "rgba(36, 41, 46, 0.20)", + "contrastText": "rgba(36, 41, 46, 1)", + "text": "rgba(36, 41, 46, 1)", + "border": "rgba(36, 41, 46, 0.40)", + "name": "secondary", + "transparent": "rgba(36, 41, 46, 0.08)" + }, + "info": { + "main": "#3871DC", + "text": "#1F62E0", + "name": "info", + "border": "#1F62E0", + "shade": "rgb(44, 90, 176)", + "transparent": "#3871DC14", + "contrastText": "#FFFFFF" + }, + "error": { + "main": "#E0226E", + "text": "#CF0E5B", + "border": "#CF0E5B", + "name": "error", + "shade": "rgb(179, 27, 88)", + "transparent": "#E0226E14", + "contrastText": "#FFFFFF" + }, + "success": { + "main": "#1B855E", + "text": "#0A764E", + "name": "success", + "border": "#0A764E", + "shade": "rgb(21, 106, 75)", + "transparent": "#1B855E14", + "contrastText": "#FFFFFF" + }, + "warning": { + "main": "#FAD34A", + "text": "#8A6C00", + "name": "warning", + "border": "#8A6C00", + "shade": "rgb(200, 168, 59)", + "transparent": "#FAD34A14", + "contrastText": "#000000" + }, + "background": { + "canvas": "#F4F5F5", + "primary": "#FFFFFF", + "secondary": "#F4F5F5" + }, + "action": { + "hover": "rgba(36, 41, 46, 0.12)", + "selected": "rgba(36, 41, 46, 0.08)", + "hoverOpacity": 0.08, + "focus": "rgba(36, 41, 46, 0.12)", + "disabledBackground": "rgba(36, 41, 46, 0.04)", + "disabledText": "rgba(36, 41, 46, 0.50)", + "disabledOpacity": 0.38 + }, + "gradients": { + "brandHorizontal": "linear-gradient(90deg, #FF8833 0%, #F53E4C 100%);", + "brandVertical": "linear-gradient(0.01deg, #F53E4C -31.2%, #FF8833 113.07%);" + }, + "contrastThreshold": 3, + "hoverFactor": 0.03, + "tonalOffset": 0.2 + }, + "breakpoints": { + "values": { + "xs": 0, + "sm": 544, + "md": 769, + "lg": 992, + "xl": 1200, + "xxl": 1440 + }, + "keys": ["xs", "sm", "md", "lg", "xl", "xxl"], + "unit": "px" + }, + "shape": {}, + "components": { + "height": { + "sm": 3, + "md": 4, + "lg": 6 + }, + "input": { + "borderColor": "rgba(36, 41, 46, 0.30)", + "borderHover": "rgba(36, 41, 46, 0.40)", + "text": "rgba(36, 41, 46, 1)", + "background": "#FFFFFF" + }, + "panel": { + "padding": 1, + "headerHeight": 4, + "background": "#FFFFFF", + "borderColor": "rgba(36, 41, 46, 0.12)", + "boxShadow": "none" + }, + "dropdown": { + "background": "#FFFFFF" + }, + "tooltip": { + "background": "#555", + "text": "#FFF" + }, + "dashboard": { + "background": "#F4F5F5", + "padding": 1 + }, + "overlay": { + "background": "rgba(208, 209, 211, 0.24)" + }, + "sidemenu": { + "width": 48 + } + }, + "typography": { + "htmlFontSize": 14, + "fontFamily": "\"Roboto\", \"Helvetica\", \"Arial\", sans-serif", + "fontFamilyMonospace": "'Roboto Mono', monospace", + "fontSize": 14, + "fontWeightLight": 300, + "fontWeightRegular": 400, + "fontWeightMedium": 500, + "fontWeightBold": 500, + "size": { + "base": "14px", + "xs": "10px", + "sm": "12px", + "md": "14px", + "lg": "18px" + }, + "h1": { + "fontFamily": "\"Roboto\", \"Helvetica\", \"Arial\", sans-serif", + "fontWeight": 300, + "fontSize": "2rem", + "lineHeight": 1.167, + "letterSpacing": "-0.00893em" + }, + "h2": { + "fontFamily": "\"Roboto\", \"Helvetica\", \"Arial\", sans-serif", + "fontWeight": 300, + "fontSize": "1.7142857142857142rem", + "lineHeight": 1.2, + "letterSpacing": "0em" + }, + "h3": { + "fontFamily": "\"Roboto\", \"Helvetica\", \"Arial\", sans-serif", + "fontWeight": 400, + "fontSize": "1.5rem", + "lineHeight": 1.167, + "letterSpacing": "0em" + }, + "h4": { + "fontFamily": "\"Roboto\", \"Helvetica\", \"Arial\", sans-serif", + "fontWeight": 400, + "fontSize": "1.2857142857142858rem", + "lineHeight": 1.235, + "letterSpacing": "0.01389em" + }, + "h5": { + "fontFamily": "\"Roboto\", \"Helvetica\", \"Arial\", sans-serif", + "fontWeight": 400, + "fontSize": "1.1428571428571428rem", + "lineHeight": 1.334, + "letterSpacing": "0em" + }, + "h6": { + "fontFamily": "\"Roboto\", \"Helvetica\", \"Arial\", sans-serif", + "fontWeight": 500, + "fontSize": "1rem", + "lineHeight": 1.6, + "letterSpacing": "0.01071em" + }, + "body": { + "fontFamily": "\"Roboto\", \"Helvetica\", \"Arial\", sans-serif", + "fontWeight": 400, + "fontSize": "1rem", + "lineHeight": 1.5, + "letterSpacing": "0.01071em" + }, + "bodySmall": { + "fontFamily": "\"Roboto\", \"Helvetica\", \"Arial\", sans-serif", + "fontWeight": 400, + "fontSize": "0.8571428571428571rem", + "lineHeight": 1.5, + "letterSpacing": "0.0125em" + } + }, + "shadows": { + "z1": "0px 1px 2px rgba(24, 26, 27, 0.2)", + "z2": "0px 4px 8px rgba(24, 26, 27, 0.2)", + "z3": "0px 13px 20px 1px rgba(24, 26, 27, 0.18)" + }, + "transitions": { + "duration": { + "shortest": 150, + "shorter": 200, + "short": 250, + "standard": 300, + "complex": 375, + "enteringScreen": 225, + "leavingScreen": 195 + }, + "easing": { + "easeInOut": "cubic-bezier(0.4, 0, 0.2, 1)", + "easeOut": "cubic-bezier(0.0, 0, 0.2, 1)", + "easeIn": "cubic-bezier(0.4, 0, 1, 1)", + "sharp": "cubic-bezier(0.4, 0, 0.6, 1)" + } + }, + "visualization": { + "hues": [ + { + "name": "red", + "shades": [ + { + "color": "#FF7383", + "name": "super-light-red" + }, + { + "color": "#F2495C", + "name": "light-red" + }, + { + "color": "#E02F44", + "name": "red", + "primary": true + }, + { + "color": "#C4162A", + "name": "semi-dark-red" + }, + { + "color": "#AD0317", + "name": "dark-red" + } + ] + }, + { + "name": "orange", + "shades": [ + { + "color": "#FFB357", + "name": "super-light-orange", + "aliases": [] + }, + { + "color": "#FF9830", + "name": "light-orange", + "aliases": [] + }, + { + "color": "#FF780A", + "name": "orange", + "aliases": [], + "primary": true + }, + { + "color": "#FA6400", + "name": "semi-dark-orange", + "aliases": [] + }, + { + "color": "#E55400", + "name": "dark-orange", + "aliases": [] + } + ] + }, + { + "name": "yellow", + "shades": [ + { + "color": "#FFEE52", + "name": "super-light-yellow", + "aliases": [] + }, + { + "color": "#FADE2A", + "name": "light-yellow", + "aliases": [] + }, + { + "color": "#F2CC0C", + "name": "yellow", + "aliases": [], + "primary": true + }, + { + "color": "#E0B400", + "name": "semi-dark-yellow", + "aliases": [] + }, + { + "color": "#CC9D00", + "name": "dark-yellow", + "aliases": [] + } + ] + }, + { + "name": "green", + "shades": [ + { + "color": "#96D98D", + "name": "super-light-green", + "aliases": [] + }, + { + "color": "#73BF69", + "name": "light-green", + "aliases": [] + }, + { + "color": "#56A64B", + "name": "green", + "aliases": [], + "primary": true + }, + { + "color": "#37872D", + "name": "semi-dark-green", + "aliases": [] + }, + { + "color": "#19730E", + "name": "dark-green", + "aliases": [] + } + ] + }, + { + "name": "blue", + "shades": [ + { + "color": "#8AB8FF", + "name": "super-light-blue", + "aliases": [] + }, + { + "color": "#5794F2", + "name": "light-blue", + "aliases": [] + }, + { + "color": "#3274D9", + "name": "blue", + "aliases": [], + "primary": true + }, + { + "color": "#1F60C4", + "name": "semi-dark-blue", + "aliases": [] + }, + { + "color": "#1250B0", + "name": "dark-blue", + "aliases": [] + } + ] + }, + { + "name": "purple", + "shades": [ + { + "color": "#CA95E5", + "name": "super-light-purple", + "aliases": [] + }, + { + "color": "#B877D9", + "name": "light-purple", + "aliases": [] + }, + { + "color": "#A352CC", + "name": "purple", + "aliases": [], + "primary": true + }, + { + "color": "#8F3BB8", + "name": "semi-dark-purple", + "aliases": [] + }, + { + "color": "#7C2EA3", + "name": "dark-purple", + "aliases": [] + } + ] + } + ], + "palette": [ + "green", + "semi-dark-yellow", + "light-blue", + "semi-dark-orange", + "red", + "blue", + "purple", + "#705DA0", + "dark-green", + "yellow", + "#447EBC", + "#C15C17", + "#890F02", + "#0A437C", + "#6D1F62", + "#584477", + "#B7DBAB", + "#F4D598", + "#70DBED", + "#F9BA8F", + "#F29191", + "#82B5D8", + "#E5A8E2", + "#AEA2E0", + "#629E51", + "#E5AC0E", + "#64B0C8", + "#E0752D", + "#BF1B00", + "#0A50A1", + "#962D82", + "#614D93", + "#9AC48A", + "#F2C96D", + "#65C5DB", + "#F9934E", + "#EA6460", + "#5195CE", + "#D683CE", + "#806EB7", + "#3F6833", + "#967302", + "#2F575E", + "#99440A", + "#58140C", + "#052B51", + "#511749", + "#3F2B5B", + "#E0F9D7", + "#FCEACA", + "#CFFAFF", + "#F9E2D2", + "#FCE2DE", + "#BADFF4", + "#F9D9F9", + "#DEDAF7" + ] + }, + "zIndex": { + "navbarFixed": 1000, + "sidemenu": 1020, + "dropdown": 1030, + "typeahead": 1030, + "tooltip": 1040, + "modalBackdrop": 1050, + "modal": 1060, + "portal": 1061 + }, + "v1": { + "name": "Dark", + "typography": { + "fontFamily": { + "sansSerif": "\"Roboto\", \"Helvetica\", \"Arial\", sans-serif", + "monospace": "'Roboto Mono', monospace" + }, + "size": { + "base": "14px", + "xs": "10px", + "sm": "12px", + "md": "14px", + "lg": "18px" + }, + "heading": { + "h1": "2rem", + "h2": "1.7142857142857142rem", + "h3": "1.5rem", + "h4": "1.2857142857142858rem", + "h5": "1.1428571428571428rem", + "h6": "1rem" + }, + "weight": { + "light": 300, + "regular": 400, + "semibold": 500, + "bold": 500 + }, + "lineHeight": { + "xs": 1.5, + "sm": 1.5, + "md": 1.5, + "lg": 1.2 + }, + "link": { + "decoration": "none", + "hoverDecoration": "none" + } + }, + "breakpoints": { + "xs": "0px", + "sm": "544px", + "md": "769px", + "lg": "992px", + "xl": "1200px", + "xxl": "1440px" + }, + "spacing": { + "base": 8, + "insetSquishMd": "4px 8px", + "d": "16px", + "xxs": "2px", + "xs": "4px", + "sm": "8px", + "md": "16px", + "lg": "24px", + "xl": "32px", + "gutter": "32px", + "formSpacingBase": 8, + "formMargin": "32px", + "formFieldsetMargin": "16px", + "formInputHeight": 32, + "formButtonHeight": 32, + "formInputPaddingHorizontal": "8px", + "formInputAffixPaddingHorizontal": "4px", + "formInputMargin": "16px", + "formLabelPadding": "0 0 0 2px", + "formLabelMargin": "0 0 4px 0", + "formValidationMessagePadding": "4px 8px", + "formValidationMessageMargin": "4px 0 0 0", + "inlineFormMargin": "4px" + }, + "border": { + "radius": { + "sm": "2px", + "md": "4px", + "lg": "6px" + }, + "width": { + "sm": "1px" + } + }, + "height": { + "sm": 24, + "md": 32, + "lg": 48 + }, + "panelPadding": 8, + "panelHeaderHeight": 32, + "zIndex": { + "navbarFixed": 1000, + "sidemenu": 1020, + "dropdown": 1030, + "typeahead": 1030, + "tooltip": 1040, + "modalBackdrop": 1050, + "modal": 1060, + "portal": 1061 + }, + "type": "light", + "isDark": false, + "isLight": true, + "palette": { + "gray98": "#f7f8fa", + "gray97": "#f1f5f9", + "gray95": "#e9edf2", + "gray90": "#dce1e6", + "gray85": "#c7d0d9", + "gray70": "#9fa7b3", + "gray60": "#7b8087", + "gray33": "#464c54", + "gray25": "#2c3235", + "gray15": "#202226", + "gray10": "#141619", + "gray05": "#0b0c0e", + "blue95": "#5794f2", + "blue85": "#33a2e5", + "blue80": "#3274d9", + "blue77": "#1f60c4", + "red88": "#e02f44", + "black": "#000000", + "white": "#ffffff", + "dark1": "#141414", + "dark2": "#161719", + "dark3": "#1f1f20", + "dark4": "#212124", + "dark5": "#222426", + "dark6": "#262628", + "dark7": "#292a2d", + "dark8": "#2f2f32", + "dark9": "#343436", + "dark10": "#424345", + "gray1": "#555555", + "gray2": "#8e8e8e", + "gray3": "#b3b3b3", + "gray4": "#d8d9da", + "gray5": "#ececec", + "gray6": "#f4f5f8", + "gray7": "#fbfbfb", + "redBase": "#e02f44", + "redShade": "#c4162a", + "greenBase": "#299c46", + "greenShade": "#23843b", + "red": "#d44a3a", + "yellow": "#ecbb13", + "purple": "#9933cc", + "variable": "#32d1df", + "orange": "#eb7b18", + "orangeDark": "#ff780a", + "brandPrimary": "#eb7b18", + "brandSuccess": "#1B855E", + "brandWarning": "#FAD34A", + "brandDanger": "#E0226E", + "queryRed": "#CF0E5B", + "queryGreen": "#0A764E", + "queryPurple": "#fe85fc", + "queryOrange": "#eb7b18", + "online": "#1B855E", + "warn": "#1B855E", + "critical": "#1B855E" + }, + "colors": { + "bg1": "#FFFFFF", + "bg2": "#F4F5F5", + "bg3": "rgba(36, 41, 46, 0.12)", + "dashboardBg": "#F4F5F5", + "bgBlue1": "#3871DC", + "bgBlue2": "rgb(44, 90, 176)", + "border1": "rgba(36, 41, 46, 0.12)", + "border2": "rgba(36, 41, 46, 0.30)", + "border3": "rgba(36, 41, 46, 0.40)", + "formLabel": "rgba(36, 41, 46, 1)", + "formDescription": "rgba(36, 41, 46, 0.75)", + "formInputBg": "#FFFFFF", + "formInputBgDisabled": "rgba(36, 41, 46, 0.04)", + "formInputBorder": "rgba(36, 41, 46, 0.30)", + "formInputBorderHover": "rgba(36, 41, 46, 0.40)", + "formInputBorderActive": "#1F62E0", + "formInputBorderInvalid": "#CF0E5B", + "formInputPlaceholderText": "rgba(36, 41, 46, 0.50)", + "formInputText": "rgba(36, 41, 46, 1)", + "formInputDisabledText": "rgba(36, 41, 46, 0.50)", + "formFocusOutline": "#3871DC", + "formValidationMessageText": "#FFFFFF", + "formValidationMessageBg": "#E0226E", + "textStrong": "#000000", + "textHeading": "rgba(36, 41, 46, 1)", + "text": "rgba(36, 41, 46, 1)", + "textSemiWeak": "rgba(36, 41, 46, 0.75)", + "textWeak": "rgba(36, 41, 46, 0.75)", + "textFaint": "rgba(36, 41, 46, 0.50)", + "textBlue": "#1F62E0", + "bodyBg": "#F4F5F5", + "panelBg": "#FFFFFF", + "panelBorder": "rgba(36, 41, 46, 0.12)", + "pageHeaderBg": "#F4F5F5", + "pageHeaderBorder": "#F4F5F5", + "dropdownBg": "#FFFFFF", + "dropdownShadow": "#000000", + "dropdownOptionHoverBg": "#F4F5F5", + "link": "rgba(36, 41, 46, 1)", + "linkDisabled": "rgba(36, 41, 46, 0.50)", + "linkHover": "#000000", + "linkExternal": "#1F62E0" + }, + "shadows": { + "listItem": "none" + }, + "visualization": { + "hues": [ + { + "name": "red", + "shades": [ + { + "color": "#FF7383", + "name": "super-light-red" + }, + { + "color": "#F2495C", + "name": "light-red" + }, + { + "color": "#E02F44", + "name": "red", + "primary": true + }, + { + "color": "#C4162A", + "name": "semi-dark-red" + }, + { + "color": "#AD0317", + "name": "dark-red" + } + ] + }, + { + "name": "orange", + "shades": [ + { + "color": "#FFB357", + "name": "super-light-orange", + "aliases": [] + }, + { + "color": "#FF9830", + "name": "light-orange", + "aliases": [] + }, + { + "color": "#FF780A", + "name": "orange", + "aliases": [], + "primary": true + }, + { + "color": "#FA6400", + "name": "semi-dark-orange", + "aliases": [] + }, + { + "color": "#E55400", + "name": "dark-orange", + "aliases": [] + } + ] + }, + { + "name": "yellow", + "shades": [ + { + "color": "#FFEE52", + "name": "super-light-yellow", + "aliases": [] + }, + { + "color": "#FADE2A", + "name": "light-yellow", + "aliases": [] + }, + { + "color": "#F2CC0C", + "name": "yellow", + "aliases": [], + "primary": true + }, + { + "color": "#E0B400", + "name": "semi-dark-yellow", + "aliases": [] + }, + { + "color": "#CC9D00", + "name": "dark-yellow", + "aliases": [] + } + ] + }, + { + "name": "green", + "shades": [ + { + "color": "#96D98D", + "name": "super-light-green", + "aliases": [] + }, + { + "color": "#73BF69", + "name": "light-green", + "aliases": [] + }, + { + "color": "#56A64B", + "name": "green", + "aliases": [], + "primary": true + }, + { + "color": "#37872D", + "name": "semi-dark-green", + "aliases": [] + }, + { + "color": "#19730E", + "name": "dark-green", + "aliases": [] + } + ] + }, + { + "name": "blue", + "shades": [ + { + "color": "#8AB8FF", + "name": "super-light-blue", + "aliases": [] + }, + { + "color": "#5794F2", + "name": "light-blue", + "aliases": [] + }, + { + "color": "#3274D9", + "name": "blue", + "aliases": [], + "primary": true + }, + { + "color": "#1F60C4", + "name": "semi-dark-blue", + "aliases": [] + }, + { + "color": "#1250B0", + "name": "dark-blue", + "aliases": [] + } + ] + }, + { + "name": "purple", + "shades": [ + { + "color": "#CA95E5", + "name": "super-light-purple", + "aliases": [] + }, + { + "color": "#B877D9", + "name": "light-purple", + "aliases": [] + }, + { + "color": "#A352CC", + "name": "purple", + "aliases": [], + "primary": true + }, + { + "color": "#8F3BB8", + "name": "semi-dark-purple", + "aliases": [] + }, + { + "color": "#7C2EA3", + "name": "dark-purple", + "aliases": [] + } + ] + } + ], + "palette": [ + "green", + "semi-dark-yellow", + "light-blue", + "semi-dark-orange", + "red", + "blue", + "purple", + "#705DA0", + "dark-green", + "yellow", + "#447EBC", + "#C15C17", + "#890F02", + "#0A437C", + "#6D1F62", + "#584477", + "#B7DBAB", + "#F4D598", + "#70DBED", + "#F9BA8F", + "#F29191", + "#82B5D8", + "#E5A8E2", + "#AEA2E0", + "#629E51", + "#E5AC0E", + "#64B0C8", + "#E0752D", + "#BF1B00", + "#0A50A1", + "#962D82", + "#614D93", + "#9AC48A", + "#F2C96D", + "#65C5DB", + "#F9934E", + "#EA6460", + "#5195CE", + "#D683CE", + "#806EB7", + "#3F6833", + "#967302", + "#2F575E", + "#99440A", + "#58140C", + "#052B51", + "#511749", + "#3F2B5B", + "#E0F9D7", + "#FCEACA", + "#CFFAFF", + "#F9E2D2", + "#FCE2DE", + "#BADFF4", + "#F9D9F9", + "#DEDAF7" + ] + } + } +} diff --git a/scripts/cli/generateSassVariableFiles.ts b/scripts/cli/generateSassVariableFiles.ts index 4e7cb99d5b7..8a08a69aaf4 100644 --- a/scripts/cli/generateSassVariableFiles.ts +++ b/scripts/cli/generateSassVariableFiles.ts @@ -9,6 +9,9 @@ const darkThemeVariablesPath = __dirname + '/../../public/sass/_variables.dark.g const lightThemeVariablesPath = __dirname + '/../../public/sass/_variables.light.generated.scss'; const defaultThemeVariablesPath = __dirname + '/../../public/sass/_variables.generated.scss'; +const darkThemeJsonPath = __dirname + '/../../public/sass/theme.dark.generated.json'; +const lightThemeJsonPath = __dirname + '/../../public/sass/theme.light.generated.json'; + const writeVariablesFile = async (path: string, data: string) => { return new Promise((resolve, reject) => { fs.writeFile(path, data, (e) => { @@ -36,6 +39,16 @@ const generateSassVariableFiles = async () => { console.error('\nWriting SASS variable files failed', error); process.exit(1); } + + try { + const darkJson = JSON.stringify(darkTheme, null, 2); + const lightJson = JSON.stringify(lightTheme, null, 2); + + writeVariablesFile(darkThemeJsonPath, darkJson); + writeVariablesFile(lightThemeJsonPath, lightJson); + } catch (error) { + console.error('\nWriting JSON variable files failed', error); + } }; generateSassVariableFiles(); From a98fae32fcfaefada152712bf62d46d0ea709de5 Mon Sep 17 00:00:00 2001 From: L-M-K-B <48948963+L-M-K-B@users.noreply.github.com> Date: Thu, 5 May 2022 15:55:59 +0200 Subject: [PATCH 068/440] Laura/chore/refactor test of secondary actions (#48745) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Chore: transfer three tests to testing-library * Chore: transfer last test to testing-library * chore: add suggestion from code review Co-authored-by: Piotr Jamróz Co-authored-by: Piotr Jamróz --- .betterer.results | 3 -- .../explore/SecondaryActions.test.tsx | 51 +++++++++++-------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/.betterer.results b/.betterer.results index 865f680adee..333c18a6cba 100644 --- a/.betterer.results +++ b/.betterer.results @@ -218,9 +218,6 @@ exports[`no enzyme tests`] = { "public/app/features/explore/RichHistory/RichHistoryStarredTab.test.tsx:3933225580": [ [0, 17, 13, "RegExp match", "2409514259"] ], - "public/app/features/explore/SecondaryActions.test.tsx:1177396128": [ - [0, 19, 13, "RegExp match", "2409514259"] - ], "public/app/features/folders/FolderSettingsPage.test.tsx:1109052730": [ [0, 19, 13, "RegExp match", "2409514259"] ], diff --git a/public/app/features/explore/SecondaryActions.test.tsx b/public/app/features/explore/SecondaryActions.test.tsx index fb9c32e34c9..a8dbbd1d9df 100644 --- a/public/app/features/explore/SecondaryActions.test.tsx +++ b/public/app/features/explore/SecondaryActions.test.tsx @@ -1,28 +1,27 @@ -import { shallow } from 'enzyme'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { noop } from 'lodash'; import React from 'react'; import { SecondaryActions } from './SecondaryActions'; -const addQueryRowButtonSelector = '[aria-label="Add row button"]'; -const richHistoryButtonSelector = '[aria-label="Rich history button"]'; -const queryInspectorButtonSelector = '[aria-label="Query inspector button"]'; - describe('SecondaryActions', () => { - it('should render component two buttons', () => { - const wrapper = shallow( + it('should render component with three buttons', () => { + render( ); - expect(wrapper.find(addQueryRowButtonSelector)).toHaveLength(1); - expect(wrapper.find(richHistoryButtonSelector)).toHaveLength(1); + + expect(screen.getByRole('button', { name: /Add row button/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Rich history button/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Query inspector button/i })).toBeInTheDocument(); }); it('should not render add row button if addQueryRowButtonHidden=true', () => { - const wrapper = shallow( + render( { onClickQueryInspectorButton={noop} /> ); - expect(wrapper.find(addQueryRowButtonSelector)).toHaveLength(0); - expect(wrapper.find(richHistoryButtonSelector)).toHaveLength(1); + + expect(screen.queryByRole('button', { name: /Add row button/i })).not.toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Rich history button/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Query inspector button/i })).toBeInTheDocument(); }); it('should disable add row button if addQueryRowButtonDisabled=true', () => { - const wrapper = shallow( + render( { onClickQueryInspectorButton={noop} /> ); - expect(wrapper.find(addQueryRowButtonSelector).props().disabled).toBe(true); + + expect(screen.getByRole('button', { name: /Add row button/i })).toBeDisabled(); + expect(screen.getByRole('button', { name: /Rich history button/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Query inspector button/i })).toBeInTheDocument(); }); - it('should map click handlers correctly', () => { + it('should map click handlers correctly', async () => { + const user = userEvent.setup(); + const onClickAddRow = jest.fn(); const onClickHistory = jest.fn(); const onClickQueryInspector = jest.fn(); - const wrapper = shallow( + + render( { /> ); - wrapper.find(addQueryRowButtonSelector).simulate('click'); - expect(onClickAddRow).toBeCalled(); + await user.click(screen.getByRole('button', { name: /Add row button/i })); + expect(onClickAddRow).toBeCalledTimes(1); - wrapper.find(richHistoryButtonSelector).simulate('click'); - expect(onClickHistory).toBeCalled(); + await user.click(screen.getByRole('button', { name: /Rich history button/i })); + expect(onClickHistory).toBeCalledTimes(1); - wrapper.find(queryInspectorButtonSelector).simulate('click'); - expect(onClickQueryInspector).toBeCalled(); + await user.click(screen.getByRole('button', { name: /Query inspector button/i })); + expect(onClickQueryInspector).toBeCalledTimes(1); }); }); From b04fb8522dbaaa11f9e31a63457bad94aa19bb51 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Thu, 5 May 2022 15:09:00 +0100 Subject: [PATCH 069/440] QueryEditor: Set data source type in mixed query data source ref (#48734) --- .../app/features/query/components/QueryEditorRows.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/public/app/features/query/components/QueryEditorRows.tsx b/public/app/features/query/components/QueryEditorRows.tsx index b81512e7eaf..727bb639f97 100644 --- a/public/app/features/query/components/QueryEditorRows.tsx +++ b/public/app/features/query/components/QueryEditorRows.tsx @@ -5,6 +5,7 @@ import { CoreApp, DataQuery, DataSourceInstanceSettings, + DataSourceRef, EventBusExtended, HistoryItem, PanelData, @@ -60,13 +61,18 @@ export class QueryEditorRows extends PureComponent { return item; } + const dataSourceRef: DataSourceRef = { + type: dataSource.type, + uid: dataSource.uid, + }; + if (item.datasource) { const previous = getDataSourceSrv().getInstanceSettings(item.datasource); if (previous?.type === dataSource.type) { return { ...item, - datasource: { uid: dataSource.uid }, + datasource: dataSourceRef, }; } } @@ -74,7 +80,7 @@ export class QueryEditorRows extends PureComponent { return { refId: item.refId, hide: item.hide, - datasource: { uid: dataSource.uid }, + datasource: dataSourceRef, }; }) ); From 250b72cc1ba09064031a1d1000aa6181e0c237ef Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Thu, 5 May 2022 16:16:34 +0200 Subject: [PATCH 070/440] Elasticsearch: Remove support for versions after their end of the life (<7.10.0) (#48715) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Elasticsearch: Remove support for versions after their EOL * Update docs * Remove old versions from config * Update pkg/tsdb/elasticsearch/elasticsearch.go Co-authored-by: Gábor Farkas * Fix tests * Fix typecheck errors Co-authored-by: Gábor Farkas --- docs/sources/datasources/elasticsearch.md | 6 - pkg/tsdb/elasticsearch/elasticsearch.go | 15 +- .../components/QueryEditor/index.test.tsx | 13 +- .../components/QueryEditor/index.tsx | 34 +- .../configuration/ConfigEditor.tsx | 10 +- .../configuration/ElasticDetails.test.tsx | 15 +- .../configuration/ElasticDetails.tsx | 6 - .../elasticsearch/datasource.test.ts | 310 +++++++++--------- .../datasource/elasticsearch/datasource.ts | 9 +- .../plugins/datasource/elasticsearch/utils.ts | 6 +- 10 files changed, 213 insertions(+), 211 deletions(-) diff --git a/docs/sources/datasources/elasticsearch.md b/docs/sources/datasources/elasticsearch.md index 4419fce4c53..caca3987fad 100644 --- a/docs/sources/datasources/elasticsearch.md +++ b/docs/sources/datasources/elasticsearch.md @@ -13,15 +13,9 @@ visualize logs or metrics stored in Elasticsearch. You can also annotate your gr Supported Elasticsearch versions: -- v2.0+ (deprecated) -- v5.0+ (deprecated) -- v6.0+ (deprecated) -- v7.0-v7.9+ (deprecated) - v7.10+ - v8.0+ (experimental) -> **Note:** Deprecated versions (v2.0+, v5.0+, v6.0+, and v7.0-v7.9+) will be removed in the next major release. - ## Adding the data source 1. Open the side menu by clicking the Grafana icon in the top header. diff --git a/pkg/tsdb/elasticsearch/elasticsearch.go b/pkg/tsdb/elasticsearch/elasticsearch.go index 2f2b2ca8d8f..547e70ac986 100644 --- a/pkg/tsdb/elasticsearch/elasticsearch.go +++ b/pkg/tsdb/elasticsearch/elasticsearch.go @@ -36,15 +36,21 @@ func ProvideService(httpClientProvider httpclient.Provider) *Service { } func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - if len(req.Queries) == 0 { - return &backend.QueryDataResponse{}, fmt.Errorf("query contains no queries") - } - dsInfo, err := s.getDSInfo(req.PluginContext) if err != nil { return &backend.QueryDataResponse{}, err } + // Support for version after their end-of-life (currently <7.10.0) was removed + lastSupportedVersion, _ := semver.NewVersion("7.10.0") + if dsInfo.ESVersion.LessThan(lastSupportedVersion) { + return &backend.QueryDataResponse{}, fmt.Errorf("support for elasticsearch versions after their end-of-life (currently versions < 7.10) was removed") + } + + if len(req.Queries) == 0 { + return &backend.QueryDataResponse{}, fmt.Errorf("query contains no queries") + } + client, err := es.NewClient(ctx, s.httpClientProvider, dsInfo, req.Queries[0].TimeRange) if err != nil { return &backend.QueryDataResponse{}, err @@ -72,7 +78,6 @@ func newInstanceSettings() datasource.InstanceFactoryFunc { } version, err := coerceVersion(jsonData["esVersion"]) - if err != nil { return nil, fmt.Errorf("elasticsearch version is required, err=%v", err) } diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.test.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.test.tsx index 24775ff5691..f95d6273cd5 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.test.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.test.tsx @@ -7,6 +7,9 @@ import { ElasticsearchQuery } from '../../types'; import { QueryEditor } from '.'; const noop = () => void 0; +const datasourceMock = { + esVersion: '7.10.0', +} as ElasticDatasource; describe('QueryEditor', () => { describe('Alias Field', () => { @@ -27,7 +30,7 @@ describe('QueryEditor', () => { const onChange = jest.fn(); - render(); + render(); let aliasField = screen.getByLabelText('Alias') as HTMLInputElement; @@ -61,7 +64,7 @@ describe('QueryEditor', () => { bucketAggs: [{ id: '2', type: 'terms' }], }; - render(); + render(); expect(screen.getByLabelText('Alias')).toBeDisabled(); }); @@ -79,7 +82,7 @@ describe('QueryEditor', () => { bucketAggs: [{ id: '2', type: 'date_histogram' }], }; - render(); + render(); expect(screen.getByLabelText('Alias')).toBeEnabled(); }); @@ -99,7 +102,7 @@ describe('QueryEditor', () => { bucketAggs: [{ id: '2', type: 'date_histogram' }], }; - render(); + render(); expect(screen.queryByLabelText('Group By')).not.toBeInTheDocument(); }); @@ -117,7 +120,7 @@ describe('QueryEditor', () => { bucketAggs: [{ id: '2', type: 'date_histogram' }], }; - render(); + render(); expect(screen.getByText('Group By')).toBeInTheDocument(); }); diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.tsx index 1e7c9d32d56..3c77d6c5800 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/index.tsx @@ -2,12 +2,13 @@ import { css } from '@emotion/css'; import React from 'react'; import { getDefaultTimeRange, GrafanaTheme2, QueryEditorProps } from '@grafana/data'; -import { InlineField, InlineLabel, Input, QueryField, useStyles2 } from '@grafana/ui'; +import { Alert, InlineField, InlineLabel, Input, QueryField, useStyles2 } from '@grafana/ui'; import { ElasticDatasource } from '../../datasource'; import { useNextId } from '../../hooks/useNextId'; import { useDispatch } from '../../hooks/useStatelessReducer'; import { ElasticsearchOptions, ElasticsearchQuery } from '../../types'; +import { isSupportedVersion } from '../../utils'; import { BucketAggregationsEditor } from './BucketAggregationsEditor'; import { ElasticsearchProvider } from './ElasticsearchQueryContext'; @@ -17,17 +18,26 @@ import { changeAliasPattern, changeQuery } from './state'; export type ElasticQueryEditorProps = QueryEditorProps; -export const QueryEditor = ({ query, onChange, onRunQuery, datasource, range }: ElasticQueryEditorProps) => ( - - - -); +export const QueryEditor = ({ query, onChange, onRunQuery, datasource, range }: ElasticQueryEditorProps) => { + if (!isSupportedVersion(datasource.esVersion)) { + return ( + + ); + } + return ( + + + + ); +}; const getStyles = (theme: GrafanaTheme2) => ({ root: css` diff --git a/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.tsx b/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.tsx index bfdbba25758..c9ed7c53e29 100644 --- a/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.tsx +++ b/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.tsx @@ -5,7 +5,7 @@ import { Alert, DataSourceHttpSettings } from '@grafana/ui'; import { config } from 'app/core/config'; import { ElasticsearchOptions } from '../types'; -import { isDeprecatedVersion } from '../utils'; +import { isSupportedVersion } from '../utils'; import { DataLinks } from './DataLinks'; import { ElasticDetails } from './ElasticDetails'; @@ -27,7 +27,7 @@ export const ConfigEditor = (props: Props) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const deprecatedVersion = isDeprecatedVersion(options.jsonData.esVersion); + const supportedVersion = isSupportedVersion(options.jsonData.esVersion); return ( <> @@ -36,9 +36,9 @@ export const ConfigEditor = (props: Props) => { Browser access mode in the Elasticsearch datasource is deprecated and will be removed in a future release. )} - {deprecatedVersion && ( - - {`Support for Elasticsearch versions after their end-of-life (currently versions < 7.10) is deprecated and will be removed in a future release.`} + {!supportedVersion && ( + + {`Support for Elasticsearch versions after their end-of-life (currently versions < 7.10) was removed`} )} diff --git a/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.test.tsx b/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.test.tsx index 444c41707e9..265f8051dc0 100644 --- a/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.test.tsx +++ b/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.test.tsx @@ -51,18 +51,7 @@ describe('ElasticDetails', () => { }); describe('version change', () => { - const testCases = [ - { version: '5.x', expectedMaxConcurrentShardRequests: 256 }, - { version: '5.x', maxConcurrentShardRequests: 50, expectedMaxConcurrentShardRequests: 50 }, - { version: '5.6+', expectedMaxConcurrentShardRequests: 256 }, - { version: '5.6+', maxConcurrentShardRequests: 256, expectedMaxConcurrentShardRequests: 256 }, - { version: '5.6+', maxConcurrentShardRequests: 5, expectedMaxConcurrentShardRequests: 256 }, - { version: '5.6+', maxConcurrentShardRequests: 200, expectedMaxConcurrentShardRequests: 200 }, - { version: '7.0+', expectedMaxConcurrentShardRequests: 5 }, - { version: '7.0+', maxConcurrentShardRequests: 256, expectedMaxConcurrentShardRequests: 5 }, - { version: '7.0+', maxConcurrentShardRequests: 5, expectedMaxConcurrentShardRequests: 5 }, - { version: '7.0+', maxConcurrentShardRequests: 6, expectedMaxConcurrentShardRequests: 6 }, - ]; + const testCases = [{ version: '7.10+', maxConcurrentShardRequests: 6, expectedMaxConcurrentShardRequests: 6 }]; testCases.forEach((tc) => { const onChangeMock = jest.fn(); @@ -72,7 +61,7 @@ describe('ElasticDetails', () => { onChange={onChangeMock} value={createDefaultConfigOptions({ maxConcurrentShardRequests: tc.maxConcurrentShardRequests, - esVersion: '2.0.0', + esVersion: '7.0.0', })} /> ); diff --git a/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.tsx b/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.tsx index 16d3c42ad85..731225a7c3d 100644 --- a/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.tsx +++ b/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.tsx @@ -18,12 +18,6 @@ const indexPatternTypes: Array> = [ ]; const esVersions: SelectableValue[] = [ - { label: '2.x', value: '2.0.0' }, - { label: '5.x', value: '5.0.0' }, - { label: '5.6+', value: '5.6.0' }, - { label: '6.0+', value: '6.0.0' }, - { label: '7.0+', value: '7.0.0' }, - { label: '7.7+', value: '7.7.0' }, { label: '7.10+', value: '7.10.0' }, { label: '8.0+', diff --git a/public/app/plugins/datasource/elasticsearch/datasource.test.ts b/public/app/plugins/datasource/elasticsearch/datasource.test.ts index dca0bbfe1be..fd473ab6083 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.test.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.test.ts @@ -116,7 +116,7 @@ function getTestContext({ describe('ElasticDatasource', function (this: any) { describe('When testing datasource with index pattern', () => { it('should translate index pattern to current day', () => { - const { ds, fetchMock } = getTestContext({ jsonData: { interval: 'Daily', esVersion: 2 } }); + const { ds, fetchMock } = getTestContext({ jsonData: { interval: 'Daily', esVersion: '7.10.0' } }); ds.testDatasource(); @@ -154,7 +154,7 @@ describe('ElasticDatasource', function (this: any) { }, ], }; - const { ds, fetchMock } = getTestContext({ jsonData: { interval: 'Daily', esVersion: 2 }, data }); + const { ds, fetchMock } = getTestContext({ jsonData: { interval: 'Daily', esVersion: '7.10.0' }, data }); let result: any = {}; await expect(ds.query(query)).toEmitValuesWith((received) => { @@ -207,7 +207,7 @@ describe('ElasticDatasource', function (this: any) { async function setupDataSource(jsonData?: Partial) { jsonData = { interval: 'Daily', - esVersion: '2.0.0', + esVersion: '7.10.0', timeField: '@timestamp', ...(jsonData || {}), }; @@ -279,7 +279,7 @@ describe('ElasticDatasource', function (this: any) { const query: any = { range, targets }; const data = { responses: [] }; - const { ds, fetchMock } = getTestContext({ jsonData: { esVersion: 2 }, data, database: 'test' }); + const { ds, fetchMock } = getTestContext({ jsonData: { esVersion: '7.10.0' }, data, database: 'test' }); await expect(ds.query(query)).toEmitValuesWith((received) => { expect(received.length).toBe(1); @@ -322,7 +322,7 @@ describe('ElasticDatasource', function (this: any) { it('should process it properly', async () => { const { ds } = getTestContext({ - jsonData: { interval: 'Daily', esVersion: 7 }, + jsonData: { interval: 'Daily', esVersion: '7.10.0' }, data: { took: 1, responses: [ @@ -368,6 +368,8 @@ describe('ElasticDatasource', function (this: any) { const { ds } = getTestContext({ mockImplementation: () => throwError(response), + from: undefined, + jsonData: { esVersion: '7.10.0' }, }); const errObject = { @@ -383,7 +385,7 @@ describe('ElasticDatasource', function (this: any) { it('should properly throw an unknown error', async () => { const { ds } = getTestContext({ - jsonData: { interval: 'Daily', esVersion: 7 }, + jsonData: { interval: 'Daily', esVersion: '7.10.0' }, data: { took: 1, responses: [ @@ -410,101 +412,101 @@ describe('ElasticDatasource', function (this: any) { }); }); - describe('When getting fields', () => { - const data = { - metricbeat: { - mappings: { - metricsets: { - _all: {}, - _meta: { - test: 'something', - }, - properties: { - '@timestamp': { type: 'date' }, - __timestamp: { type: 'date' }, - '@timestampnano': { type: 'date_nanos' }, - beat: { - properties: { - name: { - fields: { raw: { type: 'keyword' } }, - type: 'string', - }, - hostname: { type: 'string' }, - }, - }, - system: { - properties: { - cpu: { - properties: { - system: { type: 'float' }, - user: { type: 'float' }, - }, - }, - process: { - properties: { - cpu: { - properties: { - total: { type: 'float' }, - }, - }, - name: { type: 'string' }, - }, - }, - }, - }, - }, - }, - }, - }, - }; + // describe('When getting fields', () => { + // const data = { + // metricbeat: { + // mappings: { + // metricsets: { + // _all: {}, + // _meta: { + // test: 'something', + // }, + // properties: { + // '@timestamp': { type: 'date' }, + // __timestamp: { type: 'date' }, + // '@timestampnano': { type: 'date_nanos' }, + // beat: { + // properties: { + // name: { + // fields: { raw: { type: 'keyword' } }, + // type: 'string', + // }, + // hostname: { type: 'string' }, + // }, + // }, + // system: { + // properties: { + // cpu: { + // properties: { + // system: { type: 'float' }, + // user: { type: 'float' }, + // }, + // }, + // process: { + // properties: { + // cpu: { + // properties: { + // total: { type: 'float' }, + // }, + // }, + // name: { type: 'string' }, + // }, + // }, + // }, + // }, + // }, + // }, + // }, + // }, + // }; - it('should return nested fields', async () => { - const { ds } = getTestContext({ data, jsonData: { esVersion: 50 }, database: 'metricbeat' }); + // it('should return nested fields', async () => { + // const { ds } = getTestContext({ data, jsonData: { esVersion: 50 }, database: 'metricbeat' }); - await expect(ds.getFields()).toEmitValuesWith((received) => { - expect(received.length).toBe(1); - const fieldObjects = received[0]; - const fields = map(fieldObjects, 'text'); + // await expect(ds.getFields()).toEmitValuesWith((received) => { + // expect(received.length).toBe(1); + // const fieldObjects = received[0]; + // const fields = map(fieldObjects, 'text'); - expect(fields).toEqual([ - '@timestamp', - '__timestamp', - '@timestampnano', - 'beat.name.raw', - 'beat.name', - 'beat.hostname', - 'system.cpu.system', - 'system.cpu.user', - 'system.process.cpu.total', - 'system.process.name', - ]); - }); - }); + // expect(fields).toEqual([ + // '@timestamp', + // '__timestamp', + // '@timestampnano', + // 'beat.name.raw', + // 'beat.name', + // 'beat.hostname', + // 'system.cpu.system', + // 'system.cpu.user', + // 'system.process.cpu.total', + // 'system.process.name', + // ]); + // }); + // }); - it('should return number fields', async () => { - const { ds } = getTestContext({ data, jsonData: { esVersion: 50 }, database: 'metricbeat' }); + // it('should return number fields', async () => { + // const { ds } = getTestContext({ data, jsonData: { esVersion: 50 }, database: 'metricbeat' }); - await expect(ds.getFields(['number'])).toEmitValuesWith((received) => { - expect(received.length).toBe(1); - const fieldObjects = received[0]; - const fields = map(fieldObjects, 'text'); + // await expect(ds.getFields(['number'])).toEmitValuesWith((received) => { + // expect(received.length).toBe(1); + // const fieldObjects = received[0]; + // const fields = map(fieldObjects, 'text'); - expect(fields).toEqual(['system.cpu.system', 'system.cpu.user', 'system.process.cpu.total']); - }); - }); + // expect(fields).toEqual(['system.cpu.system', 'system.cpu.user', 'system.process.cpu.total']); + // }); + // }); - it('should return date fields', async () => { - const { ds } = getTestContext({ data, jsonData: { esVersion: 50 }, database: 'metricbeat' }); + // it('should return date fields', async () => { + // const { ds } = getTestContext({ data, jsonData: { esVersion: 50 }, database: 'metricbeat' }); - await expect(ds.getFields(['date'])).toEmitValuesWith((received) => { - expect(received.length).toBe(1); - const fieldObjects = received[0]; - const fields = map(fieldObjects, 'text'); + // await expect(ds.getFields(['date'])).toEmitValuesWith((received) => { + // expect(received.length).toBe(1); + // const fieldObjects = received[0]; + // const fields = map(fieldObjects, 'text'); - expect(fields).toEqual(['@timestamp', '__timestamp', '@timestampnano']); - }); - }); - }); + // expect(fields).toEqual(['@timestamp', '__timestamp', '@timestampnano']); + // }); + // }); + // }); describe('When getting field mappings on indices with gaps', () => { const basicResponse = { @@ -525,54 +527,54 @@ describe('ElasticDatasource', function (this: any) { }, }; - const alternateResponse = { - metricbeat: { - mappings: { - metricsets: { - _all: {}, - properties: { - '@timestamp': { type: 'date' }, - }, - }, - }, - }, - }; + // const alternateResponse = { + // metricbeat: { + // mappings: { + // metricsets: { + // _all: {}, + // properties: { + // '@timestamp': { type: 'date' }, + // }, + // }, + // }, + // }, + // }; - it('should return fields of the newest available index', async () => { - const twoDaysBefore = toUtc().subtract(2, 'day').format('YYYY.MM.DD'); - const threeDaysBefore = toUtc().subtract(3, 'day').format('YYYY.MM.DD'); - const baseUrl = `${ELASTICSEARCH_MOCK_URL}/asd-${twoDaysBefore}/_mapping`; - const alternateUrl = `${ELASTICSEARCH_MOCK_URL}/asd-${threeDaysBefore}/_mapping`; + // it('should return fields of the newest available index', async () => { + // const twoDaysBefore = toUtc().subtract(2, 'day').format('YYYY.MM.DD'); + // const threeDaysBefore = toUtc().subtract(3, 'day').format('YYYY.MM.DD'); + // const baseUrl = `${ELASTICSEARCH_MOCK_URL}/asd-${twoDaysBefore}/_mapping`; + // const alternateUrl = `${ELASTICSEARCH_MOCK_URL}/asd-${threeDaysBefore}/_mapping`; - const { ds, timeSrv } = getTestContext({ - from: 'now-2w', - jsonData: { interval: 'Daily', esVersion: 50 }, - mockImplementation: (options) => { - if (options.url === baseUrl) { - return of(createFetchResponse(basicResponse)); - } else if (options.url === alternateUrl) { - return of(createFetchResponse(alternateResponse)); - } - return throwError({ status: 404 }); - }, - }); + // const { ds, timeSrv } = getTestContext({ + // from: 'now-2w', + // jsonData: { interval: 'Daily', esVersion: 50 }, + // mockImplementation: (options) => { + // if (options.url === baseUrl) { + // return of(createFetchResponse(basicResponse)); + // } else if (options.url === alternateUrl) { + // return of(createFetchResponse(alternateResponse)); + // } + // return throwError({ status: 404 }); + // }, + // }); - const range = timeSrv.timeRange(); + // const range = timeSrv.timeRange(); - await expect(ds.getFields(undefined, range)).toEmitValuesWith((received) => { - expect(received.length).toBe(1); - const fieldObjects = received[0]; - const fields = map(fieldObjects, 'text'); - expect(fields).toEqual(['@timestamp', 'beat.hostname']); - }); - }); + // await expect(ds.getFields(undefined, range)).toEmitValuesWith((received) => { + // expect(received.length).toBe(1); + // const fieldObjects = received[0]; + // const fields = map(fieldObjects, 'text'); + // expect(fields).toEqual(['@timestamp', 'beat.hostname']); + // }); + // }); it('should not retry when ES is down', async () => { const twoDaysBefore = toUtc().subtract(2, 'day').format('YYYY.MM.DD'); const { ds, timeSrv, fetchMock } = getTestContext({ from: 'now-2w', - jsonData: { interval: 'Daily', esVersion: 50 }, + jsonData: { interval: 'Daily', esVersion: '7.10.0' }, mockImplementation: (options) => { if (options.url === `${ELASTICSEARCH_MOCK_URL}/asd-${twoDaysBefore}/_mapping`) { return of(createFetchResponse(basicResponse)); @@ -593,7 +595,7 @@ describe('ElasticDatasource', function (this: any) { it('should not retry more than 7 indices', async () => { const { ds, timeSrv, fetchMock } = getTestContext({ from: 'now-2w', - jsonData: { interval: 'Daily', esVersion: 50 }, + jsonData: { interval: 'Daily', esVersion: '7.10.0' }, mockImplementation: (options) => { return throwError({ status: 404 }); }, @@ -703,7 +705,11 @@ describe('ElasticDatasource', function (this: any) { ]; it('should return nested fields', async () => { - const { ds } = getTestContext({ data, database: 'genuine.es7._mapping.response', jsonData: { esVersion: 70 } }); + const { ds } = getTestContext({ + data, + database: 'genuine.es7._mapping.response', + jsonData: { esVersion: '7.10.0' }, + }); await expect(ds.getFields()).toEmitValuesWith((received) => { expect(received.length).toBe(1); @@ -730,7 +736,11 @@ describe('ElasticDatasource', function (this: any) { }); it('should return number fields', async () => { - const { ds } = getTestContext({ data, database: 'genuine.es7._mapping.response', jsonData: { esVersion: 70 } }); + const { ds } = getTestContext({ + data, + database: 'genuine.es7._mapping.response', + jsonData: { esVersion: '7.10.0' }, + }); await expect(ds.getFields(['number'])).toEmitValuesWith((received) => { expect(received.length).toBe(1); @@ -742,7 +752,11 @@ describe('ElasticDatasource', function (this: any) { }); it('should return date fields', async () => { - const { ds } = getTestContext({ data, database: 'genuine.es7._mapping.response', jsonData: { esVersion: 70 } }); + const { ds } = getTestContext({ + data, + database: 'genuine.es7._mapping.response', + jsonData: { esVersion: '7.10.0' }, + }); await expect(ds.getFields(['date'])).toEmitValuesWith((received) => { expect(received.length).toBe(1); @@ -768,7 +782,7 @@ describe('ElasticDatasource', function (this: any) { const query: any = { range, targets }; const data = { responses: [] }; - const { ds, fetchMock } = getTestContext({ jsonData: { esVersion: 5 }, data, database: 'test' }); + const { ds, fetchMock } = getTestContext({ jsonData: { esVersion: '7.10.0' }, data, database: 'test' }); await expect(ds.query(query)).toEmitValuesWith((received) => { expect(received.length).toBe(1); @@ -816,7 +830,7 @@ describe('ElasticDatasource', function (this: any) { ], }; - const { ds, fetchMock } = getTestContext({ jsonData: { esVersion: 5 }, data, database: 'test' }); + const { ds, fetchMock } = getTestContext({ jsonData: { esVersion: '7.10.0' }, data, database: 'test' }); const results = await ds.metricFindQuery('{"find": "terms", "field": "test"}'); @@ -858,7 +872,7 @@ describe('ElasticDatasource', function (this: any) { describe('query', () => { it('should replace range as integer not string', async () => { - const { ds } = getTestContext({ jsonData: { interval: 'Daily', esVersion: 2, timeField: '@time' } }); + const { ds } = getTestContext({ jsonData: { interval: 'Daily', esVersion: '7.10.0', timeField: '@time' } }); const postMock = jest.fn((url: string, data: any) => of(createFetchResponse({ responses: [] }))); ds['post'] = postMock; @@ -902,39 +916,25 @@ describe('ElasticDatasource', function (this: any) { }); describe('getMultiSearchUrl', () => { - describe('When esVersion >= 6.6.0', () => { + describe('When esVersion >= 7.10.0', () => { it('Should add correct params to URL if "includeFrozen" is enabled', () => { - const { ds } = getTestContext({ jsonData: { esVersion: '6.6.0', includeFrozen: true, xpack: true } }); + const { ds } = getTestContext({ jsonData: { esVersion: '7.10.0', includeFrozen: true, xpack: true } }); expect(ds.getMultiSearchUrl()).toMatch(/ignore_throttled=false/); }); it('Should NOT add ignore_throttled if "includeFrozen" is disabled', () => { - const { ds } = getTestContext({ jsonData: { esVersion: '6.6.0', includeFrozen: false, xpack: true } }); + const { ds } = getTestContext({ jsonData: { esVersion: '7.10.0', includeFrozen: false, xpack: true } }); expect(ds.getMultiSearchUrl()).not.toMatch(/ignore_throttled=false/); }); it('Should NOT add ignore_throttled if "xpack" is disabled', () => { - const { ds } = getTestContext({ jsonData: { esVersion: '6.6.0', includeFrozen: true, xpack: false } }); + const { ds } = getTestContext({ jsonData: { esVersion: '7.10.0', includeFrozen: true, xpack: false } }); expect(ds.getMultiSearchUrl()).not.toMatch(/ignore_throttled=false/); }); }); - - describe('When esVersion < 6.6.0', () => { - it('Should NOT add ignore_throttled params regardless of includeFrozen', () => { - const { ds: dsWithIncludeFrozen } = getTestContext({ - jsonData: { esVersion: '5.6.0', includeFrozen: false, xpack: true }, - }); - const { ds: dsWithoutIncludeFrozen } = getTestContext({ - jsonData: { esVersion: '5.6.0', includeFrozen: true, xpack: true }, - }); - - expect(dsWithIncludeFrozen.getMultiSearchUrl()).not.toMatch(/ignore_throttled=false/); - expect(dsWithoutIncludeFrozen.getMultiSearchUrl()).not.toMatch(/ignore_throttled=false/); - }); - }); }); describe('enhanceDataFrame', () => { diff --git a/public/app/plugins/datasource/elasticsearch/datasource.ts b/public/app/plugins/datasource/elasticsearch/datasource.ts index 98d5f91399b..af4cc347a48 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.ts @@ -48,7 +48,7 @@ import LanguageProvider from './language_provider'; import { ElasticQueryBuilder } from './query_builder'; import { defaultBucketAgg, hasMetricOfType } from './query_def'; import { DataLinkConfig, ElasticsearchOptions, ElasticsearchQuery, TermsQuery } from './types'; -import { coerceESVersion, getScriptValue } from './utils'; +import { coerceESVersion, getScriptValue, isSupportedVersion } from './utils'; // Those are metadata fields as defined in https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-fields.html#_identity_metadata_fields. // custom fields can start with underscores, therefore is not safe to exclude anything that starts with one. @@ -132,6 +132,13 @@ export class ElasticDatasource data?: undefined, headers?: BackendSrvRequest['headers'] ): Observable { + if (!isSupportedVersion(this.esVersion)) { + const error = new Error( + 'Support for Elasticsearch versions after their end-of-life (currently versions < 7.10) was removed.' + ); + return throwError(() => error); + } + const options: BackendSrvRequest = { url: this.url + '/' + url, method, diff --git a/public/app/plugins/datasource/elasticsearch/utils.ts b/public/app/plugins/datasource/elasticsearch/utils.ts index 05f7ad8c2df..e83e936655d 100644 --- a/public/app/plugins/datasource/elasticsearch/utils.ts +++ b/public/app/plugins/datasource/elasticsearch/utils.ts @@ -119,10 +119,10 @@ export const coerceESVersion = (version: string | number): string => { } }; -export const isDeprecatedVersion = (version: string): boolean => { +export const isSupportedVersion = (version: string): boolean => { if (gte(version, '7.10.0')) { - return false; + return true; } - return true; + return false; }; From ec666f878561e54635f38106cb93120b58d1d9c4 Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Thu, 5 May 2022 10:57:24 -0400 Subject: [PATCH 071/440] Converter: Add result type to frame meta (#48769) --- pkg/util/converter/prom.go | 22 ++++++++---- .../testdata/prom-exemplars-frame.json | 10 ++++++ .../testdata/prom-exemplars-golden.txt | 16 ++++++--- .../converter/testdata/prom-matrix-frame.json | 18 ++++++---- .../converter/testdata/prom-matrix-golden.txt | 14 +++++--- .../testdata/prom-matrix-with-nans-frame.json | 9 +++-- .../testdata/prom-matrix-with-nans-golden.txt | 7 ++-- .../converter/testdata/prom-scalar-frame.json | 5 ++- .../converter/testdata/prom-scalar-golden.txt | 7 ++-- .../converter/testdata/prom-string-frame.json | 5 ++- .../converter/testdata/prom-string-golden.txt | 7 ++-- .../converter/testdata/prom-vector-frame.json | 33 ++++++++++++----- .../converter/testdata/prom-vector-golden.txt | 35 +++++++++++++------ .../testdata/prom-warnings-frame.json | 15 ++++++++ .../testdata/prom-warnings-golden.txt | 25 ++++++++++--- 15 files changed, 173 insertions(+), 55 deletions(-) diff --git a/pkg/util/converter/prom.go b/pkg/util/converter/prom.go index dcde552106c..15d74dc2bbd 100644 --- a/pkg/util/converter/prom.go +++ b/pkg/util/converter/prom.go @@ -107,9 +107,9 @@ func readPrometheusData(iter *jsoniter.Iterator) *backend.DataResponse { case "result": switch resultType { case "matrix": - rsp = readMatrixOrVector(iter) + rsp = readMatrixOrVector(iter, resultType) case "vector": - rsp = readMatrixOrVector(iter) + rsp = readMatrixOrVector(iter, resultType) case "streams": rsp = readStream(iter) case "string": @@ -237,6 +237,9 @@ func readLabelsOrExemplars(iter *jsoniter.Iterator) (*data.Frame, [][2]string) { delete(labels, "__name__") valueField.Labels = labels frame = data.NewFrame("", timeField, valueField) + frame.Meta = &data.FrameMeta{ + Custom: resultTypeToCustomMeta("exemplar"), + } for iter.ReadArray() { for l2Field := iter.ReadObject(); l2Field != ""; l2Field = iter.ReadObject() { switch l2Field { @@ -312,7 +315,8 @@ func readString(iter *jsoniter.Iterator) *backend.DataResponse { frame := data.NewFrame("", timeField, valueField) frame.Meta = &data.FrameMeta{ - Type: data.FrameTypeTimeSeriesMany, + Type: data.FrameTypeTimeSeriesMany, + Custom: resultTypeToCustomMeta("string"), } return &backend.DataResponse{ @@ -335,7 +339,8 @@ func readScalar(iter *jsoniter.Iterator) *backend.DataResponse { frame := data.NewFrame("", timeField, valueField) frame.Meta = &data.FrameMeta{ - Type: data.FrameTypeTimeSeriesMany, + Type: data.FrameTypeTimeSeriesMany, + Custom: resultTypeToCustomMeta("scalar"), } return &backend.DataResponse{ @@ -343,7 +348,7 @@ func readScalar(iter *jsoniter.Iterator) *backend.DataResponse { } } -func readMatrixOrVector(iter *jsoniter.Iterator) *backend.DataResponse { +func readMatrixOrVector(iter *jsoniter.Iterator, resultType string) *backend.DataResponse { rsp := &backend.DataResponse{} for iter.ReadArray() { @@ -379,7 +384,8 @@ func readMatrixOrVector(iter *jsoniter.Iterator) *backend.DataResponse { frame := data.NewFrame("", timeField, valueField) frame.Meta = &data.FrameMeta{ - Type: data.FrameTypeTimeSeriesMany, + Type: data.FrameTypeTimeSeriesMany, + Custom: resultTypeToCustomMeta(resultType), } rsp.Frames = append(rsp.Frames, frame) } @@ -457,6 +463,10 @@ func readStream(iter *jsoniter.Iterator) *backend.DataResponse { return rsp } +func resultTypeToCustomMeta(resultType string) map[string]string { + return map[string]string{"resultType": resultType} +} + func timeFromFloat(fv float64) time.Time { return time.UnixMilli(int64(fv * 1000.0)).UTC() } diff --git a/pkg/util/converter/testdata/prom-exemplars-frame.json b/pkg/util/converter/testdata/prom-exemplars-frame.json index 9f1a87297e8..451ea18fe84 100644 --- a/pkg/util/converter/testdata/prom-exemplars-frame.json +++ b/pkg/util/converter/testdata/prom-exemplars-frame.json @@ -2,6 +2,11 @@ "frames": [ { "schema": { + "meta": { + "custom": { + "resultType": "exemplar" + } + }, "fields": [ { "type": "time", @@ -56,6 +61,11 @@ }, { "schema": { + "meta": { + "custom": { + "resultType": "exemplar" + } + }, "fields": [ { "type": "time", diff --git a/pkg/util/converter/testdata/prom-exemplars-golden.txt b/pkg/util/converter/testdata/prom-exemplars-golden.txt index 2ed17bd815e..97342b99207 100644 --- a/pkg/util/converter/testdata/prom-exemplars-golden.txt +++ b/pkg/util/converter/testdata/prom-exemplars-golden.txt @@ -1,6 +1,10 @@ 🌟 This was machine generated. Do not edit. 🌟 -Frame[0] +Frame[0] { + "custom": { + "resultType": "exemplar" + } +} Name: Dimensions: 4 Fields by 1 Rows +-----------------------------------+--------------------------------------------------------------+------------------+----------------+ @@ -13,7 +17,11 @@ Dimensions: 4 Fields by 1 Rows -Frame[1] +Frame[1] { + "custom": { + "resultType": "exemplar" + } +} Name: Dimensions: 3 Fields by 2 Rows +-----------------------------------+--------------------------------------------------------------+------------------+ @@ -27,5 +35,5 @@ Dimensions: 3 Fields by 2 Rows ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=QVJST1cxAAD/////oAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAADk/f//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAAT+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAABAAAALgBAAC4AAAAWAAAAAQAAABq/v//FAAAADgAAAA4AAAAAAAABTQAAAABAAAABAAAAFj+//8IAAAADAAAAAEAAABhAAAABAAAAG5hbWUAAAAAAAAAAKz///8BAAAAYQAAALr+//8UAAAAPAAAAEAAAAAAAAAFPAAAAAEAAAAEAAAAqP7//wgAAAAQAAAABwAAAHRyYWNlSUQABAAAAG5hbWUAAAAAAAAAAAQABAAEAAAABwAAAHRyYWNlSUQAFv///xQAAAC0AAAAtAAAAAAAAAO0AAAAAgAAAEAAAAAEAAAACP///wgAAAAkAAAAGgAAAHRlc3RfZXhlbXBsYXJfbWV0cmljX3RvdGFsAAAEAAAAbmFtZQAAAABA////CAAAAEwAAABAAAAAeyJpbnN0YW5jZSI6ImxvY2FsaG9zdDo4MDkwIiwiam9iIjoicHJvbWV0aGV1cyIsInNlcnZpY2UiOiJiYXIifQAAAAAGAAAAbGFiZWxzAAAAAAAAdv///wAAAgAaAAAAdGVzdF9leGVtcGxhcl9tZXRyaWNfdG90YWwAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABAAAAASAAAAAAAAApIAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAAAAAAAAAAAP////84AQAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAQAAAAAAAAAAUAAAAAAAAAwQACgAYAAwACAAEAAoAAAAUAAAAuAAAAAEAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAACAAAAAAAAAAYAAAAAAAAABAAAAAAAAAAKAAAAAAAAAAAAAAAAAAAACgAAAAAAAAACAAAAAAAAAAwAAAAAAAAAAsAAAAAAAAAAAAAAAQAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAwJ92ubGvNBYAAAAAAAAYQAAAAAAQAAAARXBUeE1KNDBmVXVzN2FHWQAAAAALAAAAbm90IGluIG5leHQAAAAAABAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA8AAAAAAAEAAEAAACwAgAAAAAAAEABAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAFAAAAACAAAAKAAAAAQAAADk/f//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAAT+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAABAAAALgBAAC4AAAAWAAAAAQAAABq/v//FAAAADgAAAA4AAAAAAAABTQAAAABAAAABAAAAFj+//8IAAAADAAAAAEAAABhAAAABAAAAG5hbWUAAAAAAAAAAKz///8BAAAAYQAAALr+//8UAAAAPAAAAEAAAAAAAAAFPAAAAAEAAAAEAAAAqP7//wgAAAAQAAAABwAAAHRyYWNlSUQABAAAAG5hbWUAAAAAAAAAAAQABAAEAAAABwAAAHRyYWNlSUQAFv///xQAAAC0AAAAtAAAAAAAAAO0AAAAAgAAAEAAAAAEAAAACP///wgAAAAkAAAAGgAAAHRlc3RfZXhlbXBsYXJfbWV0cmljX3RvdGFsAAAEAAAAbmFtZQAAAABA////CAAAAEwAAABAAAAAeyJpbnN0YW5jZSI6ImxvY2FsaG9zdDo4MDkwIiwiam9iIjoicHJvbWV0aGV1cyIsInNlcnZpY2UiOiJiYXIifQAAAAAGAAAAbGFiZWxzAAAAAAAAdv///wAAAgAaAAAAdGVzdF9leGVtcGxhcl9tZXRyaWNfdG90YWwAAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABAAAAASAAAAAAAAApIAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAAAAAAAAAAANACAABBUlJPVzE= -FRAME=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 +FRAME=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 +FRAME=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 diff --git a/pkg/util/converter/testdata/prom-matrix-frame.json b/pkg/util/converter/testdata/prom-matrix-frame.json index 11484d37e8a..31d88c2acff 100644 --- a/pkg/util/converter/testdata/prom-matrix-frame.json +++ b/pkg/util/converter/testdata/prom-matrix-frame.json @@ -3,7 +3,10 @@ { "schema": { "meta": { - "type": "timeseries-many" + "type": "timeseries-many", + "custom": { + "resultType": "matrix" + } }, "fields": [ { @@ -20,9 +23,9 @@ "frame": "float64" }, "labels": { - "job": "prometheus", + "__name__": "up", "instance": "localhost:9090", - "__name__": "up" + "job": "prometheus" } } ] @@ -41,7 +44,10 @@ { "schema": { "meta": { - "type": "timeseries-many" + "type": "timeseries-many", + "custom": { + "resultType": "matrix" + } }, "fields": [ { @@ -59,8 +65,8 @@ }, "labels": { "__name__": "up", - "job": "node", - "instance": "localhost:9091" + "instance": "localhost:9091", + "job": "node" } } ] diff --git a/pkg/util/converter/testdata/prom-matrix-golden.txt b/pkg/util/converter/testdata/prom-matrix-golden.txt index c9cf2dc5e93..1b1672403ee 100644 --- a/pkg/util/converter/testdata/prom-matrix-golden.txt +++ b/pkg/util/converter/testdata/prom-matrix-golden.txt @@ -1,7 +1,10 @@ 🌟 This was machine generated. Do not edit. 🌟 Frame[0] { - "type": "timeseries-many" + "type": "timeseries-many", + "custom": { + "resultType": "matrix" + } } Name: Dimensions: 2 Fields by 3 Rows @@ -18,7 +21,10 @@ Dimensions: 2 Fields by 3 Rows Frame[1] { - "type": "timeseries-many" + "type": "timeseries-many", + "custom": { + "resultType": "matrix" + } } Name: Dimensions: 2 Fields by 3 Rows @@ -34,5 +40,5 @@ Dimensions: 2 Fields by 3 Rows ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=QVJST1cxAAD/////CAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAjP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAMz+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAANgAAAAEAAAAQv///xQAAACgAAAAoAAAAAAAAAOgAAAAAgAAACwAAAAEAAAANP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAWP///wgAAABMAAAAQAAAAHsiX19uYW1lX18iOiJ1cCIsImluc3RhbmNlIjoibG9jYWxob3N0OjkwOTAiLCJqb2IiOiJwcm9tZXRoZXVzIn0AAAAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAAAAAAD/////uAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAADAAAAAAAAAAFAAAAAAAAAMEAAoAGAAMAAgABAAKAAAAFAAAAFgAAAADAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAYAAAAAAAAAAAAAAACAAAAAwAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAABAuc2smuvsE0CP3yqe6+wTQGXxqKHr7BMAAAAAAADwPwAAAAAAAPA/AAAAAAAA8D8QAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAABAABAAAAGAIAAAAAAADAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAjP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAMz+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAANgAAAAEAAAAQv///xQAAACgAAAAoAAAAAAAAAOgAAAAAgAAACwAAAAEAAAANP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAWP///wgAAABMAAAAQAAAAHsiX19uYW1lX18iOiJ1cCIsImluc3RhbmNlIjoibG9jYWxob3N0OjkwOTAiLCJqb2IiOiJwcm9tZXRoZXVzIn0AAAAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAADACAABBUlJPVzE= -FRAME=QVJST1cxAAD/////AAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAlP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAAC0/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAANT+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAANAAAAAEAAAASv///xQAAACYAAAAmAAAAAAAAAOYAAAAAgAAACwAAAAEAAAAPP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAYP///wgAAABEAAAAOgAAAHsiX19uYW1lX18iOiJ1cCIsImluc3RhbmNlIjoibG9jYWxob3N0OjkwOTEiLCJqb2IiOiJub2RlIn0AAAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAwAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAwAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAGAAAAAAAAAAAAAAAAgAAAAMAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAQLnNrJrr7BNAj98qnuvsE0Bl8aih6+wTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPA/EAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADgAAAAAAAQAAQAAABACAAAAAAAAwAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAKAAwAAAAIAAQACgAAAAgAAACMAAAAAwAAAEwAAAAoAAAABAAAAJT+//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAAtP7//wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAADU/v//CAAAACQAAAAaAAAAeyJ0eXBlIjoidGltZXNlcmllcy1tYW55In0AAAQAAABtZXRhAAAAAAIAAADQAAAABAAAAEr///8UAAAAmAAAAJgAAAAAAAADmAAAAAIAAAAsAAAABAAAADz///8IAAAAEAAAAAUAAABWYWx1ZQAAAAQAAABuYW1lAAAAAGD///8IAAAARAAAADoAAAB7Il9fbmFtZV9fIjoidXAiLCJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MDkxIiwiam9iIjoibm9kZSJ9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAKAIAAEFSUk9XMQ== +FRAME=QVJST1cxAAD/////KAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAKwAAAADAAAATAAAACgAAAAEAAAAbP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAACM/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAKz+//8IAAAARAAAADsAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJjdXN0b20iOnsicmVzdWx0VHlwZSI6Im1hdHJpeCJ9fQAEAAAAbWV0YQAAAAACAAAA2AAAAAQAAABC////FAAAAKAAAACgAAAAAAAAA6AAAAACAAAALAAAAAQAAAA0////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAABY////CAAAAEwAAABAAAAAeyJfX25hbWVfXyI6InVwIiwiaW5zdGFuY2UiOiJsb2NhbGhvc3Q6OTA5MCIsImpvYiI6InByb21ldGhldXMifQAAAAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAAAAAAP////+4AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAMAAAAAAAAAAUAAAAAAAAAwQACgAYAAwACAAEAAoAAAAUAAAAWAAAAAMAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAABgAAAAAAAAAAAAAAAIAAAADAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAEC5zaya6+wTQI/fKp7r7BNAZfGooevsEwAAAAAAAPA/AAAAAAAA8D8AAAAAAADwPxAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAAA4AgAAAAAAAMAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAArAAAAAMAAABMAAAAKAAAAAQAAABs/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAIz+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAArP7//wgAAABEAAAAOwAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSIsImN1c3RvbSI6eyJyZXN1bHRUeXBlIjoibWF0cml4In19AAQAAABtZXRhAAAAAAIAAADYAAAABAAAAEL///8UAAAAoAAAAKAAAAAAAAADoAAAAAIAAAAsAAAABAAAADT///8IAAAAEAAAAAUAAABWYWx1ZQAAAAQAAABuYW1lAAAAAFj///8IAAAATAAAAEAAAAB7Il9fbmFtZV9fIjoidXAiLCJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MDkwIiwiam9iIjoicHJvbWV0aGV1cyJ9AAAAAAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAABQAgAAQVJST1cx +FRAME=QVJST1cxAAD/////IAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAKwAAAADAAAATAAAACgAAAAEAAAAdP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAACU/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAALT+//8IAAAARAAAADsAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJjdXN0b20iOnsicmVzdWx0VHlwZSI6Im1hdHJpeCJ9fQAEAAAAbWV0YQAAAAACAAAA0AAAAAQAAABK////FAAAAJgAAACYAAAAAAAAA5gAAAACAAAALAAAAAQAAAA8////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAABg////CAAAAEQAAAA6AAAAeyJfX25hbWVfXyI6InVwIiwiaW5zdGFuY2UiOiJsb2NhbGhvc3Q6OTA5MSIsImpvYiI6Im5vZGUifQAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAAAAAAD/////uAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAADAAAAAAAAAAFAAAAAAAAAMEAAoAGAAMAAgABAAKAAAAFAAAAFgAAAADAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAYAAAAAAAAAAAAAAACAAAAAwAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAABAuc2smuvsE0CP3yqe6+wTQGXxqKHr7BMAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8D8QAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAABAABAAAAMAIAAAAAAADAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAKwAAAADAAAATAAAACgAAAAEAAAAdP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAACU/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAALT+//8IAAAARAAAADsAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJjdXN0b20iOnsicmVzdWx0VHlwZSI6Im1hdHJpeCJ9fQAEAAAAbWV0YQAAAAACAAAA0AAAAAQAAABK////FAAAAJgAAACYAAAAAAAAA5gAAAACAAAALAAAAAQAAAA8////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAABg////CAAAAEQAAAA6AAAAeyJfX25hbWVfXyI6InVwIiwiaW5zdGFuY2UiOiJsb2NhbGhvc3Q6OTA5MSIsImpvYiI6Im5vZGUifQAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAEgCAABBUlJPVzE= diff --git a/pkg/util/converter/testdata/prom-matrix-with-nans-frame.json b/pkg/util/converter/testdata/prom-matrix-with-nans-frame.json index 93ba618a21c..f8e3d800b34 100644 --- a/pkg/util/converter/testdata/prom-matrix-with-nans-frame.json +++ b/pkg/util/converter/testdata/prom-matrix-with-nans-frame.json @@ -3,7 +3,10 @@ { "schema": { "meta": { - "type": "timeseries-many" + "type": "timeseries-many", + "custom": { + "resultType": "matrix" + } }, "fields": [ { @@ -20,8 +23,8 @@ "frame": "float64" }, "labels": { - "job": "prometheus", - "handler": "/api/v1/query_range" + "handler": "/api/v1/query_range", + "job": "prometheus" } } ] diff --git a/pkg/util/converter/testdata/prom-matrix-with-nans-golden.txt b/pkg/util/converter/testdata/prom-matrix-with-nans-golden.txt index 7aa69989cf2..0cbfb74dc3b 100644 --- a/pkg/util/converter/testdata/prom-matrix-with-nans-golden.txt +++ b/pkg/util/converter/testdata/prom-matrix-with-nans-golden.txt @@ -1,7 +1,10 @@ 🌟 This was machine generated. Do not edit. 🌟 Frame[0] { - "type": "timeseries-many" + "type": "timeseries-many", + "custom": { + "resultType": "matrix" + } } Name: Dimensions: 2 Fields by 3 Rows @@ -17,4 +20,4 @@ Dimensions: 2 Fields by 3 Rows ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=QVJST1cxAAD/////+AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAmP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAAC4/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAANj+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAAMwAAAAEAAAATv///xQAAACUAAAAlAAAAAAAAAOUAAAAAgAAACwAAAAEAAAAQP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAZP///wgAAABAAAAANAAAAHsiaGFuZGxlciI6Ii9hcGkvdjEvcXVlcnlfcmFuZ2UiLCJqb2IiOiJwcm9tZXRoZXVzIn0AAAAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAP////+4AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAMAAAAAAAAAAUAAAAAAAAAwQACgAYAAwACAAEAAoAAAAUAAAAWAAAAAMAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAYAAAAAAAAABgAAAAAAAAAAAAAAAIAAAADAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAABEFRTUKckWAA6wT9QpyRYA2EqL1CnJFgAAAAAAAPB/AQAAAAAA+H8AAAAAAADw/xAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA8AAAAAAAEAAEAAAAIAgAAAAAAAMAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAmP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAAC4/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAANj+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAAMwAAAAEAAAATv///xQAAACUAAAAlAAAAAAAAAOUAAAAAgAAACwAAAAEAAAAQP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAZP///wgAAABAAAAANAAAAHsiaGFuZGxlciI6Ii9hcGkvdjEvcXVlcnlfcmFuZ2UiLCJqb2IiOiJwcm9tZXRoZXVzIn0AAAAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAACgCAABBUlJPVzE= +FRAME=QVJST1cxAAD/////GAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAKwAAAADAAAATAAAACgAAAAEAAAAeP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAACY/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAALj+//8IAAAARAAAADsAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJjdXN0b20iOnsicmVzdWx0VHlwZSI6Im1hdHJpeCJ9fQAEAAAAbWV0YQAAAAACAAAAzAAAAAQAAABO////FAAAAJQAAACUAAAAAAAAA5QAAAACAAAALAAAAAQAAABA////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAABk////CAAAAEAAAAA0AAAAeyJoYW5kbGVyIjoiL2FwaS92MS9xdWVyeV9yYW5nZSIsImpvYiI6InByb21ldGhldXMifQAAAAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAwAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAwAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAGAAAAAAAAAAAAAAAAAAAABgAAAAAAAAAGAAAAAAAAAAAAAAAAgAAAAMAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAEQVFNQpyRYADrBP1CnJFgDYSovUKckWAAAAAAAA8H8BAAAAAAD4fwAAAAAAAPD/EAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADwAAAAAAAQAAQAAACgCAAAAAAAAwAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAArAAAAAMAAABMAAAAKAAAAAQAAAB4/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAJj+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAuP7//wgAAABEAAAAOwAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSIsImN1c3RvbSI6eyJyZXN1bHRUeXBlIjoibWF0cml4In19AAQAAABtZXRhAAAAAAIAAADMAAAABAAAAE7///8UAAAAlAAAAJQAAAAAAAADlAAAAAIAAAAsAAAABAAAAED///8IAAAAEAAAAAUAAABWYWx1ZQAAAAQAAABuYW1lAAAAAGT///8IAAAAQAAAADQAAAB7ImhhbmRsZXIiOiIvYXBpL3YxL3F1ZXJ5X3JhbmdlIiwiam9iIjoicHJvbWV0aGV1cyJ9AAAAAAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAABIAgAAQVJST1cx diff --git a/pkg/util/converter/testdata/prom-scalar-frame.json b/pkg/util/converter/testdata/prom-scalar-frame.json index 9c1a546bfd7..bfac8c9ad91 100644 --- a/pkg/util/converter/testdata/prom-scalar-frame.json +++ b/pkg/util/converter/testdata/prom-scalar-frame.json @@ -3,7 +3,10 @@ { "schema": { "meta": { - "type": "timeseries-many" + "type": "timeseries-many", + "custom": { + "resultType": "scalar" + } }, "fields": [ { diff --git a/pkg/util/converter/testdata/prom-scalar-golden.txt b/pkg/util/converter/testdata/prom-scalar-golden.txt index 5bfe0964155..22ebd05e759 100644 --- a/pkg/util/converter/testdata/prom-scalar-golden.txt +++ b/pkg/util/converter/testdata/prom-scalar-golden.txt @@ -1,7 +1,10 @@ 🌟 This was machine generated. Do not edit. 🌟 Frame[0] { - "type": "timeseries-many" + "type": "timeseries-many", + "custom": { + "resultType": "scalar" + } } Name: Dimensions: 2 Fields by 1 Rows @@ -15,4 +18,4 @@ Dimensions: 2 Fields by 1 Rows ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=QVJST1cxAAD/////yAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAzP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAAz///8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAAJgAAAAEAAAAgv///xQAAABgAAAAYAAAAAAAAANgAAAAAgAAACwAAAAEAAAAdP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAmP///wgAAAAMAAAAAgAAAHt9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAAAAAAP////+4AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAEAAAAAAAAAAUAAAAAAAAAwQACgAYAAwACAAEAAoAAAAUAAAAWAAAAAEAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAACY3OVV8usW8VWfaZEG+j4QAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAABAABAAAA2AEAAAAAAADAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAzP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAAz///8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAAJgAAAAEAAAAgv///xQAAABgAAAAYAAAAAAAAANgAAAAAgAAACwAAAAEAAAAdP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAmP///wgAAAAMAAAAAgAAAHt9AAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAA8AEAAEFSUk9XMQ== +FRAME=QVJST1cxAAD/////6AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAKwAAAADAAAATAAAACgAAAAEAAAArP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADM/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOz+//8IAAAARAAAADsAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJjdXN0b20iOnsicmVzdWx0VHlwZSI6InNjYWxhciJ9fQAEAAAAbWV0YQAAAAACAAAAmAAAAAQAAACC////FAAAAGAAAABgAAAAAAAAA2AAAAACAAAALAAAAAQAAAB0////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAACY////CAAAAAwAAAACAAAAe30AAAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAJjc5VXy6xbxVZ9pkQb6PhAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAAD4AQAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAArAAAAAMAAABMAAAAKAAAAAQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAMz+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA7P7//wgAAABEAAAAOwAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSIsImN1c3RvbSI6eyJyZXN1bHRUeXBlIjoic2NhbGFyIn19AAQAAABtZXRhAAAAAAIAAACYAAAABAAAAIL///8UAAAAYAAAAGAAAAAAAAADYAAAAAIAAAAsAAAABAAAAHT///8IAAAAEAAAAAUAAABWYWx1ZQAAAAQAAABuYW1lAAAAAJj///8IAAAADAAAAAIAAAB7fQAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAABACAABBUlJPVzE= diff --git a/pkg/util/converter/testdata/prom-string-frame.json b/pkg/util/converter/testdata/prom-string-frame.json index 49f65d538b1..dccf8a826b9 100644 --- a/pkg/util/converter/testdata/prom-string-frame.json +++ b/pkg/util/converter/testdata/prom-string-frame.json @@ -3,7 +3,10 @@ { "schema": { "meta": { - "type": "timeseries-many" + "type": "timeseries-many", + "custom": { + "resultType": "string" + } }, "fields": [ { diff --git a/pkg/util/converter/testdata/prom-string-golden.txt b/pkg/util/converter/testdata/prom-string-golden.txt index 3ff1517f6c4..9de84775012 100644 --- a/pkg/util/converter/testdata/prom-string-golden.txt +++ b/pkg/util/converter/testdata/prom-string-golden.txt @@ -1,7 +1,10 @@ 🌟 This was machine generated. Do not edit. 🌟 Frame[0] { - "type": "timeseries-many" + "type": "timeseries-many", + "custom": { + "resultType": "string" + } } Name: Dimensions: 2 Fields by 1 Rows @@ -15,4 +18,4 @@ Dimensions: 2 Fields by 1 Rows ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=QVJST1cxAAD/////yAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAzP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAAz///8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAAJgAAAAEAAAAgv///xQAAABgAAAAZAAAAAAAAAVgAAAAAgAAACwAAAAEAAAAdP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAmP///wgAAAAMAAAAAgAAAHt9AAAGAAAAbGFiZWxzAAAAAAAABAAEAAQAAAAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAAAAAAP/////IAAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAGAAAAAAAAAAUAAAAAAAAAwQACgAYAAwACAAEAAoAAAAUAAAAaAAAAAEAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAEAAAAAAAAAAHAAAAAAAAAAAAAAACAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAmNzlVfLrFgAAAAAHAAAAZXhhbXBsZQAQAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAABAABAAAA2AEAAAAAAADQAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAzP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAAz///8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAAJgAAAAEAAAAgv///xQAAABgAAAAZAAAAAAAAAVgAAAAAgAAACwAAAAEAAAAdP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAmP///wgAAAAMAAAAAgAAAHt9AAAGAAAAbGFiZWxzAAAAAAAABAAEAAQAAAAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAA8AEAAEFSUk9XMQ== +FRAME=QVJST1cxAAD/////6AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAKwAAAADAAAATAAAACgAAAAEAAAArP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADM/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOz+//8IAAAARAAAADsAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJjdXN0b20iOnsicmVzdWx0VHlwZSI6InN0cmluZyJ9fQAEAAAAbWV0YQAAAAACAAAAmAAAAAQAAACC////FAAAAGAAAABkAAAAAAAABWAAAAACAAAALAAAAAQAAAB0////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAACY////CAAAAAwAAAACAAAAe30AAAYAAABsYWJlbHMAAAAAAAAEAAQABAAAAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////8gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAYAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABoAAAAAQAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAQAAAAAAAAAAcAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAACY3OVV8usWAAAAAAcAAABleGFtcGxlABAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAAD4AQAAAAAAANAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAArAAAAAMAAABMAAAAKAAAAAQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAMz+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA7P7//wgAAABEAAAAOwAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSIsImN1c3RvbSI6eyJyZXN1bHRUeXBlIjoic3RyaW5nIn19AAQAAABtZXRhAAAAAAIAAACYAAAABAAAAIL///8UAAAAYAAAAGQAAAAAAAAFYAAAAAIAAAAsAAAABAAAAHT///8IAAAAEAAAAAUAAABWYWx1ZQAAAAQAAABuYW1lAAAAAJj///8IAAAADAAAAAIAAAB7fQAABgAAAGxhYmVscwAAAAAAAAQABAAEAAAABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAABACAABBUlJPVzE= diff --git a/pkg/util/converter/testdata/prom-vector-frame.json b/pkg/util/converter/testdata/prom-vector-frame.json index eaff88b9a5a..d09de2ac9e7 100644 --- a/pkg/util/converter/testdata/prom-vector-frame.json +++ b/pkg/util/converter/testdata/prom-vector-frame.json @@ -3,7 +3,10 @@ { "schema": { "meta": { - "type": "timeseries-many" + "type": "timeseries-many", + "custom": { + "resultType": "vector" + } }, "fields": [ { @@ -21,8 +24,8 @@ }, "labels": { "__name__": "up", - "job": "prometheus", - "instance": "localhost:9090" + "instance": "localhost:9090", + "job": "prometheus" } } ] @@ -41,7 +44,10 @@ { "schema": { "meta": { - "type": "timeseries-many" + "type": "timeseries-many", + "custom": { + "resultType": "vector" + } }, "fields": [ { @@ -58,9 +64,9 @@ "frame": "float64" }, "labels": { - "job": "node", + "__name__": "up", "instance": "localhost:9100", - "__name__": "up" + "job": "node" } } ] @@ -79,7 +85,10 @@ { "schema": { "meta": { - "type": "timeseries-many" + "type": "timeseries-many", + "custom": { + "resultType": "vector" + } }, "fields": [ { @@ -124,7 +133,10 @@ { "schema": { "meta": { - "type": "timeseries-many" + "type": "timeseries-many", + "custom": { + "resultType": "vector" + } }, "fields": [ { @@ -169,7 +181,10 @@ { "schema": { "meta": { - "type": "timeseries-many" + "type": "timeseries-many", + "custom": { + "resultType": "vector" + } }, "fields": [ { diff --git a/pkg/util/converter/testdata/prom-vector-golden.txt b/pkg/util/converter/testdata/prom-vector-golden.txt index 0c4c0d3150e..de0a3e0ad5a 100644 --- a/pkg/util/converter/testdata/prom-vector-golden.txt +++ b/pkg/util/converter/testdata/prom-vector-golden.txt @@ -1,7 +1,10 @@ 🌟 This was machine generated. Do not edit. 🌟 Frame[0] { - "type": "timeseries-many" + "type": "timeseries-many", + "custom": { + "resultType": "vector" + } } Name: Dimensions: 2 Fields by 1 Rows @@ -16,7 +19,10 @@ Dimensions: 2 Fields by 1 Rows Frame[1] { - "type": "timeseries-many" + "type": "timeseries-many", + "custom": { + "resultType": "vector" + } } Name: Dimensions: 2 Fields by 1 Rows @@ -31,7 +37,10 @@ Dimensions: 2 Fields by 1 Rows Frame[2] { - "type": "timeseries-many" + "type": "timeseries-many", + "custom": { + "resultType": "vector" + } } Name: Dimensions: 2 Fields by 1 Rows @@ -46,7 +55,10 @@ Dimensions: 2 Fields by 1 Rows Frame[3] { - "type": "timeseries-many" + "type": "timeseries-many", + "custom": { + "resultType": "vector" + } } Name: Dimensions: 2 Fields by 1 Rows @@ -61,7 +73,10 @@ Dimensions: 2 Fields by 1 Rows Frame[4] { - "type": "timeseries-many" + "type": "timeseries-many", + "custom": { + "resultType": "vector" + } } Name: Dimensions: 2 Fields by 1 Rows @@ -75,8 +90,8 @@ Dimensions: 2 Fields by 1 Rows ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=QVJST1cxAAD/////CAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAjP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAMz+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAANgAAAAEAAAAQv///xQAAACgAAAAoAAAAAAAAAOgAAAAAgAAACwAAAAEAAAANP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAWP///wgAAABMAAAAQAAAAHsiX19uYW1lX18iOiJ1cCIsImluc3RhbmNlIjoibG9jYWxob3N0OjkwOTAiLCJqb2IiOiJwcm9tZXRoZXVzIn0AAAAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAAAAAAD/////uAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAABAAAAAAAAAAFAAAAAAAAAMEAAoAGAAMAAgABAAKAAAAFAAAAFgAAAABAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAACAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAABAS4CQn+vsEwAAAAAAAPA/EAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADgAAAAAAAQAAQAAABgCAAAAAAAAwAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAKAAwAAAAIAAQACgAAAAgAAACMAAAAAwAAAEwAAAAoAAAABAAAAIz+//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAArP7//wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAADM/v//CAAAACQAAAAaAAAAeyJ0eXBlIjoidGltZXNlcmllcy1tYW55In0AAAQAAABtZXRhAAAAAAIAAADYAAAABAAAAEL///8UAAAAoAAAAKAAAAAAAAADoAAAAAIAAAAsAAAABAAAADT///8IAAAAEAAAAAUAAABWYWx1ZQAAAAQAAABuYW1lAAAAAFj///8IAAAATAAAAEAAAAB7Il9fbmFtZV9fIjoidXAiLCJpbnN0YW5jZSI6ImxvY2FsaG9zdDo5MDkwIiwiam9iIjoicHJvbWV0aGV1cyJ9AAAAAAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAwAgAAQVJST1cx -FRAME=QVJST1cxAAD/////AAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAAlP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAAC0/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAANT+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAANAAAAAEAAAASv///xQAAACYAAAAmAAAAAAAAAOYAAAAAgAAACwAAAAEAAAAPP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAYP///wgAAABEAAAAOgAAAHsiX19uYW1lX18iOiJ1cCIsImluc3RhbmNlIjoibG9jYWxob3N0OjkxMDAiLCJqb2IiOiJub2RlIn0AAAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAQEuAkJ/r7BMAAAAAAAAAABAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAAAQAgAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAjAAAAAMAAABMAAAAKAAAAAQAAACU/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAALT+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA1P7//wgAAAAkAAAAGgAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSJ9AAAEAAAAbWV0YQAAAAACAAAA0AAAAAQAAABK////FAAAAJgAAACYAAAAAAAAA5gAAAACAAAALAAAAAQAAAA8////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAABg////CAAAAEQAAAA6AAAAeyJfX25hbWVfXyI6InVwIiwiaW5zdGFuY2UiOiJsb2NhbGhvc3Q6OTEwMCIsImpvYiI6Im5vZGUifQAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAACgCAABBUlJPVzE= -FRAME=QVJST1cxAAD/////6AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAArP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADM/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOz+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAALgAAAAEAAAAYv///xQAAACAAAAAgAAAAAAAAAOAAAAAAgAAACwAAAAEAAAAVP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAeP///wgAAAAsAAAAIwAAAHsibGV2ZWwiOiJlcnJvciIsImxvY2F0aW9uIjoibW9vbiJ9AAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAN7cpctR1BYAAAAAAADwfxAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAAD4AQAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAjAAAAAMAAABMAAAAKAAAAAQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAMz+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA7P7//wgAAAAkAAAAGgAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSJ9AAAEAAAAbWV0YQAAAAACAAAAuAAAAAQAAABi////FAAAAIAAAACAAAAAAAAAA4AAAAACAAAALAAAAAQAAABU////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAAB4////CAAAACwAAAAjAAAAeyJsZXZlbCI6ImVycm9yIiwibG9jYXRpb24iOiJtb29uIn0ABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAABACAABBUlJPVzE= -FRAME=QVJST1cxAAD/////6AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAArP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADM/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOz+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAALgAAAAEAAAAYv///xQAAACAAAAAgAAAAAAAAAOAAAAAAgAAACwAAAAEAAAAVP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAeP///wgAAAAsAAAAIgAAAHsibGV2ZWwiOiJpbmZvIiwibG9jYXRpb24iOiJtb29uIn0AAAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAN7cpctR1BYAAAAAAADw/xAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAAD4AQAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAjAAAAAMAAABMAAAAKAAAAAQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAMz+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA7P7//wgAAAAkAAAAGgAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSJ9AAAEAAAAbWV0YQAAAAACAAAAuAAAAAQAAABi////FAAAAIAAAACAAAAAAAAAA4AAAAACAAAALAAAAAQAAABU////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAAB4////CAAAACwAAAAiAAAAeyJsZXZlbCI6ImluZm8iLCJsb2NhdGlvbiI6Im1vb24ifQAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAABACAABBUlJPVzE= -FRAME=QVJST1cxAAD/////6AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAIwAAAADAAAATAAAACgAAAAEAAAArP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADM/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAOz+//8IAAAAJAAAABoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkifQAABAAAAG1ldGEAAAAAAgAAALgAAAAEAAAAYv///xQAAACAAAAAgAAAAAAAAAOAAAAAAgAAACwAAAAEAAAAVP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAeP///wgAAAAsAAAAIwAAAHsibGV2ZWwiOiJkZWJ1ZyIsImxvY2F0aW9uIjoibW9vbiJ9AAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAN7cpctR1BYBAAAAAAD4fxAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAAD4AQAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAjAAAAAMAAABMAAAAKAAAAAQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAMz+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAA7P7//wgAAAAkAAAAGgAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSJ9AAAEAAAAbWV0YQAAAAACAAAAuAAAAAQAAABi////FAAAAIAAAACAAAAAAAAAA4AAAAACAAAALAAAAAQAAABU////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAAB4////CAAAACwAAAAjAAAAeyJsZXZlbCI6ImRlYnVnIiwibG9jYXRpb24iOiJtb29uIn0ABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAABACAABBUlJPVzE= +FRAME=QVJST1cxAAD/////KAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAKwAAAADAAAATAAAACgAAAAEAAAAbP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAACM/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAKz+//8IAAAARAAAADsAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJjdXN0b20iOnsicmVzdWx0VHlwZSI6InZlY3RvciJ9fQAEAAAAbWV0YQAAAAACAAAA2AAAAAQAAABC////FAAAAKAAAACgAAAAAAAAA6AAAAACAAAALAAAAAQAAAA0////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAABY////CAAAAEwAAABAAAAAeyJfX25hbWVfXyI6InVwIiwiaW5zdGFuY2UiOiJsb2NhbGhvc3Q6OTA5MCIsImpvYiI6InByb21ldGhldXMifQAAAAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAAAAAAP////+4AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAEAAAAAAAAAAUAAAAAAAAAwQACgAYAAwACAAEAAoAAAAUAAAAWAAAAAEAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAEBLgJCf6+wTAAAAAAAA8D8QAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAABAABAAAAOAIAAAAAAADAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAKwAAAADAAAATAAAACgAAAAEAAAAbP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAACM/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAKz+//8IAAAARAAAADsAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJjdXN0b20iOnsicmVzdWx0VHlwZSI6InZlY3RvciJ9fQAEAAAAbWV0YQAAAAACAAAA2AAAAAQAAABC////FAAAAKAAAACgAAAAAAAAA6AAAAACAAAALAAAAAQAAAA0////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAABY////CAAAAEwAAABAAAAAeyJfX25hbWVfXyI6InVwIiwiaW5zdGFuY2UiOiJsb2NhbGhvc3Q6OTA5MCIsImpvYiI6InByb21ldGhldXMifQAAAAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAUAIAAEFSUk9XMQ== +FRAME=QVJST1cxAAD/////IAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAKwAAAADAAAATAAAACgAAAAEAAAAdP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAACU/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAALT+//8IAAAARAAAADsAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJjdXN0b20iOnsicmVzdWx0VHlwZSI6InZlY3RvciJ9fQAEAAAAbWV0YQAAAAACAAAA0AAAAAQAAABK////FAAAAJgAAACYAAAAAAAAA5gAAAACAAAALAAAAAQAAAA8////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAABg////CAAAAEQAAAA6AAAAeyJfX25hbWVfXyI6InVwIiwiaW5zdGFuY2UiOiJsb2NhbGhvc3Q6OTEwMCIsImpvYiI6Im5vZGUifQAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAAAAAAD/////uAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAABAAAAAAAAAAFAAAAAAAAAMEAAoAGAAMAAgABAAKAAAAFAAAAFgAAAABAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAACAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAABAS4CQn+vsEwAAAAAAAAAAEAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADgAAAAAAAQAAQAAADACAAAAAAAAwAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAKAAwAAAAIAAQACgAAAAgAAACsAAAAAwAAAEwAAAAoAAAABAAAAHT+//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAAlP7//wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAAC0/v//CAAAAEQAAAA7AAAAeyJ0eXBlIjoidGltZXNlcmllcy1tYW55IiwiY3VzdG9tIjp7InJlc3VsdFR5cGUiOiJ2ZWN0b3IifX0ABAAAAG1ldGEAAAAAAgAAANAAAAAEAAAASv///xQAAACYAAAAmAAAAAAAAAOYAAAAAgAAACwAAAAEAAAAPP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAYP///wgAAABEAAAAOgAAAHsiX19uYW1lX18iOiJ1cCIsImluc3RhbmNlIjoibG9jYWxob3N0OjkxMDAiLCJqb2IiOiJub2RlIn0AAAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAABIAgAAQVJST1cx +FRAME=QVJST1cxAAD/////CAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAKwAAAADAAAATAAAACgAAAAEAAAAjP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAMz+//8IAAAARAAAADsAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJjdXN0b20iOnsicmVzdWx0VHlwZSI6InZlY3RvciJ9fQAEAAAAbWV0YQAAAAACAAAAuAAAAAQAAABi////FAAAAIAAAACAAAAAAAAAA4AAAAACAAAALAAAAAQAAABU////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAAB4////CAAAACwAAAAjAAAAeyJsZXZlbCI6ImVycm9yIiwibG9jYXRpb24iOiJtb29uIn0ABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAAAAAAD/////uAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAABAAAAAAAAAAFAAAAAAAAAMEAAoAGAAMAAgABAAKAAAAFAAAAFgAAAABAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAACAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAA3tyly1HUFgAAAAAAAPB/EAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADgAAAAAAAQAAQAAABgCAAAAAAAAwAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAKAAwAAAAIAAQACgAAAAgAAACsAAAAAwAAAEwAAAAoAAAABAAAAIz+//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAArP7//wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAADM/v//CAAAAEQAAAA7AAAAeyJ0eXBlIjoidGltZXNlcmllcy1tYW55IiwiY3VzdG9tIjp7InJlc3VsdFR5cGUiOiJ2ZWN0b3IifX0ABAAAAG1ldGEAAAAAAgAAALgAAAAEAAAAYv///xQAAACAAAAAgAAAAAAAAAOAAAAAAgAAACwAAAAEAAAAVP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAeP///wgAAAAsAAAAIwAAAHsibGV2ZWwiOiJlcnJvciIsImxvY2F0aW9uIjoibW9vbiJ9AAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAwAgAAQVJST1cx +FRAME=QVJST1cxAAD/////CAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAKwAAAADAAAATAAAACgAAAAEAAAAjP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAMz+//8IAAAARAAAADsAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJjdXN0b20iOnsicmVzdWx0VHlwZSI6InZlY3RvciJ9fQAEAAAAbWV0YQAAAAACAAAAuAAAAAQAAABi////FAAAAIAAAACAAAAAAAAAA4AAAAACAAAALAAAAAQAAABU////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAAB4////CAAAACwAAAAiAAAAeyJsZXZlbCI6ImluZm8iLCJsb2NhdGlvbiI6Im1vb24ifQAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAAAAAAD/////uAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAABAAAAAAAAAAFAAAAAAAAAMEAAoAGAAMAAgABAAKAAAAFAAAAFgAAAABAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAACAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAA3tyly1HUFgAAAAAAAPD/EAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADgAAAAAAAQAAQAAABgCAAAAAAAAwAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAKAAwAAAAIAAQACgAAAAgAAACsAAAAAwAAAEwAAAAoAAAABAAAAIz+//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAArP7//wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAADM/v//CAAAAEQAAAA7AAAAeyJ0eXBlIjoidGltZXNlcmllcy1tYW55IiwiY3VzdG9tIjp7InJlc3VsdFR5cGUiOiJ2ZWN0b3IifX0ABAAAAG1ldGEAAAAAAgAAALgAAAAEAAAAYv///xQAAACAAAAAgAAAAAAAAAOAAAAAAgAAACwAAAAEAAAAVP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAeP///wgAAAAsAAAAIgAAAHsibGV2ZWwiOiJpbmZvIiwibG9jYXRpb24iOiJtb29uIn0AAAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAwAgAAQVJST1cx +FRAME=QVJST1cxAAD/////CAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAKwAAAADAAAATAAAACgAAAAEAAAAjP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAMz+//8IAAAARAAAADsAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJjdXN0b20iOnsicmVzdWx0VHlwZSI6InZlY3RvciJ9fQAEAAAAbWV0YQAAAAACAAAAuAAAAAQAAABi////FAAAAIAAAACAAAAAAAAAA4AAAAACAAAALAAAAAQAAABU////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAAB4////CAAAACwAAAAjAAAAeyJsZXZlbCI6ImRlYnVnIiwibG9jYXRpb24iOiJtb29uIn0ABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAAAAAAD/////uAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAABAAAAAAAAAAFAAAAAAAAAMEAAoAGAAMAAgABAAKAAAAFAAAAFgAAAABAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAACAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAA3tyly1HUFgEAAAAAAPh/EAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADgAAAAAAAQAAQAAABgCAAAAAAAAwAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAKAAwAAAAIAAQACgAAAAgAAACsAAAAAwAAAEwAAAAoAAAABAAAAIz+//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAArP7//wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAADM/v//CAAAAEQAAAA7AAAAeyJ0eXBlIjoidGltZXNlcmllcy1tYW55IiwiY3VzdG9tIjp7InJlc3VsdFR5cGUiOiJ2ZWN0b3IifX0ABAAAAG1ldGEAAAAAAgAAALgAAAAEAAAAYv///xQAAACAAAAAgAAAAAAAAAOAAAAAAgAAACwAAAAEAAAAVP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAeP///wgAAAAsAAAAIwAAAHsibGV2ZWwiOiJkZWJ1ZyIsImxvY2F0aW9uIjoibW9vbiJ9AAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAwAgAAQVJST1cx diff --git a/pkg/util/converter/testdata/prom-warnings-frame.json b/pkg/util/converter/testdata/prom-warnings-frame.json index 0fb7dde53f6..7a3f54e6e0b 100644 --- a/pkg/util/converter/testdata/prom-warnings-frame.json +++ b/pkg/util/converter/testdata/prom-warnings-frame.json @@ -4,6 +4,9 @@ "schema": { "meta": { "type": "timeseries-many", + "custom": { + "resultType": "vector" + }, "notices": [ { "severity": "warning", @@ -52,6 +55,9 @@ "schema": { "meta": { "type": "timeseries-many", + "custom": { + "resultType": "vector" + }, "notices": [ { "severity": "warning", @@ -100,6 +106,9 @@ "schema": { "meta": { "type": "timeseries-many", + "custom": { + "resultType": "vector" + }, "notices": [ { "severity": "warning", @@ -155,6 +164,9 @@ "schema": { "meta": { "type": "timeseries-many", + "custom": { + "resultType": "vector" + }, "notices": [ { "severity": "warning", @@ -210,6 +222,9 @@ "schema": { "meta": { "type": "timeseries-many", + "custom": { + "resultType": "vector" + }, "notices": [ { "severity": "warning", diff --git a/pkg/util/converter/testdata/prom-warnings-golden.txt b/pkg/util/converter/testdata/prom-warnings-golden.txt index 81afb1b6749..630c1f20965 100644 --- a/pkg/util/converter/testdata/prom-warnings-golden.txt +++ b/pkg/util/converter/testdata/prom-warnings-golden.txt @@ -2,6 +2,9 @@ Frame[0] { "type": "timeseries-many", + "custom": { + "resultType": "vector" + }, "notices": [ { "severity": "warning", @@ -27,6 +30,9 @@ Dimensions: 2 Fields by 1 Rows Frame[1] { "type": "timeseries-many", + "custom": { + "resultType": "vector" + }, "notices": [ { "severity": "warning", @@ -52,6 +58,9 @@ Dimensions: 2 Fields by 1 Rows Frame[2] { "type": "timeseries-many", + "custom": { + "resultType": "vector" + }, "notices": [ { "severity": "warning", @@ -77,6 +86,9 @@ Dimensions: 2 Fields by 1 Rows Frame[3] { "type": "timeseries-many", + "custom": { + "resultType": "vector" + }, "notices": [ { "severity": "warning", @@ -102,6 +114,9 @@ Dimensions: 2 Fields by 1 Rows Frame[4] { "type": "timeseries-many", + "custom": { + "resultType": "vector" + }, "notices": [ { "severity": "warning", @@ -125,8 +140,8 @@ Dimensions: 2 Fields by 1 Rows ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 -FRAME=QVJST1cxAAD/////YAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAOwAAAADAAAATAAAACgAAAAEAAAANP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAABU/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAHT+//8IAAAAhAAAAHoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJub3RpY2VzIjpbeyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAxIn0seyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAyIn1dfQAABAAAAG1ldGEAAAAAAgAAANAAAAAEAAAASv///xQAAACYAAAAmAAAAAAAAAOYAAAAAgAAACwAAAAEAAAAPP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAYP///wgAAABEAAAAOgAAAHsiX19uYW1lX18iOiJ1cCIsImluc3RhbmNlIjoibG9jYWxob3N0OjkxMDAiLCJqb2IiOiJub2RlIn0AAAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAQEuAkJ/r7BMAAAAAAAAAABAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAABwAgAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAA7AAAAAMAAABMAAAAKAAAAAQAAAA0/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAFT+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAdP7//wgAAACEAAAAegAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSIsIm5vdGljZXMiOlt7InNldmVyaXR5Ijoid2FybmluZyIsInRleHQiOiJ3YXJuaW5nIDEifSx7InNldmVyaXR5Ijoid2FybmluZyIsInRleHQiOiJ3YXJuaW5nIDIifV19AAAEAAAAbWV0YQAAAAACAAAA0AAAAAQAAABK////FAAAAJgAAACYAAAAAAAAA5gAAAACAAAALAAAAAQAAAA8////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAABg////CAAAAEQAAAA6AAAAeyJfX25hbWVfXyI6InVwIiwiaW5zdGFuY2UiOiJsb2NhbGhvc3Q6OTEwMCIsImpvYiI6Im5vZGUifQAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAIgCAABBUlJPVzE= -FRAME=QVJST1cxAAD/////SAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAOwAAAADAAAATAAAACgAAAAEAAAATP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAABs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAIz+//8IAAAAhAAAAHoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJub3RpY2VzIjpbeyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAxIn0seyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAyIn1dfQAABAAAAG1ldGEAAAAAAgAAALgAAAAEAAAAYv///xQAAACAAAAAgAAAAAAAAAOAAAAAAgAAACwAAAAEAAAAVP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAeP///wgAAAAsAAAAIwAAAHsibGV2ZWwiOiJlcnJvciIsImxvY2F0aW9uIjoibW9vbiJ9AAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAN7cpctR1BYAAAAAAADwfxAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAABYAgAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAA7AAAAAMAAABMAAAAKAAAAAQAAABM/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAGz+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAjP7//wgAAACEAAAAegAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSIsIm5vdGljZXMiOlt7InNldmVyaXR5Ijoid2FybmluZyIsInRleHQiOiJ3YXJuaW5nIDEifSx7InNldmVyaXR5Ijoid2FybmluZyIsInRleHQiOiJ3YXJuaW5nIDIifV19AAAEAAAAbWV0YQAAAAACAAAAuAAAAAQAAABi////FAAAAIAAAACAAAAAAAAAA4AAAAACAAAALAAAAAQAAABU////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAAB4////CAAAACwAAAAjAAAAeyJsZXZlbCI6ImVycm9yIiwibG9jYXRpb24iOiJtb29uIn0ABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAHACAABBUlJPVzE= -FRAME=QVJST1cxAAD/////SAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAOwAAAADAAAATAAAACgAAAAEAAAATP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAABs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAIz+//8IAAAAhAAAAHoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJub3RpY2VzIjpbeyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAxIn0seyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAyIn1dfQAABAAAAG1ldGEAAAAAAgAAALgAAAAEAAAAYv///xQAAACAAAAAgAAAAAAAAAOAAAAAAgAAACwAAAAEAAAAVP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAeP///wgAAAAsAAAAIgAAAHsibGV2ZWwiOiJpbmZvIiwibG9jYXRpb24iOiJtb29uIn0AAAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAN7cpctR1BYAAAAAAADw/xAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAABYAgAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAA7AAAAAMAAABMAAAAKAAAAAQAAABM/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAGz+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAjP7//wgAAACEAAAAegAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSIsIm5vdGljZXMiOlt7InNldmVyaXR5Ijoid2FybmluZyIsInRleHQiOiJ3YXJuaW5nIDEifSx7InNldmVyaXR5Ijoid2FybmluZyIsInRleHQiOiJ3YXJuaW5nIDIifV19AAAEAAAAbWV0YQAAAAACAAAAuAAAAAQAAABi////FAAAAIAAAACAAAAAAAAAA4AAAAACAAAALAAAAAQAAABU////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAAB4////CAAAACwAAAAiAAAAeyJsZXZlbCI6ImluZm8iLCJsb2NhdGlvbiI6Im1vb24ifQAABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAHACAABBUlJPVzE= -FRAME=QVJST1cxAAD/////SAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAOwAAAADAAAATAAAACgAAAAEAAAATP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAABs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAIz+//8IAAAAhAAAAHoAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJub3RpY2VzIjpbeyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAxIn0seyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAyIn1dfQAABAAAAG1ldGEAAAAAAgAAALgAAAAEAAAAYv///xQAAACAAAAAgAAAAAAAAAOAAAAAAgAAACwAAAAEAAAAVP///wgAAAAQAAAABQAAAFZhbHVlAAAABAAAAG5hbWUAAAAAeP///wgAAAAsAAAAIwAAAHsibGV2ZWwiOiJkZWJ1ZyIsImxvY2F0aW9uIjoibW9vbiJ9AAYAAABsYWJlbHMAAAAAAACK////AAACAAUAAABWYWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAACkwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAFRpbWUAAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAEAAAAVGltZQAAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAN7cpctR1BYBAAAAAAD4fxAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAAEAAEAAABYAgAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAA7AAAAAMAAABMAAAAKAAAAAQAAABM/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAGz+//8IAAAADAAAAAAAAAAAAAAABAAAAG5hbWUAAAAAjP7//wgAAACEAAAAegAAAHsidHlwZSI6InRpbWVzZXJpZXMtbWFueSIsIm5vdGljZXMiOlt7InNldmVyaXR5Ijoid2FybmluZyIsInRleHQiOiJ3YXJuaW5nIDEifSx7InNldmVyaXR5Ijoid2FybmluZyIsInRleHQiOiJ3YXJuaW5nIDIifV19AAAEAAAAbWV0YQAAAAACAAAAuAAAAAQAAABi////FAAAAIAAAACAAAAAAAAAA4AAAAACAAAALAAAAAQAAABU////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAAB4////CAAAACwAAAAjAAAAeyJsZXZlbCI6ImRlYnVnIiwibG9jYXRpb24iOiJtb29uIn0ABgAAAGxhYmVscwAAAAAAAIr///8AAAIABQAAAFZhbHVlABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAAKTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAVGltZQAAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAQAAABUaW1lAAAAAHACAABBUlJPVzE= +FRAME=QVJST1cxAAD/////iAIAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAAwBAAADAAAATAAAACgAAAAEAAAADP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAAAs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAEz+//8IAAAApAAAAJsAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJjdXN0b20iOnsicmVzdWx0VHlwZSI6InZlY3RvciJ9LCJub3RpY2VzIjpbeyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAxIn0seyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAyIn1dfQAEAAAAbWV0YQAAAAACAAAA2AAAAAQAAABC////FAAAAKAAAACgAAAAAAAAA6AAAAACAAAALAAAAAQAAAA0////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAABY////CAAAAEwAAABAAAAAeyJfX25hbWVfXyI6InVwIiwiaW5zdGFuY2UiOiJsb2NhbGhvc3Q6OTA5MCIsImpvYiI6InByb21ldGhldXMifQAAAAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAAAAAAP////+4AAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAEAAAAAAAAAAUAAAAAAAAAwQACgAYAAwACAAEAAoAAAAUAAAAWAAAAAEAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAEBLgJCf6+wTAAAAAAAA8D8QAAAADAAUABIADAAIAAQADAAAABAAAAAsAAAAOAAAAAAABAABAAAAmAIAAAAAAADAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAAwBAAADAAAATAAAACgAAAAEAAAADP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAAAs/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAAEz+//8IAAAApAAAAJsAAAB7InR5cGUiOiJ0aW1lc2VyaWVzLW1hbnkiLCJjdXN0b20iOnsicmVzdWx0VHlwZSI6InZlY3RvciJ9LCJub3RpY2VzIjpbeyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAxIn0seyJzZXZlcml0eSI6Indhcm5pbmciLCJ0ZXh0Ijoid2FybmluZyAyIn1dfQAEAAAAbWV0YQAAAAACAAAA2AAAAAQAAABC////FAAAAKAAAACgAAAAAAAAA6AAAAACAAAALAAAAAQAAAA0////CAAAABAAAAAFAAAAVmFsdWUAAAAEAAAAbmFtZQAAAABY////CAAAAEwAAABAAAAAeyJfX25hbWVfXyI6InVwIiwiaW5zdGFuY2UiOiJsb2NhbGhvc3Q6OTA5MCIsImpvYiI6InByb21ldGhldXMifQAAAAAGAAAAbGFiZWxzAAAAAAAAiv///wAAAgAFAAAAVmFsdWUAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAATAAAAAAAAApMAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABUaW1lAAAAAAQAAABuYW1lAAAAAAAAAAAAAAYACAAGAAYAAAAAAAMABAAAAFRpbWUAAAAAsAIAAEFSUk9XMQ== +FRAME=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 +FRAME=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 +FRAME=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 +FRAME=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 From fca52a1c83d3a738d560e19091e22440de592720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 5 May 2022 17:15:59 +0200 Subject: [PATCH 072/440] CommandPalette: Make dashboard nav work when under grafana is under sub path (#48744) --- .../features/commandPalette/actions/dashboard.nav.actions.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/features/commandPalette/actions/dashboard.nav.actions.ts b/public/app/features/commandPalette/actions/dashboard.nav.actions.ts index 703b928d47c..6291eede598 100644 --- a/public/app/features/commandPalette/actions/dashboard.nav.actions.ts +++ b/public/app/features/commandPalette/actions/dashboard.nav.actions.ts @@ -1,5 +1,6 @@ import { Action } from 'kbar'; +import { locationUtil } from '@grafana/data'; import { locationService, getBackendSrv } from '@grafana/runtime'; async function getDashboardNav(parentId: string): Promise { @@ -12,7 +13,7 @@ async function getDashboardNav(parentId: string): Promise { id: `go/dashboard/${item.url}`, name: `Go to dashboard ${item.title}`, perform: () => { - locationService.push(item.url); + locationService.push(locationUtil.stripBaseFromUrl(item.url)); }, })); From a5672758d8054ec94bf0fc3bd9c9a2779cfc7cc9 Mon Sep 17 00:00:00 2001 From: Ieva Date: Thu, 5 May 2022 16:31:14 +0100 Subject: [PATCH 073/440] Access control: further reduce access control feature toggle checks (#48171) * reduce the usage of access control flag further by removing it from SQL store methods * fixing tests * fix another test * linting * remove AC feature toggle use from API keys * remove unneeded function --- pkg/api/common_test.go | 27 +++++++------------ pkg/services/accesscontrol/accesscontrol.go | 6 +++++ .../ossaccesscontrol/ossaccesscontrol.go | 2 +- .../dashboards/manager/dashboard_service.go | 2 +- .../dashboard_service_integration_test.go | 16 ++++++++--- .../dashboards/manager/folder_service.go | 2 +- .../dashboards/manager/folder_service_test.go | 2 ++ .../libraryelements/libraryelements_test.go | 21 ++++++++++----- .../librarypanels/librarypanels_test.go | 5 +++- .../serviceaccounts/database/database.go | 3 +-- pkg/services/sqlstore/annotation.go | 3 +-- pkg/services/sqlstore/annotation_test.go | 5 +--- pkg/services/sqlstore/apikey.go | 3 +-- pkg/services/sqlstore/dashboard.go | 4 +-- pkg/services/sqlstore/org_users.go | 5 ++-- pkg/services/sqlstore/team.go | 7 +++-- 16 files changed, 61 insertions(+), 52 deletions(-) diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index d7d80894624..52629efbe27 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -227,15 +227,13 @@ func (s *fakeRenderService) Init() error { } func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url string, permissions []*accesscontrol.Permission) (*scenarioContext, *HTTPServer) { - features := featuremgmt.WithFeatures(featuremgmt.FlagAccesscontrol) - cfg.IsFeatureToggleEnabled = features.IsEnabled cfg.Quota.Enabled = false store := sqlstore.InitTestDB(t) hs := &HTTPServer{ Cfg: cfg, Live: newTestLive(t, store), - Features: features, + Features: featuremgmt.WithFeatures(), QuotaService: "a.QuotaService{Cfg: cfg}, RouteRegister: routing.NewRouteRegister(), AccessControl: accesscontrolmock.New().WithPermissions(permissions), @@ -329,39 +327,32 @@ func setupSimpleHTTPServer(features *featuremgmt.FeatureManager) *HTTPServer { } func setupHTTPServer(t *testing.T, useFakeAccessControl bool, enableAccessControl bool) accessControlScenarioContext { - // Use a new conf - features := featuremgmt.WithFeatures("accesscontrol", enableAccessControl) - cfg := setting.NewCfg() - cfg.IsFeatureToggleEnabled = features.IsEnabled - - return setupHTTPServerWithCfg(t, useFakeAccessControl, enableAccessControl, cfg) + return setupHTTPServerWithCfg(t, useFakeAccessControl, enableAccessControl, setting.NewCfg()) } func setupHTTPServerWithMockDb(t *testing.T, useFakeAccessControl bool, enableAccessControl bool) accessControlScenarioContext { // Use a new conf - features := featuremgmt.WithFeatures("accesscontrol", enableAccessControl) cfg := setting.NewCfg() - cfg.IsFeatureToggleEnabled = features.IsEnabled - db := sqlstore.InitTestDB(t) - db.Cfg = cfg + db.Cfg = setting.NewCfg() return setupHTTPServerWithCfgDb(t, useFakeAccessControl, enableAccessControl, cfg, db, mockstore.NewSQLStoreMock()) } func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessControl bool, cfg *setting.Cfg) accessControlScenarioContext { - var featureFlags []string - if enableAccessControl { - featureFlags = append(featureFlags, featuremgmt.FlagAccesscontrol) + var db *sqlstore.SQLStore + if useFakeAccessControl && enableAccessControl { + db = sqlstore.InitTestDB(t, sqlstore.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagAccesscontrol}}) + } else { + db = sqlstore.InitTestDB(t, sqlstore.InitTestDBOpt{}) } - db := sqlstore.InitTestDB(t, sqlstore.InitTestDBOpt{FeatureFlags: featureFlags}) return setupHTTPServerWithCfgDb(t, useFakeAccessControl, enableAccessControl, cfg, db, db) } func setupHTTPServerWithCfgDb(t *testing.T, useFakeAccessControl, enableAccessControl bool, cfg *setting.Cfg, db *sqlstore.SQLStore, store sqlstore.Store) accessControlScenarioContext { t.Helper() - features := featuremgmt.WithFeatures("accesscontrol", enableAccessControl) + features := featuremgmt.WithFeatures(featuremgmt.FlagAccesscontrol, enableAccessControl) cfg.IsFeatureToggleEnabled = features.IsEnabled var acmock *accesscontrolmock.Mock diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index faab1529485..4330c8bacbb 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -7,6 +7,8 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" ) type Options struct { @@ -222,3 +224,7 @@ func extractPrefixes(prefix string) (string, string, bool) { attributePrefix := rootPrefix + parts[1] + ":" return rootPrefix, attributePrefix, true } + +func IsDisabled(cfg *setting.Cfg) bool { + return !cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) +} diff --git a/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go b/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go index 2e2aea961ff..57de31b6695 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go @@ -155,7 +155,7 @@ func (ac *OSSAccessControlService) GetUserBuiltInRoles(user *models.SignedInUser builtInRoles := []string{string(user.OrgRole)} // With built-in role simplifying, inheritance is performed upon role registration. - if !ac.features.IsEnabled(featuremgmt.FlagAccesscontrolBuiltins) { + if ac.IsDisabled() { for _, br := range user.OrgRole.Children() { builtInRoles = append(builtInRoles, string(br)) } diff --git a/pkg/services/dashboards/manager/dashboard_service.go b/pkg/services/dashboards/manager/dashboard_service.go index 56c41c3568c..5205653df5f 100644 --- a/pkg/services/dashboards/manager/dashboard_service.go +++ b/pkg/services/dashboards/manager/dashboard_service.go @@ -447,7 +447,7 @@ func (dr *DashboardServiceImpl) GetDashboardsByPluginID(ctx context.Context, que func (dr *DashboardServiceImpl) setDefaultPermissions(ctx context.Context, dto *m.SaveDashboardDTO, dash *models.Dashboard, provisioned bool) error { inFolder := dash.FolderId > 0 - if dr.features.IsEnabled(featuremgmt.FlagAccesscontrol) { + if !accesscontrol.IsDisabled(dr.cfg) { var permissions []accesscontrol.SetResourcePermissionCommand if !provisioned { permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{ diff --git a/pkg/services/dashboards/manager/dashboard_service_integration_test.go b/pkg/services/dashboards/manager/dashboard_service_integration_test.go index 2bb0d4d571c..99a9d6cb97a 100644 --- a/pkg/services/dashboards/manager/dashboard_service_integration_test.go +++ b/pkg/services/dashboards/manager/dashboard_service_integration_test.go @@ -858,8 +858,10 @@ func callSaveWithResult(t *testing.T, cmd models.SaveDashboardCommand, sqlStore dto := toSaveDashboardDto(cmd) dashboardStore := database.ProvideDashboardStore(sqlStore) + cfg := setting.NewCfg() + cfg.IsFeatureToggleEnabled = featuremgmt.WithFeatures().IsEnabled service := ProvideDashboardService( - setting.NewCfg(), dashboardStore, &dummyDashAlertExtractor{}, + cfg, dashboardStore, &dummyDashAlertExtractor{}, featuremgmt.WithFeatures(), accesscontrolmock.NewPermissionsServicesMock(), ) res, err := service.SaveDashboard(context.Background(), &dto, false) @@ -871,8 +873,10 @@ func callSaveWithResult(t *testing.T, cmd models.SaveDashboardCommand, sqlStore func callSaveWithError(cmd models.SaveDashboardCommand, sqlStore *sqlstore.SQLStore) error { dto := toSaveDashboardDto(cmd) dashboardStore := database.ProvideDashboardStore(sqlStore) + cfg := setting.NewCfg() + cfg.IsFeatureToggleEnabled = featuremgmt.WithFeatures().IsEnabled service := ProvideDashboardService( - setting.NewCfg(), dashboardStore, &dummyDashAlertExtractor{}, + cfg, dashboardStore, &dummyDashAlertExtractor{}, featuremgmt.WithFeatures(), accesscontrolmock.NewPermissionsServicesMock(), ) _, err := service.SaveDashboard(context.Background(), &dto, false) @@ -902,8 +906,10 @@ func saveTestDashboard(t *testing.T, title string, orgID, folderID int64, sqlSto } dashboardStore := database.ProvideDashboardStore(sqlStore) + cfg := setting.NewCfg() + cfg.IsFeatureToggleEnabled = featuremgmt.WithFeatures().IsEnabled service := ProvideDashboardService( - setting.NewCfg(), dashboardStore, &dummyDashAlertExtractor{}, + cfg, dashboardStore, &dummyDashAlertExtractor{}, featuremgmt.WithFeatures(), accesscontrolmock.NewPermissionsServicesMock(), ) res, err := service.SaveDashboard(context.Background(), &dto, false) @@ -934,8 +940,10 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore *sqlstore. } dashboardStore := database.ProvideDashboardStore(sqlStore) + cfg := setting.NewCfg() + cfg.IsFeatureToggleEnabled = featuremgmt.WithFeatures().IsEnabled service := ProvideDashboardService( - setting.NewCfg(), dashboardStore, &dummyDashAlertExtractor{}, + cfg, dashboardStore, &dummyDashAlertExtractor{}, featuremgmt.WithFeatures(), accesscontrolmock.NewPermissionsServicesMock(), ) res, err := service.SaveDashboard(context.Background(), &dto, false) diff --git a/pkg/services/dashboards/manager/folder_service.go b/pkg/services/dashboards/manager/folder_service.go index d7c926133a4..cca5041390a 100644 --- a/pkg/services/dashboards/manager/folder_service.go +++ b/pkg/services/dashboards/manager/folder_service.go @@ -171,7 +171,7 @@ func (f *FolderServiceImpl) CreateFolder(ctx context.Context, user *models.Signe } var permissionErr error - if f.features.IsEnabled(featuremgmt.FlagAccesscontrol) { + if !accesscontrol.IsDisabled(f.cfg) { _, permissionErr = f.permissions.SetPermissions(ctx, orgID, folder.Uid, []accesscontrol.SetResourcePermissionCommand{ {UserID: userID, Permission: models.PERMISSION_ADMIN.String()}, {BuiltinRole: string(models.ROLE_EDITOR), Permission: models.PERMISSION_EDIT.String()}, diff --git a/pkg/services/dashboards/manager/folder_service_test.go b/pkg/services/dashboards/manager/folder_service_test.go index 69243876a5a..1cc03e0cb4a 100644 --- a/pkg/services/dashboards/manager/folder_service_test.go +++ b/pkg/services/dashboards/manager/folder_service_test.go @@ -31,6 +31,7 @@ func TestProvideFolderService(t *testing.T) { store := &dashboards.FakeDashboardStore{} cfg := setting.NewCfg() features := featuremgmt.WithFeatures() + cfg.IsFeatureToggleEnabled = features.IsEnabled permissionsServices := acmock.NewPermissionsServicesMock() dashboardService := ProvideDashboardService(cfg, store, nil, features, permissionsServices) ac := acmock.New() @@ -49,6 +50,7 @@ func TestFolderService(t *testing.T) { store := &dashboards.FakeDashboardStore{} cfg := setting.NewCfg() features := featuremgmt.WithFeatures() + cfg.IsFeatureToggleEnabled = features.IsEnabled permissionsServices := acmock.NewPermissionsServicesMock() dashboardService := ProvideDashboardService(cfg, store, nil, features, permissionsServices) mockStore := mockstore.NewSQLStoreMock() diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index a4bc6ed6cfa..8021c262aa0 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -202,9 +202,12 @@ func createDashboard(t *testing.T, sqlStore *sqlstore.SQLStore, user models.Sign dashboardStore := database.ProvideDashboardStore(sqlStore) dashAlertExtractor := alerting.ProvideDashAlertExtractorService(nil, nil, nil) + features := featuremgmt.WithFeatures() + cfg := setting.NewCfg() + cfg.IsFeatureToggleEnabled = features.IsEnabled service := dashboardservice.ProvideDashboardService( - setting.NewCfg(), dashboardStore, dashAlertExtractor, - featuremgmt.WithFeatures(), acmock.NewPermissionsServicesMock(), + cfg, dashboardStore, dashAlertExtractor, + features, acmock.NewPermissionsServicesMock(), ) dashboard, err := service.SaveDashboard(context.Background(), dashItem, true) require.NoError(t, err) @@ -218,6 +221,7 @@ func createFolderWithACL(t *testing.T, sqlStore *sqlstore.SQLStore, title string cfg := setting.NewCfg() features := featuremgmt.WithFeatures() + cfg.IsFeatureToggleEnabled = features.IsEnabled permissionsServices := acmock.NewPermissionsServicesMock() dashboardStore := database.ProvideDashboardStore(sqlStore) @@ -317,17 +321,20 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo sqlStore := sqlstore.InitTestDB(t) guardian.InitLegacyGuardian(sqlStore) dashboardStore := database.ProvideDashboardStore(sqlStore) + features := featuremgmt.WithFeatures() + cfg := setting.NewCfg() + cfg.IsFeatureToggleEnabled = features.IsEnabled dashboardService := dashboardservice.ProvideDashboardService( - setting.NewCfg(), dashboardStore, nil, - featuremgmt.WithFeatures(), acmock.NewPermissionsServicesMock(), + cfg, dashboardStore, nil, + features, acmock.NewPermissionsServicesMock(), ) ac := acmock.New() service := LibraryElementService{ - Cfg: setting.NewCfg(), + Cfg: cfg, SQLStore: sqlStore, folderService: dashboardservice.ProvideFolderService( - setting.NewCfg(), dashboardService, dashboardStore, nil, - featuremgmt.WithFeatures(), acmock.NewPermissionsServicesMock(), ac, nil, + cfg, dashboardService, dashboardStore, nil, + features, acmock.NewPermissionsServicesMock(), ac, nil, ), } diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index cfd3a9896e9..a2493c8e1c7 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -1368,8 +1368,10 @@ func createDashboard(t *testing.T, sqlStore *sqlstore.SQLStore, user *models.Sig dashboardStore := database.ProvideDashboardStore(sqlStore) dashAlertService := alerting.ProvideDashAlertExtractorService(nil, nil, nil) + cfg := setting.NewCfg() + cfg.IsFeatureToggleEnabled = featuremgmt.WithFeatures().IsEnabled service := dashboardservice.ProvideDashboardService( - setting.NewCfg(), dashboardStore, dashAlertService, + cfg, dashboardStore, dashAlertService, featuremgmt.WithFeatures(), acmock.NewPermissionsServicesMock(), ) dashboard, err := service.SaveDashboard(context.Background(), dashItem, true) @@ -1383,6 +1385,7 @@ func createFolderWithACL(t *testing.T, sqlStore *sqlstore.SQLStore, title string t.Helper() cfg := setting.NewCfg() + cfg.IsFeatureToggleEnabled = featuremgmt.WithFeatures().IsEnabled features := featuremgmt.WithFeatures() permissionsServices := acmock.NewPermissionsServicesMock() dashboardStore := database.ProvideDashboardStore(sqlStore) diff --git a/pkg/services/serviceaccounts/database/database.go b/pkg/services/serviceaccounts/database/database.go index aa11773c867..4dd44d14193 100644 --- a/pkg/services/serviceaccounts/database/database.go +++ b/pkg/services/serviceaccounts/database/database.go @@ -11,7 +11,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/sqlstore" "xorm.io/xorm" @@ -354,7 +353,7 @@ func (s *ServiceAccountsStoreImpl) SearchOrgServiceAccounts( s.sqlStore.Dialect.Quote("user"), s.sqlStore.Dialect.BooleanStr(true))) - if s.sqlStore.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) { + if !accesscontrol.IsDisabled(s.sqlStore.Cfg) { acFilter, err := accesscontrol.Filter(signedInUser, "org_user.user_id", "serviceaccounts:id:", serviceaccounts.ActionRead) if err != nil { return err diff --git a/pkg/services/sqlstore/annotation.go b/pkg/services/sqlstore/annotation.go index 05da314540d..6d9f90c0b4b 100644 --- a/pkg/services/sqlstore/annotation.go +++ b/pkg/services/sqlstore/annotation.go @@ -11,7 +11,6 @@ import ( "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/annotations" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/sqlstore/permissions" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" ) @@ -229,7 +228,7 @@ func (r *SQLAnnotationRepo) Find(ctx context.Context, query *annotations.ItemQue } } - if r.sql.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) { + if !ac.IsDisabled(r.sql.Cfg) { acFilter, acArgs, err := getAccessControlFilter(query.SignedInUser) if err != nil { return err diff --git a/pkg/services/sqlstore/annotation_test.go b/pkg/services/sqlstore/annotation_test.go index 52d57374a6d..00461d0f5e1 100644 --- a/pkg/services/sqlstore/annotation_test.go +++ b/pkg/services/sqlstore/annotation_test.go @@ -339,10 +339,7 @@ func TestAnnotations(t *testing.T) { } func TestAnnotationListingWithRBAC(t *testing.T) { - sql := sqlstore.InitTestDB(t) - sql.Cfg.IsFeatureToggleEnabled = func(key string) bool { - return key == featuremgmt.FlagAccesscontrol - } + sql := sqlstore.InitTestDB(t, sqlstore.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagAccesscontrol}}) repo := sqlstore.NewSQLAnnotationRepo(sql) dashboardStore := dashboardstore.ProvideDashboardStore(sql) diff --git a/pkg/services/sqlstore/apikey.go b/pkg/services/sqlstore/apikey.go index a06bc3e3c61..de92da3940f 100644 --- a/pkg/services/sqlstore/apikey.go +++ b/pkg/services/sqlstore/apikey.go @@ -8,7 +8,6 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/featuremgmt" ) // GetAPIKeys queries the database based @@ -29,7 +28,7 @@ func (ss *SQLStore) GetAPIKeys(ctx context.Context, query *models.GetApiKeysQuer sess = sess.Where("service_account_id IS NULL") - if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) { + if !accesscontrol.IsDisabled(ss.Cfg) { filter, err := accesscontrol.Filter(query.User, "id", "apikeys:id:", accesscontrol.ActionAPIKeyRead) if err != nil { return err diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 4017e8d8872..4f961ee2c92 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -7,7 +7,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/sqlstore/permissions" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/util" @@ -74,7 +74,7 @@ func (ss *SQLStore) FindDashboards(ctx context.Context, query *models.FindPersis }, } - if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) { + if !accesscontrol.IsDisabled(ss.Cfg) { // if access control is enabled, overwrite the filters so far filters = []interface{}{ permissions.NewAccessControlDashboardPermissionFilter(query.SignedInUser, query.Permission, query.Type), diff --git a/pkg/services/sqlstore/org_users.go b/pkg/services/sqlstore/org_users.go index 66148ce3b41..88b85dfbb1e 100644 --- a/pkg/services/sqlstore/org_users.go +++ b/pkg/services/sqlstore/org_users.go @@ -8,7 +8,6 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/util" ) @@ -110,7 +109,7 @@ func (ss *SQLStore) GetOrgUsers(ctx context.Context, query *models.GetOrgUsersQu whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = ?", ss.Dialect.Quote("user"))) whereParams = append(whereParams, ss.Dialect.BooleanStr(false)) - if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) && query.User != nil { + if !accesscontrol.IsDisabled(ss.Cfg) && query.User != nil { acFilter, err := accesscontrol.Filter(query.User, "org_user.user_id", "users:id:", accesscontrol.ActionOrgUsersRead) if err != nil { return err @@ -175,7 +174,7 @@ func (ss *SQLStore) SearchOrgUsers(ctx context.Context, query *models.SearchOrgU whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = %s", ss.Dialect.Quote("user"), ss.Dialect.BooleanStr(false))) - if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) { + if !accesscontrol.IsDisabled(ss.Cfg) { acFilter, err := accesscontrol.Filter(query.User, "org_user.user_id", "users:id:", accesscontrol.ActionOrgUsersRead) if err != nil { return err diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index d8325604d7f..cf0756c5f67 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -9,7 +9,6 @@ import ( "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/featuremgmt" ) type TeamStore interface { @@ -214,7 +213,7 @@ func (ss *SQLStore) SearchTeams(ctx context.Context, query *models.SearchTeamsQu acFilter ac.SQLFilter err error ) - if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) { + if !ac.IsDisabled(ss.Cfg) { acFilter, err = ac.Filter(query.SignedInUser, "team.id", "teams:id:", ac.ActionTeamsRead) if err != nil { return err @@ -259,7 +258,7 @@ func (ss *SQLStore) SearchTeams(ctx context.Context, query *models.SearchTeamsQu } // Only count teams user can see - if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) { + if !ac.IsDisabled(ss.Cfg) { countSess.Where(acFilter.Where, acFilter.Args...) } @@ -516,7 +515,7 @@ func (ss *SQLStore) GetTeamMembers(ctx context.Context, query *models.GetTeamMem // With accesscontrol we filter out users based on the SignedInUser's permissions // Note we assume that checking SignedInUser is allowed to see team members for this team has already been performed // If the signed in user is not set no member will be returned - if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) { + if !ac.IsDisabled(ss.Cfg) { sqlID := fmt.Sprintf("%s.%s", ss.engine.Dialect().Quote("user"), ss.engine.Dialect().Quote("id")) *acFilter, err = ac.Filter(query.SignedInUser, sqlID, "users:id:", ac.ActionOrgUsersRead) if err != nil { From ea96c42545976bbe7a1a740b1f53f0fb73fbb9e0 Mon Sep 17 00:00:00 2001 From: sam boyer Date: Thu, 5 May 2022 10:56:53 -0500 Subject: [PATCH 074/440] schemas: Update to cuetsy v0.0.1 (#48753) * Add explicit cuetsy annotations to all models.cue * Update to first cuetsy release * Clean up cuetsify a bit; remove unification * Update changes to news codegen output --- go.mod | 9 ++++---- go.sum | 21 ++++++++++++------- .../grafana-cli/commands/cuetsify_command.go | 15 ++----------- public/app/plugins/panel/barchart/models.cue | 6 +++--- public/app/plugins/panel/bargauge/models.cue | 4 ++-- .../app/plugins/panel/candlestick/models.cue | 4 ++-- public/app/plugins/panel/canvas/models.cue | 2 +- public/app/plugins/panel/dashlist/models.cue | 4 ++-- public/app/plugins/panel/gauge/models.cue | 4 ++-- .../app/plugins/panel/heatmap-new/models.cue | 4 ++-- public/app/plugins/panel/histogram/models.cue | 4 ++-- public/app/plugins/panel/news/models.cue | 2 +- public/app/plugins/panel/news/models.gen.ts | 2 +- public/app/plugins/panel/stat/models.cue | 4 ++-- .../plugins/panel/state-timeline/models.cue | 6 +++--- .../plugins/panel/status-history/models.cue | 6 +++--- public/app/plugins/panel/table/models.cue | 4 ++-- public/app/plugins/panel/text/models.cue | 4 ++-- .../app/plugins/panel/timeseries/models.cue | 4 ++-- 19 files changed, 52 insertions(+), 57 deletions(-) diff --git a/go.mod b/go.mod index 5d7ab9c25e3..28e966b25e3 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ replace github.com/russellhaering/goxmldsig@v1.1.0 => github.com/russellhaering/ require ( cloud.google.com/go/storage v1.21.0 - cuelang.org/go v0.4.1 + cuelang.org/go v0.4.3 github.com/Azure/azure-sdk-for-go v59.3.0+incompatible github.com/Azure/go-autorest/autorest v0.11.22 github.com/BurntSushi/toml v0.3.1 @@ -50,7 +50,7 @@ require ( github.com/google/wire v0.5.0 github.com/gorilla/websocket v1.4.2 github.com/gosimple/slug v1.9.0 - github.com/grafana/cuetsy v0.0.0-20211119211437-8c25464cc9bf + github.com/grafana/cuetsy v0.0.1 github.com/grafana/grafana-aws-sdk v0.10.3 github.com/grafana/grafana-azure-sdk-go v1.1.0 github.com/grafana/grafana-plugin-sdk-go v0.134.0 @@ -107,7 +107,7 @@ require ( golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 - golang.org/x/tools v0.1.9 + golang.org/x/tools v0.1.10 gonum.org/v1/gonum v0.11.0 google.golang.org/api v0.74.0 google.golang.org/grpc v1.45.0 @@ -234,7 +234,7 @@ require ( go.uber.org/goleak v1.1.12 // indirect golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect golang.org/x/text v0.3.7 // indirect - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3 gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect @@ -290,6 +290,7 @@ require ( github.com/opencontainers/image-spec v1.0.2 // indirect github.com/pierrec/lz4/v4 v4.1.8 // indirect github.com/segmentio/asm v1.1.1 // indirect + github.com/xlab/treeprint v1.1.0 // indirect github.com/yudai/pp v2.0.1+incompatible // indirect go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.6.3 // indirect go.opentelemetry.io/proto/otlp v0.15.0 // indirect diff --git a/go.sum b/go.sum index f038edcc4fc..073095a20d6 100644 --- a/go.sum +++ b/go.sum @@ -88,9 +88,9 @@ contrib.go.opencensus.io/exporter/ocagent v0.6.0/go.mod h1:zmKjrJcdo0aYcVS7bmEeS contrib.go.opencensus.io/exporter/prometheus v0.3.0/go.mod h1:rpCPVQKhiyH8oomWgm34ZmgIdZa8OVYO5WAIygPbBBE= contrib.go.opencensus.io/exporter/stackdriver v0.13.10/go.mod h1:I5htMbyta491eUxufwwZPQdcKvvgzMB4O9ni41YnIM8= contrib.go.opencensus.io/integrations/ocsql v0.1.7/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE= -cuelang.org/go v0.4.0/go.mod h1:tz/edkPi+T37AZcb5GlPY+WJkL6KiDlDVupKwL3vvjs= -cuelang.org/go v0.4.1 h1:rxG2cyZzdAymrNICI0L/wagK2EitoR8Xsiiq2D7qkaQ= cuelang.org/go v0.4.1/go.mod h1:P09/R4UfAEzLkV9DXxwlxQnIZbkaT4uIhiEgs6Vsz2Q= +cuelang.org/go v0.4.3 h1:W3oBBjDTm7+IZfCKZAmC8uDG0eYfJL4Pp/xbbCMKaVo= +cuelang.org/go v0.4.3/go.mod h1:7805vR9H+VoBNdWFdI7jyDR3QLUPp4+naHfbcgp55HI= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= @@ -1411,8 +1411,8 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gosimple/slug v1.9.0 h1:r5vDcYrFz9BmfIAMC829un9hq7hKM4cHUrsv36LbEqs= github.com/gosimple/slug v1.9.0/go.mod h1:AMZ+sOVe65uByN3kgEyf9WEBKBCSS+dJjMX9x4vDJbg= -github.com/grafana/cuetsy v0.0.0-20211119211437-8c25464cc9bf h1:RY69RBqlr3yWNgpAqrSRiUvqa/Jw6uqVqiHNHLzifhU= -github.com/grafana/cuetsy v0.0.0-20211119211437-8c25464cc9bf/go.mod h1:H9Ei+Q808FCWyeEzpaW5GMfBvXCuFOfQa4x/vzKY+Fg= +github.com/grafana/cuetsy v0.0.1 h1:HEoYBiEb8zRq70kc0WSVG+bZ85W8nQ8BQr4l7f2kl0s= +github.com/grafana/cuetsy v0.0.1/go.mod h1:h8fQHb+IHbLjJ6UpaAuRtd8qV6D2JEm+j9rSk3G2Dik= github.com/grafana/dskit v0.0.0-20211011144203-3a88ec0b675f h1:FvvSVEbnGeM2bUivGmsiXTi8URJyBU7TcFEEoRe5wWI= github.com/grafana/dskit v0.0.0-20211011144203-3a88ec0b675f/go.mod h1:uPG2nyK4CtgNDmWv7qyzYcdI+S90kHHRWvHnBtEMBXM= github.com/grafana/go-mssqldb v0.0.0-20210326084033-d0ce3c521036 h1:GplhUk6Xes5JIhUUrggPcPBhOn+eT8+WsHiebvq7GgA= @@ -2383,8 +2383,9 @@ github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM= @@ -2511,6 +2512,7 @@ github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJ github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= +github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -2681,6 +2683,7 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= github.com/xlab/treeprint v1.0.0/go.mod h1:IoImgRak9i3zJyuxOKUP1v4UZd1tMoKkq/Cimt1uhCg= +github.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk= github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/xorcare/pointer v1.1.0 h1:sFwXOhRF8QZ0tyVZrtxWGIoVZNEmRzBCaFWdONPQIUM= github.com/xorcare/pointer v1.1.0/go.mod h1:6KLhkOh6YbuvZkT4YbxIbR/wzLBjyMxOiNzZhJTor2Y= @@ -2990,6 +2993,7 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -3449,15 +3453,17 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.9 h1:j9KsMiaP1c3B0OTQGth0/k+miLGTgLsAFUCrF2vLcF8= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wireguard v0.0.20200121/go.mod h1:P2HsVp8SKwZEufsnezXZA4GRX/T49/HlU7DGuelXsU4= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200205215550-e35592f146e4/go.mod h1:UdS9frhv65KTfwxME1xE8+rHYoFpbm36gOud1GhBe9c= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= @@ -3779,7 +3785,6 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200603094226-e3079894b1e8/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/cmd/grafana-cli/commands/cuetsify_command.go b/pkg/cmd/grafana-cli/commands/cuetsify_command.go index 8ce9853bb68..44d0d96a1fb 100644 --- a/pkg/cmd/grafana-cli/commands/cuetsify_command.go +++ b/pkg/cmd/grafana-cli/commands/cuetsify_command.go @@ -95,15 +95,6 @@ func (cmd Command) generateTypescript(c utils.CommandLine) error { Module: "github.com/grafana/grafana", } - // One-time load of the panel-plugin scuemata family def, for unifying to easily apply cuetsy attributes - clcfg.Dir = "cue/scuemata" - v := ctx.BuildInstance(cload.Instances(nil, clcfg)[0]) - if v.Err() != nil { - return v.Err() - } - ppf := v.LookupPath(cue.ParsePath("#PanelSchema")) - _ = ppf - // FIXME hardcoding paths to exclude is not the way to handle this excl := map[string]bool{ "cue.mod": true, @@ -172,7 +163,6 @@ func (cmd Command) generateTypescript(c utils.CommandLine) error { } bi := insts[0] - // dumpBuildInst(bi) v := ctx.BuildInstance(bi) if v.Err() != nil { return v.Err() @@ -202,7 +192,7 @@ func (cmd Command) generateTypescript(c utils.CommandLine) error { } // val := v.LookupPath(cue.ParsePath("Panel.lineages[0][0]")) - // Extract the latest schema and its version number + // Extract the latest schema and its version number. (All of this goes away with Thema, whew) f.V = &tsModver{} lins := v.LookupPath(cue.ParsePath("Panel.lineages")) f.V.Lin, _ = lins.Len().Int64() @@ -212,8 +202,7 @@ func (cmd Command) generateTypescript(c utils.CommandLine) error { f.V.Sch = f.V.Sch - 1 latest := schs.LookupPath(cue.MakePath(cue.Index(int(f.V.Sch)))) - sch := latest.UnifyAccept(ppf, latest) - b, err = cuetsy.Generate(sch, cuetsy.Config{}) + b, err = cuetsy.Generate(latest, cuetsy.Config{}) default: b, err = cuetsy.Generate(v, cuetsy.Config{}) } diff --git a/public/app/plugins/panel/barchart/models.cue b/public/app/plugins/panel/barchart/models.cue index c39a85e0ce9..6c67992ae11 100644 --- a/public/app/plugins/panel/barchart/models.cue +++ b/public/app/plugins/panel/barchart/models.cue @@ -32,16 +32,16 @@ Panel: { showValue: ui.VisibilityMode barWidth: number groupWidth: number - } + } @cuetsy(kind="interface") PanelFieldConfig: { ui.AxisConfig ui.HideableFieldConfig lineWidth?: number fillOpacity?: number gradientMode?: ui.GraphGradientMode - } + } @cuetsy(kind="interface") } ] ] migrations: [] -} \ No newline at end of file +} diff --git a/public/app/plugins/panel/bargauge/models.cue b/public/app/plugins/panel/bargauge/models.cue index 52e7d0ddc10..30a7a4515f7 100644 --- a/public/app/plugins/panel/bargauge/models.cue +++ b/public/app/plugins/panel/bargauge/models.cue @@ -24,9 +24,9 @@ Panel: { ui.SingleStatBaseOptions displayMode: ui.BarGaugeDisplayMode showUnfilled: bool - } + } @cuetsy(kind="interface") } ] ] migrations: [] -} \ No newline at end of file +} diff --git a/public/app/plugins/panel/candlestick/models.cue b/public/app/plugins/panel/candlestick/models.cue index 1bb9205ed9a..38d0a0ee6e6 100644 --- a/public/app/plugins/panel/candlestick/models.cue +++ b/public/app/plugins/panel/candlestick/models.cue @@ -21,11 +21,11 @@ Panel: { PanelOptions: { // anything for now ... - } + } @cuetsy(kind="interface") PanelFieldConfig: { // anything for now ... - } + } @cuetsy(kind="interface") } ] ] diff --git a/public/app/plugins/panel/canvas/models.cue b/public/app/plugins/panel/canvas/models.cue index 2bade18a534..23ed67c7635 100644 --- a/public/app/plugins/panel/canvas/models.cue +++ b/public/app/plugins/panel/canvas/models.cue @@ -21,7 +21,7 @@ Panel: { PanelOptions: { // anything for now ... - } + } @cuetsy(kind="interface") } ] ] diff --git a/public/app/plugins/panel/dashlist/models.cue b/public/app/plugins/panel/dashlist/models.cue index 153f609e9c9..b13be7c61d0 100644 --- a/public/app/plugins/panel/dashlist/models.cue +++ b/public/app/plugins/panel/dashlist/models.cue @@ -28,8 +28,8 @@ Panel: { query: string | *"" folderId?: int tags: [...string] | *[] - }, - } + } @cuetsy(kind="interface") + }, ] ] migrations: [] diff --git a/public/app/plugins/panel/gauge/models.cue b/public/app/plugins/panel/gauge/models.cue index 640363a4aea..b180c966220 100644 --- a/public/app/plugins/panel/gauge/models.cue +++ b/public/app/plugins/panel/gauge/models.cue @@ -24,9 +24,9 @@ Panel: { ui.SingleStatBaseOptions showThresholdLabels: bool showThresholdMarkers: bool - } + } @cuetsy(kind="interface") } ] ] migrations: [] -} \ No newline at end of file +} diff --git a/public/app/plugins/panel/heatmap-new/models.cue b/public/app/plugins/panel/heatmap-new/models.cue index 1bb9205ed9a..38d0a0ee6e6 100644 --- a/public/app/plugins/panel/heatmap-new/models.cue +++ b/public/app/plugins/panel/heatmap-new/models.cue @@ -21,11 +21,11 @@ Panel: { PanelOptions: { // anything for now ... - } + } @cuetsy(kind="interface") PanelFieldConfig: { // anything for now ... - } + } @cuetsy(kind="interface") } ] ] diff --git a/public/app/plugins/panel/histogram/models.cue b/public/app/plugins/panel/histogram/models.cue index 3dd35689bf5..e85440f0edd 100644 --- a/public/app/plugins/panel/histogram/models.cue +++ b/public/app/plugins/panel/histogram/models.cue @@ -26,9 +26,9 @@ Panel: { bucketSize?: int bucketOffset: int | *0 combine?: bool - } + } @cuetsy(kind="interface") - PanelFieldConfig: ui.GraphFieldConfig + PanelFieldConfig: ui.GraphFieldConfig & {} @cuetsy(kind="interface") } ] ] diff --git a/public/app/plugins/panel/news/models.cue b/public/app/plugins/panel/news/models.cue index b4e632e9c66..7ab474371a3 100644 --- a/public/app/plugins/panel/news/models.cue +++ b/public/app/plugins/panel/news/models.cue @@ -22,7 +22,7 @@ Panel: { // empty/missing will default to grafana blog feedUrl?: string showImage?: bool | *true - } + } @cuetsy(kind="interface") } ] ] diff --git a/public/app/plugins/panel/news/models.gen.ts b/public/app/plugins/panel/news/models.gen.ts index fa953457c4a..6351ddcbc67 100644 --- a/public/app/plugins/panel/news/models.gen.ts +++ b/public/app/plugins/panel/news/models.gen.ts @@ -11,6 +11,6 @@ export interface PanelOptions { showImage?: boolean; } -export const defaultPanelOptions: PanelOptions = { +export const defaultPanelOptions: Partial = { showImage: true, }; diff --git a/public/app/plugins/panel/stat/models.cue b/public/app/plugins/panel/stat/models.cue index e5a13591d2a..37da0bf3fa2 100644 --- a/public/app/plugins/panel/stat/models.cue +++ b/public/app/plugins/panel/stat/models.cue @@ -26,9 +26,9 @@ Panel: { colorMode: ui.BigValueColorMode justifyMode: ui.BigValueJustifyMode textMode: ui.BigValueTextMode - } + } @cuetsy(kind="interface") } ] ] migrations: [] -} \ No newline at end of file +} diff --git a/public/app/plugins/panel/state-timeline/models.cue b/public/app/plugins/panel/state-timeline/models.cue index bcdcc190e84..e32a9b7e9de 100644 --- a/public/app/plugins/panel/state-timeline/models.cue +++ b/public/app/plugins/panel/state-timeline/models.cue @@ -34,14 +34,14 @@ Panel: { colWidth?: number mergeValues?: bool | *true alignValue?: TimelineValueAlignment | *"left" - } + } @cuetsy(kind="interface") PanelFieldConfig: { ui.HideableFieldConfig lineWidth?: number | *0 fillOpacity?: number | *70 - } + } @cuetsy(kind="interface") } ] ] migrations: [] -} \ No newline at end of file +} diff --git a/public/app/plugins/panel/status-history/models.cue b/public/app/plugins/panel/status-history/models.cue index 96250734e6a..69f760ef4fd 100644 --- a/public/app/plugins/panel/status-history/models.cue +++ b/public/app/plugins/panel/status-history/models.cue @@ -29,14 +29,14 @@ Panel: { rowHeight: number colWidth?: number alignValue: "center" | *"left" | "right" - } + } @cuetsy(kind="interface") PanelFieldConfig: { ui.HideableFieldConfig lineWidth?: number | *1 fillOpacity?: number | *70 - } + } @cuetsy(kind="interface") } ] ] migrations: [] -} \ No newline at end of file +} diff --git a/public/app/plugins/panel/table/models.cue b/public/app/plugins/panel/table/models.cue index 5d4b481ec8a..40fab037763 100644 --- a/public/app/plugins/panel/table/models.cue +++ b/public/app/plugins/panel/table/models.cue @@ -27,8 +27,8 @@ Panel: { showHeader: bool | *true showTypeIcons: bool | *false sortBy?: [...ui.TableSortByFieldState] - } - PanelFieldConfig: ui.TableFieldOptions + } @cuetsy(kind="interface") + PanelFieldConfig: ui.TableFieldOptions & {} @cuetsy(kind="interface") }, ] ] diff --git a/public/app/plugins/panel/text/models.cue b/public/app/plugins/panel/text/models.cue index ea5c77aef8e..a4a5a3a114d 100644 --- a/public/app/plugins/panel/text/models.cue +++ b/public/app/plugins/panel/text/models.cue @@ -20,13 +20,13 @@ Panel: { { TextMode: "html" | "markdown" @cuetsy(kind="enum",memberNames="HTML|Markdown") PanelOptions: { - mode: TextMode | *"markdown" + mode: TextMode | *"markdown" content: string | *""" # Title For markdown syntax help: [commonmark.org/help](https://commonmark.org/help/) """ - } + } @cuetsy(kind="interface") } ] ] diff --git a/public/app/plugins/panel/timeseries/models.cue b/public/app/plugins/panel/timeseries/models.cue index 7e1148bb69c..5560ae3f71a 100644 --- a/public/app/plugins/panel/timeseries/models.cue +++ b/public/app/plugins/panel/timeseries/models.cue @@ -25,8 +25,8 @@ Panel: { PanelOptions: { legend: ui.VizLegendOptions tooltip: ui.VizTooltipOptions - } - PanelFieldConfig: ui.GraphFieldConfig + } @cuetsy(kind="interface") + PanelFieldConfig: ui.GraphFieldConfig & {} @cuetsy(kind="interface") } ] ] From 10db6182046964cda9eb1be1f1b627e636e90c48 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Thu, 5 May 2022 17:07:27 +0100 Subject: [PATCH 075/440] FileUpload: associate the label with the input (#48766) * FileUpload: associate the label with the input * generate a unique id and set the correct role * add a test to prevent regressions --- .../components/FileUpload/FileUpload.test.tsx | 16 +++++++++++++++- .../src/components/FileUpload/FileUpload.tsx | 6 ++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/grafana-ui/src/components/FileUpload/FileUpload.test.tsx b/packages/grafana-ui/src/components/FileUpload/FileUpload.test.tsx index 01f1d9c0d78..ac4dff166f4 100644 --- a/packages/grafana-ui/src/components/FileUpload/FileUpload.test.tsx +++ b/packages/grafana-ui/src/components/FileUpload/FileUpload.test.tsx @@ -1,4 +1,5 @@ import { render, waitFor, fireEvent, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import React from 'react'; import { selectors } from '@grafana/e2e-selectors'; @@ -8,10 +9,23 @@ import { FileUpload } from './FileUpload'; describe('FileUpload', () => { it('should render upload button with default text and no file name', () => { render( {}} />); - expect(screen.getByText('Upload file')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Upload file' })).toBeInTheDocument(); expect(screen.queryByLabelText('File name')).toBeNull(); }); + it('clicking the button should trigger the input', async () => { + const mockInputOnClick = jest.fn(); + const { getByTestId } = render( {}} />); + const button = screen.getByRole('button', { name: 'Upload file' }); + const input = getByTestId(selectors.components.FileUpload.inputField); + + // attach a click listener to the input + input.onclick = mockInputOnClick; + + await userEvent.click(button); + expect(mockInputOnClick).toHaveBeenCalled(); + }); + it('should display uploaded file name', async () => { const testFileName = 'grafana.png'; const file = new File(['(⌐□_□)'], testFileName, { type: 'image/png' }); diff --git a/packages/grafana-ui/src/components/FileUpload/FileUpload.tsx b/packages/grafana-ui/src/components/FileUpload/FileUpload.tsx index 96a35fc5c00..55f46a064b5 100644 --- a/packages/grafana-ui/src/components/FileUpload/FileUpload.tsx +++ b/packages/grafana-ui/src/components/FileUpload/FileUpload.tsx @@ -1,5 +1,6 @@ import { css, cx } from '@emotion/css'; import React, { FC, FormEvent, useCallback, useState } from 'react'; +import { v4 as uuidv4 } from 'uuid'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; @@ -31,6 +32,7 @@ export const FileUpload: FC = ({ }) => { const style = useStyles2(getStyles(size)); const [fileName, setFileName] = useState(''); + const id = uuidv4(); const onChange = useCallback( (event: FormEvent) => { @@ -47,14 +49,14 @@ export const FileUpload: FC = ({ <> -
    - {link ? ( - + {links?.logLinks?.[0] ? ( + ) : null}
    diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanLinks.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanLinks.tsx new file mode 100644 index 00000000000..27fbf1cc3fd --- /dev/null +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanLinks.tsx @@ -0,0 +1,121 @@ +import { css } from '@emotion/css'; +import React, { useState } from 'react'; + +import { useStyles2, MenuGroup, MenuItem, Icon, ContextMenu } from '@grafana/ui'; + +import { SpanLinks } from '../types/links'; + +interface SpanLinksProps { + links: SpanLinks; +} + +const renderMenuItems = (links: SpanLinks, styles: ReturnType, closeMenu: () => void) => { + return ( + <> + {!!links.logLinks?.length ? ( + + {links.logLinks.map((link, i) => ( + { + if (link.onClick) { + link.onClick(e); + } + closeMenu(); + }} + url={link.href} + className={styles.menuItem} + /> + ))} + + ) : null} + {!!links.metricLinks?.length ? ( + + {links.metricLinks.map((link, i) => ( + { + if (link.onClick) { + link.onClick(e); + } + closeMenu(); + }} + url={link.href} + className={styles.menuItem} + /> + ))} + + ) : null} + {!!links.traceLinks?.length ? ( + + {links.traceLinks.map((link, i) => ( + { + if (link.onClick) { + link.onClick(e); + } + closeMenu(); + }} + url={link.href} + className={styles.menuItem} + /> + ))} + + ) : null} + + ); +}; + +export const SpanLinksMenu = ({ links }: SpanLinksProps) => { + const styles = useStyles2(getStyles); + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 }); + + const closeMenu = () => setIsMenuOpen(false); + + return ( + <> + + + {isMenuOpen ? ( + setIsMenuOpen(false)} + renderMenuItems={() => renderMenuItems(links, styles, closeMenu)} + focusOnOpen={true} + x={menuPosition.x} + y={menuPosition.y} + /> + ) : null} + + ); +}; + +const getStyles = () => { + return { + button: css` + background: transparent; + border: none; + padding: 0; + margin: 0 3px 0 0; + `, + menuItem: css` + max-width: 60ch; + overflow: hidden; + `, + }; +}; diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx index bf3411015f8..bb71a941b29 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx @@ -387,7 +387,6 @@ export class UnthemedVirtualizedTraceView extends React.Component void; content: React.ReactNode; + title?: string; }; -export type SpanLinkFunc = (span: TraceSpan) => SpanLinkDef | undefined; +export type SpanLinks = { + logLinks?: SpanLinkDef[]; + traceLinks?: SpanLinkDef[]; + metricLinks?: SpanLinkDef[]; +}; + +export type SpanLinkFunc = (span: TraceSpan) => SpanLinks | undefined; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 5080805e73a..bc60b9b5ca9 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -248,5 +248,11 @@ var ( RequiresDevMode: true, FrontendOnly: true, }, + { + Name: "traceToMetrics", + Description: "Enable trace to metrics links", + State: FeatureStateAlpha, + FrontendOnly: true, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index ded1ce2e065..8d21cf68d13 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -182,4 +182,8 @@ const ( // FlagAzureMonitorExperimentalUI // Use grafana-experimental UI in Azure Monitor FlagAzureMonitorExperimentalUI = "azureMonitorExperimentalUI" + + // FlagTraceToMetrics + // Enable trace to metrics links + FlagTraceToMetrics = "traceToMetrics" ) diff --git a/public/app/core/components/TraceToLogs/TraceToLogsSettings.tsx b/public/app/core/components/TraceToLogs/TraceToLogsSettings.tsx index bfafdfee442..0dd4f62c50a 100644 --- a/public/app/core/components/TraceToLogs/TraceToLogsSettings.tsx +++ b/public/app/core/components/TraceToLogs/TraceToLogsSettings.tsx @@ -40,7 +40,7 @@ export function TraceToLogsSettings({ options, onOptionsChange }: Props) {

    Trace to logs

    - Trace to logs lets you navigate from a trace span to the selected data source's log. + Trace to logs lets you navigate from a trace span to the selected data source's logs.
    diff --git a/public/app/core/components/TraceToMetrics/TraceToMetricsSettings.tsx b/public/app/core/components/TraceToMetrics/TraceToMetricsSettings.tsx new file mode 100644 index 00000000000..fc450315580 --- /dev/null +++ b/public/app/core/components/TraceToMetrics/TraceToMetricsSettings.tsx @@ -0,0 +1,80 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { + DataSourceJsonData, + DataSourcePluginOptionsEditorProps, + GrafanaTheme, + updateDatasourcePluginJsonDataOption, +} from '@grafana/data'; +import { DataSourcePicker } from '@grafana/runtime'; +import { Button, InlineField, InlineFieldRow, useStyles } from '@grafana/ui'; + +export interface TraceToMetricsOptions { + datasourceUid?: string; +} + +export interface TraceToMetricsData extends DataSourceJsonData { + tracesToMetrics?: TraceToMetricsOptions; +} + +interface Props extends DataSourcePluginOptionsEditorProps {} + +export function TraceToMetricsSettings({ options, onOptionsChange }: Props) { + const styles = useStyles(getStyles); + + return ( +
    +

    Trace to metrics

    + +
    + Trace to metrics lets you navigate from a trace span to the selected data source. +
    + + + + + updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToMetrics', { + ...options.jsonData.tracesToMetrics, + datasourceUid: ds.uid, + }) + } + /> + + {options.jsonData.tracesToMetrics?.datasourceUid ? ( + + ) : null} + +
    + ); +} + +const getStyles = (theme: GrafanaTheme) => ({ + infoText: css` + padding-bottom: ${theme.spacing.md}; + color: ${theme.colors.textSemiWeak}; + `, + row: css` + label: row; + align-items: baseline; + `, +}); diff --git a/public/app/features/explore/TraceView/TraceView.tsx b/public/app/features/explore/TraceView/TraceView.tsx index ae36bb1b8d9..c0e09e3356f 100644 --- a/public/app/features/explore/TraceView/TraceView.tsx +++ b/public/app/features/explore/TraceView/TraceView.tsx @@ -20,6 +20,7 @@ import { getTemplateSrv } from '@grafana/runtime'; import { useStyles2 } from '@grafana/ui'; import { Trace, TracePageHeader, TraceTimelineViewer, TTraceTimeline } from '@jaegertracing/jaeger-ui-components'; import { TraceToLogsData } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; +import { TraceToMetricsData } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { getTimeZone } from 'app/features/profile/state/selectors'; import { StoreState } from 'app/types'; @@ -117,12 +118,20 @@ export function TraceView(props: Props) { [childrenHiddenIDs, detailStates, hoverIndentGuideIds, spanNameColumnWidth, props.traceProp?.traceID] ); - const traceToLogsOptions = (getDatasourceSrv().getInstanceSettings(datasource?.name)?.jsonData as TraceToLogsData) - ?.tracesToLogs; + const instanceSettings = getDatasourceSrv().getInstanceSettings(datasource?.name); + const traceToLogsOptions = (instanceSettings?.jsonData as TraceToLogsData)?.tracesToLogs; + const traceToMetricsOptions = (instanceSettings?.jsonData as TraceToMetricsData)?.tracesToMetrics; + const createSpanLink = useMemo( () => - createSpanLinkFactory({ splitOpenFn: props.splitOpenFn!, traceToLogsOptions, dataFrame: props.dataFrames[0] }), - [props.splitOpenFn, traceToLogsOptions, props.dataFrames] + createSpanLinkFactory({ + splitOpenFn: props.splitOpenFn!, + traceToLogsOptions, + traceToMetricsOptions, + dataFrame: props.dataFrames[0], + createFocusSpanLink, + }), + [props.splitOpenFn, traceToLogsOptions, traceToMetricsOptions, props.dataFrames, createFocusSpanLink] ); const onSlimViewClicked = useCallback(() => setSlim(!slim), [slim]); const timeZone = useSelector((state: StoreState) => getTimeZone(state.user)); diff --git a/public/app/features/explore/TraceView/createSpanLink.test.ts b/public/app/features/explore/TraceView/createSpanLink.test.ts index e88ce728b73..89d2e05c16a 100644 --- a/public/app/features/explore/TraceView/createSpanLink.test.ts +++ b/public/app/features/explore/TraceView/createSpanLink.test.ts @@ -1,6 +1,7 @@ import { DataSourceInstanceSettings, MutableDataFrame } from '@grafana/data'; import { setDataSourceSrv, setTemplateSrv } from '@grafana/runtime'; import { TraceSpan } from '@jaegertracing/jaeger-ui-components'; +import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; import { TraceToLogsOptions } from '../../../core/components/TraceToLogs/TraceToLogsSettings'; import { LinkSrv, setLinkSrv } from '../../panel/panellinks/link_srv'; @@ -9,10 +10,13 @@ import { TemplateSrv } from '../../templating/template_srv'; import { createSpanLinkFactory } from './createSpanLink'; describe('createSpanLinkFactory', () => { - it('returns undefined if there is no data source uid', () => { + it('returns no links if there is no data source uid', () => { const splitOpenFn = jest.fn(); const createLink = createSpanLinkFactory({ splitOpenFn: splitOpenFn }); - expect(createLink).not.toBeDefined(); + const links = createLink!(createTraceSpan()); + expect(links?.logLinks).toBeUndefined(); + expect(links?.metricLinks).toBeUndefined(); + expect(links?.traceLinks).toHaveLength(0); }); describe('should return loki link', () => { @@ -30,7 +34,9 @@ describe('createSpanLinkFactory', () => { it('with default keys when tags not configured', () => { const createLink = setupSpanLinkFactory(); expect(createLink).toBeDefined(); - const linkDef = createLink!(createTraceSpan()); + const links = createLink!(createTraceSpan()); + const linkDef = links?.logLinks?.[0]; + expect(linkDef).toBeDefined(); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{cluster=\\"cluster1\\", hostname=\\"hostname1\\"}","refId":""}],"panelsState":{}}' @@ -43,7 +49,7 @@ describe('createSpanLinkFactory', () => { tags: ['ip', 'newTag'], }); expect(createLink).toBeDefined(); - const linkDef = createLink!( + const links = createLink!( createTraceSpan({ process: { serviceName: 'service', @@ -54,6 +60,8 @@ describe('createSpanLinkFactory', () => { }, }) ); + const linkDef = links?.logLinks?.[0]; + expect(linkDef).toBeDefined(); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{ip=\\"192.168.0.1\\"}","refId":""}],"panelsState":{}}' @@ -66,7 +74,7 @@ describe('createSpanLinkFactory', () => { tags: ['ip', 'host'], }); expect(createLink).toBeDefined(); - const linkDef = createLink!( + const links = createLink!( createTraceSpan({ process: { serviceName: 'service', @@ -77,6 +85,8 @@ describe('createSpanLinkFactory', () => { }, }) ); + const linkDef = links?.logLinks?.[0]; + expect(linkDef).toBeDefined(); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{ip=\\"192.168.0.1\\", host=\\"host\\"}","refId":""}],"panelsState":{}}' @@ -90,7 +100,7 @@ describe('createSpanLinkFactory', () => { spanEndTimeShift: '1m', }); expect(createLink).toBeDefined(); - const linkDef = createLink!( + const links = createLink!( createTraceSpan({ process: { serviceName: 'service', @@ -101,6 +111,8 @@ describe('createSpanLinkFactory', () => { }, }) ); + const linkDef = links?.logLinks?.[0]; + expect(linkDef).toBeDefined(); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T01:01:00.000Z","to":"2020-10-14T01:01:01.000Z"},"datasource":"loki1","queries":[{"expr":"{hostname=\\"hostname1\\"}","refId":""}],"panelsState":{}}' @@ -114,8 +126,10 @@ describe('createSpanLinkFactory', () => { filterByTraceID: true, }); expect(createLink).toBeDefined(); - const linkDef = createLink!(createTraceSpan()); + const links = createLink!(createTraceSpan()); + const linkDef = links?.logLinks?.[0]; + expect(linkDef).toBeDefined(); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{cluster=\\"cluster1\\", hostname=\\"hostname1\\"} |=\\"7946b05c2e2e4e5a\\" |=\\"6605c7b08e715d6c\\"","refId":""}],"panelsState":{}}' @@ -139,8 +153,10 @@ describe('createSpanLinkFactory', () => { }), }); expect(createLink).toBeDefined(); - const linkDef = createLink!(createTraceSpan()); + const links = createLink!(createTraceSpan()); + const linkDef = links?.logLinks?.[0]; + expect(linkDef).toBeDefined(); expect(linkDef!.href).toBe('testSpanId'); }); @@ -153,7 +169,7 @@ describe('createSpanLinkFactory', () => { ], }); expect(createLink).toBeDefined(); - const linkDef = createLink!( + const links = createLink!( createTraceSpan({ process: { serviceName: 'service', @@ -164,6 +180,9 @@ describe('createSpanLinkFactory', () => { }, }) ); + + const linkDef = links?.logLinks?.[0]; + expect(linkDef).toBeDefined(); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{service=\\"serviceName\\", pod=\\"podName\\"}","refId":""}],"panelsState":{}}' @@ -180,7 +199,7 @@ describe('createSpanLinkFactory', () => { ], }); expect(createLink).toBeDefined(); - const linkDef = createLink!( + const links = createLink!( createTraceSpan({ process: { serviceName: 'service', @@ -191,6 +210,9 @@ describe('createSpanLinkFactory', () => { }, }) ); + + const linkDef = links?.logLinks?.[0]; + expect(linkDef).toBeDefined(); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{service.name=\\"serviceName\\", pod=\\"podName\\"}","refId":""}],"panelsState":{}}' @@ -203,7 +225,7 @@ describe('createSpanLinkFactory', () => { tags: [], }); expect(createLink).toBeDefined(); - const linkDef = createLink!( + const links = createLink!( createTraceSpan({ process: { serviceName: 'service', @@ -214,7 +236,7 @@ describe('createSpanLinkFactory', () => { }, }) ); - expect(linkDef).toBeUndefined(); + expect(links?.logLinks).toBeUndefined(); }); }); @@ -236,7 +258,10 @@ describe('createSpanLinkFactory', () => { const createLink = setupSpanLinkFactory({ datasourceUid: splunkUID, }); - const linkDef = createLink!(createTraceSpan()); + const links = createLink!(createTraceSpan()); + + const linkDef = links?.logLinks?.[0]; + expect(linkDef).toBeDefined(); expect(linkDef!.href).toContain(`${encodeURIComponent('datasource":"Splunk 8","queries":[{"query"')}`); expect(linkDef!.href).not.toContain(`${encodeURIComponent('datasource":"Splunk 8","queries":[{"expr"')}`); }); @@ -245,7 +270,10 @@ describe('createSpanLinkFactory', () => { const createLink = setupSpanLinkFactory({ datasourceUid: splunkUID, }); - const linkDef = createLink!(createTraceSpan()); + const links = createLink!(createTraceSpan()); + + const linkDef = links?.logLinks?.[0]; + expect(linkDef).toBeDefined(); expect(linkDef!.href).toContain( `${encodeURIComponent('{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"}')}` ); @@ -262,8 +290,10 @@ describe('createSpanLinkFactory', () => { }); expect(createLink).toBeDefined(); - const linkDef = createLink!(createTraceSpan()); + const links = createLink!(createTraceSpan()); + const linkDef = links?.logLinks?.[0]; + expect(linkDef).toBeDefined(); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"Splunk 8","queries":[{"query":"cluster=\\"cluster1\\" hostname=\\"hostname1\\" \\"7946b05c2e2e4e5a\\" \\"6605c7b08e715d6c\\"","refId":""}],"panelsState":{}}' @@ -276,7 +306,7 @@ describe('createSpanLinkFactory', () => { tags: ['ip'], }); expect(createLink).toBeDefined(); - const linkDef = createLink!( + const links = createLink!( createTraceSpan({ process: { serviceName: 'service', @@ -285,6 +315,8 @@ describe('createSpanLinkFactory', () => { }) ); + const linkDef = links?.logLinks?.[0]; + expect(linkDef).toBeDefined(); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"Splunk 8","queries":[{"query":"ip=\\"192.168.0.1\\"","refId":""}],"panelsState":{}}' @@ -297,7 +329,7 @@ describe('createSpanLinkFactory', () => { tags: ['ip', 'hostname'], }); expect(createLink).toBeDefined(); - const linkDef = createLink!( + const links = createLink!( createTraceSpan({ process: { serviceName: 'service', @@ -309,6 +341,8 @@ describe('createSpanLinkFactory', () => { }) ); + const linkDef = links?.logLinks?.[0]; + expect(linkDef).toBeDefined(); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"Splunk 8","queries":[{"query":"hostname=\\"hostname1\\" ip=\\"192.168.0.1\\"","refId":""}],"panelsState":{}}' @@ -325,7 +359,7 @@ describe('createSpanLinkFactory', () => { ], }); expect(createLink).toBeDefined(); - const linkDef = createLink!( + const links = createLink!( createTraceSpan({ process: { serviceName: 'service', @@ -336,6 +370,9 @@ describe('createSpanLinkFactory', () => { }, }) ); + + const linkDef = links?.logLinks?.[0]; + expect(linkDef).toBeDefined(); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"Splunk 8","queries":[{"query":"service=\\"serviceName\\" pod=\\"podName\\"","refId":""}],"panelsState":{}}' @@ -343,6 +380,94 @@ describe('createSpanLinkFactory', () => { ); }); }); + + describe('should return metric link', () => { + beforeAll(() => { + setDataSourceSrv({ + getInstanceSettings(uid: string): DataSourceInstanceSettings | undefined { + return { uid: 'prom1', name: 'prom1', type: 'prometheus' } as any; + }, + } as any); + + setLinkSrv(new LinkSrv()); + setTemplateSrv(new TemplateSrv()); + }); + + it('returns query with span', () => { + const splitOpenFn = jest.fn(); + const createLink = createSpanLinkFactory({ + splitOpenFn, + traceToMetricsOptions: { + datasourceUid: 'prom1', + }, + }); + expect(createLink).toBeDefined(); + + const links = createLink!(createTraceSpan()); + const linkDef = links?.metricLinks?.[0]; + + expect(linkDef).toBeDefined(); + expect(linkDef!.href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"prom1","queries":[{"expr":"histogram_quantile(0.5, sum(rate(tempo_spanmetrics_latency_bucket{operation=\\"operation\\"}[5m])) by (le))","refId":""}],"panelsState":{}}' + )}` + ); + }); + }); + + describe('should return span links', () => { + beforeAll(() => { + setDataSourceSrv(new DatasourceSrv()); + setLinkSrv(new LinkSrv()); + setTemplateSrv(new TemplateSrv()); + }); + + it('ignores parent span link', () => { + const createLink = setupSpanLinkFactory(); + expect(createLink).toBeDefined(); + const links = createLink!( + createTraceSpan({ references: [{ refType: 'CHILD_OF', spanID: 'parent', traceID: 'traceID' }] }) + ); + + const traceLinks = links?.traceLinks; + expect(traceLinks).toBeDefined(); + expect(traceLinks).toHaveLength(0); + }); + + it('returns links for references and subsidiarilyReferencedBy references', () => { + const createLink = setupSpanLinkFactory(); + expect(createLink).toBeDefined(); + const links = createLink!( + createTraceSpan({ + references: [ + { + refType: 'FOLLOWS_FROM', + spanID: 'span1', + traceID: 'traceID', + span: { operationName: 'SpanName' } as any, + }, + ], + subsidiarilyReferencedBy: [{ refType: 'FOLLOWS_FROM', spanID: 'span3', traceID: 'traceID2' }], + }) + ); + + const traceLinks = links?.traceLinks; + expect(traceLinks).toBeDefined(); + expect(traceLinks).toHaveLength(2); + expect(traceLinks![0]).toEqual( + expect.objectContaining({ + href: 'traceID-span1', + title: 'SpanName', + }) + ); + expect(traceLinks![1]).toEqual( + expect.objectContaining({ + href: 'traceID2-span3', + title: 'View linked span', + }) + ); + }); + }); }); function setupSpanLinkFactory(options: Partial = {}, datasourceUid = 'lokiUid') { @@ -353,6 +478,11 @@ function setupSpanLinkFactory(options: Partial = {}, datasou datasourceUid, ...options, }, + createFocusSpanLink: (traceId, spanId) => { + return { + href: `${traceId}-${spanId}`, + } as any; + }, }); } diff --git a/public/app/features/explore/TraceView/createSpanLink.tsx b/public/app/features/explore/TraceView/createSpanLink.tsx index a036eeb05d8..9eb6e5f075a 100644 --- a/public/app/features/explore/TraceView/createSpanLink.tsx +++ b/public/app/features/explore/TraceView/createSpanLink.tsx @@ -1,3 +1,4 @@ +import { SpanLinks } from '@jaegertracing/jaeger-ui-components/src/types/links'; import React from 'react'; import { @@ -5,6 +6,7 @@ import { DataLink, DataQuery, DataSourceInstanceSettings, + DataSourceJsonData, dateTime, Field, KeyValue, @@ -16,9 +18,11 @@ import { } from '@grafana/data'; import { getTemplateSrv } from '@grafana/runtime'; import { Icon } from '@grafana/ui'; -import { SpanLinkDef, SpanLinkFunc, TraceSpan } from '@jaegertracing/jaeger-ui-components'; +import { SpanLinkFunc, TraceSpan } from '@jaegertracing/jaeger-ui-components'; import { TraceToLogsOptions } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; +import { TraceToMetricsOptions } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; +import { PromQuery } from 'app/plugins/datasource/prometheus/types'; import { LokiQuery } from '../../../plugins/datasource/loki/types'; import { getFieldLinksForExplore } from '../utils/links'; @@ -31,18 +35,22 @@ import { getFieldLinksForExplore } from '../utils/links'; export function createSpanLinkFactory({ splitOpenFn, traceToLogsOptions, + traceToMetricsOptions, dataFrame, + createFocusSpanLink, }: { splitOpenFn: SplitOpen; traceToLogsOptions?: TraceToLogsOptions; + traceToMetricsOptions?: TraceToMetricsOptions; dataFrame?: DataFrame; + createFocusSpanLink?: (traceId: string, spanId: string) => LinkModel; }): SpanLinkFunc | undefined { if (!dataFrame || dataFrame.fields.length === 1 || !dataFrame.fields.some((f) => Boolean(f.config.links?.length))) { // if the dataframe contains just a single blob of data (legacy format) or does not have any links configured, // let's try to use the old legacy path. - return legacyCreateSpanLinkFactory(splitOpenFn, traceToLogsOptions); + return legacyCreateSpanLinkFactory(splitOpenFn, traceToLogsOptions, traceToMetricsOptions, createFocusSpanLink); } else { - return function SpanLink(span: TraceSpan): SpanLinkDef | undefined { + return function SpanLink(span: TraceSpan): SpanLinks | undefined { // We should be here only if there are some links in the dataframe const field = dataFrame.fields.find((f) => Boolean(f.config.links?.length))!; try { @@ -55,9 +63,13 @@ export function createSpanLinkFactory({ }); return { - href: links[0].href, - onClick: links[0].onClick, - content: , + logLinks: [ + { + href: links[0].href, + onClick: links[0].onClick, + content: , + }, + ], }; } catch (error) { // It's fairly easy to crash here for example if data source defines wrong interpolation in the data link @@ -68,65 +80,144 @@ export function createSpanLinkFactory({ } } -function legacyCreateSpanLinkFactory(splitOpenFn: SplitOpen, traceToLogsOptions?: TraceToLogsOptions) { - // We should return if dataSourceUid is undefined otherwise getInstanceSettings would return testDataSource. - if (!traceToLogsOptions?.datasourceUid) { - return undefined; +function legacyCreateSpanLinkFactory( + splitOpenFn: SplitOpen, + traceToLogsOptions?: TraceToLogsOptions, + traceToMetricsOptions?: TraceToMetricsOptions, + createFocusSpanLink?: (traceId: string, spanId: string) => LinkModel +) { + let logsDataSourceSettings: DataSourceInstanceSettings | undefined; + const isSplunkDS = logsDataSourceSettings?.type === 'grafana-splunk-datasource'; + if (traceToLogsOptions?.datasourceUid) { + logsDataSourceSettings = getDatasourceSrv().getInstanceSettings(traceToLogsOptions.datasourceUid); } - const dataSourceSettings = getDatasourceSrv().getInstanceSettings(traceToLogsOptions.datasourceUid); - const isSplunkDS = dataSourceSettings?.type === 'grafana-splunk-datasource'; - - if (!dataSourceSettings) { - return undefined; + let metricsDataSourceSettings: DataSourceInstanceSettings | undefined; + if (traceToMetricsOptions?.datasourceUid) { + metricsDataSourceSettings = getDatasourceSrv().getInstanceSettings(traceToMetricsOptions.datasourceUid); } - return function SpanLink(span: TraceSpan): SpanLinkDef | undefined { + return function SpanLink(span: TraceSpan): SpanLinks { + const links: SpanLinks = { traceLinks: [] }; // This is reusing existing code from derived fields which may not be ideal match so some data is a bit faked at // the moment. Issue is that the trace itself isn't clearly mapped to dataFrame (right now it's just a json blob // inside a single field) so the dataLinks as config of that dataFrame abstraction breaks down a bit and we do // it manually here instead of leaving it for the data source to supply the config. let dataLink: DataLink | undefined = {} as DataLink | undefined; - let link: LinkModel; - switch (dataSourceSettings?.type) { - case 'loki': - dataLink = getLinkForLoki(span, traceToLogsOptions, dataSourceSettings); - if (!dataLink) { - return undefined; - } - break; - case 'grafana-splunk-datasource': - dataLink = getLinkForSplunk(span, traceToLogsOptions, dataSourceSettings); - break; - default: - return undefined; + // Get logs link + if (logsDataSourceSettings && traceToLogsOptions) { + switch (logsDataSourceSettings?.type) { + case 'loki': + dataLink = getLinkForLoki(span, traceToLogsOptions, logsDataSourceSettings); + break; + case 'grafana-splunk-datasource': + dataLink = getLinkForSplunk(span, traceToLogsOptions, logsDataSourceSettings); + break; + } + + if (dataLink) { + const link = mapInternalLinkToExplore({ + link: dataLink, + internalLink: dataLink.internal!, + scopedVars: {}, + range: getTimeRangeFromSpan( + span, + { + startMs: traceToLogsOptions.spanStartTimeShift + ? rangeUtil.intervalToMs(traceToLogsOptions.spanStartTimeShift) + : 0, + endMs: traceToLogsOptions.spanEndTimeShift + ? rangeUtil.intervalToMs(traceToLogsOptions.spanEndTimeShift) + : 0, + }, + isSplunkDS + ), + field: {} as Field, + onClickFn: splitOpenFn, + replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()), + }); + + links.logLinks = [ + { + href: link.href, + onClick: link.onClick, + content: , + }, + ]; + } } - link = mapInternalLinkToExplore({ - link: dataLink, - internalLink: dataLink?.internal!, - scopedVars: {}, - range: getTimeRangeFromSpan( - span, - { - startMs: traceToLogsOptions.spanStartTimeShift - ? rangeUtil.intervalToMs(traceToLogsOptions.spanStartTimeShift) - : 0, - endMs: traceToLogsOptions.spanEndTimeShift ? rangeUtil.intervalToMs(traceToLogsOptions.spanEndTimeShift) : 0, + // Get metrics links + if (metricsDataSourceSettings && traceToMetricsOptions) { + const dataLink: DataLink = { + title: metricsDataSourceSettings.name, + url: '', + internal: { + datasourceUid: metricsDataSourceSettings.uid, + datasourceName: metricsDataSourceSettings.name, + query: { + expr: `histogram_quantile(0.5, sum(rate(tempo_spanmetrics_latency_bucket{operation="${span.operationName}"}[5m])) by (le))`, + refId: '', + }, }, - isSplunkDS - ), - field: {} as Field, - onClickFn: splitOpenFn, - replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()), - }); + }; - return { - href: link.href, - onClick: link.onClick, - content: , - }; + const link = mapInternalLinkToExplore({ + link: dataLink, + internalLink: dataLink.internal!, + scopedVars: {}, + range: getTimeRangeFromSpan(span, { + startMs: 0, + endMs: 0, + }), + field: {} as Field, + onClickFn: splitOpenFn, + replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()), + }); + + links.metricLinks = [ + { + href: link.href, + onClick: link.onClick, + content: , + }, + ]; + } + + // Get trace links + if (span.references && createFocusSpanLink) { + for (const reference of span.references) { + // Ignore parent-child links + if (reference.refType === 'CHILD_OF') { + continue; + } + + const link = createFocusSpanLink(reference.traceID, reference.spanID); + + links.traceLinks!.push({ + href: link.href, + title: reference.span ? reference.span.operationName : 'View linked span', + content: , + onClick: link.onClick, + }); + } + } + + if (span.subsidiarilyReferencedBy && createFocusSpanLink) { + for (const reference of span.subsidiarilyReferencedBy) { + const link = createFocusSpanLink(reference.traceID, reference.spanID); + + links.traceLinks!.push({ + href: link.href, + title: reference.span ? reference.span.operationName : 'View linked span', + content: , + onClick: link.onClick, + }); + } + } + + return links; }; } diff --git a/public/app/plugins/datasource/jaeger/components/ConfigEditor.tsx b/public/app/plugins/datasource/jaeger/components/ConfigEditor.tsx index 114a7ef8a3e..46fcb204394 100644 --- a/public/app/plugins/datasource/jaeger/components/ConfigEditor.tsx +++ b/public/app/plugins/datasource/jaeger/components/ConfigEditor.tsx @@ -1,9 +1,11 @@ import React from 'react'; import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { DataSourceHttpSettings } from '@grafana/ui'; import { NodeGraphSettings } from 'app/core/components/NodeGraphSettings'; import { TraceToLogsSettings } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; +import { TraceToMetricsSettings } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings'; export type Props = DataSourcePluginOptionsEditorProps; @@ -20,6 +22,13 @@ export const ConfigEditor: React.FC = ({ options, onOptionsChange }) => {
    + + {config.featureToggles.traceToMetrics ? ( +
    + +
    + ) : null} +
    diff --git a/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx b/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx index fc21868589b..618c0bc28a3 100644 --- a/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx +++ b/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx @@ -5,6 +5,7 @@ import { config } from '@grafana/runtime'; import { DataSourceHttpSettings } from '@grafana/ui'; import { NodeGraphSettings } from 'app/core/components/NodeGraphSettings'; import { TraceToLogsSettings } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; +import { TraceToMetricsSettings } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings'; import { LokiSearchSettings } from './LokiSearchSettings'; import { SearchSettings } from './SearchSettings'; @@ -25,19 +26,29 @@ export const ConfigEditor: React.FC = ({ options, onOptionsChange }) => {
    + + {config.featureToggles.traceToMetrics ? ( +
    + +
    + ) : null} + {config.featureToggles.tempoServiceGraph && (
    )} + {config.featureToggles.tempoSearch && (
    )} +
    +
    diff --git a/public/app/plugins/datasource/tempo/configuration/ServiceGraphSettings.tsx b/public/app/plugins/datasource/tempo/configuration/ServiceGraphSettings.tsx index 8e5712ef9f7..b5c40fc8464 100644 --- a/public/app/plugins/datasource/tempo/configuration/ServiceGraphSettings.tsx +++ b/public/app/plugins/datasource/tempo/configuration/ServiceGraphSettings.tsx @@ -39,19 +39,21 @@ export function ServiceGraphSettings({ options, onOptionsChange }: Props) { } /> - + {options.jsonData.serviceMap?.datasourceUid ? ( + + ) : null}
    ); diff --git a/public/app/plugins/datasource/zipkin/ConfigEditor.tsx b/public/app/plugins/datasource/zipkin/ConfigEditor.tsx index 267dabcf9fc..fc876e5a91e 100644 --- a/public/app/plugins/datasource/zipkin/ConfigEditor.tsx +++ b/public/app/plugins/datasource/zipkin/ConfigEditor.tsx @@ -1,9 +1,11 @@ import React from 'react'; import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { DataSourceHttpSettings } from '@grafana/ui'; import { NodeGraphSettings } from 'app/core/components/NodeGraphSettings'; import { TraceToLogsSettings } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; +import { TraceToMetricsSettings } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings'; export type Props = DataSourcePluginOptionsEditorProps; @@ -21,6 +23,12 @@ export const ConfigEditor: React.FC = ({ options, onOptionsChange }) => {
    + {config.featureToggles.traceToMetrics ? ( +
    + +
    + ) : null} +
    From 454e8046573752d67371ca7493c211f762133de3 Mon Sep 17 00:00:00 2001 From: Stephanie Closson Date: Thu, 5 May 2022 19:27:28 -0300 Subject: [PATCH 081/440] Heatmap (new): add exemplar mapping function (#48780) --- .../panel/heatmap-new/HeatmapPanel.tsx | 2 +- .../plugins/panel/heatmap-new/fields.test.ts | 312 +++++++++++++++++- .../app/plugins/panel/heatmap-new/fields.ts | 105 +++++- .../plugins/panel/heatmap-new/suggestions.ts | 2 +- .../plugins/panel/heatmap-new/utils.test.ts | 5 + 5 files changed, 411 insertions(+), 15 deletions(-) create mode 100644 public/app/plugins/panel/heatmap-new/utils.test.ts diff --git a/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx b/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx index d1cf7093238..7814e421a1e 100644 --- a/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx +++ b/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx @@ -42,7 +42,7 @@ export const HeatmapPanel: React.FC = ({ let timeRangeRef = useRef(timeRange); timeRangeRef.current = timeRange; - const info = useMemo(() => prepareHeatmapData(data.series, options, theme), [data, options, theme]); + const info = useMemo(() => prepareHeatmapData(data, options, theme), [data, options, theme]); const facets = useMemo(() => [null, info.heatmap?.fields.map((f) => f.values.toArray())], [info.heatmap]); diff --git a/public/app/plugins/panel/heatmap-new/fields.test.ts b/public/app/plugins/panel/heatmap-new/fields.test.ts index 7bc2e1b1441..ce8be363bfa 100644 --- a/public/app/plugins/panel/heatmap-new/fields.test.ts +++ b/public/app/plugins/panel/heatmap-new/fields.test.ts @@ -1,5 +1,6 @@ -import { createTheme } from '@grafana/data'; +import { createTheme, ArrayVector, DataFrameType, FieldType } from '@grafana/data'; +import { BucketLayout, getAnnotationMapping, HEATMAP_NOT_SCANLINES_ERROR } from './fields'; import { PanelOptions } from './models.gen'; const theme = createTheme(); @@ -12,3 +13,312 @@ describe('Heatmap data', () => { expect(options).toBeDefined(); }); }); + +describe('creating a heatmap data mapping', () => { + describe('generates a simple data mapping with orderly data', () => { + const mapping = getAnnotationMapping( + { + heatmap: { + name: 'test', + meta: { + type: DataFrameType.HeatmapScanlines, + }, + fields: [ + { + name: 'xMin', + type: FieldType.number, + config: {}, + values: new ArrayVector([1]), + }, + { + name: 'yMin', + type: FieldType.number, + config: {}, + values: new ArrayVector([1, 4, 7]), + }, + { + name: 'count', + type: FieldType.number, + config: {}, + values: new ArrayVector([3, 3, 3]), + }, + ], + length: 3, + }, + xBucketCount: 1, + xBucketSize: 9, + xLayout: BucketLayout.ge, + yBucketCount: 3, + yBucketSize: 3, + yLayout: BucketLayout.ge, + }, + { + name: 'origdata', + fields: [ + { + name: 'time', + type: FieldType.time, + config: {}, + values: new ArrayVector([1, 2, 3, 4, 5, 6, 7, 8, 9]), + }, + { + name: 'value', + type: FieldType.number, + config: {}, + values: new ArrayVector([1, 2, 3, 4, 5, 6, 7, 8, 9]), + }, + ], + length: 2, + } + ); + + it('takes good data, and delivers a working data mapping', () => { + expect(mapping.lookup.length).toEqual(3); + expect(mapping.lookup[0]).toEqual([0, 1, 2]); + expect(mapping.lookup[1]).toEqual([3, 4, 5]); + expect(mapping.lookup[2]).toEqual([6, 7, 8]); + }); + }); + + describe('generates a data mapping with less orderly data (counts are different)', () => { + const heatmap = { + heatmap: { + name: 'test', + meta: { + type: DataFrameType.HeatmapScanlines, + }, + fields: [ + { + name: 'xMin', + type: FieldType.number, + config: {}, + values: new ArrayVector([1]), + }, + { + name: 'yMin', + type: FieldType.number, + config: {}, + values: new ArrayVector([1, 4, 7]), + }, + { + name: 'count', + type: FieldType.number, + config: {}, + values: new ArrayVector([2, 1, 6]), + }, + ], + length: 3, + }, + xBucketCount: 1, + xBucketSize: 9, + xLayout: BucketLayout.ge, + yBucketCount: 3, + yBucketSize: 3, + yLayout: BucketLayout.ge, + }; + const rawData = { + name: 'origdata', + fields: [ + { + name: 'time', + type: FieldType.time, + config: {}, + values: new ArrayVector([1, 2, 3, 4, 5, 6, 7, 8, 9]), + }, + { + name: 'value', + type: FieldType.number, + config: {}, + values: new ArrayVector([7, 1, 8, 7, 2, 7, 8, 8, 4]), + }, + ], + length: 2, + }; + + it("Puts data into the proper buckets, when we don't care about the count", () => { + // In this case, we are just finding proper values, but don't care if a values + // exists in the bucket in the original data or not. Therefore, we should see + // a value mapped into the second mapping bucket containing the value '8'. + const mapping = getAnnotationMapping(heatmap, rawData); + expect(mapping.lookup.length).toEqual(3); + expect(mapping.lookup[0]).toEqual([1, 4]); + expect(mapping.lookup[1]).toEqual([8]); + expect(mapping.lookup[2]).toEqual([0, 2, 3, 5, 6, 7]); + }); + }); + + describe('Handles a larger data set that will not fill all buckets', () => { + const mapping = getAnnotationMapping( + { + heatmap: { + name: 'test', + meta: { + type: DataFrameType.HeatmapScanlines, + }, + fields: [ + { + name: 'xMin', + type: FieldType.number, + config: {}, + values: new ArrayVector([1, 1, 1, 4, 4, 4, 7, 7, 7]), + }, + { + name: 'yMin', + type: FieldType.number, + config: {}, + values: new ArrayVector([1, 4, 7, 1, 4, 7, 1, 4, 7]), + }, + { + name: 'count', + type: FieldType.number, + config: {}, + values: new ArrayVector([0, 0, 2, 0, 0, 3, 0, 2, 0]), + }, + ], + length: 3, + }, + xBucketCount: 3, + xBucketSize: 3, + xLayout: BucketLayout.ge, + yBucketCount: 3, + yBucketSize: 3, + yLayout: BucketLayout.ge, + }, + { + name: 'origdata', + fields: [ + { + name: 'time', + type: FieldType.time, + config: {}, + values: new ArrayVector([1, 2, 3, 4, 5, 6, 7, 8, 9]), + }, + { + name: 'value', + type: FieldType.number, + config: {}, + values: new ArrayVector([8, 0, 8, 7, 7, 8, 6, 10, 6]), + }, + ], + length: 2, + } + ); + + it('Creates the data mapping correctly', () => { + expect(mapping.lookup.length).toEqual(9); + expect(mapping).toEqual({ + lookup: [null, null, [0, 2], null, null, [3, 4, 5], null, [6, 8], null], + low: [1], + high: [7], + }); + }); + + it('filters out minimum and maximum values', () => { + expect(mapping.lookup.flat()).not.toContainEqual(1); + expect(mapping.lookup.flat()).not.toContainEqual(10); + }); + }); + + describe('Error scenarios', () => { + const heatmap = { + heatmap: { + name: 'test', + meta: { + type: DataFrameType.HeatmapBuckets, + }, + fields: [ + { + name: 'xMin', + type: FieldType.number, + config: {}, + values: new ArrayVector([1]), + }, + { + name: 'yMin', + type: FieldType.number, + config: {}, + values: new ArrayVector([1, 4, 7]), + }, + { + name: 'count', + type: FieldType.number, + config: {}, + values: new ArrayVector([2, 0, 6]), + }, + ], + length: 3, + }, + xBucketCount: 1, + xBucketSize: 9, + xLayout: BucketLayout.ge, + yBucketCount: 3, + yBucketSize: 3, + yLayout: BucketLayout.ge, + }; + const rawData = { + name: 'origdata', + fields: [ + { + name: 'time', + type: FieldType.time, + config: {}, + values: new ArrayVector([1, 2, 3, 4, 5, 6, 7, 8, 9]), + }, + { + name: 'value', + type: FieldType.number, + config: {}, + values: new ArrayVector([7, 1, 8, 7, 2, 7, 8, 8, 4]), + }, + ], + length: 2, + }; + + it('Will not process heatmap buckets', () => { + expect(() => + getAnnotationMapping( + { + ...heatmap, + heatmap: { + ...heatmap.heatmap, + meta: { + type: DataFrameType.HeatmapBuckets, + }, + }, + }, + rawData + ) + ).toThrow(HEATMAP_NOT_SCANLINES_ERROR); + + expect(() => + getAnnotationMapping( + { + ...heatmap, + heatmap: { + ...heatmap.heatmap, + meta: { + type: DataFrameType.TimeSeriesWide, + }, + }, + }, + rawData + ) + ).toThrow(HEATMAP_NOT_SCANLINES_ERROR); + + expect(() => + getAnnotationMapping( + { + ...heatmap, + heatmap: { + ...heatmap.heatmap, + meta: { + type: undefined, + }, + }, + }, + rawData + ) + ).toThrow(HEATMAP_NOT_SCANLINES_ERROR); + }); + }); +}); diff --git a/public/app/plugins/panel/heatmap-new/fields.ts b/public/app/plugins/panel/heatmap-new/fields.ts index 4d120439e38..145ad8443fe 100644 --- a/public/app/plugins/panel/heatmap-new/fields.ts +++ b/public/app/plugins/panel/heatmap-new/fields.ts @@ -1,12 +1,16 @@ import { DataFrame, DataFrameType, + Field, FieldType, formattedValueToString, getDisplayProcessor, getFieldDisplayName, getValueFormat, GrafanaTheme2, + incrRoundDn, + incrRoundUp, + PanelData, } from '@grafana/data'; import { calculateHeatmapFromData, bucketsToScanlines } from 'app/features/transformers/calculateHeatmap/heatmap'; @@ -17,9 +21,19 @@ export const enum BucketLayout { ge = 'ge', } +export interface HeatmapDataMapping { + lookup: Array; + high: number[]; // index of values bigger than the max Y + low: number[]; // index of values less than the min Y +} + +export const HEATMAP_NOT_SCANLINES_ERROR = 'A calculated heatmap was expected, but not found'; + export interface HeatmapData { // List of heatmap frames heatmap?: DataFrame; + annotations?: DataFrame; + annotationMappings?: HeatmapDataMapping; yAxisValues?: Array; @@ -39,25 +53,25 @@ export interface HeatmapData { warning?: string; } -export function prepareHeatmapData( - frames: DataFrame[] | undefined, - options: PanelOptions, - theme: GrafanaTheme2 -): HeatmapData { +export function prepareHeatmapData(data: PanelData, options: PanelOptions, theme: GrafanaTheme2): HeatmapData { + const frames = data.series; if (!frames?.length) { return {}; } const { source } = options; + + const annotations = data.annotations?.[0]; // TODO: Maybe join on time with frames + if (source === HeatmapSourceMode.Calculate) { // TODO, check for error etc - return getHeatmapData(calculateHeatmapFromData(frames, options.heatmap ?? {}), theme); + return getHeatmapData(calculateHeatmapFromData(frames, options.heatmap ?? {}), annotations, theme); } // Find a well defined heatmap let scanlinesHeatmap = frames.find((f) => f.meta?.type === DataFrameType.HeatmapScanlines); if (scanlinesHeatmap) { - return getHeatmapData(scanlinesHeatmap, theme); + return getHeatmapData(scanlinesHeatmap, annotations, theme); } let bucketsHeatmap = frames.find((f) => f.meta?.type === DataFrameType.HeatmapBuckets); @@ -66,19 +80,79 @@ export function prepareHeatmapData( yAxisValues: frames[0].fields.flatMap((field) => field.type === FieldType.number ? getFieldDisplayName(field) : [] ), - ...getHeatmapData(bucketsToScanlines(bucketsHeatmap), theme), + ...getHeatmapData(bucketsToScanlines(bucketsHeatmap), annotations, theme), }; } if (source === HeatmapSourceMode.Data) { - return getHeatmapData(bucketsToScanlines(frames[0]), theme); + return getHeatmapData(bucketsToScanlines(frames[0]), annotations, theme); } // TODO, check for error etc - return getHeatmapData(calculateHeatmapFromData(frames, options.heatmap ?? {}), theme); + return getHeatmapData(calculateHeatmapFromData(frames, options.heatmap ?? {}), annotations, theme); } -const getHeatmapData = (frame: DataFrame, theme: GrafanaTheme2): HeatmapData => { +const getHeatmapFields = (dataFrame: DataFrame): Array => { + const xField: Field | undefined = dataFrame.fields.find((f) => f.name === 'xMin'); + const yField: Field | undefined = dataFrame.fields.find((f) => f.name === 'yMin'); + const countField: Field | undefined = dataFrame.fields.find((f) => f.name === 'count'); + + return [xField, yField, countField]; +}; + +export const getAnnotationMapping = (heatmapData: HeatmapData, rawData: DataFrame): HeatmapDataMapping => { + if (heatmapData.heatmap?.meta?.type !== DataFrameType.HeatmapScanlines) { + throw HEATMAP_NOT_SCANLINES_ERROR; + } + + const [fxs, fys] = getHeatmapFields(heatmapData.heatmap!); + + if (!fxs || !fys) { + throw HEATMAP_NOT_SCANLINES_ERROR; + } + + const mapping: HeatmapDataMapping = { + lookup: new Array(heatmapData.xBucketCount! * heatmapData.yBucketCount!).fill(null), + high: [], + low: [], + }; + + const xos: number[] | undefined = rawData.fields.find((f: Field) => f.type === 'time')?.values.toArray(); + const yos: number[] | undefined = rawData.fields.find((f: Field) => f.type === 'number')?.values.toArray(); + + if (!xos || !yos) { + return mapping; + } + + const xsmin = fxs.values.get(0); + const ysmin = fys.values.get(0); + const xsmax = fxs.values.get(fxs.values.length - 1) + heatmapData.xBucketSize!; + const ysmax = fys.values.get(fys.values.length - 1) + heatmapData.yBucketSize!; + xos.forEach((xo: number, i: number) => { + const yo = yos[i]; + const xBucketIdx = Math.floor(incrRoundDn(incrRoundUp((xo - xsmin) / heatmapData.xBucketSize!, 1e-7), 1e-7)); + const yBucketIdx = Math.floor(incrRoundDn(incrRoundUp((yo - ysmin) / heatmapData.yBucketSize!, 1e-7), 1e-7)); + + if (xo < xsmin || yo < ysmin) { + mapping.low.push(i); + return; + } + + if (xo >= xsmax || yo >= ysmax) { + mapping.high.push(i); + return; + } + + const index = xBucketIdx * heatmapData.yBucketCount! + yBucketIdx; + if (mapping.lookup[index] === null) { + mapping.lookup[index] = []; + } + mapping.lookup[index]?.push(i); + }); + return mapping; +}; + +const getHeatmapData = (frame: DataFrame, annotations: DataFrame | undefined, theme: GrafanaTheme2): HeatmapData => { if (frame.meta?.type !== DataFrameType.HeatmapScanlines) { return { warning: 'Expected heatmap scanlines format', @@ -114,8 +188,9 @@ const getHeatmapData = (frame: DataFrame, theme: GrafanaTheme2): HeatmapData => // The "count" field const disp = frame.fields[2].display ?? getValueFormat('short'); - return { + const data: HeatmapData = { heatmap: frame, + annotations, xBucketSize: xBinIncr, yBucketSize: yBinIncr, xBucketCount: xBinQty, @@ -127,4 +202,10 @@ const getHeatmapData = (frame: DataFrame, theme: GrafanaTheme2): HeatmapData => display: (v) => formattedValueToString(disp(v)), }; + + if (annotations) { + data.annotationMappings = getAnnotationMapping(data, annotations); + } + + return data; }; diff --git a/public/app/plugins/panel/heatmap-new/suggestions.ts b/public/app/plugins/panel/heatmap-new/suggestions.ts index 70d84a51dbc..057e39acf2e 100644 --- a/public/app/plugins/panel/heatmap-new/suggestions.ts +++ b/public/app/plugins/panel/heatmap-new/suggestions.ts @@ -18,7 +18,7 @@ export class HeatmapSuggestionsSupplier { return; } - const info = prepareHeatmapData(builder.data.series, defaultPanelOptions, config.theme2); + const info = prepareHeatmapData(builder.data, defaultPanelOptions, config.theme2); if (!info || info.warning) { return; } diff --git a/public/app/plugins/panel/heatmap-new/utils.test.ts b/public/app/plugins/panel/heatmap-new/utils.test.ts new file mode 100644 index 00000000000..26fecaad53b --- /dev/null +++ b/public/app/plugins/panel/heatmap-new/utils.test.ts @@ -0,0 +1,5 @@ +describe('a test', () => { + it('has to have at least one test', () => { + expect(true).toBeTruthy(); + }); +}); From ee8e1251340688bc98e0d2f580e8ce1ce456ff29 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Fri, 6 May 2022 09:34:10 +0200 Subject: [PATCH 082/440] Alerting: Fix notification route removal (#48774) * Fix notification route removal * fix tests Co-authored-by: gillesdemey --- .../components/amroutes/AmRoutesTable.test.ts | 4 +- .../components/amroutes/AmRoutesTable.tsx | 115 +++++++++--------- 2 files changed, 62 insertions(+), 57 deletions(-) diff --git a/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.test.ts b/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.test.ts index c15778b1cee..f9846440a5b 100644 --- a/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.test.ts +++ b/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.test.ts @@ -166,7 +166,7 @@ describe('deleteRoute', () => { const routeToDelete = routes[1]; // Act - const updatedRoutes = deleteRoute(routes, routeToDelete); + const updatedRoutes = deleteRoute(routes, routeToDelete.id); // Assert expect(updatedRoutes).toHaveLength(2); @@ -179,7 +179,7 @@ describe('deleteRoute', () => { const routes: FormAmRoute[] = [buildAmRoute({ id: '1' }), buildAmRoute({ id: '2' }), buildAmRoute({ id: '3' })]; // Act - const updatedRoutes = deleteRoute(routes, buildAmRoute({ id: '-1' })); + const updatedRoutes = deleteRoute(routes, '-1'); // Assert expect(updatedRoutes).toHaveLength(3); diff --git a/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx b/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx index 9da90b15ba9..366e0407aac 100644 --- a/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx +++ b/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx @@ -63,8 +63,8 @@ export const updatedRoute = (routes: FormAmRoute[], updatedRoute: FormAmRoute): return newRoutes; }; -export const deleteRoute = (routes: FormAmRoute[], routeToRemove: FormAmRoute): FormAmRoute[] => { - return routes.filter((route) => route.id !== routeToRemove.id); +export const deleteRoute = (routes: FormAmRoute[], routeId: string): FormAmRoute[] => { + return routes.filter((route) => route.id !== routeId); }; export const AmRoutesTable: FC = ({ @@ -78,7 +78,7 @@ export const AmRoutesTable: FC = ({ alertManagerSourceName, }) => { const [editMode, setEditMode] = useState(false); - const [showDeleteModal, setShowDeleteModal] = useState(false); + const [deletingRouteId, setDeletingRouteId] = useState(undefined); const [expandedId, setExpandedId] = useState(); const permissions = getNotificationsPermissions(alertManagerSourceName); const canEditRoutes = contextSrv.hasPermission(permissions.update); @@ -155,23 +155,11 @@ export const AmRoutesTable: FC = ({ aria-label="Delete route" name="trash-alt" onClick={() => { - setShowDeleteModal(true); + setDeletingRouteId(item.data.id); }} type="button" /> - { - const newRoutes = deleteRoute(routes, item.data); - onChange(newRoutes); - }} - onDismiss={() => setShowDeleteModal(false)} - /> ); }, @@ -209,45 +197,62 @@ export const AmRoutesTable: FC = ({ } return ( - 'am-routes-row'} - onCollapse={collapseItem} - onExpand={expandItem} - isExpanded={(item) => expandedId === item.id} - renderExpandedContent={(item: RouteTableItemProps) => - isAddMode || editMode ? ( - { - if (isAddMode) { - onCancelAdd(); - } - setEditMode(false); - }} - onSave={(data) => { - const newRoutes = updatedRoute(routes, data); + <> + 'am-routes-row'} + onCollapse={collapseItem} + onExpand={expandItem} + isExpanded={(item) => expandedId === item.id} + renderExpandedContent={(item: RouteTableItemProps) => + isAddMode || editMode ? ( + { + if (isAddMode) { + onCancelAdd(); + } + setEditMode(false); + }} + onSave={(data) => { + const newRoutes = updatedRoute(routes, data); - setEditMode(false); - onChange(newRoutes); - }} - receivers={receivers} - routes={item.data} - /> - ) : ( - { - const newRoutes = updatedRoute(routes, data); - onChange(newRoutes); - }} - receivers={receivers} - routes={item.data} - readOnly={readOnly} - alertManagerSourceName={alertManagerSourceName} - /> - ) - } - /> + setEditMode(false); + onChange(newRoutes); + }} + receivers={receivers} + routes={item.data} + /> + ) : ( + { + const newRoutes = updatedRoute(routes, data); + onChange(newRoutes); + }} + receivers={receivers} + routes={item.data} + readOnly={readOnly} + alertManagerSourceName={alertManagerSourceName} + /> + ) + } + /> + { + if (deletingRouteId) { + const newRoutes = deleteRoute(routes, deletingRouteId); + onChange(newRoutes); + setDeletingRouteId(undefined); + } + }} + onDismiss={() => setDeletingRouteId(undefined)} + /> + ); }; From bcb0bfce3aa26a98d4a1c177224b549b3386eaa7 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 6 May 2022 09:14:00 +0100 Subject: [PATCH 083/440] Navigation: Add create icons to expanded menu (#48768) * add create icons to expanded menu * update translations --- pkg/api/index.go | 4 ++-- public/app/core/components/NavBar/Next/NavBarMenu.tsx | 1 + public/app/core/components/NavBar/navBarItem-translations.ts | 2 +- public/locales/en/messages.po | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/api/index.go b/pkg/api/index.go index 1e3ad32d444..50c93285805 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -546,7 +546,7 @@ func (hs *HTTPServer) buildAlertNavLinks(c *models.ReqContext) []*dtos.NavLink { }) alertChildNavs = append(alertChildNavs, &dtos.NavLink{ - Text: "Alert rule", SubTitle: "Create an alert rule", Id: "alert", + Text: "New alert rule", SubTitle: "Create an alert rule", Id: "alert", Icon: "plus", Url: hs.Cfg.AppSubURL + "/alerting/new", HideFromTabs: true, ShowIconInNavbar: true, }) } @@ -595,7 +595,7 @@ func (hs *HTTPServer) buildCreateNavLinks(c *models.ReqContext) []*dtos.NavLink if uaVisibleForOrg && hasAccess(ac.ReqSignedIn, ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleCreate), ac.EvalPermission(ac.ActionAlertingRuleExternalWrite))) { children = append(children, &dtos.NavLink{ - Text: "Alert rule", SubTitle: "Create an alert rule", Id: "alert", + Text: "New alert rule", SubTitle: "Create an alert rule", Id: "alert", Icon: "bell", Url: hs.Cfg.AppSubURL + "/alerting/new", }) } diff --git a/public/app/core/components/NavBar/Next/NavBarMenu.tsx b/public/app/core/components/NavBar/Next/NavBarMenu.tsx index aab4a5d4343..27b9ac872c5 100644 --- a/public/app/core/components/NavBar/Next/NavBarMenu.tsx +++ b/public/app/core/components/NavBar/Next/NavBarMenu.tsx @@ -228,6 +228,7 @@ function NavItem({ key={`${link.text}-${childLink.text}`} isActive={activeItem === childLink} isDivider={childLink.divider} + icon={childLink.showIconInNavbar ? (childLink.icon as IconName) : undefined} onClick={() => { childLink.onClick?.(); onClose(); diff --git a/public/app/core/components/NavBar/navBarItem-translations.ts b/public/app/core/components/NavBar/navBarItem-translations.ts index 51fbb29f32d..04e3951d0a5 100644 --- a/public/app/core/components/NavBar/navBarItem-translations.ts +++ b/public/app/core/components/NavBar/navBarItem-translations.ts @@ -13,7 +13,7 @@ const TRANSLATED_MENU_ITEMS: Record = { 'create-dashboard': defineMessage({ id: 'nav.create-dashboard', message: 'Dashboard' }), folder: defineMessage({ id: 'nav.create-folder', message: 'Folder' }), import: defineMessage({ id: 'nav.create-import', message: 'Import' }), - alert: defineMessage({ id: 'nav.create-alert', message: 'Alert rule' }), + alert: defineMessage({ id: 'nav.create-alert', message: 'New alert rule' }), dashboards: defineMessage({ id: 'nav.dashboards', message: 'Dashboards' }), 'manage-dashboards': defineMessage({ id: 'nav.manage-dashboards', message: 'Browse' }), diff --git a/public/locales/en/messages.po b/public/locales/en/messages.po index 141b5976a1c..daceaecb9ee 100644 --- a/public/locales/en/messages.po +++ b/public/locales/en/messages.po @@ -68,7 +68,7 @@ msgstr "Create" #: public/app/core/components/NavBar/navBarItem-translations.ts msgid "nav.create-alert" -msgstr "Alert rule" +msgstr "New alert rule" #: public/app/core/components/NavBar/navBarItem-translations.ts msgid "nav.create-dashboard" From 0e1dffc655780d63f88eeb9c7fcf02c566c44555 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 6 May 2022 01:16:12 -0700 Subject: [PATCH 084/440] Search: manage selection in state (#48793) --- .../src/components/Table/TableCell.tsx | 2 +- .../app/features/search/page/SearchPage.tsx | 268 +++++++++++------- .../search/page/components/ActionRow.tsx | 4 +- .../search/page/components/ManageActions.tsx | 53 ++++ .../page/components/SearchResultsTable.tsx | 16 +- .../search/page/components/columns.tsx | 24 +- .../features/search/page/selection.test.ts | 15 + public/app/features/search/page/selection.ts | 58 ++++ .../features/search/service/minisearcher.ts | 3 + .../features/search/service/searcher.test.ts | 4 +- 10 files changed, 323 insertions(+), 124 deletions(-) create mode 100644 public/app/features/search/page/components/ManageActions.tsx create mode 100644 public/app/features/search/page/selection.test.ts create mode 100644 public/app/features/search/page/selection.ts diff --git a/packages/grafana-ui/src/components/Table/TableCell.tsx b/packages/grafana-ui/src/components/Table/TableCell.tsx index 09b6f4bb6d5..9ba4ed6cc3a 100644 --- a/packages/grafana-ui/src/components/Table/TableCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableCell.tsx @@ -16,7 +16,7 @@ export const TableCell: FC = ({ cell, tableStyles, onCellFilterAdded, col const cellProps = cell.getCellProps(); const field = (cell.column as any as GrafanaTableColumn).field; - if (!field.display) { + if (!field?.display) { return null; } diff --git a/public/app/features/search/page/SearchPage.tsx b/public/app/features/search/page/SearchPage.tsx index 19393c7d443..55fb1a3a11b 100644 --- a/public/app/features/search/page/SearchPage.tsx +++ b/public/app/features/search/page/SearchPage.tsx @@ -6,7 +6,7 @@ import { FixedSizeGrid } from 'react-window'; import { DataFrameView, GrafanaTheme2, NavModelItem } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { Input, useStyles2, Spinner, InlineSwitch, InlineFieldRow, InlineField } from '@grafana/ui'; +import { Input, useStyles2, Spinner, InlineSwitch, InlineFieldRow, InlineField, Button } from '@grafana/ui'; import Page from 'app/core/components/Page/Page'; import { TermCount } from 'app/core/components/TagFilter/TagFilter'; @@ -17,8 +17,10 @@ import { getGrafanaSearcher, QueryFilters, QueryResult } from '../service'; import { getTermCounts } from '../service/backend'; import { DashboardSearchItemType, DashboardSectionItem, SearchLayout } from '../types'; -import { ActionRow } from './components/ActionRow'; +import { ActionRow, getValidQueryLayout } from './components/ActionRow'; +import { ManageActions } from './components/ManageActions'; import { SearchResultsTable } from './components/SearchResultsTable'; +import { newSearchSelection, updateSearchSelection } from './selection'; const node: NavModelItem = { id: 'search', @@ -35,6 +37,8 @@ export default function SearchPage() { ); const [showManage, setShowManage] = useState(false); // grid vs list view + const [searchSelection, setSearchSelection] = useState(newSearchSelection()); + const results = useAsync(() => { const { query: searchQuery, tag: tags, datasource } = query; @@ -71,7 +75,127 @@ export default function SearchPage() { onTagFilterChange([...new Set(query.tag as string[]).add(tag)]); }; - const showPreviews = query.layout === SearchLayout.Grid && config.featureToggles.dashboardPreviews; + const toggleSelection = (kind: string, uid: string) => { + const current = searchSelection.isSelected(kind, uid); + if (kind === 'folder') { + // ??? also select all children? + } + setSearchSelection(updateSearchSelection(searchSelection, !current, kind, [uid])); + }; + + const layout = getValidQueryLayout(query); + const showPreviews = layout === SearchLayout.Grid && config.featureToggles.dashboardPreviews; + + const renderResults = () => { + if (results.loading) { + return ; + } + + const df = results.value?.body; + if (!df || !df.length) { + return ( +
    +
    No results found for your query.
    +
    + +
    + ); + } + + return ( + + {({ width, height }) => { + if (showPreviews) { + const view = new DataFrameView(df); + + // Hacked to reuse existing SearchCard (and old DashboardSectionItem) + const itemProps = { + editable: showManage, + onToggleChecked: (item: any) => { + const d = item as DashboardSectionItem; + const t = d.type === DashboardSearchItemType.DashFolder ? 'folder' : 'dashboard'; + toggleSelection(t, d.uid!); + }, + onTagSelected, + }; + + const numColumns = Math.ceil(width / 320); + const cellWidth = width / numColumns; + const cellHeight = (cellWidth - 64) * 0.75 + 56 + 8; + const numRows = Math.ceil(df.length / numColumns); + return ( + + {({ columnIndex, rowIndex, style }) => { + const index = rowIndex * numColumns + columnIndex; + const item = view.get(index); + const kind = item.kind ?? 'dashboard'; + const facade: DashboardSectionItem = { + uid: item.uid, + title: item.name, + url: item.url, + uri: item.url, + type: kind === 'folder' ? DashboardSearchItemType.DashFolder : DashboardSearchItemType.DashDB, + id: 666, // do not use me! + isStarred: false, + tags: item.tags ?? [], + checked: searchSelection.isSelected(kind, item.uid), + }; + + // The wrapper div is needed as the inner SearchItem has margin-bottom spacing + // And without this wrapper there is no room for that margin + return item ? ( +
  • + +
  • + ) : null; + }} +
    + ); + } + + return ( + <> + + + ); + }} +
    + ); + }; return ( @@ -84,113 +208,42 @@ export default function SearchPage() { placeholder="Search for dashboards and panels" /> - + setShowManage(!showManage)} />
    - {results.loading && } - {results.value?.body && ( -
    - { - if (v === SearchLayout.Folders) { - if (query.query) { - onQueryChange(''); // parent will clear the sort - } +
    + + {Boolean(searchSelection.items.size > 0) ? ( + + ) : ( + { + if (v === SearchLayout.Folders) { + if (query.query) { + onQueryChange(''); // parent will clear the sort } - onLayoutChange(v); - }} - onSortChange={onSortChange} - onTagFilterChange={onTagFilterChange} - getTagOptions={getTagOptions} - onDatasourceChange={onDatasourceChange} - query={query} - /> - - onLayoutChange(SearchLayout.List)} - /> - - - {({ width, height }) => { - if (showPreviews) { - const df = results.value?.body!; - const view = new DataFrameView(df); - - // HACK for grid view - const itemProps = { - editable: showManage, - onToggleChecked: (v: any) => { - console.log('CHECKED?', v); - }, - onTagSelected, - }; - - const numColumns = Math.ceil(width / 320); - const cellWidth = width / numColumns; - const cellHeight = (cellWidth - 64) * 0.75 + 56 + 8; - const numRows = Math.ceil(df.length / numColumns); - return ( - - {({ columnIndex, rowIndex, style }) => { - const index = rowIndex * numColumns + columnIndex; - const item = view.get(index); - const facade: DashboardSectionItem = { - uid: item.uid, - title: item.name, - url: item.url, - uri: item.url, - type: - item.kind === 'folder' - ? DashboardSearchItemType.DashFolder - : DashboardSearchItemType.DashDB, - id: 666, // do not use me! - isStarred: false, - tags: item.tags ?? [], - }; - - // The wrapper div is needed as the inner SearchItem has margin-bottom spacing - // And without this wrapper there is no room for that margin - return item ? ( -
  • - -
  • - ) : null; - }} -
    - ); - } - - return ( - <> - - - ); - }} -
    -
    + } + onLayoutChange(v); + }} + onSortChange={onSortChange} + onTagFilterChange={onTagFilterChange} + getTagOptions={getTagOptions} + onDatasourceChange={onDatasourceChange} + query={query} + /> )} + + {showPreviews && ( + onLayoutChange(SearchLayout.List)} + /> + )} + + {renderResults()}
    ); @@ -205,7 +258,6 @@ const getStyles = (theme: GrafanaTheme2) => ({ height: 100%; font-size: 18px; `, - virtualizedGridItemWrapper: css` padding: 4px; `, @@ -217,4 +269,10 @@ const getStyles = (theme: GrafanaTheme2) => ({ list-style: none; } `, + noResults: css` + padding: ${theme.v1.spacing.md}; + background: ${theme.v1.colors.bg2}; + font-style: italic; + margin-top: ${theme.v1.spacing.md}; + `, }); diff --git a/public/app/features/search/page/components/ActionRow.tsx b/public/app/features/search/page/components/ActionRow.tsx index 2f0fc3a5295..d79af608095 100644 --- a/public/app/features/search/page/components/ActionRow.tsx +++ b/public/app/features/search/page/components/ActionRow.tsx @@ -30,7 +30,7 @@ interface Props { hideLayout?: boolean; } -function getValidQueryLayout(q: DashboardQuery): SearchLayout { +export function getValidQueryLayout(q: DashboardQuery): SearchLayout { // Folders is not valid when a query exists if (q.layout === SearchLayout.Folders) { if (q.query || q.sort) { @@ -82,7 +82,7 @@ export const ActionRow: FC = ({ ActionRow.displayName = 'ActionRow'; -const getStyles = (theme: GrafanaTheme2) => { +export const getStyles = (theme: GrafanaTheme2) => { return { actionRow: css` display: none; diff --git a/public/app/features/search/page/components/ManageActions.tsx b/public/app/features/search/page/components/ManageActions.tsx new file mode 100644 index 00000000000..50bff96f534 --- /dev/null +++ b/public/app/features/search/page/components/ManageActions.tsx @@ -0,0 +1,53 @@ +import React from 'react'; + +import { Button, Checkbox, HorizontalGroup, useStyles2 } from '@grafana/ui'; + +import { getStyles } from './ActionRow'; + +type Props = { + items: Map>; +}; + +export function ManageActions({ items }: Props) { + const styles = useStyles2(getStyles); + + const canMove = true; + const canDelete = true; + + const onMove = () => { + alert('TODO, move....'); + }; + + const onDelete = () => { + alert('TODO, delete....'); + }; + + const onToggleAll = () => { + alert('TODO, toggle all....'); + }; + + return ( +
    +
    + + + + + + {[...items.keys()].map((k) => { + const vals = items.get(k); + return ( +
    + {k} ({vals?.size}) +
    + ); + })} +
    +
    +
    + ); +} diff --git a/public/app/features/search/page/components/SearchResultsTable.tsx b/public/app/features/search/page/components/SearchResultsTable.tsx index e858451f45b..22cc6b758f5 100644 --- a/public/app/features/search/page/components/SearchResultsTable.tsx +++ b/public/app/features/search/page/components/SearchResultsTable.tsx @@ -3,13 +3,14 @@ import React, { useMemo } from 'react'; import { useTable, Column, TableOptions, Cell, useAbsoluteLayout } from 'react-table'; import { FixedSizeList } from 'react-window'; -import { DataFrame, DataFrameType, DataFrameView, DataSourceRef, Field, GrafanaTheme2 } from '@grafana/data'; +import { DataFrame, DataFrameView, DataSourceRef, Field, GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '@grafana/ui'; import { TableCell } from '@grafana/ui/src/components/Table/TableCell'; import { getTableStyles } from '@grafana/ui/src/components/Table/styles'; import { LocationInfo } from '../../service'; import { SearchLayout } from '../../types'; +import { SelectionChecker, SelectionToggle } from '../selection'; import { generateColumns } from './columns'; @@ -17,7 +18,8 @@ type Props = { data: DataFrame; width: number; height: number; - showCheckbox: boolean; + selection?: SelectionChecker; + selectionToggle?: SelectionToggle; layout: SearchLayout; tags: string[]; onTagFilterChange: (tags: string[]) => void; @@ -51,7 +53,8 @@ export const SearchResultsTable = ({ width, height, tags, - showCheckbox, + selection, + selectionToggle, layout, onTagFilterChange, onDatasourceChange, @@ -72,18 +75,19 @@ export const SearchResultsTable = ({ // React-table column definitions const access = useMemo(() => new DataFrameView(data), [data]); const memoizedColumns = useMemo(() => { - const isDashboardList = data.meta?.type === DataFrameType.DirectoryListing || layout === SearchLayout.Folders; + const isDashboardList = layout === SearchLayout.Folders; return generateColumns( access, isDashboardList, width, - showCheckbox, + selection, + selectionToggle, styles, tags, onTagFilterChange, onDatasourceChange ); - }, [data.meta?.type, layout, access, width, styles, tags, showCheckbox, onTagFilterChange, onDatasourceChange]); + }, [layout, access, width, styles, tags, selection, selectionToggle, onTagFilterChange, onDatasourceChange]); const options: TableOptions<{}> = useMemo( () => ({ diff --git a/public/app/features/search/page/components/columns.tsx b/public/app/features/search/page/components/columns.tsx index 8f9f7b14cc4..7abdc5aea81 100644 --- a/public/app/features/search/page/components/columns.tsx +++ b/public/app/features/search/page/components/columns.tsx @@ -7,6 +7,7 @@ import { Checkbox, Icon, IconName, TagList } from '@grafana/ui'; import { DefaultCell } from '@grafana/ui/src/components/Table/DefaultCell'; import { LocationInfo } from '../../service'; +import { SelectionChecker, SelectionToggle } from '../selection'; import { FieldAccess, TableColumn } from './SearchResultsTable'; @@ -14,7 +15,8 @@ export const generateColumns = ( data: DataFrameView, isDashboardList: boolean, availableWidth: number, - showCheckbox: boolean, + selection: SelectionChecker | undefined, + selectionToggle: SelectionToggle | undefined, styles: { [key: string]: string }, tags: string[], onTagFilterChange: (tags: string[]) => void, @@ -22,13 +24,14 @@ export const generateColumns = ( ): TableColumn[] => { const columns: TableColumn[] = []; const uidField = data.fields.uid!; + const kindField = data.fields.kind!; const access = data.fields; availableWidth -= 8; // ??? let width = 50; - // TODO: Add optional checkbox support - if (showCheckbox) { + if (selection && selectionToggle) { + width = 30; columns.push({ id: `column-checkbox`, Header: () => ( @@ -37,20 +40,25 @@ export const generateColumns = ( onChange={(e) => { e.stopPropagation(); e.preventDefault(); - console.log('SELECT ALL!!!', e); + alert('SELECT ALL!!!'); }} />
    ), - width: 30, + width, Cell: (p) => { const uid = uidField.values.get(p.row.index); + const kind = kindField ? kindField.values.get(p.row.index) : 'dashboard'; // HACK for now + const selected = selection(kind, uid); + const hasUID = uid != null; // Panels don't have UID! Likely should not be shown on pages with manage options return (
    { - console.log('SELECTED!!!', uid); + selectionToggle(kind, uid); }} />
    @@ -260,10 +268,10 @@ function makeTypeColumn( return { Cell: DefaultCell, id: `column-type`, - field: kindField, + field: kindField ?? typeField, Header: 'Type', accessor: (row: any, i: number) => { - const kind = kindField.values.get(i); + const kind = kindField?.values.get(i) ?? 'dashboard'; let icon = 'public/img/icons/unicons/apps.svg'; let txt = 'Dashboard'; if (kind) { diff --git a/public/app/features/search/page/selection.test.ts b/public/app/features/search/page/selection.test.ts new file mode 100644 index 00000000000..9045b7b4e91 --- /dev/null +++ b/public/app/features/search/page/selection.test.ts @@ -0,0 +1,15 @@ +import { newSearchSelection, updateSearchSelection } from './selection'; + +describe('Search selection helper', () => { + it('simple dashboard selection', () => { + let sel = newSearchSelection(); + expect(sel.isSelected('dash', 'aaa')).toBe(false); + + sel = updateSearchSelection(sel, true, 'dash', ['aaa']); + expect(sel.isSelected('dash', 'aaa')).toBe(true); + + sel = updateSearchSelection(sel, false, 'dash', ['aaa']); + expect(sel.isSelected('dash', 'aaa')).toBe(false); + expect(sel.items).toMatchInlineSnapshot(`Map {}`); + }); +}); diff --git a/public/app/features/search/page/selection.ts b/public/app/features/search/page/selection.ts new file mode 100644 index 00000000000..3902b3c2d5e --- /dev/null +++ b/public/app/features/search/page/selection.ts @@ -0,0 +1,58 @@ +export type SelectionChecker = (kind: string, uid: string) => boolean; +export type SelectionToggle = (kind: string, uid: string) => void; + +export interface SearchSelection { + // Check if an item is selected + isSelected: SelectionChecker; + + // Selected items by kind + items: Map>; +} + +export function newSearchSelection(): SearchSelection { + // the check is called often, on potentially large (all) results so using Map/Set is better than simple array + const items = new Map>(); + + const isSelected = (kind: string, uid: string) => { + return Boolean(items.get(kind)?.has(uid)); + }; + + return { + items, + isSelected, + }; +} + +export function updateSearchSelection( + old: SearchSelection, + selected: boolean, + kind: string, + uids: string[] +): SearchSelection { + const items = old.items; // mutate! :/ + + if (uids.length) { + const k = items.get(kind); + if (k) { + for (const uid of uids) { + if (selected) { + k.add(uid); + } else { + k.delete(uid); + } + } + if (k.size < 1) { + items.delete(kind); + } + } else if (selected) { + items.set(kind, new Set(uids)); + } + } + + return { + items, + isSelected: (kind: string, uid: string) => { + return Boolean(items.get(kind)?.has(uid)); + }, + }; +} diff --git a/public/app/features/search/service/minisearcher.ts b/public/app/features/search/service/minisearcher.ts index 9a7bf738009..194d9e90ee9 100644 --- a/public/app/features/search/service/minisearcher.ts +++ b/public/app/features/search/service/minisearcher.ts @@ -173,6 +173,7 @@ export class MiniSearcher implements GrafanaSearcher { const found = this.index!.search(query); // frame fields + const uid: string[] = []; const url: string[] = []; const kind: string[] = []; const type: string[] = []; @@ -195,6 +196,7 @@ export class MiniSearcher implements GrafanaSearcher { continue; } + uid.push(input.uid?.get(index)!); url.push(input.url?.get(index) ?? '?'); location.push(input.location?.get(index) as any); datasource.push(input.datasource?.get(index) as any); @@ -206,6 +208,7 @@ export class MiniSearcher implements GrafanaSearcher { score.push(res.score); } const fields: Field[] = [ + { name: 'uid', config: {}, type: FieldType.string, values: new ArrayVector(uid) }, { name: 'kind', config: {}, type: FieldType.string, values: new ArrayVector(kind) }, { name: 'name', config: {}, type: FieldType.string, values: new ArrayVector(name) }, { diff --git a/public/app/features/search/service/searcher.test.ts b/public/app/features/search/service/searcher.test.ts index bbc762eb0b4..26742a1bc3e 100644 --- a/public/app/features/search/service/searcher.test.ts +++ b/public/app/features/search/service/searcher.test.ts @@ -27,7 +27,7 @@ describe('simple search', () => { const searcher = new MiniSearcher(supplier); let results = await searcher.search('name'); - expect(results.body.fields[1].values.toArray()).toMatchInlineSnapshot(` + expect(results.body.fields[2].values.toArray()).toMatchInlineSnapshot(` Array [ "A name (dash)", "B name (dash)", @@ -37,7 +37,7 @@ describe('simple search', () => { `); results = await searcher.search('B'); - expect(results.body.fields[1].values.toArray()).toMatchInlineSnapshot(` + expect(results.body.fields[2].values.toArray()).toMatchInlineSnapshot(` Array [ "B name (dash)", "B name (panels)", From 9826a694a879a66ffe0bb18f4d19af8793ed60f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joan=20L=C3=B3pez=20de=20la=20Franca=20Beltran?= <5459617+joanlopez@users.noreply.github.com> Date: Fri, 6 May 2022 10:21:55 +0200 Subject: [PATCH 085/440] Encryption: Add Prometheus metrics (#48603) --- pkg/infra/metrics/metrics.go | 11 +++++- .../usagestats/statscollector/service.go | 2 + .../usagestats/statscollector/service_test.go | 8 ++-- pkg/models/stats.go | 1 + pkg/services/secrets/manager/cache.go | 8 ++++ pkg/services/secrets/manager/manager.go | 37 ++++++++++++++++--- pkg/services/secrets/manager/metrics.go | 37 +++++++++++++++++++ pkg/services/sqlstore/stats.go | 1 + 8 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 pkg/services/secrets/manager/metrics.go diff --git a/pkg/infra/metrics/metrics.go b/pkg/infra/metrics/metrics.go index 1feca5c9ff0..b900ca7a168 100644 --- a/pkg/infra/metrics/metrics.go +++ b/pkg/infra/metrics/metrics.go @@ -12,7 +12,6 @@ import ( const ExporterName = "grafana" var ( - // MInstanceStart is a metric counter for started instances MInstanceStart prometheus.Counter @@ -191,6 +190,9 @@ var ( // StatsTotalLibraryVariables is a metric of total number of library variables stored in Grafana. StatsTotalLibraryVariables prometheus.Gauge + + // StatsTotalDataKeys is a metric of total number of data keys stored in Grafana. + StatsTotalDataKeys prometheus.Gauge ) func init() { @@ -565,6 +567,12 @@ func init() { Help: "total amount of library variables in the database", Namespace: ExporterName, }) + + StatsTotalDataKeys = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "stat_totals_data_keys", + Help: "total amount of data keys in the database", + Namespace: ExporterName, + }) } // SetBuildInformation sets the build information for this binary @@ -660,6 +668,7 @@ func initMetricVars() { MAccessEvaluationCount, StatsTotalLibraryPanels, StatsTotalLibraryVariables, + StatsTotalDataKeys, ) } diff --git a/pkg/infra/usagestats/statscollector/service.go b/pkg/infra/usagestats/statscollector/service.go index a6ce770325a..c88b79dcb09 100644 --- a/pkg/infra/usagestats/statscollector/service.go +++ b/pkg/infra/usagestats/statscollector/service.go @@ -138,6 +138,7 @@ func (s *Service) collect(ctx context.Context) (map[string]interface{}, error) { m["stats.folders_viewers_can_edit.count"] = statsQuery.Result.FoldersViewersCanEdit m["stats.folders_viewers_can_admin.count"] = statsQuery.Result.FoldersViewersCanAdmin m["stats.api_keys.count"] = statsQuery.Result.APIKeys + m["stats.data_keys.count"] = statsQuery.Result.DataKeys ossEditionCount := 1 enterpriseEditionCount := 0 @@ -326,6 +327,7 @@ func (s *Service) updateTotalStats(ctx context.Context) bool { metrics.StatsTotalAlertRules.Set(float64(statsQuery.Result.AlertRules)) metrics.StatsTotalLibraryPanels.Set(float64(statsQuery.Result.LibraryPanels)) metrics.StatsTotalLibraryVariables.Set(float64(statsQuery.Result.LibraryVariables)) + metrics.StatsTotalDataKeys.Set(float64(statsQuery.Result.DataKeys)) dsStats := models.GetDataSourceStatsQuery{} if err := s.sqlstore.GetDataSourceStats(ctx, &dsStats); err != nil { diff --git a/pkg/infra/usagestats/statscollector/service_test.go b/pkg/infra/usagestats/statscollector/service_test.go index 02006bf2175..a060dd9215c 100644 --- a/pkg/infra/usagestats/statscollector/service_test.go +++ b/pkg/infra/usagestats/statscollector/service_test.go @@ -8,18 +8,17 @@ import ( "time" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - "github.com/grafana/grafana/pkg/registry" - - "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -281,6 +280,8 @@ func TestCollectingUsageStats(t *testing.T) { assert.EqualValues(t, 1, metrics["stats.packaging.deb.count"]) assert.EqualValues(t, 1, metrics["stats.distributor.hosted-grafana.count"]) + assert.EqualValues(t, 11, metrics["stats.data_keys.count"]) + assert.InDelta(t, int64(65), metrics["stats.uptime"], 6) } @@ -323,6 +324,7 @@ func mockSystemStats(sqlStore *mockstore.SQLStoreMock) { FoldersViewersCanAdmin: 1, FoldersViewersCanEdit: 5, APIKeys: 2, + DataKeys: 11, } } diff --git a/pkg/models/stats.go b/pkg/models/stats.go index cd992d7f870..0636edba62a 100644 --- a/pkg/models/stats.go +++ b/pkg/models/stats.go @@ -39,6 +39,7 @@ type SystemStats struct { DailyActiveEditors int64 DailyActiveViewers int64 DailyActiveSessions int64 + DataKeys int64 } type DataSourceStats struct { diff --git a/pkg/services/secrets/manager/cache.go b/pkg/services/secrets/manager/cache.go index 2e7ba7a4a02..e6fe94a7768 100644 --- a/pkg/services/secrets/manager/cache.go +++ b/pkg/services/secrets/manager/cache.go @@ -1,8 +1,11 @@ package manager import ( + "strconv" "sync" "time" + + "github.com/prometheus/client_golang/prometheus" ) var ( @@ -36,6 +39,11 @@ func (c *dataKeyCache) get(id string) ([]byte, bool) { defer c.RUnlock() entry, exists := c.entries[id] + + cacheReadsCounter.With(prometheus.Labels{ + "hit": strconv.FormatBool(exists), + }).Inc() + if !exists || entry.expired() { return nil, false } diff --git a/pkg/services/secrets/manager/manager.go b/pkg/services/secrets/manager/manager.go index 2967c23ee53..2c1f23b03db 100644 --- a/pkg/services/secrets/manager/manager.go +++ b/pkg/services/secrets/manager/manager.go @@ -7,6 +7,7 @@ import ( "encoding/base64" "errors" "fmt" + "strconv" "time" "github.com/grafana/grafana/pkg/infra/log" @@ -16,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/services/kmsproviders" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/setting" + "github.com/prometheus/client_golang/prometheus" "golang.org/x/sync/errgroup" "xorm.io/xorm" ) @@ -130,11 +132,20 @@ func (s *SecretsService) EncryptWithDBSession(ctx context.Context, payload []byt return s.enc.Encrypt(ctx, payload, setting.SecretKey) } + var err error + defer func() { + opsCounter.With(prometheus.Labels{ + "success": strconv.FormatBool(err == nil), + "operation": OpEncrypt, + }).Inc() + }() + // If encryption featuremgmt.FlagEnvelopeEncryption toggle is on, use envelope encryption scope := opt() keyName := s.keyName(scope) - dataKey, err := s.dataKey(ctx, keyName) + var dataKey []byte + dataKey, err = s.dataKey(ctx, keyName) if err != nil { if errors.Is(err, secrets.ErrDataKeyNotFound) { dataKey, err = s.newDataKey(ctx, keyName, scope, sess) @@ -146,7 +157,8 @@ func (s *SecretsService) EncryptWithDBSession(ctx context.Context, payload []byt } } - encrypted, err := s.enc.Encrypt(ctx, payload, string(dataKey)) + var encrypted []byte + encrypted, err = s.enc.Encrypt(ctx, payload, string(dataKey)) if err != nil { return nil, err } @@ -174,8 +186,17 @@ func (s *SecretsService) Decrypt(ctx context.Context, payload []byte) ([]byte, e } // If encryption featuremgmt.FlagEnvelopeEncryption toggle is on, use envelope encryption + var err error + defer func() { + opsCounter.With(prometheus.Labels{ + "success": strconv.FormatBool(err == nil), + "operation": OpDecrypt, + }).Inc() + }() + if len(payload) == 0 { - return nil, fmt.Errorf("unable to decrypt empty payload") + err = fmt.Errorf("unable to decrypt empty payload") + return nil, err } var dataKey []byte @@ -187,12 +208,13 @@ func (s *SecretsService) Decrypt(ctx context.Context, payload []byte) ([]byte, e payload = payload[1:] endOfKey := bytes.Index(payload, []byte{'#'}) if endOfKey == -1 { - return nil, fmt.Errorf("could not find valid key in encrypted payload") + err = fmt.Errorf("could not find valid key in encrypted payload") + return nil, err } b64Key := payload[:endOfKey] payload = payload[endOfKey+1:] key := make([]byte, b64.DecodedLen(len(b64Key))) - _, err := b64.Decode(key, b64Key) + _, err = b64.Decode(key, b64Key) if err != nil { return nil, err } @@ -204,7 +226,10 @@ func (s *SecretsService) Decrypt(ctx context.Context, payload []byte) ([]byte, e } } - return s.enc.Decrypt(ctx, payload, string(dataKey)) + var decrypted []byte + decrypted, err = s.enc.Decrypt(ctx, payload, string(dataKey)) + + return decrypted, err } func (s *SecretsService) EncryptJsonData(ctx context.Context, kv map[string]string, opt secrets.EncryptionOptions) (map[string][]byte, error) { diff --git a/pkg/services/secrets/manager/metrics.go b/pkg/services/secrets/manager/metrics.go new file mode 100644 index 00000000000..2b1e83fec6d --- /dev/null +++ b/pkg/services/secrets/manager/metrics.go @@ -0,0 +1,37 @@ +package manager + +import ( + "github.com/grafana/grafana/pkg/infra/metrics" + "github.com/prometheus/client_golang/prometheus" +) + +const ( + OpEncrypt = "encrypt" + OpDecrypt = "decrypt" +) + +var ( + opsCounter = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: metrics.ExporterName, + Name: "encryption_ops_total", + Help: "A counter for encryption operations", + }, + []string{"success", "operation"}, + ) + cacheReadsCounter = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: metrics.ExporterName, + Name: "encryption_cache_reads_total", + Help: "A counter for encryption cache reads", + }, + []string{"hit"}, + ) +) + +func init() { + prometheus.MustRegister( + opsCounter, + cacheReadsCounter, + ) +} diff --git a/pkg/services/sqlstore/stats.go b/pkg/services/sqlstore/stats.go index e1e1500994e..6befd745b1a 100644 --- a/pkg/services/sqlstore/stats.go +++ b/pkg/services/sqlstore/stats.go @@ -102,6 +102,7 @@ func (ss *SQLStore) GetSystemStats(ctx context.Context, query *models.GetSystemS sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("api_key") + `WHERE service_account_id IS NULL) AS api_keys,`) sb.Write(`(SELECT COUNT(id) FROM `+dialect.Quote("library_element")+` WHERE kind = ?) AS library_panels,`, models.PanelElement) sb.Write(`(SELECT COUNT(id) FROM `+dialect.Quote("library_element")+` WHERE kind = ?) AS library_variables,`, models.VariableElement) + sb.Write(`(SELECT COUNT(*) FROM ` + dialect.Quote("data_keys") + `) AS data_keys,`) sb.Write(ss.roleCounterSQL(ctx)) From 817cf52744e30d3a5bede0773547b7185cfdb578 Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Fri, 6 May 2022 10:31:53 +0200 Subject: [PATCH 086/440] Access control: Allow users with permission to update team, dashboard and folder permissions to list users in OSS (#48275) * Remove banner when missing permissions to list users * For OSS allow users to list other users if they have permissions to write either team, dashboard or folder permissions --- pkg/api/api.go | 14 +++++++- pkg/services/sqlstore/org_users.go | 2 +- pkg/services/sqlstore/org_users_test.go | 4 +++ .../AccessControl/AddPermission.tsx | 34 ++++--------------- .../components/AccessControl/Permissions.tsx | 4 --- .../core/components/AccessControl/types.ts | 1 + .../AccessControlDashboardPermissions.tsx | 10 +----- .../AccessControlFolderPermissions.tsx | 8 +---- public/app/features/teams/TeamPermissions.tsx | 2 -- 9 files changed, 28 insertions(+), 51 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 3ebbb720dc8..6f06156f0ae 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -249,7 +249,19 @@ func (hs *HTTPServer) registerRoutes() { // current org without requirement of user to be org admin apiRoute.Group("/org", func(orgRoute routing.RouteRegister) { - orgRoute.Get("/users/lookup", authorize(reqOrgAdminFolderAdminOrTeamAdmin, ac.EvalPermission(ac.ActionOrgUsersRead)), routing.Wrap(hs.GetOrgUsersForCurrentOrgLookup)) + lookupEvaluator := func() ac.Evaluator { + if hs.Cfg.IsEnterprise { + return ac.EvalPermission(ac.ActionOrgUsersRead) + } + // For oss we allow users with access to update permissions on either folders, teams or dashboards to perform the lookup + return ac.EvalAny( + ac.EvalPermission(ac.ActionOrgUsersRead), + ac.EvalPermission(ac.ActionTeamsPermissionsWrite), + ac.EvalPermission(dashboards.ActionDashboardsPermissionsWrite), + ac.EvalPermission(dashboards.ActionFoldersPermissionsWrite), + ) + } + orgRoute.Get("/users/lookup", authorize(reqOrgAdminFolderAdminOrTeamAdmin, lookupEvaluator()), routing.Wrap(hs.GetOrgUsersForCurrentOrgLookup)) }) // create new org diff --git a/pkg/services/sqlstore/org_users.go b/pkg/services/sqlstore/org_users.go index 88b85dfbb1e..cf00e095141 100644 --- a/pkg/services/sqlstore/org_users.go +++ b/pkg/services/sqlstore/org_users.go @@ -109,7 +109,7 @@ func (ss *SQLStore) GetOrgUsers(ctx context.Context, query *models.GetOrgUsersQu whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = ?", ss.Dialect.Quote("user"))) whereParams = append(whereParams, ss.Dialect.BooleanStr(false)) - if !accesscontrol.IsDisabled(ss.Cfg) && query.User != nil { + if ss.Cfg.IsEnterprise && !accesscontrol.IsDisabled(ss.Cfg) && query.User != nil { acFilter, err := accesscontrol.Filter(query.User, "org_user.user_id", "users:id:", accesscontrol.ActionOrgUsersRead) if err != nil { return err diff --git a/pkg/services/sqlstore/org_users_test.go b/pkg/services/sqlstore/org_users_test.go index b68eb0d17c3..1a6431ca88e 100644 --- a/pkg/services/sqlstore/org_users_test.go +++ b/pkg/services/sqlstore/org_users_test.go @@ -62,6 +62,10 @@ func TestSQLStore_GetOrgUsers(t *testing.T) { } store := InitTestDB(t, InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagAccesscontrol}}) + store.Cfg.IsEnterprise = true + defer func() { + store.Cfg.IsEnterprise = false + }() seedOrgUsers(t, store, 10) for _, tt := range tests { diff --git a/public/app/core/components/AccessControl/AddPermission.tsx b/public/app/core/components/AccessControl/AddPermission.tsx index 5a9b38ba43a..8c17862ec7a 100644 --- a/public/app/core/components/AccessControl/AddPermission.tsx +++ b/public/app/core/components/AccessControl/AddPermission.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useState } from 'react'; -import { Alert, Button, Form, HorizontalGroup, Input, Select } from '@grafana/ui'; +import { Button, Form, HorizontalGroup, Select } from '@grafana/ui'; import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; import { TeamPicker } from 'app/core/components/Select/TeamPicker'; import { UserPicker } from 'app/core/components/Select/UserPicker'; @@ -12,20 +12,12 @@ export interface Props { title?: string; permissions: string[]; assignments: Assignments; - canListUsers: boolean; onCancel: () => void; onAdd: (state: SetPermission) => void; } -export const AddPermission = ({ - title = 'Add Permission For', - permissions, - assignments, - canListUsers, - onAdd, - onCancel, -}: Props) => { - const [target, setPermissionTarget] = useState(PermissionTarget.User); +export const AddPermission = ({ title = 'Add Permission For', permissions, assignments, onAdd, onCancel }: Props) => { + const [target, setPermissionTarget] = useState(PermissionTarget.None); const [teamId, setTeamId] = useState(0); const [userId, setUserId] = useState(0); const [builtInRole, setBuiltinRole] = useState(''); @@ -33,8 +25,8 @@ export const AddPermission = ({ const targetOptions = useMemo(() => { const options = []; - if (assignments.users && canListUsers) { - options.push({ value: PermissionTarget.User, label: 'User', isDisabled: false }); + if (assignments.users) { + options.push({ value: PermissionTarget.User, label: 'User' }); } if (assignments.teams) { options.push({ value: PermissionTarget.Team, label: 'Team' }); @@ -43,7 +35,7 @@ export const AddPermission = ({ options.push({ value: PermissionTarget.BuiltInRole, label: 'Role' }); } return options; - }, [assignments, canListUsers]); + }, [assignments]); useEffect(() => { if (permissions.length > 0) { @@ -56,22 +48,11 @@ export const AddPermission = ({ (target === PermissionTarget.User && userId > 0) || (PermissionTarget.BuiltInRole && OrgRole.hasOwnProperty(builtInRole)); - const renderMissingListUserRights = () => { - return ( - - You are missing the permission to list users (org.users:read). Please contact your administrator to get this - resolved. - - ); - }; - return (
    {title}
    - {target === PermissionTarget.User && !canListUsers && renderMissingListUserRights()} - - {target === PermissionTarget.User && canListUsers && ( + {target === PermissionTarget.User && ( setUserId(u.value || 0)} className={'width-20'} /> )} - {target === PermissionTarget.User && !canListUsers && } {target === PermissionTarget.Team && ( setTeamId(t.value?.id || 0)} className={'width-20'} /> diff --git a/public/app/core/components/AccessControl/Permissions.tsx b/public/app/core/components/AccessControl/Permissions.tsx index b7fb44aaefa..39446dedd47 100644 --- a/public/app/core/components/AccessControl/Permissions.tsx +++ b/public/app/core/components/AccessControl/Permissions.tsx @@ -29,8 +29,6 @@ export type Props = { addPermissionTitle?: string; resource: string; resourceId: ResourceId; - - canListUsers: boolean; canSetPermissions: boolean; }; @@ -39,7 +37,6 @@ export const Permissions = ({ buttonLabel = 'Add a permission', resource, resourceId, - canListUsers, canSetPermissions, addPermissionTitle, }: Props) => { @@ -145,7 +142,6 @@ export const Permissions = ({ onAdd={onAdd} permissions={desc.permissions} assignments={desc.assignments} - canListUsers={canListUsers} onCancel={() => setIsAdding(false)} /> diff --git a/public/app/core/components/AccessControl/types.ts b/public/app/core/components/AccessControl/types.ts index b6bba211c4c..e08589eefab 100644 --- a/public/app/core/components/AccessControl/types.ts +++ b/public/app/core/components/AccessControl/types.ts @@ -22,6 +22,7 @@ export type SetPermission = { }; export enum PermissionTarget { + None = 'None', Team = 'Team', User = 'User', BuiltInRole = 'builtInRole', diff --git a/public/app/features/dashboard/components/DashboardPermissions/AccessControlDashboardPermissions.tsx b/public/app/features/dashboard/components/DashboardPermissions/AccessControlDashboardPermissions.tsx index 989786dec44..1a60995f3c2 100644 --- a/public/app/features/dashboard/components/DashboardPermissions/AccessControlDashboardPermissions.tsx +++ b/public/app/features/dashboard/components/DashboardPermissions/AccessControlDashboardPermissions.tsx @@ -11,15 +11,7 @@ interface Props { } export const AccessControlDashboardPermissions = ({ dashboard }: Props) => { - const canListUsers = contextSrv.hasPermission(AccessControlAction.OrgUsersRead); const canSetPermissions = contextSrv.hasPermission(AccessControlAction.DashboardsPermissionsWrite); - return ( - - ); + return ; }; diff --git a/public/app/features/folders/AccessControlFolderPermissions.tsx b/public/app/features/folders/AccessControlFolderPermissions.tsx index 376d3ff6f09..7e5e1b4d3e5 100644 --- a/public/app/features/folders/AccessControlFolderPermissions.tsx +++ b/public/app/features/folders/AccessControlFolderPermissions.tsx @@ -33,18 +33,12 @@ export const AccessControlFolderPermissions = ({ uid, getFolderByUid, navModel } getFolderByUid(uid); }, [getFolderByUid, uid]); - const canListUsers = contextSrv.hasPermission(AccessControlAction.OrgUsersRead); const canSetPermissions = contextSrv.hasPermission(AccessControlAction.FoldersPermissionsWrite); return ( - + ); diff --git a/public/app/features/teams/TeamPermissions.tsx b/public/app/features/teams/TeamPermissions.tsx index 7dce1fef56f..51a17bc388e 100644 --- a/public/app/features/teams/TeamPermissions.tsx +++ b/public/app/features/teams/TeamPermissions.tsx @@ -11,7 +11,6 @@ type TeamPermissionsProps = { // TeamPermissions component replaces TeamMembers component when the accesscontrol feature flag is set const TeamPermissions = (props: TeamPermissionsProps) => { - const canListUsers = contextSrv.hasPermission(AccessControlAction.OrgUsersRead); const canSetPermissions = contextSrv.hasPermissionInMetadata( AccessControlAction.ActionTeamsPermissionsWrite, props.team @@ -24,7 +23,6 @@ const TeamPermissions = (props: TeamPermissionsProps) => { buttonLabel="Add member" resource="teams" resourceId={props.team.id} - canListUsers={canListUsers} canSetPermissions={canSetPermissions} /> ); From 3c78196d0be3c22671b8a92a4f2c8b463abd80eb Mon Sep 17 00:00:00 2001 From: Ieva Date: Fri, 6 May 2022 09:36:11 +0100 Subject: [PATCH 087/440] fix a bug (#48782) --- pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go b/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go index 57de31b6695..2e2aea961ff 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go @@ -155,7 +155,7 @@ func (ac *OSSAccessControlService) GetUserBuiltInRoles(user *models.SignedInUser builtInRoles := []string{string(user.OrgRole)} // With built-in role simplifying, inheritance is performed upon role registration. - if ac.IsDisabled() { + if !ac.features.IsEnabled(featuremgmt.FlagAccesscontrolBuiltins) { for _, br := range user.OrgRole.Children() { builtInRoles = append(builtInRoles, string(br)) } From b6c5f29373c0759799661647a861a279c9930b8a Mon Sep 17 00:00:00 2001 From: Szymon Szypulski Date: Fri, 6 May 2022 10:38:15 +0200 Subject: [PATCH 088/440] Cloudwatch: Add support for new AWS/RDS EBS* metrics (#48798) Add support for the missing AWS/RDS metrics, EBSIOBalance% and EBSByteBalance%. Change is based on the official AWS blog post[1]. Those metrics work on for Nitro instances. 1. https://aws.amazon.com/blogs/compute/improving-application-performance-and-reducing-costs-with-amazon-ebs-optimized-instance-burst-capability/ --- pkg/tsdb/cloudwatch/metrics.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tsdb/cloudwatch/metrics.go b/pkg/tsdb/cloudwatch/metrics.go index a095fdaa770..431c3b4c51c 100644 --- a/pkg/tsdb/cloudwatch/metrics.go +++ b/pkg/tsdb/cloudwatch/metrics.go @@ -365,7 +365,7 @@ var metricsMap = map[string][]string{ "AWS/Polly": {"2XXCount", "4XXCount", "5XXCount", "RequestCharacters", "ResponseLatency"}, "AWS/PrivateLinkEndpoints": {"ActiveConnections", "BytesProcessed", "NewConnections", "PacketsDropped", "RstPacketsReceived"}, "AWS/PrivateLinkServices": {"ActiveConnections", "BytesProcessed", "EndpointsCount", "NewConnections", "RstPacketsReceived"}, - "AWS/RDS": {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraGlobalDBDataTransferBytes", "AuroraGlobalDBReplicatedWriteIO", "AuroraGlobalDBReplicationLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "AvailabilityPercentage", "BacktrackChangeRecordsCreationRate", "BacktrackChangeRecordsStored", "BacktrackWindowActual", "BacktrackWindowAlert", "BackupRetentionPeriodStorageUsed", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "BurstBalance", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "ClientConnections", "ClientConnectionsClosed", "ClientConnectionsNoTLS", "ClientConnectionsReceived", "ClientConnectionsSetupFailedAuth", "ClientConnectionsSetupSucceeded", "ClientConnectionsTLS", "CommitLatency", "CommitThroughput", "DDLLatency", "DDLThroughput", "DMLLatency", "DMLThroughput", "DatabaseConnectionRequests", "DatabaseConnectionRequestsWithTLS", "DatabaseConnections", "DatabaseConnectionsBorrowLatency", "DatabaseConnectionsCurrentlyBorrowed", "DatabaseConnectionsCurrentlyInTransaction", "DatabaseConnectionsCurrentlySessionPinned", "DatabaseConnectionsSetupFailed", "DatabaseConnectionsSetupSucceeded", "DatabaseConnectionsWithTLS", "Deadlocks", "DeleteLatency", "DeleteThroughput", "DiskQueueDepth", "EngineUptime", "FailedSQLServerAgentJobsCount", "FreeLocalStorage", "FreeStorageSpace", "FreeableMemory", "InsertLatency", "InsertThroughput", "LoginFailures", "MaxDatabaseConnectionsAllowed", "MaximumUsedTransactionIDs", "NetworkReceiveThroughput", "NetworkThroughput", "NetworkTransmitThroughput", "OldestReplicationSlotLag", "Queries", "QueryDatabaseResponseLatency", "QueryRequests", "QueryRequestsNoTLS", "QueryRequestsTLS", "QueryResponseLatency", "RDSToAuroraPostgreSQLReplicaLag", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ReplicationSlotDiskUsage", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "ServerlessDatabaseCapacity", "SnapshotStorageUsed", "SwapUsage", "TotalBackupStorageBilled", "TransactionLogsDiskUsage", "TransactionLogsGeneration", "UpdateLatency", "UpdateThroughput", "VolumeBytesUsed", "VolumeReadIOPs", "VolumeWriteIOPs", "WriteIOPS", "WriteLatency", "WriteThroughput"}, + "AWS/RDS": {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraGlobalDBDataTransferBytes", "AuroraGlobalDBReplicatedWriteIO", "AuroraGlobalDBReplicationLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "AvailabilityPercentage", "BacktrackChangeRecordsCreationRate", "BacktrackChangeRecordsStored", "BacktrackWindowActual", "BacktrackWindowAlert", "BackupRetentionPeriodStorageUsed", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "BurstBalance", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "ClientConnections", "ClientConnectionsClosed", "ClientConnectionsNoTLS", "ClientConnectionsReceived", "ClientConnectionsSetupFailedAuth", "ClientConnectionsSetupSucceeded", "ClientConnectionsTLS", "CommitLatency", "CommitThroughput", "DDLLatency", "DDLThroughput", "DMLLatency", "DMLThroughput", "DatabaseConnectionRequests", "DatabaseConnectionRequestsWithTLS", "DatabaseConnections", "DatabaseConnectionsBorrowLatency", "DatabaseConnectionsCurrentlyBorrowed", "DatabaseConnectionsCurrentlyInTransaction", "DatabaseConnectionsCurrentlySessionPinned", "DatabaseConnectionsSetupFailed", "DatabaseConnectionsSetupSucceeded", "DatabaseConnectionsWithTLS", "Deadlocks", "DeleteLatency", "DeleteThroughput", "DiskQueueDepth", "EBSByteBalance%", "EBSIOBalance%", "EngineUptime", "FailedSQLServerAgentJobsCount", "FreeLocalStorage", "FreeStorageSpace", "FreeableMemory", "InsertLatency", "InsertThroughput", "LoginFailures", "MaxDatabaseConnectionsAllowed", "MaximumUsedTransactionIDs", "NetworkReceiveThroughput", "NetworkThroughput", "NetworkTransmitThroughput", "OldestReplicationSlotLag", "Queries", "QueryDatabaseResponseLatency", "QueryRequests", "QueryRequestsNoTLS", "QueryRequestsTLS", "QueryResponseLatency", "RDSToAuroraPostgreSQLReplicaLag", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ReplicationSlotDiskUsage", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "ServerlessDatabaseCapacity", "SnapshotStorageUsed", "SwapUsage", "TotalBackupStorageBilled", "TransactionLogsDiskUsage", "TransactionLogsGeneration", "UpdateLatency", "UpdateThroughput", "VolumeBytesUsed", "VolumeReadIOPs", "VolumeWriteIOPs", "WriteIOPS", "WriteLatency", "WriteThroughput"}, "AWS/Redshift": {"CommitQueueLength", "ConcurrencyScalingActiveClusters", "ConcurrencyScalingSeconds", "CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "MaxConfiguredConcurrencyScalingClusters", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "QueriesCompletedPerSecond", "QueryDuration", "QueryRuntimeBreakdown", "ReadIOPS", "ReadLatency", "ReadThroughput", "TotalTableCount", "WLMQueueLength", "WLMQueueWaitTime", "WLMQueriesCompletedPerSecond", "WLMQueryDuration", "WLMRunningQueries", "WriteIOPS", "WriteLatency", "WriteThroughput", "SchemaQuota", "NumExceededSchemaQuotas", "StorageUsed", "PercentageQuotaUsed"}, "AWS/Robomaker": {"RealTimeFactor", "vCPU", "Memory", "SimulationUnit"}, "AWS/Route53": {"ChildHealthCheckHealthyCount", "ConnectionTime", "DNSQueries", "HealthCheckPercentageHealthy", "HealthCheckStatus", "SSLHandshakeTime", "TimeToFirstByte"}, From c4edab884e7cdc4f28ef65b8d52891656fdae961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Fri, 6 May 2022 10:55:12 +0200 Subject: [PATCH 089/440] logs: simpler nanoscecond timestamp handling (#48773) --- public/app/core/logs_model.ts | 4 +--- .../loki/backendResultTransformer.test.ts | 1 - .../loki/backendResultTransformer.ts | 22 +------------------ 3 files changed, 2 insertions(+), 25 deletions(-) diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index bde1bd525a0..2e16093b144 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -364,9 +364,7 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi series, timeField, labelsField, - timeNanosecondField: fieldCache.hasFieldWithNameAndType('tsNs', FieldType.time) - ? fieldCache.getFieldByName('tsNs') - : undefined, + timeNanosecondField: fieldCache.getFieldByName('tsNs'), stringField, logLevelField: fieldCache.getFieldByName('level'), idField: getIdField(fieldCache), diff --git a/public/app/plugins/datasource/loki/backendResultTransformer.test.ts b/public/app/plugins/datasource/loki/backendResultTransformer.test.ts index 2d01b584b63..89ee35f8dfd 100644 --- a/public/app/plugins/datasource/loki/backendResultTransformer.test.ts +++ b/public/app/plugins/datasource/loki/backendResultTransformer.test.ts @@ -62,7 +62,6 @@ describe('loki backendResultTransformer', () => { lokiQueryStatKey: 'Summary: total bytes processed', }, }; - expectedFrame.fields[3].type = FieldType.time; const expected: DataQueryResponse = { data: [expectedFrame] }; diff --git a/public/app/plugins/datasource/loki/backendResultTransformer.ts b/public/app/plugins/datasource/loki/backendResultTransformer.ts index cff060731d7..d2d8004f6f2 100644 --- a/public/app/plugins/datasource/loki/backendResultTransformer.ts +++ b/public/app/plugins/datasource/loki/backendResultTransformer.ts @@ -28,28 +28,8 @@ function processStreamFrame(frame: DataFrame, query: LokiQuery | undefined): Dat lokiQueryStatKey: 'Summary: total bytes processed', }, }; - const newFrame = setFrameMeta(frame, meta); - const newFields = newFrame.fields.map((field) => { - switch (field.name) { - case 'tsNs': { - // we need to switch the field-type to be `time` - return { - ...field, - type: FieldType.time, - }; - } - default: { - // no modification needed - return field; - } - } - }); - - return { - ...newFrame, - fields: newFields, - }; + return setFrameMeta(frame, meta); } function processStreamsFrames(frames: DataFrame[], queryMap: Map): DataFrame[] { From f135a5c8a43029e7711c6a4f83b1da52feb029f8 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Fri, 6 May 2022 10:58:02 +0200 Subject: [PATCH 090/440] Plugins: Refactor plugin resource call with and without data source (#48754) * refactor plugin resource call with/without ds * check err * fix imports * only validate req on ds path * Update warn log Co-authored-by: Marcus Efraimsson Co-authored-by: Marcus Efraimsson --- pkg/api/datasources.go | 2 +- pkg/api/plugin_resource.go | 287 +++++++++++++++++++++ pkg/api/plugins.go | 257 +----------------- pkg/plugins/plugincontext/plugincontext.go | 54 ++-- pkg/services/live/live.go | 2 +- pkg/services/live/liveplugin/plugin.go | 20 +- 6 files changed, 336 insertions(+), 286 deletions(-) create mode 100644 pkg/api/plugin_resource.go diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index a35471d31fa..500d113cc15 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -422,7 +422,7 @@ func (hs *HTTPServer) CallDatasourceResource(c *models.ReqContext) { return } - hs.callPluginResource(c, plugin.ID, ds.Uid) + hs.callPluginResourceWithDataSource(c, plugin.ID, ds) } func (hs *HTTPServer) convertModelToDtos(ctx context.Context, ds *models.DataSource) dtos.DataSource { diff --git a/pkg/api/plugin_resource.go b/pkg/api/plugin_resource.go new file mode 100644 index 00000000000..e2e299a1fa4 --- /dev/null +++ b/pkg/api/plugin_resource.go @@ -0,0 +1,287 @@ +package api + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "sync" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/plugins/backendplugin" + "github.com/grafana/grafana/pkg/util/errutil" + "github.com/grafana/grafana/pkg/util/proxyutil" + "github.com/grafana/grafana/pkg/web" +) + +// CallResource passes a resource call from a plugin to the backend plugin. +// +// /api/plugins/:pluginId/resources/* +func (hs *HTTPServer) CallResource(c *models.ReqContext) { + hs.callPluginResource(c, web.Params(c.Req)[":pluginId"]) +} + +func (hs *HTTPServer) callPluginResource(c *models.ReqContext, pluginID string) { + pCtx, found, err := hs.PluginContextProvider.Get(c.Req.Context(), pluginID, c.SignedInUser) + if err != nil { + c.JsonApiErr(500, "Failed to get plugin settings", err) + return + } + if !found { + c.JsonApiErr(404, "Plugin not found", nil) + return + } + + req, err := hs.pluginResourceRequest(c) + if err != nil { + c.JsonApiErr(http.StatusBadRequest, "Failed for create plugin resource request", err) + return + } + + if err = hs.makePluginResourceRequest(c.Resp, req, pCtx); err != nil { + handleCallResourceError(err, c) + } +} + +func (hs *HTTPServer) callPluginResourceWithDataSource(c *models.ReqContext, pluginID string, ds *models.DataSource) { + pCtx, found, err := hs.PluginContextProvider.GetWithDataSource(c.Req.Context(), pluginID, c.SignedInUser, ds) + if err != nil { + c.JsonApiErr(500, "Failed to get plugin settings", err) + return + } + if !found { + c.JsonApiErr(404, "Plugin not found", nil) + return + } + + var dsURL string + if pCtx.DataSourceInstanceSettings != nil { + dsURL = pCtx.DataSourceInstanceSettings.URL + } + + err = hs.PluginRequestValidator.Validate(dsURL, c.Req) + if err != nil { + c.JsonApiErr(http.StatusForbidden, "Access denied", err) + return + } + + req, err := hs.pluginResourceRequest(c) + if err != nil { + c.JsonApiErr(http.StatusBadRequest, "Failed for create plugin resource request", err) + return + } + + if hs.DataProxy.OAuthTokenService.IsOAuthPassThruEnabled(ds) { + if token := hs.DataProxy.OAuthTokenService.GetCurrentOAuthToken(c.Req.Context(), c.SignedInUser); token != nil { + req.Header.Add("Authorization", fmt.Sprintf("%s %s", token.Type(), token.AccessToken)) + + idToken, ok := token.Extra("id_token").(string) + if ok && idToken != "" { + req.Header.Add("X-ID-Token", idToken) + } + } + } + + if err = hs.makePluginResourceRequest(c.Resp, req, pCtx); err != nil { + handleCallResourceError(err, c) + } +} + +func (hs *HTTPServer) pluginResourceRequest(c *models.ReqContext) (*http.Request, error) { + clonedReq := c.Req.Clone(c.Req.Context()) + rawURL := web.Params(c.Req)["*"] + if clonedReq.URL.RawQuery != "" { + rawURL += "?" + clonedReq.URL.RawQuery + } + urlPath, err := url.Parse(rawURL) + if err != nil { + return nil, err + } + clonedReq.URL = urlPath + + return clonedReq, nil +} + +func (hs *HTTPServer) makePluginResourceRequest(w http.ResponseWriter, req *http.Request, pCtx backend.PluginContext) error { + keepCookieModel := struct { + KeepCookies []string `json:"keepCookies"` + }{} + if dis := pCtx.DataSourceInstanceSettings; dis != nil { + err := json.Unmarshal(dis.JSONData, &keepCookieModel) + if err != nil { + hs.log.Warn("failed to unpack JSONData in datasource instance settings", "err", err) + } + } + proxyutil.ClearCookieHeader(req, keepCookieModel.KeepCookies) + proxyutil.PrepareProxyRequest(req) + + body, err := ioutil.ReadAll(req.Body) + if err != nil { + return fmt.Errorf("failed to read request body: %w", err) + } + + crReq := &backend.CallResourceRequest{ + PluginContext: pCtx, + Path: req.URL.Path, + Method: req.Method, + URL: req.URL.String(), + Headers: req.Header, + Body: body, + } + + childCtx, cancel := context.WithCancel(req.Context()) + defer cancel() + stream := newCallResourceResponseStream(childCtx) + + var wg sync.WaitGroup + wg.Add(1) + + defer func() { + if err := stream.Close(); err != nil { + hs.log.Warn("Failed to close plugin resource stream", "err", err) + } + wg.Wait() + }() + + var flushStreamErr error + go func() { + flushStreamErr = hs.flushStream(stream, w) + wg.Done() + }() + + if err := hs.pluginClient.CallResource(req.Context(), crReq, stream); err != nil { + return err + } + + return flushStreamErr +} + +func (hs *HTTPServer) flushStream(stream callResourceClientResponseStream, w http.ResponseWriter) error { + processedStreams := 0 + + for { + resp, err := stream.Recv() + if errors.Is(err, io.EOF) { + if processedStreams == 0 { + return errors.New("received empty resource response") + } + return nil + } + if err != nil { + if processedStreams == 0 { + return errutil.Wrap("failed to receive response from resource call", err) + } + + hs.log.Error("Failed to receive response from resource call", "err", err) + return stream.Close() + } + + // Expected that headers and status are only part of first stream + if processedStreams == 0 && resp.Headers != nil { + // Make sure a content type always is returned in response + if _, exists := resp.Headers["Content-Type"]; !exists { + resp.Headers["Content-Type"] = []string{"application/json"} + } + + for k, values := range resp.Headers { + // Due to security reasons we don't want to forward + // cookies from a backend plugin to clients/browsers. + if k == "Set-Cookie" { + continue + } + + for _, v := range values { + // TODO: Figure out if we should use Set here instead + // nolint:gocritic + w.Header().Add(k, v) + } + } + + proxyutil.SetProxyResponseHeaders(w.Header()) + + w.WriteHeader(resp.Status) + } + + if _, err := w.Write(resp.Body); err != nil { + hs.log.Error("Failed to write resource response", "err", err) + } + + if flusher, ok := w.(http.Flusher); ok { + flusher.Flush() + } + processedStreams++ + } +} + +func handleCallResourceError(err error, reqCtx *models.ReqContext) { + if errors.Is(err, backendplugin.ErrPluginUnavailable) { + reqCtx.JsonApiErr(503, "Plugin unavailable", err) + return + } + + if errors.Is(err, backendplugin.ErrMethodNotImplemented) { + reqCtx.JsonApiErr(404, "Not found", err) + return + } + + reqCtx.JsonApiErr(500, "Failed to call resource", err) +} + +// callResourceClientResponseStream is used for receiving resource call responses. +type callResourceClientResponseStream interface { + Recv() (*backend.CallResourceResponse, error) + Close() error +} + +type callResourceResponseStream struct { + ctx context.Context + stream chan *backend.CallResourceResponse + closed bool +} + +func newCallResourceResponseStream(ctx context.Context) *callResourceResponseStream { + return &callResourceResponseStream{ + ctx: ctx, + stream: make(chan *backend.CallResourceResponse), + } +} + +func (s *callResourceResponseStream) Send(res *backend.CallResourceResponse) error { + if s.closed { + return errors.New("cannot send to a closed stream") + } + + select { + case <-s.ctx.Done(): + return errors.New("cancelled") + case s.stream <- res: + return nil + } +} + +func (s *callResourceResponseStream) Recv() (*backend.CallResourceResponse, error) { + select { + case <-s.ctx.Done(): + return nil, s.ctx.Err() + case res, ok := <-s.stream: + if !ok { + return nil, io.EOF + } + return res, nil + } +} + +func (s *callResourceResponseStream) Close() error { + if s.closed { + return errors.New("cannot close a closed stream") + } + + close(s.stream) + s.closed = true + return nil +} diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 227a6d6fba1..48f938b83ea 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -5,16 +5,13 @@ import ( "encoding/json" "errors" "fmt" - "io" "io/ioutil" "net/http" - "net/url" "os" "path" "path/filepath" "sort" "strings" - "sync" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/api/dtos" @@ -26,8 +23,6 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/installer" "github.com/grafana/grafana/pkg/services/pluginsettings" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/util/errutil" - "github.com/grafana/grafana/pkg/util/proxyutil" "github.com/grafana/grafana/pkg/web" ) @@ -308,7 +303,7 @@ func (hs *HTTPServer) getPluginAssets(c *models.ReqContext) { func (hs *HTTPServer) CheckHealth(c *models.ReqContext) response.Response { pluginID := web.Params(c.Req)[":pluginId"] - pCtx, found, err := hs.PluginContextProvider.Get(c.Req.Context(), pluginID, "", c.SignedInUser, false) + pCtx, found, err := hs.PluginContextProvider.Get(c.Req.Context(), pluginID, c.SignedInUser) if err != nil { return response.Error(500, "Failed to get plugin settings", err) } @@ -346,13 +341,6 @@ func (hs *HTTPServer) CheckHealth(c *models.ReqContext) response.Response { return response.JSON(http.StatusOK, payload) } -// CallResource passes a resource call from a plugin to the backend plugin. -// -// /api/plugins/:pluginId/resources/* -func (hs *HTTPServer) CallResource(c *models.ReqContext) { - hs.callPluginResource(c, web.Params(c.Req)[":pluginId"], "") -} - func (hs *HTTPServer) GetPluginErrorsList(_ *models.ReqContext) response.Response { return response.JSON(http.StatusOK, hs.pluginErrorResolver.PluginErrors()) } @@ -471,246 +459,3 @@ func (hs *HTTPServer) pluginMarkdown(ctx context.Context, pluginId string, name func mdFilepath(mdFilename string) string { return filepath.Clean(filepath.Join("/", fmt.Sprintf("%s.md", mdFilename))) } - -func (hs *HTTPServer) callPluginResource(c *models.ReqContext, pluginID, dsUID string) { - pCtx, found, err := hs.PluginContextProvider.Get(c.Req.Context(), pluginID, dsUID, c.SignedInUser, false) - if err != nil { - c.JsonApiErr(500, "Failed to get plugin settings", err) - return - } - if !found { - c.JsonApiErr(404, "Plugin not found", nil) - return - } - - var dsURL string - if pCtx.DataSourceInstanceSettings != nil { - dsURL = pCtx.DataSourceInstanceSettings.URL - } - - err = hs.PluginRequestValidator.Validate(dsURL, c.Req) - if err != nil { - c.JsonApiErr(http.StatusForbidden, "Access denied", err) - return - } - - clonedReq := c.Req.Clone(c.Req.Context()) - rawURL := web.Params(c.Req)["*"] - if clonedReq.URL.RawQuery != "" { - rawURL += "?" + clonedReq.URL.RawQuery - } - urlPath, err := url.Parse(rawURL) - if err != nil { - handleCallResourceError(err, c) - return - } - clonedReq.URL = urlPath - - if dsUID != "" { - ds, err := hs.DataSourceCache.GetDatasourceByUID(c.Req.Context(), dsUID, c.SignedInUser, c.SkipCache) - - if err != nil { - if errors.Is(err, models.ErrDataSourceNotFound) { - c.JsonApiErr(404, "Datasource not found", err) - return - } - - c.JsonApiErr(500, "Failed to get datasource", err) - return - } - - if hs.DataProxy.OAuthTokenService.IsOAuthPassThruEnabled(ds) { - if token := hs.DataProxy.OAuthTokenService.GetCurrentOAuthToken(c.Req.Context(), c.SignedInUser); token != nil { - clonedReq.Header.Add("Authorization", fmt.Sprintf("%s %s", token.Type(), token.AccessToken)) - - idToken, ok := token.Extra("id_token").(string) - if ok && idToken != "" { - clonedReq.Header.Add("X-ID-Token", idToken) - } - } - } - } - - if err = hs.makePluginResourceRequest(c.Resp, clonedReq, pCtx); err != nil { - handleCallResourceError(err, c) - } -} - -func (hs *HTTPServer) makePluginResourceRequest(w http.ResponseWriter, req *http.Request, pCtx backend.PluginContext) error { - keepCookieModel := struct { - KeepCookies []string `json:"keepCookies"` - }{} - if dis := pCtx.DataSourceInstanceSettings; dis != nil { - err := json.Unmarshal(dis.JSONData, &keepCookieModel) - if err != nil { - hs.log.Warn("failed to to unpack JSONData in datasource instance settings", "err", err) - } - } - proxyutil.ClearCookieHeader(req, keepCookieModel.KeepCookies) - proxyutil.PrepareProxyRequest(req) - - body, err := ioutil.ReadAll(req.Body) - if err != nil { - return fmt.Errorf("failed to read request body: %w", err) - } - - crReq := &backend.CallResourceRequest{ - PluginContext: pCtx, - Path: req.URL.Path, - Method: req.Method, - URL: req.URL.String(), - Headers: req.Header, - Body: body, - } - - childCtx, cancel := context.WithCancel(req.Context()) - defer cancel() - stream := newCallResourceResponseStream(childCtx) - - var wg sync.WaitGroup - wg.Add(1) - - defer func() { - if err := stream.Close(); err != nil { - hs.log.Warn("Failed to close plugin resource stream", "err", err) - } - wg.Wait() - }() - - var flushStreamErr error - go func() { - flushStreamErr = hs.flushStream(stream, w) - wg.Done() - }() - - if err := hs.pluginClient.CallResource(req.Context(), crReq, stream); err != nil { - return err - } - - return flushStreamErr -} - -func (hs *HTTPServer) flushStream(stream callResourceClientResponseStream, w http.ResponseWriter) error { - processedStreams := 0 - - for { - resp, err := stream.Recv() - if errors.Is(err, io.EOF) { - if processedStreams == 0 { - return errors.New("received empty resource response") - } - return nil - } - if err != nil { - if processedStreams == 0 { - return errutil.Wrap("failed to receive response from resource call", err) - } - - hs.log.Error("Failed to receive response from resource call", "err", err) - return stream.Close() - } - - // Expected that headers and status are only part of first stream - if processedStreams == 0 && resp.Headers != nil { - // Make sure a content type always is returned in response - if _, exists := resp.Headers["Content-Type"]; !exists { - resp.Headers["Content-Type"] = []string{"application/json"} - } - - for k, values := range resp.Headers { - // Due to security reasons we don't want to forward - // cookies from a backend plugin to clients/browsers. - if k == "Set-Cookie" { - continue - } - - for _, v := range values { - // TODO: Figure out if we should use Set here instead - // nolint:gocritic - w.Header().Add(k, v) - } - } - - proxyutil.SetProxyResponseHeaders(w.Header()) - - w.WriteHeader(resp.Status) - } - - if _, err := w.Write(resp.Body); err != nil { - hs.log.Error("Failed to write resource response", "err", err) - } - - if flusher, ok := w.(http.Flusher); ok { - flusher.Flush() - } - processedStreams++ - } -} - -func handleCallResourceError(err error, reqCtx *models.ReqContext) { - if errors.Is(err, backendplugin.ErrPluginUnavailable) { - reqCtx.JsonApiErr(503, "Plugin unavailable", err) - return - } - - if errors.Is(err, backendplugin.ErrMethodNotImplemented) { - reqCtx.JsonApiErr(404, "Not found", err) - return - } - - reqCtx.JsonApiErr(500, "Failed to call resource", err) -} - -// callResourceClientResponseStream is used for receiving resource call responses. -type callResourceClientResponseStream interface { - Recv() (*backend.CallResourceResponse, error) - Close() error -} - -type callResourceResponseStream struct { - ctx context.Context - stream chan *backend.CallResourceResponse - closed bool -} - -func newCallResourceResponseStream(ctx context.Context) *callResourceResponseStream { - return &callResourceResponseStream{ - ctx: ctx, - stream: make(chan *backend.CallResourceResponse), - } -} - -func (s *callResourceResponseStream) Send(res *backend.CallResourceResponse) error { - if s.closed { - return errors.New("cannot send to a closed stream") - } - - select { - case <-s.ctx.Done(): - return errors.New("cancelled") - case s.stream <- res: - return nil - } -} - -func (s *callResourceResponseStream) Recv() (*backend.CallResourceResponse, error) { - select { - case <-s.ctx.Done(): - return nil, s.ctx.Err() - case res, ok := <-s.stream: - if !ok { - return nil, io.EOF - } - return res, nil - } -} - -func (s *callResourceResponseStream) Close() error { - if s.closed { - return errors.New("cannot close a closed stream") - } - - close(s.stream) - s.closed = true - return nil -} diff --git a/pkg/plugins/plugincontext/plugincontext.go b/pkg/plugins/plugincontext/plugincontext.go index e06a7821d4a..f8a3ece3ac2 100644 --- a/pkg/plugins/plugincontext/plugincontext.go +++ b/pkg/plugins/plugincontext/plugincontext.go @@ -43,11 +43,34 @@ type Provider struct { // Get allows getting plugin context by its ID. If datasourceUID is not empty string // then PluginContext.DataSourceInstanceSettings will be resolved and appended to // returned context. -func (p *Provider) Get(ctx context.Context, pluginID string, datasourceUID string, user *models.SignedInUser, skipCache bool) (backend.PluginContext, bool, error) { - pc := backend.PluginContext{} +func (p *Provider) Get(ctx context.Context, pluginID string, user *models.SignedInUser) (backend.PluginContext, bool, error) { + return p.pluginContext(ctx, pluginID, user) +} + +// GetWithDataSource allows getting plugin context by its ID and PluginContext.DataSourceInstanceSettings will be +// resolved and appended to the returned context. +func (p *Provider) GetWithDataSource(ctx context.Context, pluginID string, user *models.SignedInUser, ds *models.DataSource) (backend.PluginContext, bool, error) { + pCtx, exists, err := p.pluginContext(ctx, pluginID, user) + if err != nil { + return pCtx, exists, err + } + + datasourceSettings, err := adapters.ModelToInstanceSettings(ds, p.decryptSecureJsonDataFn(ctx)) + if err != nil { + return pCtx, exists, errutil.Wrap("Failed to convert datasource", err) + } + pCtx.DataSourceInstanceSettings = datasourceSettings + + return pCtx, true, nil +} + +const pluginSettingsCacheTTL = 5 * time.Second +const pluginSettingsCachePrefix = "plugin-setting-" + +func (p *Provider) pluginContext(ctx context.Context, pluginID string, user *models.SignedInUser) (backend.PluginContext, bool, error) { plugin, exists := p.pluginStore.Plugin(ctx, pluginID) if !exists { - return pc, false, nil + return backend.PluginContext{}, false, nil } jsonData := json.RawMessage{} @@ -59,18 +82,18 @@ func (p *Provider) Get(ctx context.Context, pluginID string, datasourceUID strin // models.ErrPluginSettingNotFound is expected if there's no row found for plugin setting in database (if non-app plugin). // If it's not this expected error something is wrong with cache or database and we return the error to the client. if !errors.Is(err, models.ErrPluginSettingNotFound) { - return pc, false, errutil.Wrap("Failed to get plugin settings", err) + return backend.PluginContext{}, false, errutil.Wrap("Failed to get plugin settings", err) } } else { jsonData, err = json.Marshal(ps.JSONData) if err != nil { - return pc, false, errutil.Wrap("Failed to unmarshal plugin json data", err) + return backend.PluginContext{}, false, errutil.Wrap("Failed to unmarshal plugin json data", err) } decryptedSecureJSONData = p.pluginSettingsService.DecryptedValues(ps) updated = ps.Updated } - pCtx := backend.PluginContext{ + return backend.PluginContext{ OrgID: user.OrgId, PluginID: plugin.ID, User: adapters.BackendUserFromSignedInUser(user), @@ -79,26 +102,9 @@ func (p *Provider) Get(ctx context.Context, pluginID string, datasourceUID strin DecryptedSecureJSONData: decryptedSecureJSONData, Updated: updated, }, - } - - if datasourceUID != "" { - ds, err := p.dataSourceCache.GetDatasourceByUID(ctx, datasourceUID, user, skipCache) - if err != nil { - return pc, false, errutil.Wrap("Failed to get datasource", err) - } - datasourceSettings, err := adapters.ModelToInstanceSettings(ds, p.decryptSecureJsonDataFn(ctx)) - if err != nil { - return pc, false, errutil.Wrap("Failed to convert datasource", err) - } - pCtx.DataSourceInstanceSettings = datasourceSettings - } - - return pCtx, true, nil + }, true, nil } -const pluginSettingsCacheTTL = 5 * time.Second -const pluginSettingsCachePrefix = "plugin-setting-" - func (p *Provider) getCachedPluginSettings(ctx context.Context, pluginID string, user *models.SignedInUser) (*pluginsettings.DTO, error) { cacheKey := pluginSettingsCachePrefix + pluginID diff --git a/pkg/services/live/live.go b/pkg/services/live/live.go index 1dba33d32b3..21bce2f69ee 100644 --- a/pkg/services/live/live.go +++ b/pkg/services/live/live.go @@ -227,7 +227,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r } } - g.contextGetter = liveplugin.NewContextGetter(g.PluginContextProvider) + g.contextGetter = liveplugin.NewContextGetter(g.PluginContextProvider, g.DataSourceCache) pipelinedChannelLocalPublisher := liveplugin.NewChannelLocalPublisher(node, g.Pipeline) numLocalSubscribersGetter := liveplugin.NewNumLocalSubscribersGetter(node) g.runStreamManager = runstream.NewManager(pipelinedChannelLocalPublisher, numLocalSubscribersGetter, g.contextGetter) diff --git a/pkg/services/live/liveplugin/plugin.go b/pkg/services/live/liveplugin/plugin.go index cf06a67cfd6..31376e7f293 100644 --- a/pkg/services/live/liveplugin/plugin.go +++ b/pkg/services/live/liveplugin/plugin.go @@ -6,8 +6,10 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins/plugincontext" + "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/live/orgchannel" "github.com/grafana/grafana/pkg/services/live/pipeline" + "github.com/grafana/grafana/pkg/util/errutil" "github.com/centrifugal/centrifuge" "github.com/grafana/grafana-plugin-sdk-go/backend" @@ -60,15 +62,25 @@ func (p *NumLocalSubscribersGetter) GetNumLocalSubscribers(channelID string) (in } type ContextGetter struct { - PluginContextProvider *plugincontext.Provider + pluginContextProvider *plugincontext.Provider + dataSourceCache datasources.CacheService } -func NewContextGetter(pluginContextProvider *plugincontext.Provider) *ContextGetter { +func NewContextGetter(pluginContextProvider *plugincontext.Provider, dataSourceCache datasources.CacheService) *ContextGetter { return &ContextGetter{ - PluginContextProvider: pluginContextProvider, + pluginContextProvider: pluginContextProvider, + dataSourceCache: dataSourceCache, } } func (g *ContextGetter) GetPluginContext(ctx context.Context, user *models.SignedInUser, pluginID string, datasourceUID string, skipCache bool) (backend.PluginContext, bool, error) { - return g.PluginContextProvider.Get(ctx, pluginID, datasourceUID, user, skipCache) + if datasourceUID == "" { + return g.pluginContextProvider.Get(ctx, pluginID, user) + } + + ds, err := g.dataSourceCache.GetDatasourceByUID(ctx, datasourceUID, user, skipCache) + if err != nil { + return backend.PluginContext{}, false, errutil.Wrap("Failed to get datasource", err) + } + return g.pluginContextProvider.GetWithDataSource(ctx, pluginID, user, ds) } From 4b5d295c8cd973a7072658fce5c85899790b64c2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 May 2022 10:01:44 +0100 Subject: [PATCH 091/440] Update typescript-eslint monorepo to v5.22.0 (#46998) Co-authored-by: Renovate Bot --- package.json | 4 +-- yarn.lock | 98 ++++++++++++++++++++++++++-------------------------- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/package.json b/package.json index c266818d655..428cbca42a9 100644 --- a/package.json +++ b/package.json @@ -165,8 +165,8 @@ "@types/testing-library__react-hooks": "^3.2.0", "@types/tinycolor2": "1.4.3", "@types/uuid": "8.3.4", - "@typescript-eslint/eslint-plugin": "5.16.0", - "@typescript-eslint/parser": "5.16.0", + "@typescript-eslint/eslint-plugin": "5.22.0", + "@typescript-eslint/parser": "5.22.0", "@wojtekmaj/enzyme-adapter-react-17": "0.6.6", "autoprefixer": "10.4.4", "axios": "0.26.1", diff --git a/yarn.lock b/yarn.lock index f75b4ac59a8..dbe448494a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11374,13 +11374,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:5.16.0": - version: 5.16.0 - resolution: "@typescript-eslint/eslint-plugin@npm:5.16.0" +"@typescript-eslint/eslint-plugin@npm:5.22.0": + version: 5.22.0 + resolution: "@typescript-eslint/eslint-plugin@npm:5.22.0" dependencies: - "@typescript-eslint/scope-manager": 5.16.0 - "@typescript-eslint/type-utils": 5.16.0 - "@typescript-eslint/utils": 5.16.0 + "@typescript-eslint/scope-manager": 5.22.0 + "@typescript-eslint/type-utils": 5.22.0 + "@typescript-eslint/utils": 5.22.0 debug: ^4.3.2 functional-red-black-tree: ^1.0.1 ignore: ^5.1.8 @@ -11393,7 +11393,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 4007cc1599503424037300e7401fb969ca441b122ef8a8f2fc8d70f84d656fdf7ab7b0d00e506a3aaf702871616c3756da17eb1508ff315dfb25170f2d28a904 + checksum: 3b083f7003f091c3ef7b3970dca9cfd507ab8c52a9b8a52259c630010adf765e9766f0e6fd9c901fc0e807319a4e8c003e12287b1f12a4b9eb4d7222e8d6db83 languageName: node linkType: hard @@ -11447,20 +11447,20 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/parser@npm:5.16.0": - version: 5.16.0 - resolution: "@typescript-eslint/parser@npm:5.16.0" +"@typescript-eslint/parser@npm:5.22.0": + version: 5.22.0 + resolution: "@typescript-eslint/parser@npm:5.22.0" dependencies: - "@typescript-eslint/scope-manager": 5.16.0 - "@typescript-eslint/types": 5.16.0 - "@typescript-eslint/typescript-estree": 5.16.0 + "@typescript-eslint/scope-manager": 5.22.0 + "@typescript-eslint/types": 5.22.0 + "@typescript-eslint/typescript-estree": 5.22.0 debug: ^4.3.2 peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: typescript: optional: true - checksum: 40006578e9ac451c80dc4b4b7e29af97b53fb9e9ea660d6ca17fb98b5c9858c648f9b17523c9de9b9b9e4155af17b65435e6163f02c4a2dfacf48274f45cba21 + checksum: 28a7d4b73154fc97336be9a4efd5ffdc659f748232c82479909e86ed87ed8a78d23280b3aaf532ca4e735caaffac43d9576e6af2dfd11865e30a9d70c8a3f275 languageName: node linkType: hard @@ -11494,13 +11494,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:5.16.0": - version: 5.16.0 - resolution: "@typescript-eslint/scope-manager@npm:5.16.0" +"@typescript-eslint/scope-manager@npm:5.22.0": + version: 5.22.0 + resolution: "@typescript-eslint/scope-manager@npm:5.22.0" dependencies: - "@typescript-eslint/types": 5.16.0 - "@typescript-eslint/visitor-keys": 5.16.0 - checksum: 008a6607d3e6ebcc59a9b28cddcc25703f39a88e27a96c69a6d988acc50a1ea7dbf50963c165ffa5b85a101209a0da3a7ec6832633a162ca4ecc78c0e54acd9f + "@typescript-eslint/types": 5.22.0 + "@typescript-eslint/visitor-keys": 5.22.0 + checksum: ebf2ad44f4e5a4dfd55225419804f81f68056086c20f1549adbcca4236634eac3aae461e30d6cab6539ce6f42346ed6e1fbbb2710d2cc058a3283ef91a0fe174 languageName: node linkType: hard @@ -11520,11 +11520,11 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:5.16.0": - version: 5.16.0 - resolution: "@typescript-eslint/type-utils@npm:5.16.0" +"@typescript-eslint/type-utils@npm:5.22.0": + version: 5.22.0 + resolution: "@typescript-eslint/type-utils@npm:5.22.0" dependencies: - "@typescript-eslint/utils": 5.16.0 + "@typescript-eslint/utils": 5.22.0 debug: ^4.3.2 tsutils: ^3.21.0 peerDependencies: @@ -11532,7 +11532,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 86d9f1dff6a096c8465453b8c7d0cc667b87a769f19073bfa9bbd36f8baa772c0384ec396b1132052383846bbbcf0d051345ed7d373260c1b506ed27100b383d + checksum: 7128085bfbeca3a9646a795a34730cdfeca110bc00240569f6a7b3dc0854680afa56e015715675a78198b414de869339bd6036cc33cb14903919780a60321a95 languageName: node linkType: hard @@ -11557,10 +11557,10 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:5.16.0": - version: 5.16.0 - resolution: "@typescript-eslint/types@npm:5.16.0" - checksum: 0450125741c3eef9581da0b75b4a987a633d77009cfb03507c3db29885b790ee80e3c0efc4f9a0dd3376ba758b49c7829722676153472616a57bb04bce5cc4fa +"@typescript-eslint/types@npm:5.22.0": + version: 5.22.0 + resolution: "@typescript-eslint/types@npm:5.22.0" + checksum: 74f822c5a3b96bba05229eea4ed370c4bd48b17f475c37f08d6ba708adf65c3aa026bb544f1d0308c96e043b30015e396fd53b1e8e4e9fbb6dc9c92d2ccc0a15 languageName: node linkType: hard @@ -11618,12 +11618,12 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:5.16.0": - version: 5.16.0 - resolution: "@typescript-eslint/typescript-estree@npm:5.16.0" +"@typescript-eslint/typescript-estree@npm:5.22.0": + version: 5.22.0 + resolution: "@typescript-eslint/typescript-estree@npm:5.22.0" dependencies: - "@typescript-eslint/types": 5.16.0 - "@typescript-eslint/visitor-keys": 5.16.0 + "@typescript-eslint/types": 5.22.0 + "@typescript-eslint/visitor-keys": 5.22.0 debug: ^4.3.2 globby: ^11.0.4 is-glob: ^4.0.3 @@ -11632,7 +11632,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 930ead4655712c3bd40885fb6b2074cd3c10fb03da864dd7a7dd2e43abfd330bb07e505f0aec8b4846178bff8befbb017f9f3370c67e9c717e4cb8d3df6e16ef + checksum: 2797a79d7d32a9a547b7f1de77a353d8e8c8519791f865f5e061bfc4918d12cdaddec51afa015f5aac5d068ef525c92bd65afc83b84dc9e52e697303acf0873a languageName: node linkType: hard @@ -11652,19 +11652,19 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:5.16.0": - version: 5.16.0 - resolution: "@typescript-eslint/utils@npm:5.16.0" +"@typescript-eslint/utils@npm:5.22.0": + version: 5.22.0 + resolution: "@typescript-eslint/utils@npm:5.22.0" dependencies: "@types/json-schema": ^7.0.9 - "@typescript-eslint/scope-manager": 5.16.0 - "@typescript-eslint/types": 5.16.0 - "@typescript-eslint/typescript-estree": 5.16.0 + "@typescript-eslint/scope-manager": 5.22.0 + "@typescript-eslint/types": 5.22.0 + "@typescript-eslint/typescript-estree": 5.22.0 eslint-scope: ^5.1.1 eslint-utils: ^3.0.0 peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - checksum: 46749091a204d7cf80d81b04704e23a86903a142a7e35cc5068a821c147c3bf098a7eff99af2b0e2ea7310013ca90300db9bab33ae5e3b5f773ed1d2961a5ed4 + checksum: 5019485e76d754a7a60c042545fd884dc666fddf9d4223ff706bbf0c275f19ea25a6b210fb5cf7ed368b019fe538fd854a925e9c6f12007d51b1731a29d95cc1 languageName: node linkType: hard @@ -11714,13 +11714,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:5.16.0": - version: 5.16.0 - resolution: "@typescript-eslint/visitor-keys@npm:5.16.0" +"@typescript-eslint/visitor-keys@npm:5.22.0": + version: 5.22.0 + resolution: "@typescript-eslint/visitor-keys@npm:5.22.0" dependencies: - "@typescript-eslint/types": 5.16.0 + "@typescript-eslint/types": 5.22.0 eslint-visitor-keys: ^3.0.0 - checksum: b587bf3b0da95bb58ff877b75fefcee6472222de1e3ec76aa4b94cae66078b62a372c7d0343374a16aab15cdcbae3f9e019624028b35827f68ef6559389f7fd0 + checksum: d30dfa98dcce75da49a6a204a0132d42e63228c35681cb9b3643e47a0a24a633e259832d48d101265bd85b8eb5a9f2b4858f9447646c1d3df6a2ac54258dfe8f languageName: node linkType: hard @@ -20978,8 +20978,8 @@ __metadata: "@types/testing-library__react-hooks": ^3.2.0 "@types/tinycolor2": 1.4.3 "@types/uuid": 8.3.4 - "@typescript-eslint/eslint-plugin": 5.16.0 - "@typescript-eslint/parser": 5.16.0 + "@typescript-eslint/eslint-plugin": 5.22.0 + "@typescript-eslint/parser": 5.22.0 "@visx/event": 2.6.0 "@visx/gradient": 2.1.0 "@visx/group": 2.1.0 From 101ae4b828a0ef9c9096a4896e706a9d46caee83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Jamr=C3=B3z?= Date: Fri, 6 May 2022 12:05:40 +0200 Subject: [PATCH 092/440] Chore: Generate missing theme JSON (#48802) --- public/sass/theme.dark.generated.json | 12 +++++++++--- public/sass/theme.light.generated.json | 14 ++++++++++---- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/public/sass/theme.dark.generated.json b/public/sass/theme.dark.generated.json index fc5311eaa6d..af7b1ee55ad 100644 --- a/public/sass/theme.dark.generated.json +++ b/public/sass/theme.dark.generated.json @@ -86,8 +86,8 @@ "disabledOpacity": 0.38 }, "gradients": { - "brandHorizontal": " linear-gradient(270deg, #F55F3E 0%, #FF8833 100%);", - "brandVertical": "linear-gradient(0.01deg, #F55F3E 0.01%, #FF8833 99.99%);" + "brandHorizontal": "linear-gradient(270deg, #F55F3E 0%, #FF8833 100%)", + "brandVertical": "linear-gradient(0.01deg, #F55F3E 0.01%, #FF8833 99.99%)" }, "contrastThreshold": 3, "hoverFactor": 0.03, @@ -129,7 +129,7 @@ "background": "#111217" }, "tooltip": { - "background": "#35383e", + "background": "#22252b", "text": "rgb(204, 204, 220)" }, "dashboard": { @@ -141,6 +141,12 @@ }, "sidemenu": { "width": 48 + }, + "menuTabs": { + "height": 41 + }, + "horizontalDrawer": { + "defaultHeight": 400 } }, "typography": { diff --git a/public/sass/theme.light.generated.json b/public/sass/theme.light.generated.json index 1cdea276243..9c8b72bcc75 100644 --- a/public/sass/theme.light.generated.json +++ b/public/sass/theme.light.generated.json @@ -86,8 +86,8 @@ "disabledOpacity": 0.38 }, "gradients": { - "brandHorizontal": "linear-gradient(90deg, #FF8833 0%, #F53E4C 100%);", - "brandVertical": "linear-gradient(0.01deg, #F53E4C -31.2%, #FF8833 113.07%);" + "brandHorizontal": "linear-gradient(90deg, #FF8833 0%, #F53E4C 100%)", + "brandVertical": "linear-gradient(0.01deg, #F53E4C -31.2%, #FF8833 113.07%)" }, "contrastThreshold": 3, "hoverFactor": 0.03, @@ -129,8 +129,8 @@ "background": "#FFFFFF" }, "tooltip": { - "background": "#555", - "text": "#FFF" + "background": "#F4F5F5", + "text": "rgba(36, 41, 46, 1)" }, "dashboard": { "background": "#F4F5F5", @@ -141,6 +141,12 @@ }, "sidemenu": { "width": 48 + }, + "menuTabs": { + "height": 41 + }, + "horizontalDrawer": { + "defaultHeight": 400 } }, "typography": { From 5be23b40b6e452e944613a9cba5983cb1c9176c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20D=C4=85browski?= Date: Fri, 6 May 2022 12:12:42 +0200 Subject: [PATCH 093/440] LDAP: allow Grafana Admin mapping without org_role field (#37189) --- pkg/services/ldap/ldap.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/services/ldap/ldap.go b/pkg/services/ldap/ldap.go index 02f9e2b2f13..42b83741513 100644 --- a/pkg/services/ldap/ldap.go +++ b/pkg/services/ldap/ldap.go @@ -333,7 +333,7 @@ func (server *Server) users(logins []string) ( // If there are no ldap group mappings access is true // otherwise a single group must match func (server *Server) validateGrafanaUser(user *models.ExternalUserInfo) error { - if len(server.Config.Groups) > 0 && len(user.OrgRoles) < 1 { + if len(server.Config.Groups) > 0 && (len(user.OrgRoles) == 0 && (user.IsGrafanaAdmin == nil || !*user.IsGrafanaAdmin)) { server.log.Error( "User does not belong in any of the specified LDAP groups", "username", user.Login, @@ -423,7 +423,10 @@ func (server *Server) buildGrafanaUser(user *ldap.Entry) (*models.ExternalUserIn } if IsMemberOf(memberOf, group.GroupDN) { - extUser.OrgRoles[group.OrgId] = group.OrgRole + if group.OrgRole != "" { + extUser.OrgRoles[group.OrgId] = group.OrgRole + } + if extUser.IsGrafanaAdmin == nil || !*extUser.IsGrafanaAdmin { extUser.IsGrafanaAdmin = group.IsGrafanaAdmin } @@ -432,7 +435,7 @@ func (server *Server) buildGrafanaUser(user *ldap.Entry) (*models.ExternalUserIn // If there are group org mappings configured, but no matching mappings, // the user will not be able to login and will be disabled - if len(server.Config.Groups) > 0 && len(extUser.OrgRoles) == 0 { + if len(server.Config.Groups) > 0 && (len(extUser.OrgRoles) == 0 && (extUser.IsGrafanaAdmin == nil || !*extUser.IsGrafanaAdmin)) { extUser.IsDisabled = true } From 25b4aa8d86013a409a50c92e4a6e8f45db790438 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Fri, 6 May 2022 14:05:58 +0300 Subject: [PATCH 094/440] RolePicker: Fix menu position on smaller screens (#48429) * RolePicker: Fix menu position on smaller screens * RolePicker: Add comment * Add offset for the bottom position --- .../core/components/RolePicker/RolePicker.tsx | 25 ++++++++++++++++--- .../components/RolePicker/RolePickerMenu.tsx | 22 ++++++++++------ .../core/components/RolePicker/constants.ts | 1 + 3 files changed, 37 insertions(+), 11 deletions(-) create mode 100644 public/app/core/components/RolePicker/constants.ts diff --git a/public/app/core/components/RolePicker/RolePicker.tsx b/public/app/core/components/RolePicker/RolePicker.tsx index a916ed9a1d2..b7c1e8de11c 100644 --- a/public/app/core/components/RolePicker/RolePicker.tsx +++ b/public/app/core/components/RolePicker/RolePicker.tsx @@ -1,10 +1,11 @@ -import React, { FormEvent, useCallback, useEffect, useState } from 'react'; +import React, { FormEvent, useCallback, useEffect, useState, useRef } from 'react'; import { ClickOutsideWrapper, HorizontalGroup, Spinner } from '@grafana/ui'; import { Role, OrgRole } from 'app/types'; import { RolePickerInput } from './RolePickerInput'; import { RolePickerMenu } from './RolePickerMenu'; +import { MENU_MAX_HEIGHT } from './constants'; export interface Props { builtInRole?: OrgRole; @@ -23,7 +24,6 @@ export const RolePicker = ({ builtInRole, appliedRoles, roleOptions, - builtInRoles, disabled, isLoading, builtinRolesDisabled, @@ -35,11 +35,28 @@ export const RolePicker = ({ const [selectedRoles, setSelectedRoles] = useState(appliedRoles); const [selectedBuiltInRole, setSelectedBuiltInRole] = useState(builtInRole); const [query, setQuery] = useState(''); + const [offset, setOffset] = useState(0); + const ref = useRef(null); useEffect(() => { setSelectedRoles(appliedRoles); }, [appliedRoles]); + useEffect(() => { + const dimensions = ref?.current?.getBoundingClientRect(); + if (!dimensions || !isOpen) { + return; + } + const { bottom, top } = dimensions; + const distance = window.innerHeight - bottom; + const offset = bottom - top + 10; // Add extra 10px to offset to account for border and outline + if (distance < MENU_MAX_HEIGHT) { + setOffset(offset); + } else { + setOffset(-offset); + } + }, [isOpen]); + const onOpen = useCallback( (event: FormEvent) => { if (!disabled) { @@ -103,7 +120,7 @@ export const RolePicker = ({ } return ( -
    +
    )} diff --git a/public/app/core/components/RolePicker/RolePickerMenu.tsx b/public/app/core/components/RolePicker/RolePickerMenu.tsx index 6ecc86fe487..1f3989d0fde 100644 --- a/public/app/core/components/RolePicker/RolePickerMenu.tsx +++ b/public/app/core/components/RolePicker/RolePickerMenu.tsx @@ -17,7 +17,7 @@ import { import { getSelectStyles } from '@grafana/ui/src/components/Select/getSelectStyles'; import { OrgRole, Role } from 'app/types'; -type BuiltInRoles = Record; +import { MENU_MAX_HEIGHT } from './constants'; const BuiltinRoles = Object.values(OrgRole); const BuiltinRoleOption: Array> = BuiltinRoles.map((r) => ({ @@ -32,7 +32,6 @@ const fixedRoleGroupNames: Record = { interface RolePickerMenuProps { builtInRole?: OrgRole; - builtInRoles?: BuiltInRoles; options: Role[]; appliedRoles: Role[]; showGroups?: boolean; @@ -42,11 +41,11 @@ interface RolePickerMenuProps { onBuiltInRoleSelect?: (role: OrgRole) => void; onUpdate: (newRoles: string[], newBuiltInRole?: OrgRole) => void; onClear?: () => void; + offset: number; } export const RolePickerMenu = ({ builtInRole, - builtInRoles, options, appliedRoles, showGroups, @@ -56,6 +55,7 @@ export const RolePickerMenu = ({ onBuiltInRoleSelect, onUpdate, onClear, + offset, }: RolePickerMenuProps): JSX.Element => { const [selectedOptions, setSelectedOptions] = useState(appliedRoles); const [selectedBuiltInRole, setSelectedBuiltInRole] = useState(builtInRole); @@ -63,7 +63,6 @@ export const RolePickerMenu = ({ const [openedMenuGroup, setOpenedMenuGroup] = useState(''); const [subMenuOptions, setSubMenuOptions] = useState([]); const subMenuNode = useRef(null); - const theme = useTheme2(); const styles = getSelectStyles(theme); const customStyles = useStyles2(getStyles); @@ -175,9 +174,18 @@ export const RolePickerMenu = ({ }; return ( -
    +
    0 ? `${offset}px` : 'unset'}; + top: ${offset < 0 ? `${Math.abs(offset)}px` : 'unset'}; + ` + )} + >
    - + {showBuiltInRole && (
    Built-in roles
    @@ -327,7 +335,7 @@ export const RolePickerSubMenu = ({ return (
    - +
    {options.map((option, i) => ( Date: Fri, 6 May 2022 14:08:13 +0300 Subject: [PATCH 095/440] DashboardPickerByID: add optionLabel prop (#47556) * DashboardPicker: add optionLabel prop * DashboardPicker: fix label prop * Update picker type --- .../components/editors/DashboardPickerByID.tsx | 15 +++++++++------ public/app/features/playlist/usePlaylistItems.tsx | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/public/app/core/components/editors/DashboardPickerByID.tsx b/public/app/core/components/editors/DashboardPickerByID.tsx index a54279e4aba..d4b2f709795 100644 --- a/public/app/core/components/editors/DashboardPickerByID.tsx +++ b/public/app/core/components/editors/DashboardPickerByID.tsx @@ -11,7 +11,7 @@ import { backendSrv } from 'app/core/services/backend_srv'; export interface DashboardPickerItem { id: number; uid: string; - label: string; + [key: string]: string | number; } interface Props { @@ -22,6 +22,7 @@ interface Props { invalid?: boolean; disabled?: boolean; id?: string; + optionLabel?: string; } /** @@ -35,9 +36,10 @@ export const DashboardPickerByID: FC = ({ invalid, disabled, id, + optionLabel = 'label', }) => { - const debouncedSearch = debounce(getDashboards, 300); - const option = value ? { value, label: value.label } : undefined; + const debouncedSearch = debounce((query: string) => getDashboards(query || '', optionLabel), 300); + const option = value ? { value, [optionLabel]: value[optionLabel] } : undefined; const onChange = (item: SelectableValue) => { propsOnChange(item?.value); }; @@ -55,19 +57,20 @@ export const DashboardPickerByID: FC = ({ value={option} invalid={invalid} disabled={disabled} + getOptionLabel={(option) => option[optionLabel]} /> ); }; -async function getDashboards(query = ''): Promise>> { +async function getDashboards(query: string, label: string): Promise>> { const result = await backendSrv.search({ type: 'dash-db', query, limit: 100 }); return result.map(({ id, uid = '', title, folderTitle }) => { const value: DashboardPickerItem = { id, uid, - label: `${folderTitle ?? 'General'}/${title}`, + [label]: `${folderTitle ?? 'General'}/${title}`, }; - return { value, label: value.label }; + return { value, [label]: value[label] }; }); } diff --git a/public/app/features/playlist/usePlaylistItems.tsx b/public/app/features/playlist/usePlaylistItems.tsx index 83e58132705..7f86df6ea89 100644 --- a/public/app/features/playlist/usePlaylistItems.tsx +++ b/public/app/features/playlist/usePlaylistItems.tsx @@ -15,7 +15,7 @@ export function usePlaylistItems(playlistItems?: PlaylistItem[]) { const newItem: PlaylistItem = { id: dashboard.id, - title: dashboard.label, + title: dashboard.label as string, type: 'dashboard_by_id', value: dashboard.id.toString(10), order: items.length + 1, From 4fbd974471b62a985e6c923d07ac0a39e69821e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Gabry=C5=9B?= Date: Fri, 6 May 2022 15:20:42 +0200 Subject: [PATCH 096/440] Doc: correct grammar in labels description (#47300) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adam Gabryś --- docs/sources/basics/timeseries-dimensions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/basics/timeseries-dimensions.md b/docs/sources/basics/timeseries-dimensions.md index c1422122391..8ea5e19f2dd 100644 --- a/docs/sources/basics/timeseries-dimensions.md +++ b/docs/sources/basics/timeseries-dimensions.md @@ -22,7 +22,7 @@ To identify unique series within a set of time series, Grafana stores dimensions ## Labels -Each time series in Grafana optionally has labels. Labels are set a of key/value pairs for identifying dimensions. Example labels could be `{location=us}` or `{country=us,state=ma,city=boston}`. Within a set of time series, the combination of its name and labels identifies each series. For example, `temperature {country=us,state=ma,city=boston}` could identify the series of temperature values for the city of Boston in the US. +Each time series in Grafana optionally has labels. Labels are a set of key/value pairs for identifying dimensions. Example labels could be `{location=us}` or `{country=us,state=ma,city=boston}`. Within a set of time series, the combination of its name and labels identifies each series. For example, `temperature {country=us,state=ma,city=boston}` could identify the series of temperature values for the city of Boston in the US. Different sources of time series data have dimensions stored natively, or common storage patterns that allow the data to be extracted into dimensions. From 2d6ab03e4f15ba05e3699a72b304de2c4ee1051a Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Fri, 6 May 2022 15:39:24 +0200 Subject: [PATCH 097/440] Alerting: automatically select last expression (#48787) --- .../components/rule-editor/ConditionField.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/public/app/features/alerting/unified/components/rule-editor/ConditionField.tsx b/public/app/features/alerting/unified/components/rule-editor/ConditionField.tsx index 6df7254f6ca..fdf03057116 100644 --- a/public/app/features/alerting/unified/components/rule-editor/ConditionField.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/ConditionField.tsx @@ -29,9 +29,20 @@ export const ConditionField: FC = () => { [queries] ); + const expressions = useMemo(() => { + return queries.filter((query) => query.datasourceUid === ExpressionDatasourceUID); + }, [queries]); + + // automatically use the last expression when new expressions have been added + useEffect(() => { + const lastExpression = last(expressions); + if (lastExpression) { + setValue('condition', lastExpression.refId, { shouldValidate: true }); + } + }, [expressions, setValue]); + // reset condition if option no longer exists or if it is unset, but there are options available useEffect(() => { - const expressions = queries.filter((query) => query.datasourceUid === ExpressionDatasourceUID); const lastExpression = last(expressions); const conditionExists = options.find(({ value }) => value === condition); @@ -40,7 +51,7 @@ export const ConditionField: FC = () => { } else if (!condition && lastExpression) { setValue('condition', lastExpression.refId, { shouldValidate: true }); } - }, [condition, options, queries, setValue]); + }, [condition, expressions, options, setValue]); return ( Date: Fri, 6 May 2022 07:21:14 -0700 Subject: [PATCH 098/440] Heatmap (new): exemplars tooltip stub (#48795) --- .../panel/heatmap-new/HeatmapHoverView.tsx | 27 ++++++++++++++++++- .../plugins/panel/heatmap-new/fields.test.ts | 14 +++++----- .../app/plugins/panel/heatmap-new/fields.ts | 27 ++++++++++--------- 3 files changed, 47 insertions(+), 21 deletions(-) diff --git a/public/app/plugins/panel/heatmap-new/HeatmapHoverView.tsx b/public/app/plugins/panel/heatmap-new/HeatmapHoverView.tsx index 97b5d3419bf..842f1b2febc 100644 --- a/public/app/plugins/panel/heatmap-new/HeatmapHoverView.tsx +++ b/public/app/plugins/panel/heatmap-new/HeatmapHoverView.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef } from 'react'; -import { Field, FieldType, formattedValueToString, getFieldDisplayName, LinkModel } from '@grafana/data'; +import { DataFrameView, Field, FieldType, formattedValueToString, getFieldDisplayName, LinkModel } from '@grafana/data'; import { LinkButton, VerticalGroup } from '@grafana/ui'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; @@ -153,6 +153,30 @@ export const HeatmapHoverView = ({ data, hover, showHistogram }: Props) => { [hover.index] ); + const renderExemplars = () => { + const exemplarIndex = data.exemplarsMappings?.lookup; //?.[hover.index]; + if (!exemplarIndex || !data.exemplars) { + return null; + } + + const ids = exemplarIndex[hover.index]; + if (ids) { + const view = new DataFrameView(data.exemplars); + return ( +
      + {ids.map((id) => ( +
    • +
      {JSON.stringify(view.get(id), null, 2)}
      +
    • + ))} +
    + ); + } + + // should not show anything... but for debugging + return
    EXEMPLARS: {JSON.stringify(exemplarIndex)}
    ; + }; + return ( <>
    @@ -175,6 +199,7 @@ export const HeatmapHoverView = ({ data, hover, showHistogram }: Props) => { {getFieldDisplayName(countField!, data.heatmap)}: {count}
    + {renderExemplars()} {links.length > 0 && ( {links.map((link, i) => ( diff --git a/public/app/plugins/panel/heatmap-new/fields.test.ts b/public/app/plugins/panel/heatmap-new/fields.test.ts index ce8be363bfa..96dd7889592 100644 --- a/public/app/plugins/panel/heatmap-new/fields.test.ts +++ b/public/app/plugins/panel/heatmap-new/fields.test.ts @@ -1,6 +1,6 @@ import { createTheme, ArrayVector, DataFrameType, FieldType } from '@grafana/data'; -import { BucketLayout, getAnnotationMapping, HEATMAP_NOT_SCANLINES_ERROR } from './fields'; +import { BucketLayout, getExemplarsMapping, HEATMAP_NOT_SCANLINES_ERROR } from './fields'; import { PanelOptions } from './models.gen'; const theme = createTheme(); @@ -16,7 +16,7 @@ describe('Heatmap data', () => { describe('creating a heatmap data mapping', () => { describe('generates a simple data mapping with orderly data', () => { - const mapping = getAnnotationMapping( + const mapping = getExemplarsMapping( { heatmap: { name: 'test', @@ -139,7 +139,7 @@ describe('creating a heatmap data mapping', () => { // In this case, we are just finding proper values, but don't care if a values // exists in the bucket in the original data or not. Therefore, we should see // a value mapped into the second mapping bucket containing the value '8'. - const mapping = getAnnotationMapping(heatmap, rawData); + const mapping = getExemplarsMapping(heatmap, rawData); expect(mapping.lookup.length).toEqual(3); expect(mapping.lookup[0]).toEqual([1, 4]); expect(mapping.lookup[1]).toEqual([8]); @@ -148,7 +148,7 @@ describe('creating a heatmap data mapping', () => { }); describe('Handles a larger data set that will not fill all buckets', () => { - const mapping = getAnnotationMapping( + const mapping = getExemplarsMapping( { heatmap: { name: 'test', @@ -276,7 +276,7 @@ describe('creating a heatmap data mapping', () => { it('Will not process heatmap buckets', () => { expect(() => - getAnnotationMapping( + getExemplarsMapping( { ...heatmap, heatmap: { @@ -291,7 +291,7 @@ describe('creating a heatmap data mapping', () => { ).toThrow(HEATMAP_NOT_SCANLINES_ERROR); expect(() => - getAnnotationMapping( + getExemplarsMapping( { ...heatmap, heatmap: { @@ -306,7 +306,7 @@ describe('creating a heatmap data mapping', () => { ).toThrow(HEATMAP_NOT_SCANLINES_ERROR); expect(() => - getAnnotationMapping( + getExemplarsMapping( { ...heatmap, heatmap: { diff --git a/public/app/plugins/panel/heatmap-new/fields.ts b/public/app/plugins/panel/heatmap-new/fields.ts index 145ad8443fe..433954fdff6 100644 --- a/public/app/plugins/panel/heatmap-new/fields.ts +++ b/public/app/plugins/panel/heatmap-new/fields.ts @@ -32,8 +32,8 @@ export const HEATMAP_NOT_SCANLINES_ERROR = 'A calculated heatmap was expected, b export interface HeatmapData { // List of heatmap frames heatmap?: DataFrame; - annotations?: DataFrame; - annotationMappings?: HeatmapDataMapping; + exemplars?: DataFrame; + exemplarsMappings?: HeatmapDataMapping; yAxisValues?: Array; @@ -61,17 +61,17 @@ export function prepareHeatmapData(data: PanelData, options: PanelOptions, theme const { source } = options; - const annotations = data.annotations?.[0]; // TODO: Maybe join on time with frames + const exemplars = data.annotations?.find((f) => f.name === 'exemplar'); if (source === HeatmapSourceMode.Calculate) { // TODO, check for error etc - return getHeatmapData(calculateHeatmapFromData(frames, options.heatmap ?? {}), annotations, theme); + return getHeatmapData(calculateHeatmapFromData(frames, options.heatmap ?? {}), exemplars, theme); } // Find a well defined heatmap let scanlinesHeatmap = frames.find((f) => f.meta?.type === DataFrameType.HeatmapScanlines); if (scanlinesHeatmap) { - return getHeatmapData(scanlinesHeatmap, annotations, theme); + return getHeatmapData(scanlinesHeatmap, exemplars, theme); } let bucketsHeatmap = frames.find((f) => f.meta?.type === DataFrameType.HeatmapBuckets); @@ -80,16 +80,16 @@ export function prepareHeatmapData(data: PanelData, options: PanelOptions, theme yAxisValues: frames[0].fields.flatMap((field) => field.type === FieldType.number ? getFieldDisplayName(field) : [] ), - ...getHeatmapData(bucketsToScanlines(bucketsHeatmap), annotations, theme), + ...getHeatmapData(bucketsToScanlines(bucketsHeatmap), exemplars, theme), }; } if (source === HeatmapSourceMode.Data) { - return getHeatmapData(bucketsToScanlines(frames[0]), annotations, theme); + return getHeatmapData(bucketsToScanlines(frames[0]), exemplars, theme); } // TODO, check for error etc - return getHeatmapData(calculateHeatmapFromData(frames, options.heatmap ?? {}), annotations, theme); + return getHeatmapData(calculateHeatmapFromData(frames, options.heatmap ?? {}), exemplars, theme); } const getHeatmapFields = (dataFrame: DataFrame): Array => { @@ -100,7 +100,7 @@ const getHeatmapFields = (dataFrame: DataFrame): Array => { return [xField, yField, countField]; }; -export const getAnnotationMapping = (heatmapData: HeatmapData, rawData: DataFrame): HeatmapDataMapping => { +export const getExemplarsMapping = (heatmapData: HeatmapData, rawData: DataFrame): HeatmapDataMapping => { if (heatmapData.heatmap?.meta?.type !== DataFrameType.HeatmapScanlines) { throw HEATMAP_NOT_SCANLINES_ERROR; } @@ -152,7 +152,7 @@ export const getAnnotationMapping = (heatmapData: HeatmapData, rawData: DataFram return mapping; }; -const getHeatmapData = (frame: DataFrame, annotations: DataFrame | undefined, theme: GrafanaTheme2): HeatmapData => { +const getHeatmapData = (frame: DataFrame, exemplars: DataFrame | undefined, theme: GrafanaTheme2): HeatmapData => { if (frame.meta?.type !== DataFrameType.HeatmapScanlines) { return { warning: 'Expected heatmap scanlines format', @@ -190,7 +190,7 @@ const getHeatmapData = (frame: DataFrame, annotations: DataFrame | undefined, th const disp = frame.fields[2].display ?? getValueFormat('short'); const data: HeatmapData = { heatmap: frame, - annotations, + exemplars, xBucketSize: xBinIncr, yBucketSize: yBinIncr, xBucketCount: xBinQty, @@ -203,8 +203,9 @@ const getHeatmapData = (frame: DataFrame, annotations: DataFrame | undefined, th display: (v) => formattedValueToString(disp(v)), }; - if (annotations) { - data.annotationMappings = getAnnotationMapping(data, annotations); + if (exemplars) { + data.exemplarsMappings = getExemplarsMapping(data, exemplars); + console.log('EXEMPLARS', data.exemplarsMappings, data.exemplars); } return data; From 66a0916d001f55979a62d8b2607a6251534b53fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Fri, 6 May 2022 16:21:59 +0200 Subject: [PATCH 099/440] Transformers: mark extract-fields transform as stable (#48810) --- .../extractFields/ExtractFieldsTransformerEditor.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/public/app/features/transformers/extractFields/ExtractFieldsTransformerEditor.tsx b/public/app/features/transformers/extractFields/ExtractFieldsTransformerEditor.tsx index deff6aade82..6d89b83b6fe 100644 --- a/public/app/features/transformers/extractFields/ExtractFieldsTransformerEditor.tsx +++ b/public/app/features/transformers/extractFields/ExtractFieldsTransformerEditor.tsx @@ -3,7 +3,6 @@ import React from 'react'; import { DataTransformerID, FieldNamePickerConfigSettings, - PluginState, SelectableValue, StandardEditorsRegistryItem, TransformerRegistryItem, @@ -91,5 +90,4 @@ export const extractFieldsTransformRegistryItem: TransformerRegistryItem Date: Fri, 6 May 2022 11:06:00 -0400 Subject: [PATCH 100/440] Add new features to DiffReport and Diff (#48788) * simplify String for Diff * add IsAddOperation and IsDeleteOperation to Diff * add method Paths to DiffReport --- pkg/util/cmputil/reporter.go | 44 ++++++++-- pkg/util/cmputil/reporter_test.go | 132 ++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+), 8 deletions(-) create mode 100644 pkg/util/cmputil/reporter_test.go diff --git a/pkg/util/cmputil/reporter.go b/pkg/util/cmputil/reporter.go index c9532d3cc0f..cae35bd8e20 100644 --- a/pkg/util/cmputil/reporter.go +++ b/pkg/util/cmputil/reporter.go @@ -78,6 +78,15 @@ func (r DiffReport) String() string { return b.String() } +// Paths returns the slice of paths of the current DiffReport +func (r DiffReport) Paths() []string { + var result = make([]string, len(r)) + for _, diff := range r { + result = append(result, diff.Path) + } + return result +} + type Diff struct { // Path to the field that has difference separated by period. Array index and key are designated by square brackets. // For example, Annotations[12345].Data.Fields[0].ID @@ -87,15 +96,34 @@ type Diff struct { } func (d *Diff) String() string { - left := d.Left.String() + return fmt.Sprintf("%v:\n\t-: %+v\n\t+: %+v\n", d.Path, describeReflectValue(d.Left), describeReflectValue(d.Right)) +} + +func describeReflectValue(v reflect.Value) interface{} { // invalid reflect.Value is produced when two collections (slices\maps) are compared and one misses value. // This way go-cmp indicates that an element was added\removed from a list. - if !d.Left.IsValid() { - left = "" + if !v.IsValid() { + return "" } - right := d.Right.String() - if !d.Right.IsValid() { - right = "" - } - return fmt.Sprintf("%v:\n\t-: %+v\n\t+: %+v", d.Path, left, right) + return v +} + +// IsAddOperation returns true when +// - Left does not have value and Right has +// - the kind of Left and Right is either reflect.Slice or reflect.Map and the length of Left is less than length of Right +// In all other cases it returns false. +// NOTE: this is applicable to diff of Maps and Slices only +func (d *Diff) IsAddOperation() bool { + return !d.Left.IsValid() && d.Right.IsValid() || (d.Left.Kind() == d.Right.Kind() && // cmp reports adding first element to a nil slice as creation of one and therefore Left is valid and it is nil and right is a new slice + (d.Left.Kind() == reflect.Slice || d.Left.Kind() == reflect.Map) && d.Left.Len() < d.Right.Len()) +} + +// IsDeleteOperation returns true when +// - Right does not have value and Left has +// - the kind of Left and Right is either reflect.Slice or reflect.Map and the length of Right is less than length of Left +// In all other cases it returns false. +// NOTE: this is applicable to diff of Maps and Slices only +func (d *Diff) IsDeleteOperation() bool { + return d.Left.IsValid() && !d.Right.IsValid() || (d.Left.Kind() == d.Right.Kind() && + (d.Left.Kind() == reflect.Slice || d.Left.Kind() == reflect.Map) && d.Left.Len() > d.Right.Len()) } diff --git a/pkg/util/cmputil/reporter_test.go b/pkg/util/cmputil/reporter_test.go new file mode 100644 index 00000000000..1ac3a096c61 --- /dev/null +++ b/pkg/util/cmputil/reporter_test.go @@ -0,0 +1,132 @@ +package cmputil + +import ( + "math/rand" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + ptr "github.com/xorcare/pointer" + + "github.com/grafana/grafana/pkg/util" +) + +type subStruct struct { + Data interface{} +} + +type testStruct struct { + Number float64 + NumberPtr *float64 + Text string + TextPtr *string + Flag bool + FlagPtr *bool + Dict map[float64]float64 + Slice []float64 + SubStruct subStruct + SubStructPtr *subStruct +} + +func testStructDiff(left, right testStruct) DiffReport { + var reporter DiffReporter + ops := make([]cmp.Option, 0, 4) + ops = append(ops, cmp.Reporter(&reporter)) + cmp.Equal(left, right, ops...) + return reporter.Diffs +} + +func TestIsAddedDeleted_Collections(t *testing.T) { + testCases := []struct { + name string + left testStruct + right testStruct + field string + }{ + { + name: "nil vs non-empty slice", + left: testStruct{ + Slice: nil, + }, + right: testStruct{ + Slice: []float64{rand.Float64()}, + }, + field: "Slice", + }, + { + name: "empty vs non-empty slice", + left: testStruct{ + Slice: []float64{}, + }, + right: testStruct{ + Slice: []float64{rand.Float64()}, + }, + field: "Slice", + }, + { + name: "nil vs non-empty map", + left: testStruct{ + Dict: nil, + }, + right: testStruct{ + Dict: map[float64]float64{rand.Float64(): rand.Float64()}, + }, + field: "Slice", + }, + { + name: "empty vs non-empty map", + left: testStruct{ + Dict: map[float64]float64{}, + }, + right: testStruct{ + Dict: map[float64]float64{rand.Float64(): rand.Float64()}, + }, + field: "Slice", + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + left := testCase.left + right := testCase.right + field := testCase.field + t.Run("IsAddOperation=true, IsDeleted=false", func(t *testing.T) { + diff := testStructDiff(left, right) + require.Lenf(t, diff, 1, "diff was expected to have only one field %s but got %v", field, diff.String()) + d := diff[0] + require.Truef(t, d.IsAddOperation(), "diff %v should be treated as Add operation but it wasn't", d) + require.Falsef(t, d.IsDeleteOperation(), "diff %v should not be treated as Delete operation but it was", d) + }) + t.Run("IsDeleted=true, IsAddOperation=false", func(t *testing.T) { + diff := testStructDiff(right, left) + require.Lenf(t, diff, 1, "diff was expected to have only one field %s but got %v", field, diff.String()) + d := diff[0] + require.Truef(t, d.IsDeleteOperation(), "diff %v should be treated as Delete operation but it wasn't", d) + require.Falsef(t, d.IsAddOperation(), "diff %v should not be treated as Delete operation but it was", d) + }) + }) + } + + t.Run("IsAddOperation=false, IsDeleted=false if changes in struct fields", func(t *testing.T) { + left := testStruct{} + right := testStruct{ + Number: rand.Float64(), + NumberPtr: ptr.Float64(rand.Float64()), + Text: util.GenerateShortUID(), + TextPtr: ptr.String(util.GenerateShortUID()), + Flag: true, + FlagPtr: ptr.Bool(true), + SubStruct: subStruct{ + Data: rand.Float64(), + }, + SubStructPtr: &subStruct{Data: rand.Float64()}, + } + + diff := testStructDiff(left, right) + require.Len(t, diff, 8) + for _, d := range diff { + assert.Falsef(t, d.IsAddOperation(), "diff %v was not supposed to be Add operation", d.String()) + assert.Falsef(t, d.IsDeleteOperation(), "diff %v was not supposed to be Delete operation", d.String()) + } + }) +} From 51ff2b8c583a35150e26934902e30a18c48be723 Mon Sep 17 00:00:00 2001 From: Emil Tullstedt Date: Fri, 6 May 2022 17:44:22 +0200 Subject: [PATCH 101/440] Logging: Unify logging fakes (#48822) --- pkg/api/login_test.go | 23 ++------- pkg/api/plugins_test.go | 34 +++++-------- pkg/api/team_test.go | 33 +++++------- pkg/infra/log/logtest/fake.go | 50 +++++++++++++++++++ .../loader/initializer/initializer_test.go | 20 ++------ pkg/plugins/manager/loader/loader_test.go | 32 +++--------- pkg/plugins/manager/manager_test.go | 19 ++----- pkg/services/updatechecker/plugins_test.go | 10 +--- pkg/setting/setting_session_test.go | 25 ++-------- 9 files changed, 99 insertions(+), 147 deletions(-) create mode 100644 pkg/infra/log/logtest/fake.go diff --git a/pkg/api/login_test.go b/pkg/api/login_test.go index 4a1807caa2a..eb151eca511 100644 --- a/pkg/api/login_test.go +++ b/pkg/api/login_test.go @@ -13,6 +13,9 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" @@ -28,8 +31,6 @@ import ( "github.com/grafana/grafana/pkg/services/secrets/fakes" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func fakeSetIndexViewData(t *testing.T) { @@ -65,22 +66,6 @@ func getBody(resp *httptest.ResponseRecorder) (string, error) { return string(responseData), nil } -type FakeLogger struct { - log.Logger -} - -func (fl *FakeLogger) Debug(testMessage string, ctx ...interface{}) { -} - -func (fl *FakeLogger) Info(testMessage string, ctx ...interface{}) { -} - -func (fl *FakeLogger) Warn(testMessage string, ctx ...interface{}) { -} - -func (fl *FakeLogger) Error(testMessage string, ctx ...interface{}) { -} - type redirectCase struct { desc string url string @@ -332,7 +317,7 @@ func TestLoginPostRedirect(t *testing.T) { fakeViewIndex(t) sc := setupScenarioContext(t, "/login") hs := &HTTPServer{ - log: &FakeLogger{}, + log: log.NewNopLogger(), Cfg: setting.NewCfg(), HooksService: &hooks.HooksService{}, License: &licensing.OSSLicensingService{}, diff --git a/pkg/api/plugins_test.go b/pkg/api/plugins_test.go index a28ec025750..9e869285efd 100644 --- a/pkg/api/plugins_test.go +++ b/pkg/api/plugins_test.go @@ -11,6 +11,8 @@ import ( "path/filepath" "testing" + "github.com/grafana/grafana/pkg/infra/log/logtest" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -55,7 +57,7 @@ func Test_GetPluginAssets(t *testing.T) { pluginID: p, }, } - l := &logger{} + l := &logtest.Fake{} url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile) pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, l, @@ -64,7 +66,7 @@ func Test_GetPluginAssets(t *testing.T) { require.Equal(t, 200, sc.resp.Code) assert.Equal(t, expectedBody, sc.resp.Body.String()) - assert.Empty(t, l.warnings) + assert.Zero(t, l.WarnLogs.Calls) }) }) @@ -80,7 +82,7 @@ func Test_GetPluginAssets(t *testing.T) { pluginID: p, }, } - l := &logger{} + l := &logtest.Fake{} url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, tmpFileInParentDir.Name()) pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, l, @@ -103,7 +105,7 @@ func Test_GetPluginAssets(t *testing.T) { pluginID: p, }, } - l := &logger{} + l := &logtest.Fake{} url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile) pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, l, @@ -112,7 +114,7 @@ func Test_GetPluginAssets(t *testing.T) { require.Equal(t, 200, sc.resp.Code) assert.Equal(t, expectedBody, sc.resp.Body.String()) - assert.Empty(t, l.warnings) + assert.Zero(t, l.WarnLogs.Calls) }) }) @@ -128,7 +130,7 @@ func Test_GetPluginAssets(t *testing.T) { pluginID: p, }, } - l := &logger{} + l := &logtest.Fake{} requestedFile := "nonExistent" url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile) @@ -141,7 +143,7 @@ func Test_GetPluginAssets(t *testing.T) { require.NoError(t, err) require.Equal(t, 404, sc.resp.Code) assert.Equal(t, "Plugin file not found", respJson["message"]) - assert.Empty(t, l.warnings) + assert.Zero(t, l.WarnLogs.Calls) }) }) @@ -149,7 +151,7 @@ func Test_GetPluginAssets(t *testing.T) { service := &fakePluginStore{ plugins: map[string]plugins.PluginDTO{}, } - l := &logger{} + l := &logtest.Fake{} requestedFile := "nonExistent" url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile) @@ -162,7 +164,7 @@ func Test_GetPluginAssets(t *testing.T) { require.NoError(t, err) assert.Equal(t, 404, sc.resp.Code) assert.Equal(t, "Plugin not found", respJson["message"]) - assert.Empty(t, l.warnings) + assert.Zero(t, l.WarnLogs.Calls) }) }) @@ -174,7 +176,7 @@ func Test_GetPluginAssets(t *testing.T) { }, }, } - l := &logger{} + l := &logtest.Fake{} url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile) pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, l, @@ -183,7 +185,7 @@ func Test_GetPluginAssets(t *testing.T) { require.Equal(t, 200, sc.resp.Code) assert.Equal(t, expectedBody, sc.resp.Body.String()) - assert.Empty(t, l.warnings) + assert.Zero(t, l.WarnLogs.Calls) }) }) } @@ -235,16 +237,6 @@ func pluginAssetScenario(t *testing.T, desc string, url string, urlPattern strin }) } -type logger struct { - log.Logger - - warnings []string -} - -func (l *logger) Warn(msg string, ctx ...interface{}) { - l.warnings = append(l.warnings, msg) -} - type fakePluginClient struct { plugins.Client diff --git a/pkg/api/team_test.go b/pkg/api/team_test.go index 97621f59056..09395b036af 100644 --- a/pkg/api/team_test.go +++ b/pkg/api/team_test.go @@ -8,7 +8,11 @@ import ( "strings" "testing" - "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/log/logtest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" pref "github.com/grafana/grafana/pkg/services/preference" @@ -17,21 +21,8 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) -type testLogger struct { - log.Logger - warnCalled bool - warnMessage string -} - -func (stub *testLogger) Warn(testMessage string, ctx ...interface{}) { - stub.warnCalled = true - stub.warnMessage = testMessage -} - func TestTeamAPIEndpoint(t *testing.T) { t.Run("Given two teams", func(t *testing.T) { hs := setupSimpleHTTPServer(nil) @@ -123,11 +114,11 @@ func TestTeamAPIEndpoint(t *testing.T) { require.NoError(t, err) t.Run("with no real signed in user", func(t *testing.T) { - stub := &testLogger{} + logger := &logtest.Fake{} c := &models.ReqContext{ Context: &web.Context{Req: req}, SignedInUser: &models.SignedInUser{}, - Logger: stub, + Logger: logger, } c.OrgRole = models.ROLE_EDITOR c.Req.Body = mockRequestBody(models.CreateTeamCommand{Name: teamName}) @@ -135,23 +126,23 @@ func TestTeamAPIEndpoint(t *testing.T) { r := hs.CreateTeam(c) assert.Equal(t, 200, r.Status()) - assert.True(t, stub.warnCalled) - assert.Equal(t, stub.warnMessage, "Could not add creator to team because is not a real user") + assert.NotZero(t, logger.WarnLogs.Calls) + assert.Equal(t, "Could not add creator to team because is not a real user", logger.WarnLogs.Message) }) t.Run("with real signed in user", func(t *testing.T) { - stub := &testLogger{} + logger := &logtest.Fake{} c := &models.ReqContext{ Context: &web.Context{Req: req}, SignedInUser: &models.SignedInUser{UserId: 42}, - Logger: stub, + Logger: logger, } c.OrgRole = models.ROLE_EDITOR c.Req.Body = mockRequestBody(models.CreateTeamCommand{Name: teamName}) c.Req.Header.Add("Content-Type", "application/json") r := hs.CreateTeam(c) assert.Equal(t, 200, r.Status()) - assert.False(t, stub.warnCalled) + assert.Zero(t, logger.WarnLogs.Calls) }) }) } diff --git a/pkg/infra/log/logtest/fake.go b/pkg/infra/log/logtest/fake.go new file mode 100644 index 00000000000..a65f6789382 --- /dev/null +++ b/pkg/infra/log/logtest/fake.go @@ -0,0 +1,50 @@ +package logtest + +import ( + "github.com/grafana/grafana/pkg/infra/log" +) + +type Fake struct { + DebugLogs Logs + InfoLogs Logs + WarnLogs Logs + ErrorLogs Logs +} + +type Logs struct { + Calls int + Message string + Ctx []interface{} +} + +func (f *Fake) New(ctx ...interface{}) *log.ConcreteLogger { + return log.NewNopLogger() +} + +func (f *Fake) Log(keyvals ...interface{}) error { + return nil +} + +func (f *Fake) Debug(msg string, ctx ...interface{}) { + f.DebugLogs.Calls++ + f.DebugLogs.Message = msg + f.DebugLogs.Ctx = ctx +} + +func (f *Fake) Info(msg string, ctx ...interface{}) { + f.InfoLogs.Calls++ + f.InfoLogs.Message = msg + f.InfoLogs.Ctx = ctx +} + +func (f *Fake) Warn(msg string, ctx ...interface{}) { + f.WarnLogs.Calls++ + f.WarnLogs.Message = msg + f.WarnLogs.Ctx = ctx +} + +func (f *Fake) Error(msg string, ctx ...interface{}) { + f.ErrorLogs.Calls++ + f.ErrorLogs.Message = msg + f.ErrorLogs.Ctx = ctx +} diff --git a/pkg/plugins/manager/loader/initializer/initializer_test.go b/pkg/plugins/manager/loader/initializer/initializer_test.go index d63a34384c3..b9281b7282e 100644 --- a/pkg/plugins/manager/loader/initializer/initializer_test.go +++ b/pkg/plugins/manager/loader/initializer/initializer_test.go @@ -35,7 +35,7 @@ func TestInitializer_Initialize(t *testing.T) { i := &Initializer{ cfg: plugins.NewCfg(), - log: &fakeLogger{}, + log: log.NewNopLogger(), backendProvider: &fakeBackendProvider{ plugin: p, }, @@ -65,7 +65,7 @@ func TestInitializer_Initialize(t *testing.T) { i := &Initializer{ cfg: plugins.NewCfg(), - log: fakeLogger{}, + log: log.NewNopLogger(), backendProvider: &fakeBackendProvider{ plugin: p, }, @@ -88,7 +88,7 @@ func TestInitializer_Initialize(t *testing.T) { i := &Initializer{ cfg: &plugins.Cfg{}, - log: fakeLogger{}, + log: log.NewNopLogger(), backendProvider: &fakeBackendProvider{ plugin: p, }, @@ -126,7 +126,7 @@ func TestInitializer_envVars(t *testing.T) { }, }, license: licensing, - log: fakeLogger{}, + log: log.NewNopLogger(), backendProvider: &fakeBackendProvider{ plugin: p, }, @@ -211,18 +211,6 @@ func (*testLicensingService) FeatureEnabled(feature string) bool { return false } -type fakeLogger struct { - *log.ConcreteLogger -} - -func (f fakeLogger) New(_ ...interface{}) *log.ConcreteLogger { - return &log.ConcreteLogger{} -} - -func (f fakeLogger) Warn(_ string, _ ...interface{}) { - -} - type fakeBackendProvider struct { plugins.BackendFactoryProvider diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index ec523fe3646..b7f399c06f3 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -7,12 +7,13 @@ import ( "sort" "testing" + "github.com/grafana/grafana/pkg/infra/log/logtest" + "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin" @@ -422,13 +423,14 @@ func TestLoader_setDefaultNavURL(t *testing.T) { }, }}, } - logger := &fakeLogger{loggedLines: []string{}} + logger := &logtest.Fake{} pluginWithDashboard.SetLogger(logger) t.Run("Default nav URL is not set if dashboard UID field not is set", func(t *testing.T) { setDefaultNavURL(pluginWithDashboard) require.Equal(t, "", pluginWithDashboard.DefaultNavURL) - require.Equal(t, []string{"Included dashboard is missing a UID field"}, logger.loggedLines) + require.NotZero(t, logger.WarnLogs.Calls) + require.Equal(t, "Included dashboard is missing a UID field", logger.WarnLogs.Message) }) t.Run("Default nav URL is set if dashboard UID field is set", func(t *testing.T) { @@ -1137,7 +1139,7 @@ func newLoader(cfg *plugins.Cfg) *Loader { pluginInitializer: initializer.New(cfg, provider.ProvideService(coreplugin.NewRegistry(make(map[string]backendplugin.PluginFactoryFunc))), &fakeLicensingService{}), signatureValidator: signature.NewValidator(signature.NewUnsignedAuthorizer(cfg)), errs: make(map[string]*plugins.SignatureError), - log: &fakeLogger{}, + log: &logtest.Fake{}, } } @@ -1177,25 +1179,3 @@ func (*fakeLicensingService) EnabledFeatures() map[string]bool { func (*fakeLicensingService) FeatureEnabled(feature string) bool { return false } - -type fakeLogger struct { - log.Logger - - loggedLines []string -} - -func (fl *fakeLogger) New(_ ...interface{}) *log.ConcreteLogger { - return &log.ConcreteLogger{} -} - -func (fl *fakeLogger) Info(l string, _ ...interface{}) { - fl.loggedLines = append(fl.loggedLines, l) -} - -func (fl *fakeLogger) Debug(l string, _ ...interface{}) { - fl.loggedLines = append(fl.loggedLines, l) -} - -func (fl *fakeLogger) Warn(l string, _ ...interface{}) { - fl.loggedLines = append(fl.loggedLines, l) -} diff --git a/pkg/plugins/manager/manager_test.go b/pkg/plugins/manager/manager_test.go index b18609f6da9..9b5435fafcb 100644 --- a/pkg/plugins/manager/manager_test.go +++ b/pkg/plugins/manager/manager_test.go @@ -10,11 +10,12 @@ import ( "github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) const ( @@ -544,7 +545,7 @@ func createPlugin(t *testing.T, pluginID, version string, class plugins.Class, m }, } - logger := fakeLogger{} + logger := log.NewNopLogger() p.SetLogger(logger) @@ -757,18 +758,6 @@ func (pc *fakePluginClient) RunStream(_ context.Context, _ *backend.RunStreamReq return backendplugin.ErrMethodNotImplemented } -type fakeLogger struct { - log.Logger -} - -func (l fakeLogger) Info(_ string, _ ...interface{}) { - -} - -func (l fakeLogger) Debug(_ string, _ ...interface{}) { - -} - type fakeSender struct { resp *backend.CallResourceResponse } diff --git a/pkg/services/updatechecker/plugins_test.go b/pkg/services/updatechecker/plugins_test.go index 1f89033f083..29cb69e268b 100644 --- a/pkg/services/updatechecker/plugins_test.go +++ b/pkg/services/updatechecker/plugins_test.go @@ -154,7 +154,7 @@ func TestPluginUpdateChecker_checkForUpdates(t *testing.T) { httpClient: &fakeHTTPClient{ fakeResp: jsonResp, }, - log: &fakeLogger{}, + log: log.NewNopLogger(), } svc.checkForUpdates(context.Background()) @@ -215,11 +215,3 @@ func (pr fakePluginStore) Plugins(_ context.Context, _ ...plugins.Type) []plugin } return result } - -type fakeLogger struct { - log.Logger -} - -func (l *fakeLogger) Debug(_ string, _ ...interface{}) {} - -func (l *fakeLogger) Warn(_ string, _ ...interface{}) {} diff --git a/pkg/setting/setting_session_test.go b/pkg/setting/setting_session_test.go index 7ad622b02e7..07554d87885 100644 --- a/pkg/setting/setting_session_test.go +++ b/pkg/setting/setting_session_test.go @@ -4,26 +4,11 @@ import ( "path/filepath" "testing" - "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/log/logtest" "github.com/stretchr/testify/require" ) -type testLogger struct { - log.Logger - warnCalled bool - warnMessage string -} - -func (stub *testLogger) Warn(testMessage string, ctx ...interface{}) { - stub.warnCalled = true - stub.warnMessage = testMessage -} - -func (stub *testLogger) Info(testMessage string, ctx ...interface{}) { - -} - func TestSessionSettings(t *testing.T) { skipStaticRootValidation = true @@ -31,8 +16,8 @@ func TestSessionSettings(t *testing.T) { cfg := NewCfg() homePath := "../../" - stub := &testLogger{} - cfg.Logger = stub + logger := &logtest.Fake{} + cfg.Logger = logger err := cfg.Load(CommandLineArgs{ HomePath: homePath, @@ -40,7 +25,7 @@ func TestSessionSettings(t *testing.T) { }) require.Nil(t, err) - require.Equal(t, true, stub.warnCalled) - require.Greater(t, len(stub.warnMessage), 0) + require.Equal(t, 1, logger.WarnLogs.Calls) + require.Greater(t, len(logger.WarnLogs.Message), 0) }) } From e16dc72c94efe91dab1e85343e7a1a14aba6a0c6 Mon Sep 17 00:00:00 2001 From: JitaC <70489351+achatterjee-grafana@users.noreply.github.com> Date: Fri, 6 May 2022 13:52:30 -0400 Subject: [PATCH 102/440] Docs:Cleanup alerting docs (#48826) * Remove What's new reference. * Moved messaging templates to under contact points. fixed broken relrefs. * Fixed some more reflrefs * Fixed a few more broken relrefs and adjusted weight --- docs/sources/alerting/_index.md | 7 +++---- docs/sources/alerting/alert-groups.md | 2 +- .../alerting/alerting-rules/alert-annotation-label.md | 2 +- .../{contact-points.md => contact-points/_index.md} | 6 +++--- .../{ => contact-points}/message-templating/_index.md | 6 +++--- .../message-templating/template-data.md | 0 .../message-templating/template-functions.md | 0 docs/sources/alerting/migrating-legacy-alerts.md | 10 +++------- docs/sources/alerting/notifications/_index.md | 6 +++--- docs/sources/alerting/silences.md | 4 ++-- docs/sources/introduction/oss-details.md | 2 +- docs/sources/whatsnew/whats-new-in-v8-0.md | 4 ++-- 12 files changed, 22 insertions(+), 27 deletions(-) rename docs/sources/alerting/{contact-points.md => contact-points/_index.md} (98%) rename docs/sources/alerting/{ => contact-points}/message-templating/_index.md (88%) rename docs/sources/alerting/{ => contact-points}/message-templating/template-data.md (100%) rename docs/sources/alerting/{ => contact-points}/message-templating/template-functions.md (100%) diff --git a/docs/sources/alerting/_index.md b/docs/sources/alerting/_index.md index 55884380259..e2ac8ae6057 100644 --- a/docs/sources/alerting/_index.md +++ b/docs/sources/alerting/_index.md @@ -1,10 +1,10 @@ +++ title = "Alerts" -weight = 455 +weight = 114 aliases = ["/docs/grafana/latest/alerting/", "/docs/grafana/latest/alerting/unified-alerting/difference-old-new/"] +++ -# Grafana alerts +# Grafana alerting Grafana alerts allow you to learn about problems in your systems moments after they occur. Robust and actionable alerts help you identify and resolve issues quickly, minimizing disruption to your services. It centralizes alerting information in a single, searchable view that allows you to: @@ -23,7 +23,6 @@ For new installations or existing installs without alerting configured, Grafana Before you begin, we recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "./fundamentals/_index.md" >}}) of Grafana alerting. Refer to [Fine-grained access control]({{< relref "../enterprise/access-control/_index.md" >}}) in Grafana Enterprise to learn more about controlling access to alerts using fine-grained permissions. -- [What's new in Grafana alerting]({{< relref "./difference-old-new.md" >}}) - [Enable Grafana alerting in OSS]({{< relref "./opt-in.md" >}}) - [Migrating legacy alerts]({{< relref "./migrating-legacy-alerts.md" >}}) - [Create Grafana managed alerting rules]({{< relref "alerting-rules/create-grafana-managed-rule.md" >}}) @@ -31,6 +30,6 @@ Before you begin, we recommend that you familiarize yourself with some of the [f - [View existing alerting rules and manage their current state]({{< relref "alerting-rules/rule-list.md" >}}) - [View the state and health of alerting rules]({{< relref "./fundamentals/state-and-health.md" >}}) - [View alert groupings]({{< relref "./alert-groups.md" >}}) -- [Add or edit an alert contact point]({{< relref "./contact-points.md" >}}) +- [Add or edit an alert contact point]({{< relref "./contact-points/_index.md" >}}) - [Add or edit notification policies]({{< relref "./notifications/_index.md" >}}) - [Add or edit silences]({{< relref "./silences.md" >}}) diff --git a/docs/sources/alerting/alert-groups.md b/docs/sources/alerting/alert-groups.md index d20c064ed1b..8e1950f49b5 100644 --- a/docs/sources/alerting/alert-groups.md +++ b/docs/sources/alerting/alert-groups.md @@ -2,7 +2,7 @@ title = "Alert groups" description = "Alert groups" keywords = ["grafana", "alerting", "alerts", "groups"] -weight = 400 +weight = 445 aliases = ["/docs/grafana/latest/alerting/unified-alerting/alert-groups/"] +++ diff --git a/docs/sources/alerting/alerting-rules/alert-annotation-label.md b/docs/sources/alerting/alerting-rules/alert-annotation-label.md index 19e72834f3d..852f3609b57 100644 --- a/docs/sources/alerting/alerting-rules/alert-annotation-label.md +++ b/docs/sources/alerting/alerting-rules/alert-annotation-label.md @@ -8,7 +8,7 @@ aliases = ["/docs/grafana/latest/alerting/unified-alerting/alerting-rules/alert- # Annotations and labels for alerting rules -Annotations and labels are key value pairs associated with alerts originating from the alerting rule, datasource response, and as a result of alerting rule evaluation. They can be used in alert notifications directly or in [templates]({{< relref "../message-templating/" >}}) and [template functions]({{< relref "../message-templating/template-functions" >}}) to create notification contact dynamically. +Annotations and labels are key value pairs associated with alerts originating from the alerting rule, datasource response, and as a result of alerting rule evaluation. They can be used in alert notifications directly or in [templates]({{< relref "../contact-points/message-templating/_index.md" >}}) and [template functions]({{< relref "../contact-points/message-templating/template-functions" >}}) to create notification contact dynamically. ## Annotations diff --git a/docs/sources/alerting/contact-points.md b/docs/sources/alerting/contact-points/_index.md similarity index 98% rename from docs/sources/alerting/contact-points.md rename to docs/sources/alerting/contact-points/_index.md index f955e8860ed..32c7550a42f 100644 --- a/docs/sources/alerting/contact-points.md +++ b/docs/sources/alerting/contact-points/_index.md @@ -10,7 +10,7 @@ aliases = ["/docs/grafana/latest/alerting/unified-alerting/contact-points/"] Use contact points to define how your contacts are notified when an alert fires. A contact point can have one or more contact point types, for example, email, slack, webhook, and so on. When an alert fires, a notification is sent to all contact point types listed for a contact point. Optionally, use [message templates]({{< relref "./message-templating/_index.md" >}}) to customize notification messages for the contact point types. -You can configure Grafana managed contact points as well as contact points for an [external Alertmanager data source]({{< relref "../datasources/alertmanager.md" >}}). For more information, see [Alertmanager]({{< relref "./fundamentals/alertmanager.md" >}}). +You can configure Grafana managed contact points as well as contact points for an [external Alertmanager data source]({{< relref "../../datasources/alertmanager.md" >}}). For more information, see [Alertmanager]({{< relref "../fundamentals/alertmanager.md" >}}). ## Add a contact point @@ -49,7 +49,7 @@ To send a test notification: 1. Find the contact point to delete, then click **Delete** (trash icon). 1. In the confirmation dialog, click **Yes, delete**. -> **Note:** You cannot delete contact points that are in use by a notification policy. You will have to either delete the [notification policy]({{< relref "./notifications/_index.md" >}}) or update it to use another contact point. +> **Note:** You cannot delete contact points that are in use by a notification policy. You will have to either delete the [notification policy]({{< relref "../notifications/_index.md" >}}) or update it to use another contact point. ## Edit Alertmanager global config @@ -61,7 +61,7 @@ To edit global configuration options for an external Alertmanager, like SMTP ser 1. Add global configuration settings. 1. Click **Save global config** to save your changes. -> **Note** This option is available only for external Alertmanagers. You can configure some global options for Grafana contact types, like email settings, via [Grafana configuration]({{< relref "../administration/configuration.md" >}}). +> **Note** This option is available only for external Alertmanagers. You can configure some global options for Grafana contact types, like email settings, via [Grafana configuration]({{< relref "../../administration/configuration.md" >}}). ## List of notifiers supported by Grafana diff --git a/docs/sources/alerting/message-templating/_index.md b/docs/sources/alerting/contact-points/message-templating/_index.md similarity index 88% rename from docs/sources/alerting/message-templating/_index.md rename to docs/sources/alerting/contact-points/message-templating/_index.md index 92f3a4da3b1..3e2e3b8e213 100644 --- a/docs/sources/alerting/message-templating/_index.md +++ b/docs/sources/alerting/contact-points/message-templating/_index.md @@ -3,12 +3,12 @@ title = "Message templating" description = "Message templating" keywords = ["grafana", "alerting", "guide", "contact point", "templating"] aliases = ["/docs/grafana/latest/alerting/message-templating/", "/docs/grafana/latest/alerting/unified-alerting/message-templating/"] -weight = 440 +weight = 400 +++ # Message templating -Notifications sent via [contact points]({{< relref "../contact-points.md" >}}) are built using messaging templates. Grafana's default templates are based on the [Go templating system](https://golang.org/pkg/text/template) where some fields are evaluated as text, while others are evaluated as HTML (which can affect escaping). The default template, defined in [default_template.go](https://github.com/grafana/grafana/blob/main/pkg/services/ngalert/notifier/channels/default_template.go), is a useful reference for custom templates. +Notifications sent via [contact points]({{< relref "../../contact-points/_index.md" >}}) are built using messaging templates. Grafana's default templates are based on the [Go templating system](https://golang.org/pkg/text/template) where some fields are evaluated as text, while others are evaluated as HTML (which can affect escaping). The default template, defined in [default_template.go](https://github.com/grafana/grafana/blob/main/pkg/services/ngalert/notifier/channels/default_template.go), is a useful reference for custom templates. Since most of the contact point fields can be templated, you can create reusable custom templates and use them in multiple contact points. The [template data]({{< relref "./template-data.md" >}}) topic lists variables that are available for templating. The default template is defined in [default_template.go](https://github.com/grafana/grafana/blob/main/pkg/services/ngalert/notifier/channels/default_template.go) which can serve as a useful reference or starting point for custom templates. @@ -28,7 +28,7 @@ The following example shows the use of a custom template within one of the conta 1. In the Grafana menu, click the **Alerting** (bell) icon to open the Alerting page listing existing alerts. 1. In the Alerting page, click **Contact points** to open the page listing existing contact points. -1. From [Alertmanager]({{< relref "../contact-points.md/#alertmanager" >}}) drop-down, select an external Alertmanager to create and manage templates for the external data source. Otherwise, keep the default option of Grafana. +1. From Alertmanager drop-down, select an external Alertmanager to create and manage templates for the external data source. Otherwise, keep the default option of Grafana. {{< figure max-width="250px" src="/static/img/docs/alerting/unified/contact-points-select-am-8-0.gif" caption="Select Alertmanager" >}} 1. Click **Add template**. 1. In **Name**, add a descriptive name. diff --git a/docs/sources/alerting/message-templating/template-data.md b/docs/sources/alerting/contact-points/message-templating/template-data.md similarity index 100% rename from docs/sources/alerting/message-templating/template-data.md rename to docs/sources/alerting/contact-points/message-templating/template-data.md diff --git a/docs/sources/alerting/message-templating/template-functions.md b/docs/sources/alerting/contact-points/message-templating/template-functions.md similarity index 100% rename from docs/sources/alerting/message-templating/template-functions.md rename to docs/sources/alerting/contact-points/message-templating/template-functions.md diff --git a/docs/sources/alerting/migrating-legacy-alerts.md b/docs/sources/alerting/migrating-legacy-alerts.md index b33e8c58d77..bc231407a2e 100644 --- a/docs/sources/alerting/migrating-legacy-alerts.md +++ b/docs/sources/alerting/migrating-legacy-alerts.md @@ -1,11 +1,11 @@ +++ -title = "Migrating legacy alerts" -description = "Enable Grafana alerts" +title = "Migrating legacy dashboard alerts" +description = "Migrate legacy dashboard alerts" weight = 114 aliases = ["/docs/grafana/latest/alerting/unified-alerting/opt-in/"] +++ -# Migrating legacy alerts to Grafana alerting +# Migrating legacy dashboard alerts When Grafana alerting is enabled or Grafana is upgraded to the latest version, existing legacy dashboard alerts migrate in a format compatible with the Grafana alerting. In the Alerting page of your Grafana instance, you can view the migrated alerts alongside new alerts. @@ -22,10 +22,6 @@ Notification channels are migrated to an Alertmanager configuration with the app Since `Hipchat` and `Sensu` notification channels are no longer supported, legacy alerts associated with these channels are not automatically migrated to Grafana alerting. Assign the legacy alerts to a supported notification channel so that you continue to receive notifications for those alerts. Silences (expiring after one year) are created for all paused dashboard alerts. -### Limitation - -Grafana alerting system can retrieve rules from all available Prometheus, Loki, and Alertmanager data sources. It might not be able to fetch alerting rules from all other supported data sources at this time. - ## Disable Grafana alerts To disable Grafana alerts and enable legacy dashboard alerts: diff --git a/docs/sources/alerting/notifications/_index.md b/docs/sources/alerting/notifications/_index.md index 90ad217b90f..569d2e52859 100644 --- a/docs/sources/alerting/notifications/_index.md +++ b/docs/sources/alerting/notifications/_index.md @@ -2,7 +2,7 @@ title = "Notification policies" description = "Notification policies" keywords = ["grafana", "alerting", "guide", "notification policies", "routes"] -weight = 450 +weight = 440 aliases = ["/docs/grafana/latest/alerting/unified-alerting/notifications/"] +++ @@ -34,7 +34,7 @@ You can configure grouping to be `group_by: [alertname]` (take note that the `en 1. Click **Notification policies**. 1. From the **Alertmanager** dropdown, select an external Alertmanager. By default, the Grafana Alertmanager is selected. 1. In the Root policy section, click **Edit** (pen icon). -1. In **Default contact point**, update the [contact point]({{< relref "../contact-points.md" >}}) to whom notifications should be sent for rules when alert rules do not match any specific policy. +1. In **Default contact point**, update the [contact point]({{< relref "../contact-points/_index.md" >}}) to whom notifications should be sent for rules when alert rules do not match any specific policy. 1. In **Group by**, choose labels to group alerts by. If multiple alerts are matched for this policy, then they are grouped by these labels. A notification is sent per group. If the field is empty (default), then all notifications are sent in a single group. Use a special label `...` to group alerts by all labels (which effectively disables grouping). 1. In **Timing options**, select from the following options: - **Group wait** Time to wait to buffer alerts of the same group before sending an initial notification. Default is 30 seconds. @@ -49,7 +49,7 @@ You can configure grouping to be `group_by: [alertname]` (take note that the `en 1. From the **Alertmanager** dropdown, select an Alertmanager. By default, the Grafana Alertmanager is selected. 1. To add a top level specific policy, go to the **Specific routing** section and click **New specific policy**. 1. In **Matching labels** section, add one or more rules for matching alert labels. For more information, see ["How label matching works"](#how-label-matching-works). -1. In **Contact point**, add the [contact point]({{< relref "../contact-points.md" >}}) to send notification to if alert matches only this specific policy and not any of the nested policies. +1. In **Contact point**, add the [contact point]({{< relref "../contact-points/_index.md" >}}) to send notification to if alert matches only this specific policy and not any of the nested policies. 1. Optionally, enable **Continue matching subsequent sibling nodes** to continue matching nested policies even after the alert matched the parent policy. When this option is enabled, you can get more than one notification. Use it to send notification to a catch-all contact point as well as to one of more specific contact points handled by nested policies. 1. Optionally, enable **Override grouping** to specify the same grouping as the root policy. If this option is not enabled, the root policy grouping is used. 1. Optionally, enable **Override general timings** to override the timing options configured in the group notification policy. diff --git a/docs/sources/alerting/silences.md b/docs/sources/alerting/silences.md index fc090985204..875ed9fb059 100644 --- a/docs/sources/alerting/silences.md +++ b/docs/sources/alerting/silences.md @@ -2,7 +2,7 @@ title = "Silences" description = "Silences alert notifications" keywords = ["grafana", "alerting", "silence", "mute"] -weight = 400 +weight = 450 aliases = ["/docs/grafana/latest/alerting/unified-alerting/silences/"] +++ @@ -22,7 +22,7 @@ To add a silence: 1. In the Grafana menu, click the **Alerting** (bell) icon to open the Alerting page listing existing alerts. 1. In the Alerting page, click **Silences** to open the page listing existing contact points. -1. From [Alertmanager]({{< relref "./contact-points.md/#alertmanager" >}}) drop-down, select an external Alertmanager to create and manage silences for the external data source. Otherwise, keep the default option of Grafana. +1. From Alertmanager drop-down, select an external Alertmanager to create and manage silences for the external data source. Otherwise, keep the default option of Grafana. 1. Click **New Silence** to open the Create silence page. 1. In **Silence start and end**, select the start and end date to indicate when the silence should go into effect and expire. 1. Optionally, in **Duration**, specify how long the silence is enforced. This automatically updates the end time in the **Silence start and end** field. diff --git a/docs/sources/introduction/oss-details.md b/docs/sources/introduction/oss-details.md index 6e24d132225..e034a0e3946 100644 --- a/docs/sources/introduction/oss-details.md +++ b/docs/sources/introduction/oss-details.md @@ -18,7 +18,7 @@ Explore your data through ad-hoc queries and dynamic drilldown. Split view and c ## Alerts -If you're using Grafana alerting, then you can have alerts sent through a number of different [alert notifiers]({{< relref "../alerting/contact-points.md#list-of-notifiers-supported-by-grafana" >}}), including PagerDuty, SMS, email, VictorOps, OpsGenie, or Slack. +If you're using Grafana alerting, then you can have alerts sent through a number of different [alert notifiers]({{< relref "../alerting/contact-points/_index.md#list-of-notifiers-supported-by-grafana" >}}), including PagerDuty, SMS, email, VictorOps, OpsGenie, or Slack. Alert hooks allow you to create different notifiers with a bit of code if you prefer some other channels of communication. Visually define [alert rules]({{< relref "../alerting/alerting-rules/_index.md" >}}) for your most important metrics. diff --git a/docs/sources/whatsnew/whats-new-in-v8-0.md b/docs/sources/whatsnew/whats-new-in-v8-0.md index 97486fb05f6..62c33befa46 100644 --- a/docs/sources/whatsnew/whats-new-in-v8-0.md +++ b/docs/sources/whatsnew/whats-new-in-v8-0.md @@ -18,13 +18,13 @@ These features are included in the Grafana open source edition. ### Grafana v8.0 alerts -The new alerts in Grafana 8.0 are an opt-in feature that centralizes alerting information for Grafana managed alerts and alerts from Prometheus-compatible data sources in one UI and API. You can create and edit alerting rules for Grafana managed alerts, Cortex alerts, and Loki alerts as well as see alerting information from prometheus-compatible data sources in a single, searchable view. For more information, on how to create and edit alerts and notifications, refer to [Overview of Grafana 8.0 alerts]({{< relref "../alerting/_index.md" >}}). +The new alerts in Grafana 8.0 are an opt-in feature that centralizes alerting information for Grafana managed alerts and alerts from Prometheus-compatible data sources in one UI and API. You can create and edit alerting rules for Grafana managed alerts, Mimir alerts, and Loki alerts as well as see alerting information from prometheus-compatible data sources in a single, searchable view. For more information, on how to create and edit alerts and notifications, refer to [Grafana alerting]({{< relref "../alerting/_index.md" >}}). As part of the new alert changes, we have introduced a new data source, Alertmanager, which includes built-in support for Prometheus Alertmanager. It is presently in alpha and it not accessible unless alpha plugins are enabled in Grafana settings. For more information, refer to [Alertmanager data source]({{< relref "../datasources/alertmanager.md" >}}). > **Note:** Out of the box, Grafana still supports old Grafana alerts. They are legacy alerts at this time, and will be deprecated in a future release. -To learn more about the differences between new alerts and the legacy alerts, refer to [What's New with Grafana 8 Alerts]({{< relref "../alerting/difference-old-new.md" >}}). +To learn more about the differences between new alerts and the legacy alerts, refer to [What's New with Grafana 8 Alerts](https://grafana.com/docs/grafana/latest/alerting/unified-alerting/difference-old-new/). ### Library panels From 30d9cc81ec0b54de39a480989bc0a35270277ae7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Fri, 6 May 2022 20:55:27 +0200 Subject: [PATCH 103/440] Alerting: check provenance of alert rules in current API (#48694) --- pkg/services/ngalert/api/api_ruler.go | 125 ++++++++++++++++----- pkg/services/ngalert/api/api_ruler_test.go | 52 +++++++++ 2 files changed, 151 insertions(+), 26 deletions(-) diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index d7a08fe8b4a..c122a19c1be 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -67,7 +67,12 @@ func (srv RulerSrv) RouteDeleteAlertRules(c *models.ReqContext) response.Respons return accesscontrol.HasAccess(srv.ac, c)(accesscontrol.ReqOrgAdminOrEditor, evaluator) } - var canDelete, cannotDelete []string + provenances, err := srv.provenanceStore.GetProvenances(c.Req.Context(), c.SignedInUser.OrgId, (&ngmodels.AlertRule{}).ResourceType()) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "failed to fetch provenances of alert rules") + } + + var deletableRules []string err = srv.xactManager.InTransaction(c.Req.Context(), func(ctx context.Context) error { q := ngmodels.ListAlertRulesQuery{ OrgID: c.SignedInUser.OrgId, @@ -83,24 +88,52 @@ func (srv RulerSrv) RouteDeleteAlertRules(c *models.ReqContext) response.Respons return nil } - canDelete = make([]string, 0, len(q.Result)) - for _, rule := range q.Result { - if authorizeDatasourceAccessForRule(rule, hasAccess) { - canDelete = append(canDelete, rule.UID) - continue + var canDelete []*ngmodels.AlertRule + var cannotDelete []string + + // partition will partation the given rules in two, one partition + // being the rules that fulfill the predicate the other partation being + // the ruleIDs not fulfilling it. + partition := func(alerts []*ngmodels.AlertRule, predicate func(rule *ngmodels.AlertRule) bool) ([]*ngmodels.AlertRule, []string) { + positive, negative := make([]*ngmodels.AlertRule, 0, len(alerts)), make([]string, 0, len(alerts)) + for _, rule := range alerts { + if predicate(rule) { + positive = append(positive, rule) + continue + } + negative = append(negative, rule.UID) } - cannotDelete = append(cannotDelete, rule.UID) + return positive, negative } + canDelete, cannotDelete = partition(q.Result, func(rule *ngmodels.AlertRule) bool { + return authorizeDatasourceAccessForRule(rule, hasAccess) + }) if len(canDelete) == 0 { return fmt.Errorf("%w to delete rules because user is not authorized to access data sources used by the rules", ErrAuthorization) } - if len(cannotDelete) > 0 { logger.Info("user cannot delete one or many alert rules because it does not have access to data sources. Those rules will be skipped", "expected", len(q.Result), "authorized", len(canDelete), "unauthorized", cannotDelete) } - return srv.store.DeleteAlertRulesByUID(ctx, c.SignedInUser.OrgId, canDelete...) + canDelete, cannotDelete = partition(canDelete, func(rule *ngmodels.AlertRule) bool { + provenance, exists := provenances[rule.UID] + return (exists && provenance == ngmodels.ProvenanceNone) || !exists + }) + + if len(canDelete) == 0 { + return fmt.Errorf("all rules have been provisioned and cannot be deleted through this api") + } + + if len(cannotDelete) > 0 { + logger.Info("user cannot delete one or many alert rules because it does have a provenance set. Those rules will be skipped", "expected", len(q.Result), "provenance_none", len(canDelete), "provenance_set", cannotDelete) + } + + for _, rule := range canDelete { + deletableRules = append(deletableRules, rule.UID) + } + + return srv.store.DeleteAlertRulesByUID(ctx, c.SignedInUser.OrgId, deletableRules...) }) if err != nil { @@ -112,7 +145,7 @@ func (srv RulerSrv) RouteDeleteAlertRules(c *models.ReqContext) response.Respons logger.Debug("rules have been deleted from the store. updating scheduler") - for _, uid := range canDelete { + for _, uid := range deletableRules { srv.scheduleService.DeleteAlertRule(ngmodels.AlertRuleKey{ OrgID: c.SignedInUser.OrgId, UID: uid, @@ -335,8 +368,9 @@ func (srv RulerSrv) RoutePostNameRulesConfig(c *models.ReqContext, ruleGroupConf // updateAlertRulesInGroup calculates changes (rules to add,update,delete), verifies that the user is authorized to do the calculated changes and updates database. // All operations are performed in a single transaction +//nolint: gocyclo func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, namespace *models.Folder, groupName string, rules []*ngmodels.AlertRule) response.Response { - var authorizedChanges *changes + var finalChanges *changes hasAccess := accesscontrol.HasAccess(srv.ac, c) err := srv.xactManager.InTransaction(c.Req.Context(), func(tranCtx context.Context) error { logger := srv.log.New("namespace_uid", namespace.Uid, "group", groupName, "org_id", c.OrgId, "user_id", c.UserId) @@ -347,12 +381,12 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, namespace *mod } if groupChanges.isEmpty() { - authorizedChanges = groupChanges + finalChanges = groupChanges logger.Info("no changes detected in the request. Do nothing") return nil } - authorizedChanges, err = authorizeRuleChanges(namespace, groupChanges, func(evaluator accesscontrol.Evaluator) bool { + authorizedChanges, err := authorizeRuleChanges(namespace, groupChanges, func(evaluator accesscontrol.Evaluator) bool { return hasAccess(accesscontrol.ReqOrgAdminOrEditor, evaluator) }) if err != nil { @@ -368,19 +402,58 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, namespace *mod logger.Info("user is not authorized to delete one or many rules in the group. those rules will be skipped", "expected", len(groupChanges.Delete), "authorized", len(authorizedChanges.Delete)) } - logger.Debug("updating database with the authorized changes", "add", len(authorizedChanges.New), "update", len(authorizedChanges.New), "delete", len(authorizedChanges.Delete)) + provenances, err := srv.provenanceStore.GetProvenances(c.Req.Context(), c.OrgId, (&ngmodels.AlertRule{}).ResourceType()) + if err != nil { + return err + } - if len(authorizedChanges.Update) > 0 || len(authorizedChanges.New) > 0 { - updates := make([]store.UpdateRule, 0, len(authorizedChanges.Update)) - inserts := make([]ngmodels.AlertRule, 0, len(authorizedChanges.New)) - for _, update := range authorizedChanges.Update { + // New rules don't need to be checked for provenance, just copy the whole slice. + finalChanges = &changes{} + finalChanges.New = authorizedChanges.New + for _, rule := range authorizedChanges.Update { + if provenance, exists := provenances[rule.Existing.UID]; (exists && provenance == ngmodels.ProvenanceNone) || !exists { + finalChanges.Update = append(finalChanges.Update, rule) + } + } + for _, rule := range authorizedChanges.Delete { + if provenance, exists := provenances[rule.UID]; (exists && provenance == ngmodels.ProvenanceNone) || !exists { + finalChanges.Delete = append(finalChanges.Delete, rule) + } + } + + if finalChanges.isEmpty() { + logger.Info("no changes detected that have 'none' provenance in the request. Do nothing", + "provenance_invalid_add", len(authorizedChanges.New), + "provenance_invalid_update", len(authorizedChanges.Update), + "provenance_invalid_delete", len(authorizedChanges.Delete)) + return nil + } + + if len(authorizedChanges.Delete) > len(finalChanges.Delete) { + logger.Info("provenance is not 'none' for one or many rules in the group that should be deleted. those rules will be skipped", + "expected", len(authorizedChanges.Delete), + "allowed", len(authorizedChanges.Delete)) + } + + if len(authorizedChanges.Update) > len(finalChanges.Update) { + logger.Info("provenance is not 'none' for one or many rules in the group that should be updated. those rules will be skipped", + "expected", len(authorizedChanges.Update), + "allowed", len(authorizedChanges.Update)) + } + + logger.Debug("updating database with the authorized changes", "add", len(finalChanges.New), "update", len(finalChanges.New), "delete", len(finalChanges.Delete)) + + if len(finalChanges.Update) > 0 || len(finalChanges.New) > 0 { + updates := make([]store.UpdateRule, 0, len(finalChanges.Update)) + inserts := make([]ngmodels.AlertRule, 0, len(finalChanges.New)) + for _, update := range finalChanges.Update { logger.Debug("updating rule", "rule_uid", update.New.UID, "diff", update.Diff.String()) updates = append(updates, store.UpdateRule{ Existing: update.Existing, New: *update.New, }) } - for _, rule := range authorizedChanges.New { + for _, rule := range finalChanges.New { inserts = append(inserts, *rule) } err = srv.store.InsertAlertRules(tranCtx, inserts) @@ -393,9 +466,9 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, namespace *mod } } - if len(authorizedChanges.Delete) > 0 { - UIDs := make([]string, 0, len(authorizedChanges.Delete)) - for _, rule := range authorizedChanges.Delete { + if len(finalChanges.Delete) > 0 { + UIDs := make([]string, 0, len(finalChanges.Delete)) + for _, rule := range finalChanges.Delete { UIDs = append(UIDs, rule.UID) } @@ -404,7 +477,7 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, namespace *mod } } - if len(authorizedChanges.New) > 0 { + if len(finalChanges.New) > 0 { limitReached, err := srv.QuotaService.CheckQuotaReached(tranCtx, "alert_rule", "a.ScopeParameters{ OrgId: c.OrgId, UserId: c.UserId, @@ -432,21 +505,21 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, namespace *mod return ErrResp(http.StatusInternalServerError, err, "failed to update rule group") } - for _, rule := range authorizedChanges.Update { + for _, rule := range finalChanges.Update { srv.scheduleService.UpdateAlertRule(ngmodels.AlertRuleKey{ OrgID: c.SignedInUser.OrgId, UID: rule.Existing.UID, }) } - for _, rule := range authorizedChanges.Delete { + for _, rule := range finalChanges.Delete { srv.scheduleService.DeleteAlertRule(ngmodels.AlertRuleKey{ OrgID: c.SignedInUser.OrgId, UID: rule.UID, }) } - if authorizedChanges.isEmpty() { + if finalChanges.isEmpty() { return response.JSON(http.StatusAccepted, util.DynMap{"message": "no changes detected in the rule group"}) } diff --git a/pkg/services/ngalert/api/api_ruler_test.go b/pkg/services/ngalert/api/api_ruler_test.go index f0bfbb70c0a..1f64133b49b 100644 --- a/pkg/services/ngalert/api/api_ruler_test.go +++ b/pkg/services/ngalert/api/api_ruler_test.go @@ -376,6 +376,32 @@ func TestRouteDeleteAlertRules(t *testing.T) { require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body())) assertRulesDeleted(t, rulesInFolderInGroup, ruleStore, scheduler) }) + t.Run("editor shouldn't be able to delete provisioned rules", func(t *testing.T) { + ruleStore := store.NewFakeRuleStore(t) + orgID := rand.Int63() + folder := randFolder() + ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder) + rulesInFolder := models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder))) + ruleStore.PutRule(context.Background(), rulesInFolder...) + ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID)))...) + + scheduler := &schedule.FakeScheduleService{} + scheduler.On("DeleteAlertRule", mock.Anything) + + ac := acMock.New().WithDisabled() + + svc := createService(ac, ruleStore, scheduler) + + err := svc.provenanceStore.SetProvenance(context.Background(), rulesInFolder[0], orgID, models.ProvenanceAPI) + require.NoError(t, err) + + request := createRequestContext(orgID, models2.ROLE_EDITOR, map[string]string{ + ":Namespace": folder.Title, + }) + response := svc.RouteDeleteAlertRules(request) + require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body())) + assertRulesDeleted(t, rulesInFolder[1:], ruleStore, scheduler) + }) }) t.Run("when fine-grained access is enabled", func(t *testing.T) { t.Run("and user does not have access to any of data sources used by alert rules", func(t *testing.T) { @@ -421,6 +447,32 @@ func TestRouteDeleteAlertRules(t *testing.T) { require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body())) assertRulesDeleted(t, rulesInFolder, ruleStore, scheduler) }) + t.Run("shouldn't be able to delete provisioned rules", func(t *testing.T) { + ruleStore := store.NewFakeRuleStore(t) + orgID := rand.Int63() + folder := randFolder() + ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder) + rulesInFolder := models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder))) + ruleStore.PutRule(context.Background(), rulesInFolder...) + ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID)))...) + + scheduler := &schedule.FakeScheduleService{} + scheduler.On("DeleteAlertRule", mock.Anything) + + ac := acMock.New().WithPermissions(createPermissionsForRules(rulesInFolder)) + svc := createService(ac, ruleStore, scheduler) + + err := svc.provenanceStore.SetProvenance(context.Background(), rulesInFolder[0], orgID, models.ProvenanceAPI) + require.NoError(t, err) + + request := createRequestContext(orgID, "None", map[string]string{ + ":Namespace": folder.Title, + }) + + response := svc.RouteDeleteAlertRules(request) + require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body())) + assertRulesDeleted(t, rulesInFolder[1:], ruleStore, scheduler) + }) }) t.Run("and user has access to data sources of some of alert rules", func(t *testing.T) { t.Run("should delete only those that are accessible in folder", func(t *testing.T) { From bb66c03f9ab78d8ed6f3c54bb1d31aa6a972fc0d Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> Date: Fri, 6 May 2022 22:05:02 +0300 Subject: [PATCH 104/440] Alerting: modify prometheus endpoints for proxying using the datasource UID (#48052) * Modify prometheus endpoints to expect the data source UID * Update frontend --- pkg/services/ngalert/api/authorization.go | 8 ++-- pkg/services/ngalert/api/forked_prom.go | 4 +- .../api/generated_base_api_prometheus.go | 12 +++--- pkg/services/ngalert/api/lotex_prom.go | 9 ++--- .../api/tooling/definitions/alertmanager.go | 4 +- .../ngalert/api/tooling/definitions/prom.go | 4 +- pkg/services/ngalert/api/tooling/post.json | 38 +++++++++---------- pkg/services/ngalert/api/tooling/spec.json | 32 ++++++++-------- pkg/services/ngalert/api/util.go | 17 --------- public/api-merged.json | 33 ++++++++-------- .../alerting/unified/api/prometheus.ts | 4 +- 11 files changed, 73 insertions(+), 92 deletions(-) diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index ffbf663b101..51c62b3d986 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -88,8 +88,8 @@ func (api *API) authorize(method, path string) web.Handler { eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalWrite, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID"))) // Lotex Prometheus-compatible Paths - case http.MethodGet + "/api/prometheus/{DatasourceID}/api/v1/rules": - eval = ac.EvalPermission(ac.ActionAlertingRuleExternalRead, datasources.ScopeProvider.GetResourceScope(ac.Parameter(":DatasourceID"))) + case http.MethodGet + "/api/prometheus/{DatasourceUID}/api/v1/rules": + eval = ac.EvalPermission(ac.ActionAlertingRuleExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID"))) // Lotex Rules testing case http.MethodPost + "/api/v1/rule/test/{DatasourceID}": @@ -140,8 +140,8 @@ func (api *API) authorize(method, path string) web.Handler { eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalWrite, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID"))) // Prometheus-compatible Paths - case http.MethodGet + "/api/prometheus/{DatasourceID}/api/v1/alerts": - eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalRead, datasources.ScopeProvider.GetResourceScope(ac.Parameter(":DatasourceID"))) + case http.MethodGet + "/api/prometheus/{DatasourceUID}/api/v1/alerts": + eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID"))) // Notification Policies, Contact Points and Templates diff --git a/pkg/services/ngalert/api/forked_prom.go b/pkg/services/ngalert/api/forked_prom.go index a0fdf927b69..b0a617ff5fb 100644 --- a/pkg/services/ngalert/api/forked_prom.go +++ b/pkg/services/ngalert/api/forked_prom.go @@ -25,7 +25,7 @@ func NewForkedProm(datasourceCache datasources.CacheService, proxy *LotexProm, g } func (f *ForkedPrometheusApi) forkRouteGetAlertStatuses(ctx *models.ReqContext) response.Response { - t, err := backendType(ctx, f.DatasourceCache) + t, err := backendTypeByUID(ctx, f.DatasourceCache) if err != nil { return ErrResp(400, err, "") } @@ -39,7 +39,7 @@ func (f *ForkedPrometheusApi) forkRouteGetAlertStatuses(ctx *models.ReqContext) } func (f *ForkedPrometheusApi) forkRouteGetRuleStatuses(ctx *models.ReqContext) response.Response { - t, err := backendType(ctx, f.DatasourceCache) + t, err := backendTypeByUID(ctx, f.DatasourceCache) if err != nil { return ErrResp(400, err, "") } diff --git a/pkg/services/ngalert/api/generated_base_api_prometheus.go b/pkg/services/ngalert/api/generated_base_api_prometheus.go index dc6c8327730..fd4cadcb18e 100644 --- a/pkg/services/ngalert/api/generated_base_api_prometheus.go +++ b/pkg/services/ngalert/api/generated_base_api_prometheus.go @@ -43,11 +43,11 @@ func (f *ForkedPrometheusApi) RouteGetRuleStatuses(ctx *models.ReqContext) respo func (api *API) RegisterPrometheusApiEndpoints(srv PrometheusApiForkingService, m *metrics.API) { api.RouteRegister.Group("", func(group routing.RouteRegister) { group.Get( - toMacaronPath("/api/prometheus/{DatasourceID}/api/v1/alerts"), - api.authorize(http.MethodGet, "/api/prometheus/{DatasourceID}/api/v1/alerts"), + toMacaronPath("/api/prometheus/{DatasourceUID}/api/v1/alerts"), + api.authorize(http.MethodGet, "/api/prometheus/{DatasourceUID}/api/v1/alerts"), metrics.Instrument( http.MethodGet, - "/api/prometheus/{DatasourceID}/api/v1/alerts", + "/api/prometheus/{DatasourceUID}/api/v1/alerts", srv.RouteGetAlertStatuses, m, ), @@ -73,11 +73,11 @@ func (api *API) RegisterPrometheusApiEndpoints(srv PrometheusApiForkingService, ), ) group.Get( - toMacaronPath("/api/prometheus/{DatasourceID}/api/v1/rules"), - api.authorize(http.MethodGet, "/api/prometheus/{DatasourceID}/api/v1/rules"), + toMacaronPath("/api/prometheus/{DatasourceUID}/api/v1/rules"), + api.authorize(http.MethodGet, "/api/prometheus/{DatasourceUID}/api/v1/rules"), metrics.Instrument( http.MethodGet, - "/api/prometheus/{DatasourceID}/api/v1/rules", + "/api/prometheus/{DatasourceUID}/api/v1/rules", srv.RouteGetRuleStatuses, m, ), diff --git a/pkg/services/ngalert/api/lotex_prom.go b/pkg/services/ngalert/api/lotex_prom.go index 011885e309d..f94797a4294 100644 --- a/pkg/services/ngalert/api/lotex_prom.go +++ b/pkg/services/ngalert/api/lotex_prom.go @@ -3,7 +3,6 @@ package api import ( "fmt" "net/http" - "strconv" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" @@ -78,12 +77,12 @@ func (p *LotexProm) RouteGetRuleStatuses(ctx *models.ReqContext) response.Respon } func (p *LotexProm) getEndpoints(ctx *models.ReqContext) (*promEndpoints, error) { - datasourceID, err := strconv.ParseInt(web.Params(ctx.Req)[":DatasourceID"], 10, 64) - if err != nil { - return nil, fmt.Errorf("datasource ID is invalid") + datasourceUID := web.Params(ctx.Req)[":DatasourceUID"] + if datasourceUID == "" { + return nil, fmt.Errorf("datasource UID is invalid") } - ds, err := p.DataProxy.DataSourceCache.GetDatasource(ctx.Req.Context(), datasourceID, ctx.SignedInUser, ctx.SkipCache) + ds, err := p.DataProxy.DataSourceCache.GetDatasourceByUID(ctx.Req.Context(), datasourceUID, ctx.SignedInUser, ctx.SkipCache) if err != nil { return nil, err } diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go index 915e601c303..084aab24421 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go @@ -438,8 +438,6 @@ type BodyAlertingConfig struct { Body PostableUserConfig } -// prom routes -// swagger:parameters RouteGetRuleStatuses RouteGetAlertStatuses // testing routes // swagger:parameters RouteTestReceiverConfig RouteTestRuleConfig type DatasourceIDReference struct { @@ -450,6 +448,8 @@ type DatasourceIDReference struct { // alertmanager routes // swagger:parameters RoutePostAlertingConfig RouteGetAlertingConfig RouteDeleteAlertingConfig RouteGetAMStatus RouteGetAMAlerts RoutePostAMAlerts RouteGetAMAlertGroups RouteGetSilences RouteCreateSilence RouteGetSilence RouteDeleteSilence RoutePostAlertingConfig RoutePostTestReceivers +// prom routes +// swagger:parameters RouteGetRuleStatuses RouteGetAlertStatuses // ruler routes // swagger:parameters RouteGetRulesConfig RoutePostNameRulesConfig RouteGetNamespaceRulesConfig RouteDeleteNamespaceRulesConfig RouteGetRulegGroupConfig RouteDeleteRuleGroupConfig type DatasourceUIDReference struct { diff --git a/pkg/services/ngalert/api/tooling/definitions/prom.go b/pkg/services/ngalert/api/tooling/definitions/prom.go index 1ba9f835178..c5519eb2fe4 100644 --- a/pkg/services/ngalert/api/tooling/definitions/prom.go +++ b/pkg/services/ngalert/api/tooling/definitions/prom.go @@ -13,7 +13,7 @@ import ( // Responses: // 200: RuleResponse -// swagger:route GET /api/prometheus/{DatasourceID}/api/v1/rules prometheus RouteGetRuleStatuses +// swagger:route GET /api/prometheus/{DatasourceUID}/api/v1/rules prometheus RouteGetRuleStatuses // // gets the evaluation statuses of all rules // @@ -27,7 +27,7 @@ import ( // Responses: // 200: AlertResponse -// swagger:route GET /api/prometheus/{DatasourceID}/api/v1/alerts prometheus RouteGetAlertStatuses +// swagger:route GET /api/prometheus/{DatasourceUID}/api/v1/alerts prometheus RouteGetAlertStatuses // // gets the current alerts // diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index acdb3595265..14a483eaa66 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -2883,7 +2883,6 @@ "x-go-package": "github.com/prometheus/alertmanager/timeinterval" }, "URL": { - "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.", "properties": { "ForceQuery": { "type": "boolean" @@ -2916,9 +2915,9 @@ "$ref": "#/definitions/Userinfo" } }, - "title": "A URL represents a parsed URL (technically, a URI reference).", + "title": "URL is a custom URL type that allows validation at configuration load time.", "type": "object", - "x-go-package": "net/url" + "x-go-package": "github.com/prometheus/common/config" }, "Userinfo": { "description": "The Userinfo type is an immutable encapsulation of username and\npassword details for a URL. An existing Userinfo value is guaranteed\nto have a username set (potentially empty, as allowed by RFC 2396),\nand optionally a password.", @@ -3115,7 +3114,6 @@ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "alertGroup": { - "description": "AlertGroup alert group", "properties": { "alerts": { "description": "alerts", @@ -3137,7 +3135,9 @@ "labels", "receiver" ], - "type": "object" + "type": "object", + "x-go-name": "AlertGroup", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "alertGroups": { "items": { @@ -3328,11 +3328,12 @@ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "gettableAlerts": { - "description": "GettableAlerts gettable alerts", "items": { "$ref": "#/definitions/gettableAlert" }, - "type": "array" + "type": "array", + "x-go-name": "GettableAlerts", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "gettableSilence": { "description": "GettableSilence gettable silence", @@ -3524,7 +3525,6 @@ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "postableSilence": { - "description": "PostableSilence postable silence", "properties": { "comment": { "description": "comment", @@ -3564,7 +3564,9 @@ "matchers", "startsAt" ], - "type": "object" + "type": "object", + "x-go-name": "PostableSilence", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "receiver": { "description": "Receiver receiver", @@ -4745,18 +4747,17 @@ ] } }, - "/api/prometheus/{DatasourceID}/api/v1/alerts": { + "/api/prometheus/{DatasourceUID}/api/v1/alerts": { "get": { "description": "gets the current alerts", "operationId": "RouteGetAlertStatuses", "parameters": [ { - "description": "DatasourceID should be the numeric datasource identifier", - "format": "int64", + "description": "DatasoureUID should be the datasource UID identifier", "in": "path", - "name": "DatasourceID", + "name": "DatasourceUID", "required": true, - "type": "integer" + "type": "string" } ], "responses": { @@ -4772,18 +4773,17 @@ ] } }, - "/api/prometheus/{DatasourceID}/api/v1/rules": { + "/api/prometheus/{DatasourceUID}/api/v1/rules": { "get": { "description": "gets the evaluation statuses of all rules", "operationId": "RouteGetRuleStatuses", "parameters": [ { - "description": "DatasourceID should be the numeric datasource identifier", - "format": "int64", + "description": "DatasoureUID should be the datasource UID identifier", "in": "path", - "name": "DatasourceID", + "name": "DatasourceUID", "required": true, - "type": "integer" + "type": "string" } ], "responses": { diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 656d563f1e2..07f858c16b9 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -1070,7 +1070,7 @@ } } }, - "/api/prometheus/{DatasourceID}/api/v1/alerts": { + "/api/prometheus/{DatasourceUID}/api/v1/alerts": { "get": { "description": "gets the current alerts", "tags": [ @@ -1079,10 +1079,9 @@ "operationId": "RouteGetAlertStatuses", "parameters": [ { - "type": "integer", - "format": "int64", - "description": "DatasourceID should be the numeric datasource identifier", - "name": "DatasourceID", + "type": "string", + "description": "DatasoureUID should be the datasource UID identifier", + "name": "DatasourceUID", "in": "path", "required": true } @@ -1097,7 +1096,7 @@ } } }, - "/api/prometheus/{DatasourceID}/api/v1/rules": { + "/api/prometheus/{DatasourceUID}/api/v1/rules": { "get": { "description": "gets the evaluation statuses of all rules", "tags": [ @@ -1106,10 +1105,9 @@ "operationId": "RouteGetRuleStatuses", "parameters": [ { - "type": "integer", - "format": "int64", - "description": "DatasourceID should be the numeric datasource identifier", - "name": "DatasourceID", + "type": "string", + "description": "DatasoureUID should be the datasource UID identifier", + "name": "DatasourceUID", "in": "path", "required": true } @@ -4864,9 +4862,8 @@ "x-go-package": "github.com/prometheus/alertmanager/timeinterval" }, "URL": { - "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.", "type": "object", - "title": "A URL represents a parsed URL (technically, a URI reference).", + "title": "URL is a custom URL type that allows validation at configuration load time.", "properties": { "ForceQuery": { "type": "boolean" @@ -4899,7 +4896,7 @@ "$ref": "#/definitions/Userinfo" } }, - "x-go-package": "net/url" + "x-go-package": "github.com/prometheus/common/config" }, "Userinfo": { "description": "The Userinfo type is an immutable encapsulation of username and\npassword details for a URL. An existing Userinfo value is guaranteed\nto have a username set (potentially empty, as allowed by RFC 2396),\nand optionally a password.", @@ -5096,7 +5093,6 @@ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "alertGroup": { - "description": "AlertGroup alert group", "type": "object", "required": [ "alerts", @@ -5119,6 +5115,8 @@ "$ref": "#/definitions/receiver" } }, + "x-go-name": "AlertGroup", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/alertGroup" }, "alertGroups": { @@ -5312,11 +5310,12 @@ "$ref": "#/definitions/gettableAlert" }, "gettableAlerts": { - "description": "GettableAlerts gettable alerts", "type": "array", "items": { "$ref": "#/definitions/gettableAlert" }, + "x-go-name": "GettableAlerts", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/gettableAlerts" }, "gettableSilence": { @@ -5511,7 +5510,6 @@ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "postableSilence": { - "description": "PostableSilence postable silence", "type": "object", "required": [ "comment", @@ -5552,6 +5550,8 @@ "x-go-name": "StartsAt" } }, + "x-go-name": "PostableSilence", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/postableSilence" }, "receiver": { diff --git a/pkg/services/ngalert/api/util.go b/pkg/services/ngalert/api/util.go index ed079702b2f..6654ba21485 100644 --- a/pkg/services/ngalert/api/util.go +++ b/pkg/services/ngalert/api/util.go @@ -35,23 +35,6 @@ func toMacaronPath(path string) string { })) } -func backendType(ctx *models.ReqContext, cache datasources.CacheService) (apimodels.Backend, error) { - datasourceID := web.Params(ctx.Req)[":DatasourceID"] - if datasourceID, err := strconv.ParseInt(datasourceID, 10, 64); err == nil { - if ds, err := cache.GetDatasource(ctx.Req.Context(), datasourceID, ctx.SignedInUser, ctx.SkipCache); err == nil { - switch ds.Type { - case "loki", "prometheus": - return apimodels.LoTexRulerBackend, nil - case "alertmanager": - return apimodels.AlertmanagerBackend, nil - default: - return 0, fmt.Errorf("unexpected backend type (%v)", ds.Type) - } - } - } - return 0, fmt.Errorf("unexpected backend type (%v)", datasourceID) -} - func backendTypeByUID(ctx *models.ReqContext, cache datasources.CacheService) (apimodels.Backend, error) { datasourceUID := web.Params(ctx.Req)[":DatasourceUID"] if ds, err := cache.GetDatasourceByUID(ctx.Req.Context(), datasourceUID, ctx.SignedInUser, ctx.SkipCache); err == nil { diff --git a/public/api-merged.json b/public/api-merged.json index 11c1856d659..8cc8b20cc78 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -6620,17 +6620,16 @@ } } }, - "/prometheus/{DatasourceID}/api/v1/alerts": { + "/prometheus/{DatasourceUID}/api/v1/alerts": { "get": { "description": "gets the current alerts", "tags": ["prometheus"], "operationId": "RouteGetAlertStatuses", "parameters": [ { - "type": "integer", - "format": "int64", - "description": "DatasourceID should be the numeric datasource identifier", - "name": "DatasourceID", + "type": "string", + "description": "DatasoureUID should be the datasource UID identifier", + "name": "DatasourceUID", "in": "path", "required": true } @@ -6645,17 +6644,16 @@ } } }, - "/prometheus/{DatasourceID}/api/v1/rules": { + "/prometheus/{DatasourceUID}/api/v1/rules": { "get": { "description": "gets the evaluation statuses of all rules", "tags": ["prometheus"], "operationId": "RouteGetRuleStatuses", "parameters": [ { - "type": "integer", - "format": "int64", - "description": "DatasourceID should be the numeric datasource identifier", - "name": "DatasourceID", + "type": "string", + "description": "DatasoureUID should be the datasource UID identifier", + "name": "DatasourceUID", "in": "path", "required": true } @@ -16375,9 +16373,8 @@ "x-go-package": "github.com/grafana/grafana/pkg/api/dtos" }, "URL": { - "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.", "type": "object", - "title": "A URL represents a parsed URL (technically, a URI reference).", + "title": "URL is a custom URL type that allows validation at configuration load time.", "properties": { "ForceQuery": { "type": "boolean" @@ -16410,7 +16407,7 @@ "$ref": "#/definitions/Userinfo" } }, - "x-go-package": "net/url" + "x-go-package": "github.com/prometheus/common/config" }, "UpdateAlertNotificationCommand": { "type": "object", @@ -17487,11 +17484,12 @@ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "gettableAlerts": { - "description": "GettableAlerts gettable alerts", "type": "array", "items": { "$ref": "#/definitions/gettableAlert" - } + }, + "x-go-name": "GettableAlerts", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "gettableSilence": { "description": "GettableSilence gettable silence", @@ -17665,7 +17663,6 @@ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "postableSilence": { - "description": "PostableSilence postable silence", "type": "object", "required": ["comment", "createdBy", "endsAt", "matchers", "startsAt"], "properties": { @@ -17699,7 +17696,9 @@ "format": "date-time", "x-go-name": "StartsAt" } - } + }, + "x-go-name": "PostableSilence", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "receiver": { "type": "object", diff --git a/public/app/features/alerting/unified/api/prometheus.ts b/public/app/features/alerting/unified/api/prometheus.ts index acb42bd3678..b14373b5fc7 100644 --- a/public/app/features/alerting/unified/api/prometheus.ts +++ b/public/app/features/alerting/unified/api/prometheus.ts @@ -4,7 +4,7 @@ import { getBackendSrv } from '@grafana/runtime'; import { RuleNamespace } from 'app/types/unified-alerting'; import { PromRulesResponse } from 'app/types/unified-alerting-dto'; -import { getDatasourceAPIId, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; +import { getDatasourceAPIUid, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; export interface FetchPromRulesFilter { dashboardUID: string; @@ -24,7 +24,7 @@ export function prometheusUrlBuilder(dataSourceConfig: PrometheusDataSourceConfi const params = prepareRulesFilterQueryParams(searchParams, filter); return { - url: `/api/prometheus/${getDatasourceAPIId(dataSourceName)}/api/v1/rules`, + url: `/api/prometheus/${getDatasourceAPIUid(dataSourceName)}/api/v1/rules`, params: params, }; }, From 809aa38103771726aa3a6208a87f8138d6d15ea6 Mon Sep 17 00:00:00 2001 From: Alexander Weaver Date: Fri, 6 May 2022 14:33:30 -0500 Subject: [PATCH 105/440] POST routes to PUT routes (#48828) --- pkg/services/ngalert/api/api_provisioning.go | 2 +- .../ngalert/api/api_provisioning_test.go | 12 +++--- pkg/services/ngalert/api/authorization.go | 2 +- .../ngalert/api/forked_provisioning.go | 4 +- .../api/generated_base_api_provisioning.go | 38 +++++++++---------- .../definitions/provisioning_policies.go | 4 +- pkg/services/ngalert/api/tooling/post.json | 9 +++-- pkg/services/ngalert/api/tooling/spec.json | 7 ++-- .../api/alerting/api_provisioning_test.go | 16 ++++---- 9 files changed, 48 insertions(+), 46 deletions(-) diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index a1b2672ec82..2242c76e525 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -53,7 +53,7 @@ func (srv *ProvisioningSrv) RouteGetPolicyTree(c *models.ReqContext) response.Re return response.JSON(http.StatusOK, policies) } -func (srv *ProvisioningSrv) RoutePostPolicyTree(c *models.ReqContext, tree apimodels.Route) response.Response { +func (srv *ProvisioningSrv) RoutePutPolicyTree(c *models.ReqContext, tree apimodels.Route) response.Response { err := srv.policies.UpdatePolicyTree(c.Req.Context(), c.OrgId, tree, alerting_models.ProvenanceAPI) if errors.Is(err, store.ErrNoAlertmanagerConfiguration) { return ErrResp(http.StatusNotFound, err, "") diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index 5eab72ef98b..a72b92b371b 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -26,24 +26,24 @@ func TestProvisioningApi(t *testing.T) { require.Equal(t, 200, response.Status()) }) - t.Run("successful POST policies returns 202", func(t *testing.T) { + t.Run("successful PUT policies returns 202", func(t *testing.T) { sut := createProvisioningSrvSut() rc := createTestRequestCtx() tree := apimodels.Route{} - response := sut.RoutePostPolicyTree(&rc, tree) + response := sut.RoutePutPolicyTree(&rc, tree) require.Equal(t, 202, response.Status()) }) t.Run("when new policy tree is invalid", func(t *testing.T) { - t.Run("POST policies returns 400", func(t *testing.T) { + t.Run("PUT policies returns 400", func(t *testing.T) { sut := createProvisioningSrvSut() sut.policies = &fakeRejectingNotificationPolicyService{} rc := createTestRequestCtx() tree := apimodels.Route{} - response := sut.RoutePostPolicyTree(&rc, tree) + response := sut.RoutePutPolicyTree(&rc, tree) require.Equal(t, 400, response.Status()) expBody := `{"error":"invalid object specification: invalid policy tree","message":"invalid object specification: invalid policy tree"}` @@ -86,13 +86,13 @@ func TestProvisioningApi(t *testing.T) { require.Contains(t, string(response.Body()), "something went wrong") }) - t.Run("POST policies returns 500", func(t *testing.T) { + t.Run("PUT policies returns 500", func(t *testing.T) { sut := createProvisioningSrvSut() sut.policies = &fakeFailingNotificationPolicyService{} rc := createTestRequestCtx() tree := apimodels.Route{} - response := sut.RoutePostPolicyTree(&rc, tree) + response := sut.RoutePutPolicyTree(&rc, tree) require.Equal(t, 500, response.Status()) require.NotEmpty(t, response.Body()) diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index 51c62b3d986..bc5074cb407 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -186,7 +186,7 @@ func (api *API) authorize(method, path string) web.Handler { http.MethodGet + "/api/provisioning/templates/{name}": return middleware.ReqSignedIn - case http.MethodPost + "/api/provisioning/policies", + case http.MethodPut + "/api/provisioning/policies", http.MethodPost + "/api/provisioning/contact-points", http.MethodPut + "/api/provisioning/contact-points", http.MethodDelete + "/api/provisioning/contact-points/{ID}", diff --git a/pkg/services/ngalert/api/forked_provisioning.go b/pkg/services/ngalert/api/forked_provisioning.go index 088cfe17e27..fb764005613 100644 --- a/pkg/services/ngalert/api/forked_provisioning.go +++ b/pkg/services/ngalert/api/forked_provisioning.go @@ -23,8 +23,8 @@ func (f *ForkedProvisioningApi) forkRouteGetPolicyTree(ctx *models.ReqContext) r return f.svc.RouteGetPolicyTree(ctx) } -func (f *ForkedProvisioningApi) forkRoutePostPolicyTree(ctx *models.ReqContext, route apimodels.Route) response.Response { - return f.svc.RoutePostPolicyTree(ctx, route) +func (f *ForkedProvisioningApi) forkRoutePutPolicyTree(ctx *models.ReqContext, route apimodels.Route) response.Response { + return f.svc.RoutePutPolicyTree(ctx, route) } func (f *ForkedProvisioningApi) forkRouteGetContactpoints(ctx *models.ReqContext) response.Response { diff --git a/pkg/services/ngalert/api/generated_base_api_provisioning.go b/pkg/services/ngalert/api/generated_base_api_provisioning.go index 81a858f92db..49f62ce24f8 100644 --- a/pkg/services/ngalert/api/generated_base_api_provisioning.go +++ b/pkg/services/ngalert/api/generated_base_api_provisioning.go @@ -27,8 +27,8 @@ type ProvisioningApiForkingService interface { RouteGetTemplate(*models.ReqContext) response.Response RouteGetTemplates(*models.ReqContext) response.Response RoutePostContactpoints(*models.ReqContext) response.Response - RoutePostPolicyTree(*models.ReqContext) response.Response RoutePutContactpoints(*models.ReqContext) response.Response + RoutePutPolicyTree(*models.ReqContext) response.Response RoutePutTemplate(*models.ReqContext) response.Response } @@ -64,14 +64,6 @@ func (f *ForkedProvisioningApi) RoutePostContactpoints(ctx *models.ReqContext) r return f.forkRoutePostContactpoints(ctx, conf) } -func (f *ForkedProvisioningApi) RoutePostPolicyTree(ctx *models.ReqContext) response.Response { - conf := apimodels.Route{} - if err := web.Bind(ctx.Req, &conf); err != nil { - return response.Error(http.StatusBadRequest, "bad request data", err) - } - return f.forkRoutePostPolicyTree(ctx, conf) -} - func (f *ForkedProvisioningApi) RoutePutContactpoints(ctx *models.ReqContext) response.Response { conf := apimodels.EmbeddedContactPoint{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -80,6 +72,14 @@ func (f *ForkedProvisioningApi) RoutePutContactpoints(ctx *models.ReqContext) re return f.forkRoutePutContactpoints(ctx, conf) } +func (f *ForkedProvisioningApi) RoutePutPolicyTree(ctx *models.ReqContext) response.Response { + conf := apimodels.Route{} + if err := web.Bind(ctx.Req, &conf); err != nil { + return response.Error(http.StatusBadRequest, "bad request data", err) + } + return f.forkRoutePutPolicyTree(ctx, conf) +} + func (f *ForkedProvisioningApi) RoutePutTemplate(ctx *models.ReqContext) response.Response { conf := apimodels.MessageTemplateContent{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -160,16 +160,6 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingServi m, ), ) - group.Post( - toMacaronPath("/api/provisioning/policies"), - api.authorize(http.MethodPost, "/api/provisioning/policies"), - metrics.Instrument( - http.MethodPost, - "/api/provisioning/policies", - srv.RoutePostPolicyTree, - m, - ), - ) group.Put( toMacaronPath("/api/provisioning/contact-points"), api.authorize(http.MethodPut, "/api/provisioning/contact-points"), @@ -180,6 +170,16 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingServi m, ), ) + group.Put( + toMacaronPath("/api/provisioning/policies"), + api.authorize(http.MethodPut, "/api/provisioning/policies"), + metrics.Instrument( + http.MethodPut, + "/api/provisioning/policies", + srv.RoutePutPolicyTree, + m, + ), + ) group.Put( toMacaronPath("/api/provisioning/templates/{name}"), api.authorize(http.MethodPut, "/api/provisioning/templates/{name}"), diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go index b2019239b36..e9df6997a9e 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go @@ -8,7 +8,7 @@ package definitions // 200: Route // 400: ValidationError -// swagger:route POST /api/provisioning/policies provisioning RoutePostPolicyTree +// swagger:route PUT /api/provisioning/policies provisioning RoutePutPolicyTree // // Sets the notification policy tree. // @@ -19,7 +19,7 @@ package definitions // 202: Accepted // 400: ValidationError -// swagger:parameters RoutePostPolicyTree +// swagger:parameters RoutePutPolicyTree type Policytree struct { // in:body Body Route diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index 14a483eaa66..8d18c5ca645 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -3569,7 +3569,6 @@ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "receiver": { - "description": "Receiver receiver", "properties": { "name": { "description": "name", @@ -3580,7 +3579,9 @@ "required": [ "name" ], - "type": "object" + "type": "object", + "x-go-name": "Receiver", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "silence": { "description": "Silence silence", @@ -4927,11 +4928,11 @@ "provisioning" ] }, - "post": { + "put": { "consumes": [ "application/json" ], - "operationId": "RoutePostPolicyTree", + "operationId": "RoutePutPolicyTree", "parameters": [ { "in": "body", diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 07f858c16b9..261c9ffef44 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -1250,7 +1250,7 @@ } } }, - "post": { + "put": { "consumes": [ "application/json" ], @@ -1258,7 +1258,7 @@ "provisioning" ], "summary": "Sets the notification policy tree.", - "operationId": "RoutePostPolicyTree", + "operationId": "RoutePutPolicyTree", "parameters": [ { "name": "Body", @@ -5555,7 +5555,6 @@ "$ref": "#/definitions/postableSilence" }, "receiver": { - "description": "Receiver receiver", "type": "object", "required": [ "name" @@ -5567,6 +5566,8 @@ "x-go-name": "Name" } }, + "x-go-name": "Receiver", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/receiver" }, "silence": { diff --git a/pkg/tests/api/alerting/api_provisioning_test.go b/pkg/tests/api/alerting/api_provisioning_test.go index eb3095ab8cb..27080e7f117 100644 --- a/pkg/tests/api/alerting/api_provisioning_test.go +++ b/pkg/tests/api/alerting/api_provisioning_test.go @@ -96,8 +96,8 @@ func TestProvisioning(t *testing.T) { require.Equal(t, 200, resp.StatusCode) }) - t.Run("un-authenticated POST should 401", func(t *testing.T) { - req := createTestRequest("POST", url, "", body) + t.Run("un-authenticated PUT should 401", func(t *testing.T) { + req := createTestRequest("PUT", url, "", body) resp, err := http.DefaultClient.Do(req) require.NoError(t, err) @@ -106,8 +106,8 @@ func TestProvisioning(t *testing.T) { require.Equal(t, 401, resp.StatusCode) }) - t.Run("viewer POST should 403", func(t *testing.T) { - req := createTestRequest("POST", url, "viewer", body) + t.Run("viewer PUT should 403", func(t *testing.T) { + req := createTestRequest("PUT", url, "viewer", body) resp, err := http.DefaultClient.Do(req) require.NoError(t, err) @@ -116,8 +116,8 @@ func TestProvisioning(t *testing.T) { require.Equal(t, 403, resp.StatusCode) }) - t.Run("editor POST should succeed", func(t *testing.T) { - req := createTestRequest("POST", url, "editor", body) + t.Run("editor PUT should succeed", func(t *testing.T) { + req := createTestRequest("PUT", url, "editor", body) resp, err := http.DefaultClient.Do(req) require.NoError(t, err) @@ -126,8 +126,8 @@ func TestProvisioning(t *testing.T) { require.Equal(t, 202, resp.StatusCode) }) - t.Run("admin POST should succeed", func(t *testing.T) { - req := createTestRequest("POST", url, "admin", body) + t.Run("admin PUT should succeed", func(t *testing.T) { + req := createTestRequest("PUT", url, "admin", body) resp, err := http.DefaultClient.Do(req) require.NoError(t, err) From 99eaa0fc206a5829fee973c02e92793d8e0a3c2d Mon Sep 17 00:00:00 2001 From: Alexander Weaver Date: Fri, 6 May 2022 16:06:30 -0500 Subject: [PATCH 106/440] Put identifier in path (#48831) --- pkg/services/ngalert/api/api_provisioning.go | 2 + pkg/services/ngalert/api/authorization.go | 2 +- .../ngalert/api/forked_provisioning.go | 2 +- .../api/generated_base_api_provisioning.go | 14 ++--- .../definitions/provisioning_contactpoints.go | 4 +- pkg/services/ngalert/api/tooling/post.json | 53 ++++++++-------- pkg/services/ngalert/api/tooling/spec.json | 63 +++++++++---------- 7 files changed, 70 insertions(+), 70 deletions(-) diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index 2242c76e525..89226730f0d 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -86,6 +86,8 @@ func (srv *ProvisioningSrv) RoutePostContactPoint(c *models.ReqContext, cp apimo } func (srv *ProvisioningSrv) RoutePutContactPoint(c *models.ReqContext, cp apimodels.EmbeddedContactPoint) response.Response { + id := web.Params(c.Req)[":ID"] + cp.UID = id err := srv.contactPointService.UpdateContactPoint(c.Req.Context(), c.OrgId, cp, alerting_models.ProvenanceAPI) if err != nil { return ErrResp(http.StatusInternalServerError, err, "") diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index bc5074cb407..eafff85133d 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -188,7 +188,7 @@ func (api *API) authorize(method, path string) web.Handler { case http.MethodPut + "/api/provisioning/policies", http.MethodPost + "/api/provisioning/contact-points", - http.MethodPut + "/api/provisioning/contact-points", + http.MethodPut + "/api/provisioning/contact-points/{ID}", http.MethodDelete + "/api/provisioning/contact-points/{ID}", http.MethodPut + "/api/provisioning/templates/{name}", http.MethodDelete + "/api/provisioning/templates/{name}": diff --git a/pkg/services/ngalert/api/forked_provisioning.go b/pkg/services/ngalert/api/forked_provisioning.go index fb764005613..9d42a3c9779 100644 --- a/pkg/services/ngalert/api/forked_provisioning.go +++ b/pkg/services/ngalert/api/forked_provisioning.go @@ -35,7 +35,7 @@ func (f *ForkedProvisioningApi) forkRoutePostContactpoints(ctx *models.ReqContex return f.svc.RoutePostContactPoint(ctx, cp) } -func (f *ForkedProvisioningApi) forkRoutePutContactpoints(ctx *models.ReqContext, cp apimodels.EmbeddedContactPoint) response.Response { +func (f *ForkedProvisioningApi) forkRoutePutContactpoint(ctx *models.ReqContext, cp apimodels.EmbeddedContactPoint) response.Response { return f.svc.RoutePutContactPoint(ctx, cp) } diff --git a/pkg/services/ngalert/api/generated_base_api_provisioning.go b/pkg/services/ngalert/api/generated_base_api_provisioning.go index 49f62ce24f8..6732a7d4047 100644 --- a/pkg/services/ngalert/api/generated_base_api_provisioning.go +++ b/pkg/services/ngalert/api/generated_base_api_provisioning.go @@ -27,7 +27,7 @@ type ProvisioningApiForkingService interface { RouteGetTemplate(*models.ReqContext) response.Response RouteGetTemplates(*models.ReqContext) response.Response RoutePostContactpoints(*models.ReqContext) response.Response - RoutePutContactpoints(*models.ReqContext) response.Response + RoutePutContactpoint(*models.ReqContext) response.Response RoutePutPolicyTree(*models.ReqContext) response.Response RoutePutTemplate(*models.ReqContext) response.Response } @@ -64,12 +64,12 @@ func (f *ForkedProvisioningApi) RoutePostContactpoints(ctx *models.ReqContext) r return f.forkRoutePostContactpoints(ctx, conf) } -func (f *ForkedProvisioningApi) RoutePutContactpoints(ctx *models.ReqContext) response.Response { +func (f *ForkedProvisioningApi) RoutePutContactpoint(ctx *models.ReqContext) response.Response { conf := apimodels.EmbeddedContactPoint{} if err := web.Bind(ctx.Req, &conf); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - return f.forkRoutePutContactpoints(ctx, conf) + return f.forkRoutePutContactpoint(ctx, conf) } func (f *ForkedProvisioningApi) RoutePutPolicyTree(ctx *models.ReqContext) response.Response { @@ -161,12 +161,12 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingServi ), ) group.Put( - toMacaronPath("/api/provisioning/contact-points"), - api.authorize(http.MethodPut, "/api/provisioning/contact-points"), + toMacaronPath("/api/provisioning/contact-points/{ID}"), + api.authorize(http.MethodPut, "/api/provisioning/contact-points/{ID}"), metrics.Instrument( http.MethodPut, - "/api/provisioning/contact-points", - srv.RoutePutContactpoints, + "/api/provisioning/contact-points/{ID}", + srv.RoutePutContactpoint, m, ), ) diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_contactpoints.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_contactpoints.go index ddbcc007f2c..52f331b7b50 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_contactpoints.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_contactpoints.go @@ -26,7 +26,7 @@ import ( // 202: Accepted // 400: ValidationError -// swagger:route PUT /api/provisioning/contact-points provisioning RoutePutContactpoints +// swagger:route PUT /api/provisioning/contact-points/{ID} provisioning RoutePutContactpoint // // Update an existing contact point. // @@ -48,7 +48,7 @@ import ( // 202: Accepted // 400: ValidationError -// swagger:parameters RoutePostContactpoints RoutePutContactpoints +// swagger:parameters RoutePostContactpoints RoutePutContactpoint type ContactPointPayload struct { // in:body Body EmbeddedContactPoint diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index 8d18c5ca645..226cbb14592 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -3391,12 +3391,11 @@ "type": "object" }, "gettableSilences": { + "description": "GettableSilences gettable silences", "items": { "$ref": "#/definitions/gettableSilence" }, - "type": "array", - "x-go-name": "GettableSilences", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" + "type": "array" }, "labelSet": { "additionalProperties": { @@ -4851,12 +4850,35 @@ "tags": [ "provisioning" ] + } + }, + "/api/provisioning/contact-points/{ID}": { + "delete": { + "consumes": [ + "application/json" + ], + "operationId": "RouteDeleteContactpoints", + "responses": { + "202": { + "$ref": "#/responses/Accepted" + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + }, + "summary": "Delete a contact point.", + "tags": [ + "provisioning" + ] }, "put": { "consumes": [ "application/json" ], - "operationId": "RoutePutContactpoints", + "operationId": "RoutePutContactpoint", "parameters": [ { "in": "body", @@ -4883,29 +4905,6 @@ ] } }, - "/api/provisioning/contact-points/{ID}": { - "delete": { - "consumes": [ - "application/json" - ], - "operationId": "RouteDeleteContactpoints", - "responses": { - "202": { - "$ref": "#/responses/Accepted" - }, - "400": { - "description": "ValidationError", - "schema": { - "$ref": "#/definitions/ValidationError" - } - } - }, - "summary": "Delete a contact point.", - "tags": [ - "provisioning" - ] - } - }, "/api/provisioning/policies": { "get": { "operationId": "RouteGetPolicyTree", diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 261c9ffef44..e2b8d0792c1 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -1144,36 +1144,6 @@ } } }, - "put": { - "consumes": [ - "application/json" - ], - "tags": [ - "provisioning" - ], - "summary": "Update an existing contact point.", - "operationId": "RoutePutContactpoints", - "parameters": [ - { - "name": "Body", - "in": "body", - "schema": { - "$ref": "#/definitions/EmbeddedContactPoint" - } - } - ], - "responses": { - "202": { - "$ref": "#/responses/Accepted" - }, - "400": { - "description": "ValidationError", - "schema": { - "$ref": "#/definitions/ValidationError" - } - } - } - }, "post": { "consumes": [ "application/json" @@ -1206,6 +1176,36 @@ } }, "/api/provisioning/contact-points/{ID}": { + "put": { + "consumes": [ + "application/json" + ], + "tags": [ + "provisioning" + ], + "summary": "Update an existing contact point.", + "operationId": "RoutePutContactpoint", + "parameters": [ + { + "name": "Body", + "in": "body", + "schema": { + "$ref": "#/definitions/EmbeddedContactPoint" + } + } + ], + "responses": { + "202": { + "$ref": "#/responses/Accepted" + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + } + }, "delete": { "consumes": [ "application/json" @@ -5375,12 +5375,11 @@ "$ref": "#/definitions/gettableSilence" }, "gettableSilences": { + "description": "GettableSilences gettable silences", "type": "array", "items": { "$ref": "#/definitions/gettableSilence" }, - "x-go-name": "GettableSilences", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/gettableSilences" }, "labelSet": { From 5c2dee19d5b1bd9e3493e01c64699d7b1592267b Mon Sep 17 00:00:00 2001 From: Shirley <4163034+fridgepoet@users.noreply.github.com> Date: Sun, 8 May 2022 09:27:03 +0200 Subject: [PATCH 107/440] CloudWatch: Refactor tests, remove unused parameters (#48815) --- pkg/tsdb/cloudwatch/query_row_response.go | 6 +- pkg/tsdb/cloudwatch/request_parser.go | 4 +- pkg/tsdb/cloudwatch/request_parser_test.go | 10 ++- pkg/tsdb/cloudwatch/response_parser.go | 2 +- pkg/tsdb/cloudwatch/response_parser_test.go | 72 ++++++++++--------- ...uts.json => multiple-outputs-query-a.json} | 29 +------- .../test-data/multiple-outputs-query-b.json | 47 ++++++++++++ 7 files changed, 93 insertions(+), 77 deletions(-) rename pkg/tsdb/cloudwatch/test-data/{multiple-outputs.json => multiple-outputs-query-a.json} (70%) create mode 100644 pkg/tsdb/cloudwatch/test-data/multiple-outputs-query-b.json diff --git a/pkg/tsdb/cloudwatch/query_row_response.go b/pkg/tsdb/cloudwatch/query_row_response.go index 3a471273fa7..d261d90d75f 100644 --- a/pkg/tsdb/cloudwatch/query_row_response.go +++ b/pkg/tsdb/cloudwatch/query_row_response.go @@ -4,9 +4,7 @@ import "github.com/aws/aws-sdk-go/service/cloudwatch" // queryRowResponse represents the GetMetricData response for a query row in the query editor. type queryRowResponse struct { - ID string ErrorCodes map[string]bool - PartialData bool Labels []string HasArithmeticError bool ArithmeticErrorMessage string @@ -14,15 +12,13 @@ type queryRowResponse struct { StatusCode string } -func newQueryRowResponse(id string) queryRowResponse { +func newQueryRowResponse() queryRowResponse { return queryRowResponse{ - ID: id, ErrorCodes: map[string]bool{ maxMetricsExceeded: false, maxQueryTimeRangeExceeded: false, maxQueryResultsExceeded: false, maxMatchingResultsExceeded: false}, - PartialData: false, HasArithmeticError: false, ArithmeticErrorMessage: "", Labels: []string{}, diff --git a/pkg/tsdb/cloudwatch/request_parser.go b/pkg/tsdb/cloudwatch/request_parser.go index f16b63608a4..e31ad779738 100644 --- a/pkg/tsdb/cloudwatch/request_parser.go +++ b/pkg/tsdb/cloudwatch/request_parser.go @@ -22,7 +22,7 @@ var validMetricDataID = regexp.MustCompile(`^[a-z][a-zA-Z0-9_]*$`) func (e *cloudWatchExecutor) parseQueries(queries []backend.DataQuery, startTime time.Time, endTime time.Time) (map[string][]*cloudWatchQuery, error) { requestQueries := make(map[string][]*cloudWatchQuery) - migratedQueries, err := migrateLegacyQuery(queries, e.features.IsEnabled(featuremgmt.FlagCloudWatchDynamicLabels), startTime, endTime) + migratedQueries, err := migrateLegacyQuery(queries, e.features.IsEnabled(featuremgmt.FlagCloudWatchDynamicLabels)) if err != nil { return nil, err } @@ -54,7 +54,7 @@ func (e *cloudWatchExecutor) parseQueries(queries []backend.DataQuery, startTime } // migrateLegacyQuery is also done in the frontend, so this should only ever be needed for alerting queries -func migrateLegacyQuery(queries []backend.DataQuery, dynamicLabelsEnabled bool, startTime time.Time, endTime time.Time) ([]*backend.DataQuery, error) { +func migrateLegacyQuery(queries []backend.DataQuery, dynamicLabelsEnabled bool) ([]*backend.DataQuery, error) { migratedQueries := []*backend.DataQuery{} for _, q := range queries { query := q diff --git a/pkg/tsdb/cloudwatch/request_parser_test.go b/pkg/tsdb/cloudwatch/request_parser_test.go index d30d47a28de..65d3ee9edc9 100644 --- a/pkg/tsdb/cloudwatch/request_parser_test.go +++ b/pkg/tsdb/cloudwatch/request_parser_test.go @@ -14,8 +14,6 @@ import ( func TestRequestParser(t *testing.T) { t.Run("Query migration ", func(t *testing.T) { t.Run("legacy statistics field is migrated", func(t *testing.T) { - startTime := time.Now() - endTime := startTime.Add(2 * time.Hour) oldQuery := &backend.DataQuery{ MaxDataPoints: 0, QueryType: "timeSeriesQuery", @@ -33,7 +31,7 @@ func TestRequestParser(t *testing.T) { "period": "600", "hide": false }`) - migratedQueries, err := migrateLegacyQuery([]backend.DataQuery{*oldQuery}, false, startTime, endTime) + migratedQueries, err := migrateLegacyQuery([]backend.DataQuery{*oldQuery}, false) require.NoError(t, err) assert.Equal(t, 1, len(migratedQueries)) @@ -404,7 +402,7 @@ func Test_Test_migrateLegacyQuery(t *testing.T) { "period": "600", "hide": false }`)}, - }, true, time.Now(), time.Now()) + }, true) require.NoError(t, err) require.Equal(t, 1, len(migratedQueries)) @@ -461,7 +459,7 @@ func Test_Test_migrateLegacyQuery(t *testing.T) { "hide": false }`), }, - }, true, time.Now(), time.Now()) + }, true) require.NoError(t, err) require.Equal(t, 2, len(migratedQueries)) @@ -532,7 +530,7 @@ func Test_Test_migrateLegacyQuery(t *testing.T) { "period": "600", "hide": false }`, tc.labelJson))}, - }, tc.dynamicLabelsFeatureToggleEnabled, time.Now(), time.Now()) + }, tc.dynamicLabelsFeatureToggleEnabled) require.NoError(t, err) require.Equal(t, 1, len(migratedQueries)) diff --git a/pkg/tsdb/cloudwatch/response_parser.go b/pkg/tsdb/cloudwatch/response_parser.go index 2d58551b6af..7022e588edf 100644 --- a/pkg/tsdb/cloudwatch/response_parser.go +++ b/pkg/tsdb/cloudwatch/response_parser.go @@ -64,7 +64,7 @@ func aggregateResponse(getMetricDataOutputs []*cloudwatch.GetMetricDataOutput) m id := *r.Id label := *r.Label - response := newQueryRowResponse(id) + response := newQueryRowResponse() if _, exists := responseByID[id]; exists { response = responseByID[id] } diff --git a/pkg/tsdb/cloudwatch/response_parser_test.go b/pkg/tsdb/cloudwatch/response_parser_test.go index 1e591c42339..4198877c11c 100644 --- a/pkg/tsdb/cloudwatch/response_parser_test.go +++ b/pkg/tsdb/cloudwatch/response_parser_test.go @@ -28,44 +28,46 @@ func loadGetMetricDataOutputsFromFile(filePath string) ([]*cloudwatch.GetMetricD func TestCloudWatchResponseParser(t *testing.T) { startTime := time.Now() endTime := startTime.Add(2 * time.Hour) - t.Run("when aggregating response", func(t *testing.T) { - getMetricDataOutputs, err := loadGetMetricDataOutputsFromFile("./test-data/multiple-outputs.json") + t.Run("when aggregating multi-outputs response", func(t *testing.T) { + getMetricDataOutputs, err := loadGetMetricDataOutputsFromFile("./test-data/multiple-outputs-query-a.json") require.NoError(t, err) aggregatedResponse := aggregateResponse(getMetricDataOutputs) - t.Run("response for id a", func(t *testing.T) { - idA := "a" - t.Run("should have two labels", func(t *testing.T) { - assert.Len(t, aggregatedResponse[idA].Labels, 2) - assert.Len(t, aggregatedResponse[idA].Metrics, 2) - }) - t.Run("should have points for label1 taken from both getMetricDataOutputs", func(t *testing.T) { - assert.Len(t, aggregatedResponse[idA].Metrics["label1"].Values, 10) - }) - t.Run("should have statuscode 'Complete'", func(t *testing.T) { - assert.Equal(t, "Complete", aggregatedResponse[idA].StatusCode) - }) - t.Run("should have exceeded request limit", func(t *testing.T) { - assert.True(t, aggregatedResponse[idA].ErrorCodes["MaxMetricsExceeded"]) - }) - t.Run("should have exceeded query time range", func(t *testing.T) { - assert.True(t, aggregatedResponse[idA].ErrorCodes["MaxQueryTimeRangeExceeded"]) - }) - t.Run("should have exceeded max query results", func(t *testing.T) { - assert.True(t, aggregatedResponse[idA].ErrorCodes["MaxQueryResultsExceeded"]) - }) - t.Run("should have exceeded max matching results", func(t *testing.T) { - assert.True(t, aggregatedResponse[idA].ErrorCodes["MaxMatchingResultsExceeded"]) - }) + idA := "a" + t.Run("should have two labels", func(t *testing.T) { + assert.Len(t, aggregatedResponse[idA].Labels, 2) + assert.Len(t, aggregatedResponse[idA].Metrics, 2) }) - t.Run("response for id b", func(t *testing.T) { - idB := "b" - t.Run("should have statuscode is 'Partial'", func(t *testing.T) { - assert.Equal(t, "Partial", aggregatedResponse[idB].StatusCode) - }) - t.Run("should have an arithmetic error and an error message", func(t *testing.T) { - assert.True(t, aggregatedResponse[idB].HasArithmeticError) - assert.Equal(t, "One or more data-points have been dropped due to non-numeric values (NaN, -Infinite, +Infinite)", aggregatedResponse[idB].ArithmeticErrorMessage) - }) + t.Run("should have points for label1 taken from both getMetricDataOutputs", func(t *testing.T) { + assert.Len(t, aggregatedResponse[idA].Metrics["label1"].Values, 10) + }) + t.Run("should have statuscode 'Complete'", func(t *testing.T) { + assert.Equal(t, "Complete", aggregatedResponse[idA].StatusCode) + }) + t.Run("should have exceeded request limit", func(t *testing.T) { + assert.True(t, aggregatedResponse[idA].ErrorCodes["MaxMetricsExceeded"]) + }) + t.Run("should have exceeded query time range", func(t *testing.T) { + assert.True(t, aggregatedResponse[idA].ErrorCodes["MaxQueryTimeRangeExceeded"]) + }) + t.Run("should have exceeded max query results", func(t *testing.T) { + assert.True(t, aggregatedResponse[idA].ErrorCodes["MaxQueryResultsExceeded"]) + }) + t.Run("should have exceeded max matching results", func(t *testing.T) { + assert.True(t, aggregatedResponse[idA].ErrorCodes["MaxMatchingResultsExceeded"]) + }) + }) + + t.Run("when aggregating multi-outputs response with PartialData and ArithmeticError", func(t *testing.T) { + getMetricDataOutputs, err := loadGetMetricDataOutputsFromFile("./test-data/multiple-outputs-query-b.json") + require.NoError(t, err) + aggregatedResponse := aggregateResponse(getMetricDataOutputs) + idB := "b" + t.Run("should have statuscode is 'PartialData'", func(t *testing.T) { + assert.Equal(t, "PartialData", aggregatedResponse[idB].StatusCode) + }) + t.Run("should have an arithmetic error and an error message", func(t *testing.T) { + assert.True(t, aggregatedResponse[idB].HasArithmeticError) + assert.Equal(t, "One or more data-points have been dropped due to non-numeric values (NaN, -Infinite, +Infinite)", aggregatedResponse[idB].ArithmeticErrorMessage) }) }) diff --git a/pkg/tsdb/cloudwatch/test-data/multiple-outputs.json b/pkg/tsdb/cloudwatch/test-data/multiple-outputs-query-a.json similarity index 70% rename from pkg/tsdb/cloudwatch/test-data/multiple-outputs.json rename to pkg/tsdb/cloudwatch/test-data/multiple-outputs-query-a.json index d0e9edcfe30..0b025a80128 100644 --- a/pkg/tsdb/cloudwatch/test-data/multiple-outputs.json +++ b/pkg/tsdb/cloudwatch/test-data/multiple-outputs-query-a.json @@ -6,7 +6,7 @@ "Id": "a", "Label": "label1", "Messages": null, - "StatusCode": "Complete", + "StatusCode": "PartialData", "Timestamps": [ "2021-01-15T19:44:00Z", "2021-01-15T19:59:00Z", @@ -33,18 +33,6 @@ "Values": [ 0.1333395078879982 ] - }, - { - "Id": "b", - "Label": "label2", - "Messages": null, - "StatusCode": "Complete", - "Timestamps": [ - "2021-01-15T19:44:00Z" - ], - "Values": [ - 0.1333395078879982 - ] } ], "NextToken": null @@ -77,21 +65,6 @@ 0.14447563659125626, 0.15519743138527173 ] - }, - { - "Id": "b", - "Label": "label2", - "Messages": [{ - "Code": "ArithmeticError", - "Value": "One or more data-points have been dropped due to non-numeric values (NaN, -Infinite, +Infinite)" - }], - "StatusCode": "Partial", - "Timestamps": [ - "2021-01-15T19:44:00Z" - ], - "Values": [ - 0.1333395078879982 - ] } ], "NextToken": null diff --git a/pkg/tsdb/cloudwatch/test-data/multiple-outputs-query-b.json b/pkg/tsdb/cloudwatch/test-data/multiple-outputs-query-b.json new file mode 100644 index 00000000000..3c4d7cae1c1 --- /dev/null +++ b/pkg/tsdb/cloudwatch/test-data/multiple-outputs-query-b.json @@ -0,0 +1,47 @@ +[ + { + "Messages": null, + "MetricDataResults": [ + { + "Id": "b", + "Label": "label2", + "Messages": null, + "StatusCode": "Complete", + "Timestamps": [ + "2021-01-15T19:44:00Z" + ], + "Values": [ + 0.1333395078879982 + ] + } + ], + "NextToken": null + }, + { + "Messages": [ + { "Code": "", "Value": null }, + { "Code": "MaxMetricsExceeded", "Value": null }, + { "Code": "MaxQueryTimeRangeExceeded", "Value": null }, + { "Code": "MaxQueryResultsExceeded", "Value": null }, + { "Code": "MaxMatchingResultsExceeded", "Value": null } + ], + "MetricDataResults": [ + { + "Id": "b", + "Label": "label2", + "Messages": [{ + "Code": "ArithmeticError", + "Value": "One or more data-points have been dropped due to non-numeric values (NaN, -Infinite, +Infinite)" + }], + "StatusCode": "PartialData", + "Timestamps": [ + "2021-01-15T19:44:00Z" + ], + "Values": [ + 0.1333395078879982 + ] + } + ], + "NextToken": null + } +] From d6d358ef267dfe4d0ba6e8b88d9f8597a677443e Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Mon, 9 May 2022 01:00:09 -0700 Subject: [PATCH 108/440] Search: create bluge based index (#48606) --- go.mod | 16 +- go.sum | 35 +- pkg/services/searchV2/bluge.go | 541 +++++++++++++++++++++++++++++++ pkg/services/searchV2/filter.go | 132 ++++++++ pkg/services/searchV2/index.go | 63 +++- pkg/services/searchV2/service.go | 39 +-- pkg/services/searchV2/types.go | 21 +- 7 files changed, 822 insertions(+), 25 deletions(-) create mode 100644 pkg/services/searchV2/bluge.go create mode 100644 pkg/services/searchV2/filter.go diff --git a/go.mod b/go.mod index 28e966b25e3..d0ce7b4dc83 100644 --- a/go.mod +++ b/go.mod @@ -141,7 +141,6 @@ require ( github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/buger/jsonparser v1.1.1 // indirect github.com/c2h5oh/datasize v0.0.0-20200112174442-28bbd4740fee // indirect github.com/cenkalti/backoff/v4 v4.1.2 // indirect github.com/centrifugal/protocol v0.7.6 // indirect @@ -153,7 +152,6 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect github.com/docker/go-units v0.4.0 // indirect - github.com/dustin/go-humanize v1.0.0 // indirect github.com/edsrzf/mmap-go v1.0.0 // indirect github.com/emicklei/proto v1.6.15 // indirect github.com/felixge/httpsnoop v1.0.2 // indirect @@ -246,6 +244,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.4.0 github.com/Azure/go-autorest/autorest/adal v0.9.17 github.com/armon/go-radix v1.0.0 + github.com/blugelabs/bluge v0.1.9 github.com/golang-migrate/migrate/v4 v4.7.0 github.com/grafana/dskit v0.0.0-20211011144203-3a88ec0b675f github.com/grafana/thema v0.0.0-20220413232647-fc54c169b508 @@ -274,9 +273,21 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.2.1 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 // indirect github.com/Microsoft/go-winio v0.5.2 // indirect + github.com/RoaringBitmap/roaring v0.9.1 // indirect + github.com/axiomhq/hyperloglog v0.0.0-20191112132149-a4c4c47bc57f // indirect + github.com/bits-and-blooms/bitset v1.2.0 // indirect + github.com/blevesearch/go-porterstemmer v1.0.3 // indirect + github.com/blevesearch/mmap-go v1.0.2 // indirect + github.com/blevesearch/segment v0.9.0 // indirect + github.com/blevesearch/snowballstem v0.9.0 // indirect + github.com/blevesearch/vellum v1.0.5 // indirect + github.com/blugelabs/bluge_segment_api v0.2.0 // indirect + github.com/blugelabs/ice v0.2.0 // indirect + github.com/caio/go-tdigest v3.1.0+incompatible // indirect github.com/chromedp/cdproto v0.0.0-20220208224320-6efb837e6bc2 // indirect github.com/containerd/containerd v1.6.2 // indirect github.com/coreos/go-semver v0.3.0 // indirect + github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc // indirect github.com/elazarl/goproxy v0.0.0-20220115173737-adb46da277ac // indirect github.com/getkin/kin-openapi v0.94.0 // indirect github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 // indirect @@ -287,6 +298,7 @@ require ( github.com/imdario/mergo v0.3.12 // indirect github.com/klauspost/compress v1.15.1 // indirect github.com/kylelemons/godebug v1.1.0 // indirect + github.com/mschoch/smat v0.2.0 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect github.com/pierrec/lz4/v4 v4.1.8 // indirect github.com/segmentio/asm v1.1.1 // indirect diff --git a/go.sum b/go.sum index 073095a20d6..cf589f5fef9 100644 --- a/go.sum +++ b/go.sum @@ -293,6 +293,10 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/RoaringBitmap/gocroaring v0.4.0/go.mod h1:NieMwz7ZqwU2DD73/vvYwv7r4eWBKuPVSXZIpsaMwCI= +github.com/RoaringBitmap/real-roaring-datasets v0.0.0-20190726190000-eb7c87156f76/go.mod h1:oM0MHmQ3nDsq609SS36p+oYbRi16+oVvU2Bw4Ipv0SE= +github.com/RoaringBitmap/roaring v0.9.1 h1:5PRizBmoN/PfV17nPNQou4dHQ7NcJi8FO/bihdYyCEM= +github.com/RoaringBitmap/roaring v0.9.1/go.mod h1:h1B7iIUOmnAeb5ytYMvnHJwxMc6LUrwBnzXWRuqTQUc= github.com/SAP/go-hdb v0.14.1/go.mod h1:7fdQLVC2lER3urZLjZCm0AuMQfApof92n3aylBPEkMo= github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= @@ -442,6 +446,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.16.3/go.mod h1:bfBj0iVmsUyUg4weDB4Nx github.com/aws/smithy-go v1.5.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= github.com/aws/smithy-go v1.11.2 h1:eG/N+CcUMAvsdffgMvjMKwfyDzIkjM6pfxMJ8Mzc6mE= github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= +github.com/axiomhq/hyperloglog v0.0.0-20191112132149-a4c4c47bc57f h1:y06x6vGnFYfXUoVMbrcP1Uzpj4JG01eB5vRps9G8agM= +github.com/axiomhq/hyperloglog v0.0.0-20191112132149-a4c4c47bc57f/go.mod h1:2stgcRjl6QmW+gU2h5E7BQXg4HU0gzxKWDuT5HviN9s= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= @@ -462,6 +468,7 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bits-and-blooms/bitset v1.2.0 h1:Kn4yilvwNtMACtf1eYDlG8H77R07mZSPbMjLyS07ChA= github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= @@ -469,6 +476,22 @@ github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnweb github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo= +github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M= +github.com/blevesearch/mmap-go v1.0.2 h1:JtMHb+FgQCTTYIhtMvimw15dJwu1Y5lrZDMOFXVWPk0= +github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+7LMvAB5IbSA= +github.com/blevesearch/segment v0.9.0 h1:5lG7yBCx98or7gK2cHMKPukPZ/31Kag7nONpoBt22Ac= +github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ= +github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s= +github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs= +github.com/blevesearch/vellum v1.0.5 h1:L5dJ7hKauRVbuH7I8uqLeSK92CPPY6FfrbAmLhAug8A= +github.com/blevesearch/vellum v1.0.5/go.mod h1:atE0EH3fvk43zzS7t1YNdNC7DbmcC3uz+eMD5xZ2OyQ= +github.com/blugelabs/bluge v0.1.9 h1:bPgXlcsWugrXNjzeoLdOnvfJpHsyODKpYaAndayl/SM= +github.com/blugelabs/bluge v0.1.9/go.mod h1:5d7LktUkQgvbh5Bmi6tPWtvo4+6uRTm6gAwP+5z6FqQ= +github.com/blugelabs/bluge_segment_api v0.2.0 h1:cCX1Y2y8v0LZ7+EEJ6gH7dW6TtVTW4RhG0vp3R+N2Lo= +github.com/blugelabs/bluge_segment_api v0.2.0/go.mod h1:95XA+ZXfRj/IXADm7gZ+iTcWOJPg5jQTY1EReIzl3LA= +github.com/blugelabs/ice v0.2.0 h1:9N/TRBqAr43emheD1ptk9mohuT6xAVq83gesgE60Qqk= +github.com/blugelabs/ice v0.2.0/go.mod h1:7foiDf4V83FIYYnGh2LOoRWsbNoCqAAMNgKn879Iyu0= github.com/bmatcuk/doublestar v1.2.2/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40/go.mod h1:8rLXio+WjiTceGBHIoTvn60HIbs7Hm7bcHjyrSqYB9c= @@ -481,7 +504,6 @@ github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQ github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/bsm/sarama-cluster v2.1.13+incompatible/go.mod h1:r7ao+4tTNXvWm+VRpRJchr2kQhqxgmAp2iEX5W96gMM= github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= -github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= @@ -491,6 +513,8 @@ github.com/c2h5oh/datasize v0.0.0-20200112174442-28bbd4740fee h1:BnPxIde0gjtTnc9 github.com/c2h5oh/datasize v0.0.0-20200112174442-28bbd4740fee/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M= github.com/cactus/go-statsd-client/statsd v0.0.0-20191106001114-12b4e2b38748/go.mod h1:l/bIBLeOl9eX+wxJAzxS4TveKRtAqlyDpHjhkfO0MEI= github.com/caio/go-tdigest v2.3.0+incompatible/go.mod h1:sHQM/ubZStBUmF1WbB8FAm8q9GjDajLC5T7ydxE3JHI= +github.com/caio/go-tdigest v3.1.0+incompatible h1:uoVMJ3Q5lXmVLCCqaMGHLBWnbGoN6Lpu7OAUPR60cds= +github.com/caio/go-tdigest v3.1.0+incompatible/go.mod h1:sHQM/ubZStBUmF1WbB8FAm8q9GjDajLC5T7ydxE3JHI= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/casbin/casbin/v2 v2.31.6/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= github.com/cenkalti/backoff v0.0.0-20181003080854-62661b46c409/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= @@ -762,6 +786,8 @@ github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/ github.com/dgryski/go-bitstream v0.0.0-20180413035011-3522498ce2c8/go.mod h1:VMaSuZ+SZcx/wljOQKvp5srsbCiKDEb6K2wC4+PiBmQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc h1:8WFBn63wegobsYAX0YjD+8suexZDga5CctH4CCTx2+8= +github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= @@ -828,7 +854,6 @@ github.com/dop251/goja v0.0.0-20210804101310-32956a348b49/go.mod h1:R9ET47fwRVRP github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= github.com/drone/envsubst v1.0.2/go.mod h1:bkZbnc/2vh1M12Ecn7EYScpI4YGYU0etwLJICOWi8Z0= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= @@ -1627,6 +1652,7 @@ github.com/influxdata/flux v0.120.1/go.mod h1:pGSAvyAA5d3et7SSzajaYShWYXmnRnJJq2 github.com/influxdata/go-syslog/v2 v2.0.1/go.mod h1:hjvie1UTaD5E1fTnDmxaCw8RRDrT4Ve+XHr5O2dKSCo= github.com/influxdata/go-syslog/v3 v3.0.1-0.20201128200927-a1889d947b48/go.mod h1:aXdIdfn2OcGnMhOTojXmwZqXKgC3MU5riiNvzwwG9OY= github.com/influxdata/httprouter v1.3.1-0.20191122104820-ee83e2772f69/go.mod h1:pwymjR6SrP3gD3pRj9RJwdl1j5s3doEEV8gS4X9qSzA= +github.com/influxdata/influxdb v1.7.6/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY= github.com/influxdata/influxdb v1.7.7/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY= github.com/influxdata/influxdb v1.8.0/go.mod h1:SIzcnsjaHRFpmlxpJ4S3NT64qtEKYweNTUMb/vh0OMQ= github.com/influxdata/influxdb v1.8.1/go.mod h1:SIzcnsjaHRFpmlxpJ4S3NT64qtEKYweNTUMb/vh0OMQ= @@ -1843,6 +1869,7 @@ github.com/lann/builder v0.0.0-20150808151131-f22ce00fd939/go.mod h1:dXGbAdH5GtB github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/leanovate/gopter v0.2.4/go.mod h1:gNcbPWNEWRe4lm+bycKqxUYoH5uoVje5SkOJ3uoLer8= +github.com/leesper/go_rng v0.0.0-20190531154944-a612b043e353 h1:X/79QL0b4YJVO5+OsPH9rF2u428CIrGL/jLmPsoOQQ4= github.com/leesper/go_rng v0.0.0-20190531154944-a612b043e353/go.mod h1:N0SVk0uhy+E1PZ3C9ctsPRlvOPAFPkCNlcPBDkt0N3U= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/ragel-machinery v0.0.0-20181214104525-299bdde78165/go.mod h1:WZxr2/6a/Ar9bMDc2rN/LJrE/hF6bXE4LPyDSIxwAfg= @@ -2050,6 +2077,8 @@ github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de h1:D5x39vF5KCwKQaw+OC9 github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de/go.mod h1:kJun4WP5gFuHZgRjZUWWuH1DTxCtxbHDOIJsudS8jzY= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= +github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= +github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= github.com/multiplay/go-ts3 v1.0.0/go.mod h1:14S6cS3fLNT3xOytrA/DkRyAFNuQLMLEqOYAsf87IbQ= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= @@ -3141,6 +3170,7 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190102155601-82a175fd1598/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -3470,6 +3500,7 @@ gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJ gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= gonum.org/v1/gonum v0.6.2/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= +gonum.org/v1/gonum v0.7.0/go.mod h1:L02bwd0sqlsvRv41G7wGWFCsVNZFv/k1xzGIxeANHGM= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= gonum.org/v1/gonum v0.11.0 h1:f1IJhK4Km5tBJmaiJXtk/PkL4cdVX6J+tGiM187uT5E= diff --git a/pkg/services/searchV2/bluge.go b/pkg/services/searchV2/bluge.go new file mode 100644 index 00000000000..19543ac31f9 --- /dev/null +++ b/pkg/services/searchV2/bluge.go @@ -0,0 +1,541 @@ +package searchV2 + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + "strings" + "time" + + "github.com/blugelabs/bluge" + "github.com/blugelabs/bluge/search" + "github.com/blugelabs/bluge/search/aggregations" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/infra/log" +) + +const ( + documentFieldUID = "_id" // actually UID!! but bluge likes "_id" + documentFieldKind = "kind" + documentFieldTag = "tag" + documentFieldURL = "url" + documentFieldName = "name" + documentFieldDescription = "description" + documentFieldLocation = "location" // parent path + documentFieldPanelType = "panel_type" + documentFieldDSUID = "ds_uid" + documentFieldDSType = "ds_type" + documentFieldInternalID = "__internal_id" // only for migrations! (indexed as a string) +) + +func initBlugeIndex(dashboards []dashboard, logger log.Logger) (*bluge.Reader, error) { + config := bluge.InMemoryOnlyConfig() + + // open an index writer using the configuration + writer, err := bluge.OpenWriter(config) + if err != nil { + return nil, fmt.Errorf("error opening writer: %v", err) + } + defer func() { + err = writer.Close() + if err != nil { + logger.Error("Error closing bluge writer", "error", err) + } + }() + + start := time.Now() + + logger.Info("Loading dashboards for bluge index", "elapsed", time.Since(start), "numDashboards", len(dashboards)) + label := time.Now() + + batch := bluge.NewBatch() + + // First index the folders + folderIdLookup := make(map[int64]string, 50) + for _, dashboard := range dashboards { + if !dashboard.isFolder { + continue + } + uid := dashboard.uid + url := fmt.Sprintf("/dashboards/f/%s/%s", dashboard.uid, dashboard.slug) + if uid == "" { + uid = "general" + url = "/dashboards" + dashboard.info.Title = "General" + dashboard.info.Description = "" + + // ARRRG, why is this not in the final index?!! + } + + doc := bluge.NewDocument(uid). + AddField(bluge.NewKeywordField(documentFieldKind, string(entityKindFolder)).Aggregatable().StoreValue()). + AddField(bluge.NewKeywordField(documentFieldURL, url).StoreValue()). + AddField(bluge.NewTextField(documentFieldName, dashboard.info.Title).StoreValue().SearchTermPositions()). + AddField(bluge.NewTextField(documentFieldDescription, dashboard.info.Description).SearchTermPositions()) + + batch.Insert(doc) + + folderIdLookup[dashboard.id] = uid + } + + // Then each dashboard + for _, dashboard := range dashboards { + if dashboard.isFolder { + continue + } + + url := fmt.Sprintf("/d/%s/%s", dashboard.uid, dashboard.slug) + folderUID := folderIdLookup[dashboard.folderID] + location := folderUID + + // Dashboard document + doc := bluge.NewDocument(dashboard.uid). + AddField(bluge.NewKeywordField(documentFieldKind, string(entityKindDashboard)).Aggregatable().StoreValue()). + AddField(bluge.NewKeywordField(documentFieldURL, url).StoreValue()). + AddField(bluge.NewKeywordField(documentFieldLocation, location).Aggregatable().StoreValue()). + AddField(bluge.NewTextField(documentFieldName, dashboard.info.Title).StoreValue().SearchTermPositions()). + AddField(bluge.NewTextField(documentFieldDescription, dashboard.info.Description).SearchTermPositions()) + + // Add legacy ID (for lookup by internal ID) + doc.AddField(bluge.NewKeywordField(documentFieldInternalID, fmt.Sprintf("%d", dashboard.id))) + + for _, tag := range dashboard.info.Tags { + doc.AddField(bluge.NewKeywordField(documentFieldTag, tag). + StoreValue(). + Aggregatable(). + SearchTermPositions()) + } + + for _, ds := range dashboard.info.Datasource { + if ds.UID != "" { + doc.AddField(bluge.NewKeywordField(documentFieldDSUID, ds.UID). + StoreValue(). + Aggregatable(). + SearchTermPositions()) + } + if ds.Type != "" { + doc.AddField(bluge.NewKeywordField(documentFieldDSType, ds.Type). + StoreValue(). + Aggregatable(). + SearchTermPositions()) + } + } + + // TODO: enterprise, add dashboard sorting fields + + batch.Insert(doc) + + location += "/" + dashboard.uid + + // Now add a doc for each panel + for _, panel := range dashboard.info.Panels { + uid := dashboard.uid + "#" + strconv.FormatInt(panel.ID, 10) + purl := url + if panel.Type != "row" { + purl = fmt.Sprintf("%s?viewPanel=%d", url, panel.ID) + } + + doc := bluge.NewDocument(uid). + AddField(bluge.NewKeywordField(documentFieldURL, purl).StoreValue()). + AddField(bluge.NewTextField(documentFieldName, panel.Title).StoreValue().SearchTermPositions()). + AddField(bluge.NewTextField(documentFieldDescription, panel.Description).SearchTermPositions()). + AddField(bluge.NewKeywordField(documentFieldPanelType, panel.Type).Aggregatable().StoreValue()). + AddField(bluge.NewKeywordField(documentFieldLocation, location).Aggregatable().StoreValue()). + AddField(bluge.NewKeywordField(documentFieldKind, string(entityKindPanel)).Aggregatable().StoreValue()) // likely want independent index for this + + batch.Insert(doc) + } + } + + logger.Info("Inserting documents into bluge batch", "elapsed", time.Since(label)) + label = time.Now() + + err = writer.Batch(batch) + if err != nil { + return nil, err + } + + reader, err := writer.Reader() + if err != nil { + return nil, err + } + + logger.Info("Inserting batch into bluge writer", "elapsed", time.Since(label)) + logger.Info("Finish building bluge index", "totalElapsed", time.Since(start)) + return reader, err +} + +//nolint: gocyclo +func doBlugeQuery(ctx context.Context, s *StandardSearchService, reader *bluge.Reader, filter ResourceFilter, q DashboardQuery) *backend.DataResponse { + response := &backend.DataResponse{} + + // Folder listing structure + idx := strings.Index(q.Query, ":") + if idx > 0 { + key := q.Query[0:idx] + val := q.Query[idx+1:] + if key == "list" { + q.Limit = 1000 + q.Query = "" + q.Location = "" + q.Explain = false + q.SkipLocation = true + q.Facet = nil + if val == "root" || val == "" { + q.Kind = []string{string(entityKindFolder)} + } else { + q.Location = val + q.Kind = []string{string(entityKindDashboard)} + } + } + } + + hasConstraints := false + fullQuery := bluge.NewBooleanQuery() + fullQuery.AddMust(newPermissionFilter(filter, s.logger)) + + // Only show dashboard / folders + if len(q.Kind) > 0 { + bq := bluge.NewBooleanQuery() + for _, k := range q.Kind { + bq.AddShould(bluge.NewTermQuery(k).SetField(documentFieldKind)) + } + fullQuery.AddMust(bq) + hasConstraints = true + } + + // Explicit UID lookup (stars etc) + if len(q.UIDs) > 0 { + bq := bluge.NewBooleanQuery() + for _, v := range q.UIDs { + bq.AddShould(bluge.NewTermQuery(v).SetField(documentFieldUID)) + } + fullQuery.AddMust(bq) + hasConstraints = true + } + + // Legacy lookup by internal ID + if len(q.IDs) > 0 { + bq := bluge.NewBooleanQuery() + for _, v := range q.IDs { + bq.AddShould(bluge.NewTermQuery(fmt.Sprintf("%d", v)).SetField(documentFieldInternalID)) + } + fullQuery.AddMust(bq) + hasConstraints = true + } + + // Tags + if len(q.Tags) > 0 { + bq := bluge.NewBooleanQuery() + for _, v := range q.Tags { + bq.AddMust(bluge.NewTermQuery(v).SetField(documentFieldTag)) + } + fullQuery.AddMust(bq) + hasConstraints = true + } + + // Datasource + if q.Datasource != "" { + fullQuery.AddMust(bluge.NewTermQuery(q.Datasource).SetField(documentFieldDSUID)) + hasConstraints = true + } + + // Folder + if q.Location != "" { + fullQuery.AddMust(bluge.NewTermQuery(q.Location).SetField(documentFieldLocation)) + hasConstraints = true + } + + if q.Query == "*" || q.Query == "" { + if !hasConstraints { + fullQuery.AddShould(bluge.NewMatchAllQuery()) + } + } else { + // The actual se + bq := bluge.NewBooleanQuery(). + AddShould(bluge.NewMatchPhraseQuery(q.Query).SetField("name").SetBoost(6)). + AddShould(bluge.NewMatchPhraseQuery(q.Query).SetField("description").SetBoost(3)). + AddShould(bluge.NewPrefixQuery(q.Query).SetField("name").SetBoost(1)) + + if len(q.Query) > 4 { + bq.AddShould(bluge.NewFuzzyQuery(q.Query).SetField("name")).SetBoost(1.5) + } + fullQuery.AddMust(bq) + } + + limit := 50 // default view + if q.Limit > 0 { + limit = q.Limit + } + + req := bluge.NewTopNSearch(limit, fullQuery) + if q.From > 0 { + req.SetFrom(q.From) + } + if q.Explain { + req.ExplainScores() + } + req.WithStandardAggregations() + + // SortBy([]string{"-_score", "name"}) + // req.SortBy([]string{documentFieldName}) + + for _, t := range q.Facet { + lim := t.Limit + if lim < 1 { + lim = 50 + } + req.AddAggregation(t.Field, aggregations.NewTermsAggregation(search.Field(t.Field), lim)) + } + + // execute this search on the reader + documentMatchIterator, err := reader.Search(ctx, req) + if err != nil { + s.logger.Error("error executing search: %v", err) + response.Error = err + return response + } + + dvfieldNames := []string{"type"} + sctx := search.NewSearchContext(0, 0) + + // numericFields := map[string]bool{"schemaVersion": true, "panelCount": true} + + fScore := data.NewFieldFromFieldType(data.FieldTypeFloat64, 0) + fUID := data.NewFieldFromFieldType(data.FieldTypeString, 0) + fKind := data.NewFieldFromFieldType(data.FieldTypeString, 0) + fPType := data.NewFieldFromFieldType(data.FieldTypeString, 0) + fName := data.NewFieldFromFieldType(data.FieldTypeString, 0) + fURL := data.NewFieldFromFieldType(data.FieldTypeString, 0) + fLocation := data.NewFieldFromFieldType(data.FieldTypeString, 0) + fTags := data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0) + fDSUIDs := data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0) + fExplain := data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0) + + fScore.Name = "score" + fUID.Name = "uid" + fKind.Name = "kind" + fName.Name = "name" + fLocation.Name = "location" + fURL.Name = "url" + fURL.Config = &data.FieldConfig{ + Links: []data.DataLink{ + {Title: "link", URL: "${__value.text}"}, + }, + } + fPType.Name = "panel_type" + fDSUIDs.Name = "ds_uid" + fTags.Name = "tags" + fExplain.Name = "explain" + + frame := data.NewFrame("Query results", fScore, fKind, fUID, fName, fPType, fURL, fTags, fDSUIDs, fLocation) + if q.Explain { + frame.Fields = append(frame.Fields, fExplain) + } + + locationItems := make(map[string]bool, 50) + + // iterate through the document matches + match, err := documentMatchIterator.Next() + for err == nil && match != nil { + err = match.LoadDocumentValues(sctx, dvfieldNames) + if err != nil { + continue + } + + uid := "" + kind := "" + ptype := "" + name := "" + url := "" + loc := "" + var ds_uids []string + var tags []string + + err = match.VisitStoredFields(func(field string, value []byte) bool { + // if numericFields[field] { + // num, err2 := bluge.DecodeNumericFloat64(value) + // if err2 != nil { + // vals[field] = num + // } + // } else { + // vals[field] = string(value) + // } + + switch field { + case documentFieldUID: + uid = string(value) + case documentFieldKind: + kind = string(value) + case documentFieldPanelType: + ptype = string(value) + case documentFieldName: + name = string(value) + case documentFieldURL: + url = string(value) + case documentFieldLocation: + loc = string(value) + case documentFieldDSUID: + ds_uids = append(ds_uids, string(value)) + case documentFieldTag: + tags = append(tags, string(value)) + } + return true + }) + if err != nil { + s.logger.Error("error loading stored fields: %v", err) + response.Error = err + return response + } + + fScore.Append(match.Score) + fKind.Append(kind) + fUID.Append(uid) + fPType.Append(ptype) + fName.Append(name) + fURL.Append(url) + fLocation.Append(loc) + + // set a key for all path parts we return + if !q.SkipLocation { + for _, v := range strings.Split(loc, "/") { + locationItems[v] = true + } + } + + if len(tags) > 0 { + js, _ := json.Marshal(tags) + jsb := json.RawMessage(js) + fTags.Append(&jsb) + } else { + fTags.Append(nil) + } + + if len(ds_uids) > 0 { + js, _ := json.Marshal(ds_uids) + jsb := json.RawMessage(js) + fDSUIDs.Append(&jsb) + } else { + fDSUIDs.Append(nil) + } + + if q.Explain { + if match.Explanation != nil { + js, _ := json.Marshal(&match.Explanation) + jsb := json.RawMessage(js) + fExplain.Append(&jsb) + } else { + fExplain.Append(nil) + } + } + + // load the next document match + match, err = documentMatchIterator.Next() + } + + // Must call after iterating :) + aggs := documentMatchIterator.Aggregations() + + header := &customMeta{ + Count: aggs.Count(), // Total cound + MaxScore: aggs.Metric("max_score"), + } + if len(locationItems) > 0 && !q.SkipLocation { + header.Locations = getLocationLookupInfo(ctx, reader, locationItems) + } + + frame.SetMeta(&data.FrameMeta{ + Type: "search-results", + Custom: header, + }) + + response.Frames = append(response.Frames, frame) + + for _, t := range q.Facet { + bbb := aggs.Buckets(t.Field) + if bbb != nil { + size := len(bbb) + + fName := data.NewFieldFromFieldType(data.FieldTypeString, size) + fName.Name = t.Field + + fCount := data.NewFieldFromFieldType(data.FieldTypeUint64, size) + fCount.Name = "Count" + + for i, v := range bbb { + fName.Set(i, v.Name()) + fCount.Set(i, v.Count()) + } + + response.Frames = append(response.Frames, data.NewFrame("Facet: "+t.Field, fName, fCount)) + } + } + + return response +} + +func getLocationLookupInfo(ctx context.Context, reader *bluge.Reader, uids map[string]bool) map[string]locationItem { + res := make(map[string]locationItem, len(uids)) + bq := bluge.NewBooleanQuery() + for k := range uids { + bq.AddShould(bluge.NewTermQuery(k).SetField(documentFieldUID)) + } + + req := bluge.NewAllMatches(bq) + + documentMatchIterator, err := reader.Search(ctx, req) + if err != nil { + return res + } + + dvfieldNames := []string{"type"} + sctx := search.NewSearchContext(0, 0) + + // execute this search on the reader + // iterate through the document matches + match, err := documentMatchIterator.Next() + for err == nil && match != nil { + err = match.LoadDocumentValues(sctx, dvfieldNames) + if err != nil { + continue + } + + uid := "" + item := locationItem{} + + _ = match.VisitStoredFields(func(field string, value []byte) bool { + switch field { + case documentFieldUID: + uid = string(value) + case documentFieldKind: + item.Kind = string(value) + case documentFieldName: + item.Name = string(value) + case documentFieldURL: + item.URL = string(value) + } + return true + }) + + res[uid] = item + + // load the next document match + match, err = documentMatchIterator.Next() + } + + return res +} + +type locationItem struct { + Name string `json:"name"` + Kind string `json:"kind"` + URL string `json:"url"` +} + +type customMeta struct { + Count uint64 `json:"count"` + MaxScore float64 `json:"max_score,omitempty"` + Locations map[string]locationItem `json:"locationInfo,omitempty"` +} diff --git a/pkg/services/searchV2/filter.go b/pkg/services/searchV2/filter.go new file mode 100644 index 00000000000..354e7fdc499 --- /dev/null +++ b/pkg/services/searchV2/filter.go @@ -0,0 +1,132 @@ +package searchV2 + +import ( + "regexp" + + "github.com/blugelabs/bluge" + "github.com/blugelabs/bluge/search" + "github.com/blugelabs/bluge/search/searcher" + "github.com/blugelabs/bluge/search/similarity" + "github.com/grafana/grafana/pkg/infra/log" +) + +type PermissionFilter struct { + log log.Logger + filter ResourceFilter +} + +type entityKind string + +const ( + entityKindPanel entityKind = "panel" + entityKindDashboard entityKind = "dashboard" + entityKindFolder entityKind = "folder" +) + +func (r entityKind) IsValid() bool { + return r == entityKindPanel || r == entityKindDashboard || r == entityKindFolder +} + +func (r entityKind) supportsAuthzCheck() bool { + return r == entityKindPanel || r == entityKindDashboard || r == entityKindFolder +} + +var ( + permissionFilterFields = []string{documentFieldUID, documentFieldKind} + panelIdFieldRegex = regexp.MustCompile(`^(.*)#([0-9]{1,4})$`) + panelIdFieldDashboardUidSubmatchIndex = 1 + panelIdFieldPanelIdSubmatchIndex = 2 + panelIdFieldRegexExpectedSubmatchCount = 3 // submatches[0] - whole string + + _ bluge.Query = (*PermissionFilter)(nil) +) + +func newPermissionFilter(resourceFilter ResourceFilter, log log.Logger) *PermissionFilter { + return &PermissionFilter{ + filter: resourceFilter, + log: log, + } +} + +func (q *PermissionFilter) logAccessDecision(decision bool, kind interface{}, id string, reason string, ctx ...interface{}) { + if true { + return // TOO much logging right now + } + + ctx = append(ctx, "kind", kind, "id", id, "reason", reason) + if decision { + q.log.Debug("allowing access", ctx...) + } else { + q.log.Info("denying access", ctx...) + } +} + +func (q *PermissionFilter) canAccess(kind entityKind, id string) bool { + if !kind.supportsAuthzCheck() { + q.logAccessDecision(false, kind, id, "entityDoesNotSupportAuthz") + return false + } + + // TODO add `kind` to the `ResourceFilter` interface so that we can move the switch out of here + // + switch kind { + case entityKindFolder: + if id == "" { + q.logAccessDecision(true, kind, id, "generalFolder") + return true + } + fallthrough + case entityKindDashboard: + decision := q.filter(id) + q.logAccessDecision(decision, kind, id, "resourceFilter") + return decision + case entityKindPanel: + matches := panelIdFieldRegex.FindStringSubmatch(id) + + submatchCount := len(matches) + if submatchCount != panelIdFieldRegexExpectedSubmatchCount { + q.logAccessDecision(false, kind, id, "invalidPanelIdFieldRegexSubmatchCount", "submatchCount", submatchCount, "expectedSubmatchCount", panelIdFieldRegexExpectedSubmatchCount) + return false + } + + dashboardUid := matches[panelIdFieldDashboardUidSubmatchIndex] + decision := q.filter(dashboardUid) + + q.logAccessDecision(decision, kind, id, "resourceFilter", "dashboardUid", dashboardUid, "panelId", matches[panelIdFieldPanelIdSubmatchIndex]) + return decision + default: + q.logAccessDecision(false, kind, id, "reason", "unknownKind") + return false + } +} + +func (q *PermissionFilter) Searcher(i search.Reader, options search.SearcherOptions) (search.Searcher, error) { + dvReader, err := i.DocumentValueReader(permissionFilterFields) + if err != nil { + return nil, err + } + + s, err := searcher.NewMatchAllSearcher(i, 1, similarity.ConstantScorer(1), options) + return searcher.NewFilteringSearcher(s, func(d *search.DocumentMatch) bool { + var kind, id string + err := dvReader.VisitDocumentValues(d.Number, func(field string, term []byte) { + if field == documentFieldKind { + kind = string(term) + } else if field == documentFieldUID { + id = string(term) + } + }) + if err != nil { + q.logAccessDecision(false, kind, id, "errorWhenVisitingDocumentValues") + return false + } + + e := entityKind(kind) + if !e.IsValid() { + q.logAccessDecision(false, kind, id, "invalidEntityKind") + return false + } + + return q.canAccess(e, id) + }), err +} diff --git a/pkg/services/searchV2/index.go b/pkg/services/searchV2/index.go index 79dbed598ed..c6ddeecc07b 100644 --- a/pkg/services/searchV2/index.go +++ b/pkg/services/searchV2/index.go @@ -13,6 +13,8 @@ import ( "github.com/grafana/grafana/pkg/services/searchV2/extract" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/store" + + "github.com/blugelabs/bluge" ) type dashboardLoader interface { @@ -32,7 +34,8 @@ type eventStore interface { type dashboardIndex struct { mu sync.RWMutex loader dashboardLoader - dashboards map[int64][]dashboard // orgId -> []dashboards + dashboards map[int64][]dashboard // orgId -> []dashboards + reader map[int64]*bluge.Reader // orgId -> bluge index eventStore eventStore logger log.Logger } @@ -53,6 +56,7 @@ func newDashboardIndex(dashLoader dashboardLoader, evStore eventStore) *dashboar loader: dashLoader, eventStore: evStore, dashboards: map[int64][]dashboard{}, + reader: map[int64]*bluge.Reader{}, logger: log.New("dashboardIndex"), } } @@ -81,6 +85,9 @@ func (i *dashboardIndex) run(ctx context.Context) error { } i.logger.Info("Indexing for main org finished", "mainOrgIndexElapsed", time.Since(started), "numDashboards", len(dashboards)) + // build bluge index on startup (will catch panics) + go i.reIndexFromScratchBluge(ctx) + for { select { case <-partialUpdateTicker.C: @@ -120,6 +127,60 @@ func (i *dashboardIndex) reIndexFromScratch(ctx context.Context) { } } +// Variation of the above function that builds bluge index from scratch +// Once the frontend is wired up, we should switch to this one +func (i *dashboardIndex) reIndexFromScratchBluge(ctx context.Context) { + // Catch Panic (just in case) + defer func() { + recv := recover() + if recv != nil { + i.logger.Error("panic in search runner", "recv", recv) // REMVOE after we are sure it works! + } + }() + + i.mu.RLock() + orgIDs := make([]int64, 0, len(i.dashboards)) + for orgID := range i.dashboards { + orgIDs = append(orgIDs, orgID) + } + i.mu.RUnlock() + if len(orgIDs) < 1 { + orgIDs = append(orgIDs, int64(1)) // make sure we index + } + + for _, orgID := range orgIDs { + started := time.Now() + ctx, cancel := context.WithTimeout(ctx, time.Minute) + + dashboards, err := i.loader.LoadDashboards(ctx, orgID, "") + if err != nil { + cancel() + i.logger.Error("Error re-indexing dashboards for organization", "orgId", orgID, "error", err) + continue + } + orgSearchIndexLoadTime := time.Since(started) + + reader, err := initBlugeIndex(dashboards, i.logger) + if err != nil { + cancel() + i.logger.Error("Error re-indexing dashboards for organization", "orgId", orgID, "error", err) + continue + } + orgSearchIndexTotalTime := time.Since(started) + orgSearchIndexBuildTime := orgSearchIndexTotalTime - orgSearchIndexLoadTime + + cancel() + i.logger.Info("Re-indexed dashboards for organization (bluge)", + "orgId", orgID, + "orgSearchIndexLoadTime", orgSearchIndexLoadTime, + "orgSearchIndexBuildTime", orgSearchIndexBuildTime, + "orgSearchIndexTotalTime", orgSearchIndexTotalTime) + i.mu.Lock() + i.reader[orgID] = reader + i.mu.Unlock() + } +} + func (i *dashboardIndex) applyIndexUpdates(ctx context.Context, lastEventID int64) int64 { events, err := i.eventStore.GetAllEventsAfter(context.Background(), lastEventID) if err != nil { diff --git a/pkg/services/searchV2/service.go b/pkg/services/searchV2/service.go index 17a098edd45..773318d45bb 100644 --- a/pkg/services/searchV2/service.go +++ b/pkg/services/searchV2/service.go @@ -105,8 +105,24 @@ func (s *StandardSearchService) getUser(ctx context.Context, backendUser *backen return user, nil } -func (s *StandardSearchService) DoDashboardQuery(ctx context.Context, user *backend.User, orgId int64, _ DashboardQuery) *backend.DataResponse { +func (s *StandardSearchService) DoDashboardQuery(ctx context.Context, user *backend.User, orgId int64, q DashboardQuery) *backend.DataResponse { rsp := &backend.DataResponse{} + signedInUser, err := s.getUser(ctx, user, orgId) + if err != nil { + rsp.Error = err + return rsp + } + + filter, err := s.auth.GetDashboardReadFilter(signedInUser) + if err != nil { + rsp.Error = err + return rsp + } + + reader := s.dashboardIndex.reader[orgId] + if reader != nil && q.Query != "" { // frontend initializes with empty string + return doBlugeQuery(ctx, s, reader, filter, q) + } dashboards, err := s.dashboardIndex.getDashboards(ctx, orgId) if err != nil { @@ -114,29 +130,14 @@ func (s *StandardSearchService) DoDashboardQuery(ctx context.Context, user *back return rsp } - signedInUser, err := s.getUser(ctx, user, orgId) - if err != nil { - rsp.Error = err - return rsp - } - - dashboards, err = s.applyAuthFilter(signedInUser, dashboards) - if err != nil { - rsp.Error = err - return rsp - } + dashboards = s.applyAuthFilter(filter, dashboards) rsp.Frames = metaToFrame(dashboards) return rsp } -func (s *StandardSearchService) applyAuthFilter(user *models.SignedInUser, dashboards []dashboard) ([]dashboard, error) { - filter, err := s.auth.GetDashboardReadFilter(user) - if err != nil { - return nil, err - } - +func (s *StandardSearchService) applyAuthFilter(filter ResourceFilter, dashboards []dashboard) []dashboard { // create a list of all viewable dashboards for this user. res := make([]dashboard, 0, len(dashboards)) for _, dash := range dashboards { @@ -144,7 +145,7 @@ func (s *StandardSearchService) applyAuthFilter(user *models.SignedInUser, dashb res = append(res, dash) } } - return res, nil + return res } type simpleCounter struct { diff --git a/pkg/services/searchV2/types.go b/pkg/services/searchV2/types.go index 12fddfb6d3e..33475bd7398 100644 --- a/pkg/services/searchV2/types.go +++ b/pkg/services/searchV2/types.go @@ -8,8 +8,27 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" ) +type FacetField struct { + Field string `json:"field"` + Limit int `json:"limit,omitempty"` // explicit page size +} + type DashboardQuery struct { - Query string + Query string `json:"query"` + Location string `json:"location,omitempty"` // parent folder ID + Sort string `json:"sort,omitempty"` // field ASC/DESC + Datasource string `json:"ds_uid,omitempty"` // "datasource" collides with the JSON value at the same leel :() + Tags []string `json:"tags,omitempty"` + Kind []string `json:"kind,omitempty"` + UIDs []string `json:"uid,omitempty"` + IDs []int64 `json:"id,omitempty"` // deprecated -- but will convert internal ID to UIDs + Explain bool `json:"explain,omitempty"` // adds details on why document matched + Facet []FacetField `json:"facet,omitempty"` + SkipLocation bool `json:"skipLocation,omitempty"` + AccessInfo bool `json:"accessInfo,omitempty"` // adds field for access control + HasPreview string `json:"hasPreview,omitempty"` // the light|dark theme + Limit int `json:"limit,omitempty"` // explicit page size + From int `json:"from,omitempty"` // for paging } type SearchService interface { From 53a4f3913c68b35afd27cd2bfcd5b24ba3899ec3 Mon Sep 17 00:00:00 2001 From: Joe Blubaugh Date: Mon, 9 May 2022 16:13:47 +0800 Subject: [PATCH 109/440] Alerting: Apply Custom Headers to datasource queries. (#47860) Unlike dashboard queries, alerting queries were not correctly applying custom headers to datasource queries. This change mimics the dashboard query code to apply custom headers. Fixes #44460 Signed-off-by: Joe Blubaugh --- pkg/tsdb/legacydata/service/service.go | 140 +++++++++++++------- pkg/tsdb/legacydata/service/service_test.go | 33 +++++ 2 files changed, 123 insertions(+), 50 deletions(-) diff --git a/pkg/tsdb/legacydata/service/service.go b/pkg/tsdb/legacydata/service/service.go index 5c7f5ac5b7a..2717207cec4 100644 --- a/pkg/tsdb/legacydata/service/service.go +++ b/pkg/tsdb/legacydata/service/service.go @@ -3,9 +3,11 @@ package service import ( "context" "fmt" + "strings" "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/adapters" @@ -14,6 +16,11 @@ import ( "github.com/grafana/grafana/pkg/tsdb/legacydata" ) +const ( + headerName = "httpHeaderName" + headerValue = "httpHeaderValue" +) + var oAuthIsOAuthPassThruEnabledFunc = func(oAuthTokenService oauthtoken.OAuthTokenService, ds *models.DataSource) bool { return oAuthTokenService.IsOAuthPassThruEnabled(ds) } @@ -35,70 +42,23 @@ func ProvideService(pluginsClient plugins.Client, oAuthTokenService oauthtoken.O //nolint: staticcheck // legacydata.DataResponse deprecated func (h *Service) HandleRequest(ctx context.Context, ds *models.DataSource, query legacydata.DataQuery) (legacydata.DataResponse, error) { - jsonDataBytes, err := ds.JsonData.MarshalJSON() + decryptedJsonData, err := h.dataSourcesService.DecryptedValues(ctx, ds) if err != nil { return legacydata.DataResponse{}, err } - decryptedValues, err := h.dataSourcesService.DecryptedValues(ctx, ds) + req, err := generateRequest(ctx, ds, decryptedJsonData, query) if err != nil { return legacydata.DataResponse{}, err } - instanceSettings := &backend.DataSourceInstanceSettings{ - ID: ds.Id, - Name: ds.Name, - URL: ds.Url, - Database: ds.Database, - User: ds.User, - BasicAuthEnabled: ds.BasicAuth, - BasicAuthUser: ds.BasicAuthUser, - JSONData: jsonDataBytes, - DecryptedSecureJSONData: decryptedValues, - Updated: ds.Updated, - UID: ds.Uid, - } - - if query.Headers == nil { - query.Headers = make(map[string]string) - } - + // Attach Auth information if oAuthIsOAuthPassThruEnabledFunc(h.oAuthTokenService, ds) { if token := h.oAuthTokenService.GetCurrentOAuthToken(ctx, query.User); token != nil { - delete(query.Headers, "Authorization") query.Headers["Authorization"] = fmt.Sprintf("%s %s", token.Type(), token.AccessToken) } } - req := &backend.QueryDataRequest{ - PluginContext: backend.PluginContext{ - OrgID: ds.OrgId, - PluginID: ds.Type, - User: adapters.BackendUserFromSignedInUser(query.User), - DataSourceInstanceSettings: instanceSettings, - }, - Queries: []backend.DataQuery{}, - Headers: query.Headers, - } - - for _, q := range query.Queries { - modelJSON, err := q.Model.MarshalJSON() - if err != nil { - return legacydata.DataResponse{}, err - } - req.Queries = append(req.Queries, backend.DataQuery{ - RefID: q.RefID, - Interval: time.Duration(q.IntervalMS) * time.Millisecond, - MaxDataPoints: q.MaxDataPoints, - TimeRange: backend.TimeRange{ - From: query.TimeRange.GetFromAsTimeUTC(), - To: query.TimeRange.GetToAsTimeUTC(), - }, - QueryType: q.QueryType, - JSON: modelJSON, - }) - } - resp, err := h.pluginsClient.QueryData(ctx, req) if err != nil { return legacydata.DataResponse{}, err @@ -131,4 +91,84 @@ func (h *Service) HandleRequest(ctx context.Context, ds *models.DataSource, quer return tR, nil } +func generateRequest(ctx context.Context, ds *models.DataSource, decryptedJsonData map[string]string, query legacydata.DataQuery) (*backend.QueryDataRequest, error) { + jsonDataBytes, err := ds.JsonData.MarshalJSON() + if err != nil { + return nil, err + } + + instanceSettings := &backend.DataSourceInstanceSettings{ + ID: ds.Id, + Name: ds.Name, + URL: ds.Url, + Database: ds.Database, + User: ds.User, + BasicAuthEnabled: ds.BasicAuth, + BasicAuthUser: ds.BasicAuthUser, + JSONData: jsonDataBytes, + DecryptedSecureJSONData: decryptedJsonData, + Updated: ds.Updated, + UID: ds.Uid, + } + + if query.Headers == nil { + query.Headers = make(map[string]string) + } + + req := &backend.QueryDataRequest{ + PluginContext: backend.PluginContext{ + OrgID: ds.OrgId, + PluginID: ds.Type, + User: adapters.BackendUserFromSignedInUser(query.User), + DataSourceInstanceSettings: instanceSettings, + }, + Queries: []backend.DataQuery{}, + Headers: query.Headers, + } + + // Apply Configured Custom Headers to query request. + for k, v := range customHeaders(ds.JsonData, instanceSettings.DecryptedSecureJSONData) { + req.Headers[k] = v + } + + for _, q := range query.Queries { + modelJSON, err := q.Model.MarshalJSON() + if err != nil { + return nil, err + } + req.Queries = append(req.Queries, backend.DataQuery{ + RefID: q.RefID, + Interval: time.Duration(q.IntervalMS) * time.Millisecond, + MaxDataPoints: q.MaxDataPoints, + TimeRange: backend.TimeRange{ + From: query.TimeRange.GetFromAsTimeUTC(), + To: query.TimeRange.GetToAsTimeUTC(), + }, + QueryType: q.QueryType, + JSON: modelJSON, + }) + } + return req, nil +} + +func customHeaders(jsonData *simplejson.Json, decryptedJsonData map[string]string) map[string]string { + if jsonData == nil { + return nil + } + + data := jsonData.MustMap() + + headers := map[string]string{} + for k := range data { + if strings.HasPrefix(k, headerName) { + if header, ok := data[k].(string); ok { + valueKey := strings.ReplaceAll(k, headerName, headerValue) + headers[header] = decryptedJsonData[valueKey] + } + } + } + + return headers +} + var _ legacydata.RequestHandler = &Service{} diff --git a/pkg/tsdb/legacydata/service/service_test.go b/pkg/tsdb/legacydata/service/service_test.go index 33bef6cf3b5..24457784b95 100644 --- a/pkg/tsdb/legacydata/service/service_test.go +++ b/pkg/tsdb/legacydata/service/service_test.go @@ -59,6 +59,39 @@ func TestHandleRequest(t *testing.T) { }) } +func Test_generateRequest(t *testing.T) { + t.Run("Should attach custom headers to request if present", func(t *testing.T) { + jsonData := simplejson.New() + jsonData.Set(headerName+"testOne", "x-test-one") + jsonData.Set("testOne", "x-test-wrong") + jsonData.Set(headerName+"testTwo", "x-test-two") + + decryptedJsonData := map[string]string{ + headerValue + "testOne": "secret-value-one", + headerValue + "testTwo": "secret-value-two", + "something": "else", + } + + ds := &models.DataSource{Id: 12, Type: "unregisteredType", JsonData: jsonData} + query := legacydata.DataQuery{ + TimeRange: &legacydata.DataTimeRange{}, + Queries: []legacydata.DataSubQuery{ + {RefID: "A", DataSource: &models.DataSource{Id: 1, Type: "test"}, Model: simplejson.New()}, + {RefID: "B", DataSource: &models.DataSource{Id: 1, Type: "test"}, Model: simplejson.New()}, + }, + } + + req, err := generateRequest(context.Background(), ds, decryptedJsonData, query) + require.NoError(t, err) + require.NotNil(t, req) + require.EqualValues(t, + map[string]string{ + "x-test-one": "secret-value-one", + "x-test-two": "secret-value-two", + }, req.Headers) + }) +} + type fakePluginsClient struct { plugins.Client backend.QueryDataHandlerFunc From 6923b4c6c6787e53d05694d3788864f6e3bd5afb Mon Sep 17 00:00:00 2001 From: Ieva Date: Mon, 9 May 2022 11:01:03 +0100 Subject: [PATCH 110/440] Dashboard: Fix dashboard update permission check (#48746) * Change dash permission check for dashboards that are moved to a different folder --- pkg/services/dashboards/database/database.go | 4 ---- .../dashboards/manager/dashboard_service.go | 5 ++-- .../dashboard_service_integration_test.go | 24 +++++++++---------- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index c3d5285f30b..2935e536a83 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -326,10 +326,6 @@ func getExistingDashboardByIdOrUidForUpdate(sess *sqlstore.DBSession, dash *mode dash.SetId(existingByUid.Id) dash.SetUid(existingByUid.Uid) existing = existingByUid - - if !dash.IsFolder { - isParentFolderChanged = true - } } if (existing.IsFolder && !dash.IsFolder) || diff --git a/pkg/services/dashboards/manager/dashboard_service.go b/pkg/services/dashboards/manager/dashboard_service.go index 5205653df5f..f8bc2c3618c 100644 --- a/pkg/services/dashboards/manager/dashboard_service.go +++ b/pkg/services/dashboards/manager/dashboard_service.go @@ -110,8 +110,9 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d } if isParentFolderChanged { - folderGuardian := guardian.New(ctx, dash.FolderId, dto.OrgId, dto.User) - if canSave, err := folderGuardian.CanSave(); err != nil || !canSave { + // Check that the user is allowed to add a dashboard to the folder + guardian := guardian.New(ctx, dash.Id, dto.OrgId, dto.User) + if canSave, err := guardian.CanCreate(dash.FolderId, dash.IsFolder); err != nil || !canSave { if err != nil { return nil, err } diff --git a/pkg/services/dashboards/manager/dashboard_service_integration_test.go b/pkg/services/dashboards/manager/dashboard_service_integration_test.go index 99a9d6cb97a..dfd700b8bad 100644 --- a/pkg/services/dashboards/manager/dashboard_service_integration_test.go +++ b/pkg/services/dashboards/manager/dashboard_service_integration_test.go @@ -127,7 +127,7 @@ func TestIntegratedDashboardService(t *testing.T) { assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId) }) - permissionScenario(t, "When creating a new dashboard by existing title in folder, it should create dashboard guardian for folder with correct arguments and result in access denied error", + permissionScenario(t, "When creating a new dashboard by existing title in folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", canSave, func(t *testing.T, sc *permissionScenarioContext) { cmd := models.SaveDashboardCommand{ OrgId: testOrgID, @@ -142,12 +142,12 @@ func TestIntegratedDashboardService(t *testing.T) { err := callSaveWithError(cmd, sc.sqlStore) require.Equal(t, models.ErrDashboardUpdateAccessDenied, err) - assert.Equal(t, sc.savedFolder.Id, sc.dashboardGuardianMock.DashId) + assert.Equal(t, sc.savedDashInFolder.Id, sc.dashboardGuardianMock.DashId) assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId) }) - permissionScenario(t, "When creating a new dashboard by existing UID in folder, it should create dashboard guardian for folder with correct arguments and result in access denied error", + permissionScenario(t, "When creating a new dashboard by existing UID in folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", canSave, func(t *testing.T, sc *permissionScenarioContext) { cmd := models.SaveDashboardCommand{ OrgId: testOrgID, @@ -163,7 +163,7 @@ func TestIntegratedDashboardService(t *testing.T) { err := callSaveWithError(cmd, sc.sqlStore) require.Equal(t, models.ErrDashboardUpdateAccessDenied, err) - assert.Equal(t, sc.savedFolder.Id, sc.dashboardGuardianMock.DashId) + assert.Equal(t, sc.savedDashInFolder.Id, sc.dashboardGuardianMock.DashId) assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId) }) @@ -210,7 +210,7 @@ func TestIntegratedDashboardService(t *testing.T) { assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId) }) - permissionScenario(t, "When moving a dashboard by existing ID to other folder from General folder, it should create dashboard guardian for other folder with correct arguments and result in access denied error", + permissionScenario(t, "When moving a dashboard by existing ID to other folder from General folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", canSave, func(t *testing.T, sc *permissionScenarioContext) { cmd := models.SaveDashboardCommand{ OrgId: testOrgID, @@ -226,12 +226,12 @@ func TestIntegratedDashboardService(t *testing.T) { err := callSaveWithError(cmd, sc.sqlStore) require.Equal(t, models.ErrDashboardUpdateAccessDenied, err) - assert.Equal(t, sc.otherSavedFolder.Id, sc.dashboardGuardianMock.DashId) + assert.Equal(t, sc.savedDashInGeneralFolder.Id, sc.dashboardGuardianMock.DashId) assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId) }) - permissionScenario(t, "When moving a dashboard by existing id to the General folder from other folder, it should create dashboard guardian for General folder with correct arguments and result in access denied error", + permissionScenario(t, "When moving a dashboard by existing id to the General folder from other folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", canSave, func(t *testing.T, sc *permissionScenarioContext) { cmd := models.SaveDashboardCommand{ OrgId: testOrgID, @@ -247,12 +247,12 @@ func TestIntegratedDashboardService(t *testing.T) { err := callSaveWithError(cmd, sc.sqlStore) assert.Equal(t, models.ErrDashboardUpdateAccessDenied, err) - assert.Equal(t, int64(0), sc.dashboardGuardianMock.DashId) + assert.Equal(t, sc.savedDashInFolder.Id, sc.dashboardGuardianMock.DashId) assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId) }) - permissionScenario(t, "When moving a dashboard by existing uid to other folder from General folder, it should create dashboard guardian for other folder with correct arguments and result in access denied error", + permissionScenario(t, "When moving a dashboard by existing uid to other folder from General folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", canSave, func(t *testing.T, sc *permissionScenarioContext) { cmd := models.SaveDashboardCommand{ OrgId: testOrgID, @@ -268,12 +268,12 @@ func TestIntegratedDashboardService(t *testing.T) { err := callSaveWithError(cmd, sc.sqlStore) require.Equal(t, models.ErrDashboardUpdateAccessDenied, err) - assert.Equal(t, sc.otherSavedFolder.Id, sc.dashboardGuardianMock.DashId) + assert.Equal(t, sc.savedDashInGeneralFolder.Id, sc.dashboardGuardianMock.DashId) assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId) }) - permissionScenario(t, "When moving a dashboard by existing UID to the General folder from other folder, it should create dashboard guardian for General folder with correct arguments and result in access denied error", + permissionScenario(t, "When moving a dashboard by existing UID to the General folder from other folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", canSave, func(t *testing.T, sc *permissionScenarioContext) { cmd := models.SaveDashboardCommand{ OrgId: testOrgID, @@ -289,7 +289,7 @@ func TestIntegratedDashboardService(t *testing.T) { err := callSaveWithError(cmd, sc.sqlStore) require.Equal(t, models.ErrDashboardUpdateAccessDenied, err) - assert.Equal(t, int64(0), sc.dashboardGuardianMock.DashId) + assert.Equal(t, sc.savedDashInFolder.Id, sc.dashboardGuardianMock.DashId) assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserId) }) From b1bde7667f97644a91cd3058737d56872ce11167 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Mon, 9 May 2022 13:43:10 +0200 Subject: [PATCH 111/440] CloudMonitoring: Allow to set a custom value or disable graph_period (#48646) --- pkg/tsdb/cloudmonitoring/cloudmonitoring.go | 1 + pkg/tsdb/cloudmonitoring/time_series_query.go | 21 +++++++-- .../cloudmonitoring/time_series_query_test.go | 10 ++++ pkg/tsdb/cloudmonitoring/types.go | 2 + .../cloud-monitoring/components/Alignment.tsx | 11 +++-- .../components/GraphPeriod.test.tsx | 39 +++++++++++++++ .../components/GraphPeriod.tsx | 47 +++++++++++++++++++ .../components/MetricQueryEditor.tsx | 19 ++++++-- .../{AlignmentPeriod.tsx => PeriodSelect.tsx} | 32 ++++++++----- .../components/SLO/SLOQueryEditor.tsx | 14 +++--- .../cloud-monitoring/components/index.ts | 2 +- .../datasource/cloud-monitoring/constants.ts | 23 ++++++++- .../datasource/cloud-monitoring/types.ts | 2 + 13 files changed, 186 insertions(+), 37 deletions(-) create mode 100644 public/app/plugins/datasource/cloud-monitoring/components/GraphPeriod.test.tsx create mode 100644 public/app/plugins/datasource/cloud-monitoring/components/GraphPeriod.tsx rename public/app/plugins/datasource/cloud-monitoring/components/{AlignmentPeriod.tsx => PeriodSelect.tsx} (63%) diff --git a/pkg/tsdb/cloudmonitoring/cloudmonitoring.go b/pkg/tsdb/cloudmonitoring/cloudmonitoring.go index e319c035e2d..6f8fbb1cf92 100644 --- a/pkg/tsdb/cloudmonitoring/cloudmonitoring.go +++ b/pkg/tsdb/cloudmonitoring/cloudmonitoring.go @@ -338,6 +338,7 @@ func (s *Service) buildQueryExecutors(req *backend.QueryDataRequest) ([]cloudMon IntervalMS: query.Interval.Milliseconds(), AliasBy: q.MetricQuery.AliasBy, timeRange: req.Queries[0].TimeRange, + GraphPeriod: q.MetricQuery.GraphPeriod, } } else { cmtsf.AliasBy = q.MetricQuery.AliasBy diff --git a/pkg/tsdb/cloudmonitoring/time_series_query.go b/pkg/tsdb/cloudmonitoring/time_series_query.go index f885432e315..addd562ece3 100644 --- a/pkg/tsdb/cloudmonitoring/time_series_query.go +++ b/pkg/tsdb/cloudmonitoring/time_series_query.go @@ -20,6 +20,21 @@ import ( "github.com/grafana/grafana/pkg/tsdb/intervalv2" ) +func (timeSeriesQuery cloudMonitoringTimeSeriesQuery) appendGraphPeriod(req *backend.QueryDataRequest) string { + // GraphPeriod needs to be explicitly disabled. + // If not set, the default behavior is to set an automatic value + if timeSeriesQuery.GraphPeriod != "disabled" { + graphPeriod := timeSeriesQuery.GraphPeriod + if graphPeriod == "auto" || graphPeriod == "" { + intervalCalculator := intervalv2.NewCalculator(intervalv2.CalculatorOptions{}) + interval := intervalCalculator.Calculate(req.Queries[0].TimeRange, time.Duration(timeSeriesQuery.IntervalMS/1000)*time.Second, req.Queries[0].MaxDataPoints) + graphPeriod = interval.Text + } + return fmt.Sprintf(" | graph_period %s", graphPeriod) + } + return "" +} + func (timeSeriesQuery cloudMonitoringTimeSeriesQuery) run(ctx context.Context, req *backend.QueryDataRequest, s *Service, dsInfo datasourceInfo, tracer tracing.Tracer) (*backend.DataResponse, cloudMonitoringResponse, string, error) { dr := &backend.DataResponse{} @@ -35,13 +50,11 @@ func (timeSeriesQuery cloudMonitoringTimeSeriesQuery) run(ctx context.Context, r slog.Info("No project name set on query, using project name from datasource", "projectName", projectName) } - intervalCalculator := intervalv2.NewCalculator(intervalv2.CalculatorOptions{}) - interval := intervalCalculator.Calculate(req.Queries[0].TimeRange, time.Duration(timeSeriesQuery.IntervalMS/1000)*time.Second, req.Queries[0].MaxDataPoints) - + timeSeriesQuery.Query += timeSeriesQuery.appendGraphPeriod(req) from := req.Queries[0].TimeRange.From to := req.Queries[0].TimeRange.To timeFormat := "2006/01/02-15:04:05" - timeSeriesQuery.Query += fmt.Sprintf(" | graph_period %s | within d'%s', d'%s'", interval.Text, from.UTC().Format(timeFormat), to.UTC().Format(timeFormat)) + timeSeriesQuery.Query += fmt.Sprintf(" | within d'%s', d'%s'", from.UTC().Format(timeFormat), to.UTC().Format(timeFormat)) buf, err := json.Marshal(map[string]interface{}{ "query": timeSeriesQuery.Query, diff --git a/pkg/tsdb/cloudmonitoring/time_series_query_test.go b/pkg/tsdb/cloudmonitoring/time_series_query_test.go index 5f1900f9e75..33912e9bf48 100644 --- a/pkg/tsdb/cloudmonitoring/time_series_query_test.go +++ b/pkg/tsdb/cloudmonitoring/time_series_query_test.go @@ -101,4 +101,14 @@ func TestTimeSeriesQuery(t *testing.T) { require.True(t, ok) assert.Equal(t, "6724404429462225363", labels["resource.label.instance_id"]) }) + + t.Run("appends graph_period to the query", func(t *testing.T) { + query := &cloudMonitoringTimeSeriesQuery{} + assert.Equal(t, query.appendGraphPeriod(&backend.QueryDataRequest{Queries: []backend.DataQuery{{}}}), " | graph_period 10ms") + }) + + t.Run("skips graph_period if disabled", func(t *testing.T) { + query := &cloudMonitoringTimeSeriesQuery{GraphPeriod: "disabled"} + assert.Equal(t, query.appendGraphPeriod(&backend.QueryDataRequest{Queries: []backend.DataQuery{{}}}), "") + }) } diff --git a/pkg/tsdb/cloudmonitoring/types.go b/pkg/tsdb/cloudmonitoring/types.go index a2cce2fb8b1..4d5d4fdbd65 100644 --- a/pkg/tsdb/cloudmonitoring/types.go +++ b/pkg/tsdb/cloudmonitoring/types.go @@ -40,6 +40,7 @@ type ( IntervalMS int64 AliasBy string timeRange backend.TimeRange + GraphPeriod string } metricQuery struct { @@ -56,6 +57,7 @@ type ( Query string Preprocessor string PreprocessorType preprocessorType + GraphPeriod string } sloQuery struct { diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Alignment.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Alignment.tsx index 7d4226183df..edb9e8163a1 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/Alignment.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/Alignment.tsx @@ -2,11 +2,11 @@ import React, { FC } from 'react'; import { SelectableValue } from '@grafana/data'; -import { SELECT_WIDTH } from '../constants'; +import { ALIGNMENT_PERIODS, SELECT_WIDTH } from '../constants'; import CloudMonitoringDatasource from '../datasource'; import { CustomMetaData, MetricQuery, SLOQuery } from '../types'; -import { AlignmentFunction, AlignmentPeriod, AlignmentPeriodLabel, QueryEditorField, QueryEditorRow } from '.'; +import { AlignmentFunction, PeriodSelect, AlignmentPeriodLabel, QueryEditorField, QueryEditorRow } from '.'; export interface Props { refId: string; @@ -39,12 +39,13 @@ export const Alignment: FC = ({ onChange={onChange} /> - onChange({ ...query, alignmentPeriod: period })} + aligmentPeriods={ALIGNMENT_PERIODS} /> diff --git a/public/app/plugins/datasource/cloud-monitoring/components/GraphPeriod.test.tsx b/public/app/plugins/datasource/cloud-monitoring/components/GraphPeriod.test.tsx new file mode 100644 index 00000000000..0c5307889cc --- /dev/null +++ b/public/app/plugins/datasource/cloud-monitoring/components/GraphPeriod.test.tsx @@ -0,0 +1,39 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { select } from 'react-select-event'; + +import { GraphPeriod, Props } from './GraphPeriod'; + +const props: Props = { + onChange: jest.fn(), + refId: 'A', + variableOptionGroup: { options: [] }, +}; + +describe('Graph Period', () => { + it('should enable graph_period by default', () => { + render(); + expect(screen.getByLabelText('Graph period')).not.toBeDisabled(); + }); + + it('should disable graph_period when toggled', async () => { + const onChange = jest.fn(); + render(); + const s = screen.getByTestId('A-switch-graph-period'); + await userEvent.click(s); + expect(onChange).toHaveBeenCalledWith('disabled'); + }); + + it('should set a different value when selected', async () => { + const onChange = jest.fn(); + render(); + const selectEl = screen.getByLabelText('Graph period'); + expect(selectEl).toBeInTheDocument(); + + await select(selectEl, '1m', { + container: document.body, + }); + expect(onChange).toHaveBeenCalledWith('1m'); + }); +}); diff --git a/public/app/plugins/datasource/cloud-monitoring/components/GraphPeriod.tsx b/public/app/plugins/datasource/cloud-monitoring/components/GraphPeriod.tsx new file mode 100644 index 00000000000..e6081a1b43f --- /dev/null +++ b/public/app/plugins/datasource/cloud-monitoring/components/GraphPeriod.tsx @@ -0,0 +1,47 @@ +import React, { FunctionComponent } from 'react'; + +import { SelectableValue } from '@grafana/data'; +import { Switch } from '@grafana/ui'; + +import { GRAPH_PERIODS, SELECT_WIDTH } from '../constants'; + +import { PeriodSelect, QueryEditorRow } from '.'; + +export interface Props { + refId: string; + onChange: (period: string) => void; + variableOptionGroup: SelectableValue; + graphPeriod?: string; +} + +export const GraphPeriod: FunctionComponent = ({ refId, onChange, graphPeriod, variableOptionGroup }) => { + return ( + <> + + Set graph_period which forces a preferred period between points. Automatically set to the + current interval if left blank. + + } + > + onChange(e.currentTarget.checked ? '' : 'disabled')} + /> + + + + ); +}; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.tsx index d6ec09e9941..89d78b7111a 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/MetricQueryEditor.tsx @@ -16,6 +16,7 @@ import { ValueTypes, } from '../types'; +import { GraphPeriod } from './GraphPeriod'; import { MQLQueryEditor } from './MQLQueryEditor'; import { AliasBy, Project, VisualMetricQueryEditor } from '.'; @@ -128,11 +129,19 @@ function Editor({ )} {editorMode === EditorMode.MQL && ( - onQueryChange({ ...query, query: q })} - onRunQuery={onRunQuery} - query={query.query} - > + <> + onQueryChange({ ...query, query: q })} + onRunQuery={onRunQuery} + query={query.query} + > + onQueryChange({ ...query, graphPeriod })} + graphPeriod={query.graphPeriod} + refId={refId} + variableOptionGroup={variableOptionGroup} + /> + )} { +export interface Props { inputId: string; - onChange(query: TQuery): void; - query: TQuery; + onChange: (period: string) => void; templateVariableOptions: Array>; + aligmentPeriods: periodOption[]; selectWidth?: number; + category?: string; + disabled?: boolean; + current?: string; } -export function AlignmentPeriod({ +export function PeriodSelect({ inputId, templateVariableOptions, onChange, - query, + current, selectWidth, -}: Props) { + disabled, + aligmentPeriods, +}: Props) { const options = useMemo( () => - ALIGNMENT_PERIODS.map((ap) => ({ + aligmentPeriods.map((ap) => ({ ...ap, label: ap.text, })), - [] + [aligmentPeriods] ); const visibleOptions = useMemo(() => options.filter((ap) => !ap.hidden), [options]); return ( ); } diff --git a/public/app/plugins/datasource/cloud-monitoring/components/SLO/SLOQueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/SLO/SLOQueryEditor.tsx index f33fe4e826b..a61376d06d8 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/SLO/SLOQueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/SLO/SLOQueryEditor.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { SelectableValue } from '@grafana/data'; -import { AliasBy, AlignmentPeriod, AlignmentPeriodLabel, Project, QueryEditorRow } from '..'; -import { SELECT_WIDTH } from '../../constants'; +import { AliasBy, PeriodSelect, AlignmentPeriodLabel, Project, QueryEditorRow } from '..'; +import { ALIGNMENT_PERIODS, SELECT_WIDTH } from '../../constants'; import CloudMonitoringDatasource from '../../datasource'; import { AlignmentTypes, CustomMetaData, SLOQuery } from '../../types'; @@ -71,15 +71,13 @@ export function SLOQueryEditor({ > - onChange({ ...query, alignmentPeriod: period })} + aligmentPeriods={ALIGNMENT_PERIODS} /> diff --git a/public/app/plugins/datasource/cloud-monitoring/components/index.ts b/public/app/plugins/datasource/cloud-monitoring/components/index.ts index 260ac778bdb..0b51aad1ead 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/index.ts +++ b/public/app/plugins/datasource/cloud-monitoring/components/index.ts @@ -5,7 +5,6 @@ export { Alignment } from './Alignment'; export { LabelFilter } from './LabelFilter'; export { AnnotationsHelp } from './AnnotationsHelp'; export { AlignmentFunction } from './AlignmentFunction'; -export { AlignmentPeriod } from './AlignmentPeriod'; export { AlignmentPeriodLabel } from './AlignmentPeriodLabel'; export { AliasBy } from './AliasBy'; export { Aggregation } from './Aggregation'; @@ -14,4 +13,5 @@ export { SLOQueryEditor } from './SLO/SLOQueryEditor'; export { MQLQueryEditor } from './MQLQueryEditor'; export { VariableQueryField, QueryEditorRow, QueryEditorField } from './Fields'; export { VisualMetricQueryEditor } from './VisualMetricQueryEditor'; +export { PeriodSelect } from './PeriodSelect'; export { Preprocessor } from './Preprocessor'; diff --git a/public/app/plugins/datasource/cloud-monitoring/constants.ts b/public/app/plugins/datasource/cloud-monitoring/constants.ts index df640dcaabf..25922d5cdd1 100644 --- a/public/app/plugins/datasource/cloud-monitoring/constants.ts +++ b/public/app/plugins/datasource/cloud-monitoring/constants.ts @@ -226,7 +226,13 @@ export const AGGREGATIONS = [ }, ]; -export const ALIGNMENT_PERIODS = [ +export type periodOption = { + text: string; + value: string; + hidden?: boolean; +}; + +export const ALIGNMENT_PERIODS: periodOption[] = [ { text: 'grafana auto', value: 'grafana-auto' }, { text: 'stackdriver auto', value: 'stackdriver-auto', hidden: true }, { text: 'cloud monitoring auto', value: 'cloud-monitoring-auto' }, @@ -243,6 +249,21 @@ export const ALIGNMENT_PERIODS = [ { text: '1w', value: '+604800s' }, ]; +export const GRAPH_PERIODS: periodOption[] = [ + { text: 'auto', value: 'auto' }, + { text: '1m', value: '1m' }, + { text: '2m', value: '2m' }, + { text: '5m', value: '5m' }, + { text: '10m', value: '10m' }, + { text: '30m', value: '30m' }, + { text: '1h', value: '1h' }, + { text: '3h', value: '3h' }, + { text: '6h', value: '6h' }, + { text: '1d', value: '1d' }, + { text: '3d', value: '3d' }, + { text: '1w', value: '1w' }, +]; + export const SYSTEM_LABELS = [ 'metadata.system_labels.cloud_account', 'metadata.system_labels.name', diff --git a/public/app/plugins/datasource/cloud-monitoring/types.ts b/public/app/plugins/datasource/cloud-monitoring/types.ts index 7f456d4ec7b..8081d8c0ed0 100644 --- a/public/app/plugins/datasource/cloud-monitoring/types.ts +++ b/public/app/plugins/datasource/cloud-monitoring/types.ts @@ -126,6 +126,8 @@ export interface MetricQuery extends BaseQuery { view?: string; query: string; preprocessor?: PreprocessorType; + // To disable the graphPeriod, it should explictly be set to 'disabled' + graphPeriod?: 'disabled' | string; } export interface SLOQuery extends BaseQuery { From 7b9929fffe88d81dd0a5dd1397bbd85e88fb9fe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Mon, 9 May 2022 13:47:50 +0200 Subject: [PATCH 112/440] loki: framing_test: more infinity/nan test cases (#48855) --- pkg/tsdb/loki/testdata/matrix_inf.golden.txt | 14 ++++++++++---- pkg/tsdb/loki/testdata/matrix_inf.json | 10 ++++++++-- pkg/tsdb/loki/testdata/matrix_nan.golden.txt | 7 ++++--- pkg/tsdb/loki/testdata/matrix_nan.json | 3 ++- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/pkg/tsdb/loki/testdata/matrix_inf.golden.txt b/pkg/tsdb/loki/testdata/matrix_inf.golden.txt index 9419715838d..437e963bed2 100644 --- a/pkg/tsdb/loki/testdata/matrix_inf.golden.txt +++ b/pkg/tsdb/loki/testdata/matrix_inf.golden.txt @@ -5,16 +5,22 @@ Frame[0] { "executedQueryString": "Expr: up(ALERTS)\nStep: 42s" } Name: {level="info", location="moon", protocol="http"} -Dimensions: 2 Fields by 2 Rows +Dimensions: 2 Fields by 8 Rows +---------------------------------+--------------------------------------------------+ | Name: Time | Name: Value | | Labels: | Labels: level=info, location=moon, protocol=http | | Type: []time.Time | Type: []float64 | +---------------------------------+--------------------------------------------------+ -| 2022-01-24 10:53:31.9 +0000 UTC | +Inf | -| 2022-01-24 10:58:31.9 +0000 UTC | -Inf | +| 2022-01-24 10:53:31.1 +0000 UTC | +Inf | +| 2022-01-24 10:58:31.2 +0000 UTC | -Inf | +| 2022-01-24 10:53:31.3 +0000 UTC | +Inf | +| 2022-01-24 10:58:31.4 +0000 UTC | -Inf | +| 2022-01-24 10:53:31.5 +0000 UTC | +Inf | +| 2022-01-24 10:58:31.6 +0000 UTC | -Inf | +| 2022-01-24 10:53:31.7 +0000 UTC | +Inf | +| 2022-01-24 10:58:31.8 +0000 UTC | -Inf | +---------------------------------+--------------------------------------------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=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 diff --git a/pkg/tsdb/loki/testdata/matrix_inf.json b/pkg/tsdb/loki/testdata/matrix_inf.json index 271888a00e1..a7f95130dc3 100644 --- a/pkg/tsdb/loki/testdata/matrix_inf.json +++ b/pkg/tsdb/loki/testdata/matrix_inf.json @@ -10,8 +10,14 @@ "protocol": "http" }, "values": [ - [1643021611.9, "+Inf"], - [1643021911.9, "-Inf"] + [1643021611.1, "+Inf"], + [1643021911.2, "-Inf"], + [1643021611.3, "+Infinity"], + [1643021911.4, "-Infinity"], + [1643021611.5, "+inf"], + [1643021911.6, "-infinity"], + [1643021611.7, "+iNf"], + [1643021911.8, "-INfInItY"] ] } ] diff --git a/pkg/tsdb/loki/testdata/matrix_nan.golden.txt b/pkg/tsdb/loki/testdata/matrix_nan.golden.txt index 031ca4ba492..8546c847064 100644 --- a/pkg/tsdb/loki/testdata/matrix_nan.golden.txt +++ b/pkg/tsdb/loki/testdata/matrix_nan.golden.txt @@ -5,16 +5,17 @@ Frame[0] { "executedQueryString": "Expr: up(ALERTS)\nStep: 42s" } Name: {} -Dimensions: 2 Fields by 2 Rows +Dimensions: 2 Fields by 3 Rows +-----------------------------------+-----------------+ | Name: Time | Name: Value | | Labels: | Labels: | | Type: []time.Time | Type: []float64 | +-----------------------------------+-----------------+ | 2022-01-24 08:54:10.417 +0000 UTC | NaN | -| 2022-01-24 08:59:10.417 +0000 UTC | NaN | +| 2022-01-24 08:59:10.517 +0000 UTC | NaN | +| 2022-01-24 08:54:10.617 +0000 UTC | NaN | +-----------------------------------+-----------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=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 diff --git a/pkg/tsdb/loki/testdata/matrix_nan.json b/pkg/tsdb/loki/testdata/matrix_nan.json index b5431401c02..e862c581992 100644 --- a/pkg/tsdb/loki/testdata/matrix_nan.json +++ b/pkg/tsdb/loki/testdata/matrix_nan.json @@ -7,7 +7,8 @@ "metric": {}, "values": [ [1643014450.417, "NaN"], - [1643014750.417, "NaN"] + [1643014750.517, "nan"], + [1643014450.617, "nAn"] ] } ] From 897db011eb6bde9c6cb57a1a1f0a96d06a7ba35b Mon Sep 17 00:00:00 2001 From: George Robinson Date: Mon, 9 May 2022 19:11:24 +0100 Subject: [PATCH 113/440] Add error options for rendering to return errors on failure (#48864) --- pkg/services/rendering/interface.go | 10 +++++++ pkg/services/rendering/rendering.go | 6 +++++ pkg/services/rendering/rendering_test.go | 33 ++++++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/pkg/services/rendering/interface.go b/pkg/services/rendering/interface.go index 0aa83dbb888..9aee779906e 100644 --- a/pkg/services/rendering/interface.go +++ b/pkg/services/rendering/interface.go @@ -41,6 +41,7 @@ func getRequestTimeout(opt TimeoutOpts) time.Duration { type Opts struct { TimeoutOpts AuthOpts + ErrorOpts Width int Height int Path string @@ -52,6 +53,15 @@ type Opts struct { Theme models.Theme } +type ErrorOpts struct { + // ErrorConcurrentLimitReached returns an ErrConcurrentLimitReached + // error instead of a rendering limit exceeded image. + ErrorConcurrentLimitReached bool + // ErrorRenderUnavailable returns an ErrRunderUnavailable error + // instead of a rendering unavailable image. + ErrorRenderUnavailable bool +} + type CSVOpts struct { TimeoutOpts AuthOpts diff --git a/pkg/services/rendering/rendering.go b/pkg/services/rendering/rendering.go index 2de807cb5a4..694d93890f6 100644 --- a/pkg/services/rendering/rendering.go +++ b/pkg/services/rendering/rendering.go @@ -235,6 +235,9 @@ func (rs *RenderingService) Render(ctx context.Context, opts Opts, session Sessi func (rs *RenderingService) render(ctx context.Context, opts Opts, renderKeyProvider renderKeyProvider) (*RenderResult, error) { if int(atomic.LoadInt32(&rs.inProgressCount)) > opts.ConcurrentLimit { rs.log.Warn("Could not render image, hit the currency limit", "concurrencyLimit", opts.ConcurrentLimit, "path", opts.Path) + if opts.ErrorConcurrentLimitReached { + return nil, ErrConcurrentLimitReached + } theme := models.ThemeDark if opts.Theme != "" { @@ -250,6 +253,9 @@ func (rs *RenderingService) render(ctx context.Context, opts Opts, renderKeyProv rs.log.Warn("Could not render image, no image renderer found/installed. " + "For image rendering support please install the grafana-image-renderer plugin. " + "Read more at https://grafana.com/docs/grafana/latest/administration/image_rendering/") + if opts.ErrorRenderUnavailable { + return nil, ErrRenderUnavailable + } return rs.renderUnavailableImage(), nil } diff --git a/pkg/services/rendering/rendering_test.go b/pkg/services/rendering/rendering_test.go index 5deb2e7805f..5f31a016d3e 100644 --- a/pkg/services/rendering/rendering_test.go +++ b/pkg/services/rendering/rendering_test.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -102,6 +103,22 @@ func TestRenderErrorImage(t *testing.T) { }) } +type unavailableRendererManager struct{} + +func (m unavailableRendererManager) Renderer() *plugins.Plugin { return nil } + +func TestRenderUnavailableError(t *testing.T) { + rs := RenderingService{ + Cfg: &setting.Cfg{}, + log: log.New("test"), + RendererPluginManager: unavailableRendererManager{}, + } + opts := Opts{ErrorOpts: ErrorOpts{ErrorRenderUnavailable: true}} + result, err := rs.Render(context.Background(), opts, nil) + assert.Equal(t, ErrRenderUnavailable, err) + assert.Nil(t, result) +} + func TestRenderLimitImage(t *testing.T) { path, err := filepath.Abs("../../../") require.NoError(t, err) @@ -146,6 +163,22 @@ func TestRenderLimitImage(t *testing.T) { } } +func TestRenderLimitImageError(t *testing.T) { + rs := RenderingService{ + Cfg: &setting.Cfg{}, + inProgressCount: 2, + log: log.New("test"), + } + opts := Opts{ + ErrorOpts: ErrorOpts{ErrorConcurrentLimitReached: true}, + ConcurrentLimit: 1, + Theme: models.ThemeDark, + } + result, err := rs.Render(context.Background(), opts, nil) + assert.Equal(t, ErrConcurrentLimitReached, err) + assert.Nil(t, result) +} + func TestRenderingServiceGetRemotePluginVersion(t *testing.T) { cfg := setting.NewCfg() rs := &RenderingService{ From 31ff23f5421f7adc7cbb8f0d9de857df78dd951d Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Tue, 10 May 2022 11:29:36 +0200 Subject: [PATCH 114/440] Fix: add default nop trace exporter to opentelemetry (#48869) --- pkg/infra/tracing/opentelemetry_tracing.go | 35 ++++++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/pkg/infra/tracing/opentelemetry_tracing.go b/pkg/infra/tracing/opentelemetry_tracing.go index 15200cbf529..2fdd5724ce0 100644 --- a/pkg/infra/tracing/opentelemetry_tracing.go +++ b/pkg/infra/tracing/opentelemetry_tracing.go @@ -24,8 +24,10 @@ import ( ) const ( - jaegerExporter string = "jaeger" - otlpExporter string = "otlp" + jaegerExporter string = "jaeger" + otlpExporter string = "otlp" + noopExporter string = "noop" + jaegerPropagator string = "jaeger" w3cPropagator string = "w3c" ) @@ -51,12 +53,18 @@ type Opentelemetry struct { propagation string log log.Logger - tracerProvider *tracesdk.TracerProvider + tracerProvider tracerProvider tracer trace.Tracer Cfg *setting.Cfg } +type tracerProvider interface { + trace.TracerProvider + + Shutdown(ctx context.Context) error +} + type OpentelemetrySpan struct { span trace.Span } @@ -72,11 +80,20 @@ func (o otelErrHandler) Handle(err error) { o(err) } +type noopTracerProvider struct { + trace.TracerProvider +} + +func (noopTracerProvider) Shutdown(ctx context.Context) error { + return nil +} + func (ots *Opentelemetry) parseSettingsOpentelemetry() error { section, err := ots.Cfg.Raw.GetSection("tracing.opentelemetry.jaeger") if err != nil { return err } + ots.enabled = noopExporter ots.address = section.Key("address").MustString("") if ots.address != "" { @@ -95,7 +112,6 @@ func (ots *Opentelemetry) parseSettingsOpentelemetry() error { ots.enabled = otlpExporter } ots.propagation = section.Key("propagation").MustString("") - return nil } @@ -148,8 +164,12 @@ func (ots *Opentelemetry) initOTLPTracerProvider() (*tracesdk.TracerProvider, er return tp, nil } +func (ots *Opentelemetry) initNoopTracerProvider() (tracerProvider, error) { + return &noopTracerProvider{TracerProvider: trace.NewNoopTracerProvider()}, nil +} + func (ots *Opentelemetry) initOpentelemetryTracer() error { - var tp *tracesdk.TracerProvider + var tp tracerProvider var err error switch ots.enabled { case jaegerExporter: @@ -163,7 +183,10 @@ func (ots *Opentelemetry) initOpentelemetryTracer() error { return err } default: - ots.log.Error("invalid trace exporter") + tp, err = ots.initNoopTracerProvider() + if err != nil { + return err + } } // Register our TracerProvider as the global so any imported From 61772a66b6aaa66d1f2d304f1139c8bb35b3cba6 Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Tue, 10 May 2022 15:48:47 +0200 Subject: [PATCH 115/440] AccessControl: Create own interface and impl for each permission service (#48871) * Create own interfaces for team, folder, dashboard and data source permissions services * Remove service container and inject them individually --- pkg/api/common_test.go | 7 +- pkg/api/dashboard_permission.go | 12 ++- pkg/api/dashboard_permission_test.go | 4 +- pkg/api/dashboard_test.go | 6 +- pkg/api/folder_permission_test.go | 20 ++-- pkg/api/http_server.go | 14 ++- pkg/api/metrics_test.go | 2 +- pkg/api/pluginproxy/ds_proxy_test.go | 56 +++++----- pkg/api/team_members.go | 2 +- pkg/api/team_test.go | 2 +- pkg/server/wire.go | 8 ++ pkg/server/wireexts_oss.go | 4 +- pkg/services/accesscontrol/accesscontrol.go | 20 +++- .../mock/permissions_services_mock.go | 39 ------- .../accesscontrol/mock/service_mock.go | 4 + .../ossaccesscontrol/permissions_services.go | 101 +++++++----------- .../accesscontrol/resourcepermissions/api.go | 12 ++- .../dashboards/manager/dashboard_service.go | 11 +- .../dashboard_service_integration_test.go | 14 ++- .../dashboards/manager/folder_service.go | 6 +- .../dashboards/manager/folder_service_test.go | 14 +-- .../datasources/service/datasource_service.go | 6 +- .../service/datasource_service_test.go | 36 +++---- .../guardian/accesscontrol_guardian.go | 44 ++++---- .../guardian/accesscontrol_guardian_test.go | 14 +-- pkg/services/guardian/provider.go | 15 ++- .../libraryelements/libraryelements_test.go | 18 ++-- .../librarypanels/librarypanels_test.go | 18 ++-- pkg/services/ngalert/tests/util.go | 7 +- pkg/services/query/query_test.go | 2 +- pkg/tsdb/legacydata/service/service_test.go | 3 +- 31 files changed, 266 insertions(+), 255 deletions(-) delete mode 100644 pkg/services/accesscontrol/mock/permissions_services_mock.go diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index 52629efbe27..800757114ba 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -370,8 +370,11 @@ func setupHTTPServerWithCfgDb(t *testing.T, useFakeAccessControl, enableAccessCo RouteRegister: routeRegister, SQLStore: store, searchUsersService: searchusers.ProvideUsersService(db, filters.ProvideOSSSearchUserFilter()), - dashboardService: dashboardservice.ProvideDashboardService(cfg, dashboardsStore, nil, features, accesscontrolmock.NewPermissionsServicesMock()), - preferenceService: preftest.NewPreferenceServiceFake(), + dashboardService: dashboardservice.ProvideDashboardService( + cfg, dashboardsStore, nil, features, + accesscontrolmock.NewMockedPermissionsService(), accesscontrolmock.NewMockedPermissionsService(), + ), + preferenceService: preftest.NewPreferenceServiceFake(), } // Defining the accesscontrol service has to be done before registering routes diff --git a/pkg/api/dashboard_permission.go b/pkg/api/dashboard_permission.go index 4f051ff266e..4ae1ce4d36e 100644 --- a/pkg/api/dashboard_permission.go +++ b/pkg/api/dashboard_permission.go @@ -203,13 +203,17 @@ func (hs *HTTPServer) updateDashboardAccessControl(ctx context.Context, orgID in } } - svc := hs.permissionServices.GetDashboardService() if isFolder { - svc = hs.permissionServices.GetFolderService() + if _, err := hs.folderPermissionsService.SetPermissions(ctx, orgID, uid, commands...); err != nil { + return err + } + return nil } - _, err := svc.SetPermissions(ctx, orgID, uid, commands...) - return err + if _, err := hs.dashboardPermissionsService.SetPermissions(ctx, orgID, uid, commands...); err != nil { + return err + } + return nil } func validatePermissionsUpdate(apiCmd dtos.UpdateDashboardAclCommand) error { diff --git a/pkg/api/dashboard_permission_test.go b/pkg/api/dashboard_permission_test.go index 136bb08e8a8..2d89109aa49 100644 --- a/pkg/api/dashboard_permission_test.go +++ b/pkg/api/dashboard_permission_test.go @@ -30,13 +30,15 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { features := featuremgmt.WithFeatures() mockSQLStore := mockstore.NewSQLStoreMock() + folderPermissions := accesscontrolmock.NewMockedPermissionsService() + dashboardPermissions := accesscontrolmock.NewMockedPermissionsService() hs := &HTTPServer{ Cfg: settings, SQLStore: mockSQLStore, Features: features, dashboardService: dashboardservice.ProvideDashboardService( - settings, dashboardStore, nil, features, accesscontrolmock.NewPermissionsServicesMock(), + settings, dashboardStore, nil, features, folderPermissions, dashboardPermissions, ), AccessControl: accesscontrolmock.New().WithDisabled(), } diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index a0f7f4fbc34..6250433791a 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -232,7 +232,8 @@ func TestDashboardAPIEndpoint(t *testing.T) { SQLStore: mockSQLStore, AccessControl: accesscontrolmock.New(), dashboardService: service.ProvideDashboardService( - cfg, dashboardStore, nil, features, accesscontrolmock.NewPermissionsServicesMock(), + cfg, dashboardStore, nil, features, + accesscontrolmock.NewMockedPermissionsService(), accesscontrolmock.NewMockedPermissionsService(), ), } hs.SQLStore = mockSQLStore @@ -937,7 +938,8 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr ProvisioningService: provisioningService, AccessControl: accesscontrolmock.New(), dashboardProvisioningService: service.ProvideDashboardService( - cfg, dashboardStore, nil, features, accesscontrolmock.NewPermissionsServicesMock(), + cfg, dashboardStore, nil, features, + accesscontrolmock.NewMockedPermissionsService(), accesscontrolmock.NewMockedPermissionsService(), ), } diff --git a/pkg/api/folder_permission_test.go b/pkg/api/folder_permission_test.go index 24556b120d7..4ca3aea5f72 100644 --- a/pkg/api/folder_permission_test.go +++ b/pkg/api/folder_permission_test.go @@ -5,17 +5,15 @@ import ( "fmt" "testing" - accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/models" + accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/dashboards" service "github.com/grafana/grafana/pkg/services/dashboards/manager" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -34,15 +32,17 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { defer dashboardStore.AssertExpectations(t) features := featuremgmt.WithFeatures() - permissionsServices := accesscontrolmock.NewPermissionsServicesMock() + folderPermissions := accesscontrolmock.NewMockedPermissionsService() + dashboardPermissions := accesscontrolmock.NewMockedPermissionsService() hs := &HTTPServer{ - Cfg: settings, - Features: features, - folderService: folderService, - permissionServices: permissionsServices, + Cfg: settings, + Features: features, + folderService: folderService, + folderPermissionsService: folderPermissions, + dashboardPermissionsService: dashboardPermissions, dashboardService: service.ProvideDashboardService( - settings, dashboardStore, nil, features, permissionsServices, + settings, dashboardStore, nil, features, folderPermissions, dashboardPermissions, ), AccessControl: accesscontrolmock.New().WithDisabled(), } diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index fb4a2438ff2..a6a42244ab5 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -137,8 +137,7 @@ type HTTPServer struct { serviceAccountsService serviceaccounts.Service authInfoService login.AuthInfoService authenticator loginpkg.Authenticator - teamPermissionsService accesscontrol.PermissionsService - permissionServices accesscontrol.PermissionsServices + teamPermissionsService accesscontrol.TeamPermissionsService NotificationService *notifications.NotificationService dashboardService dashboards.DashboardService dashboardProvisioningService dashboards.DashboardProvisioningService @@ -151,6 +150,8 @@ type HTTPServer struct { AvatarCacheServer *avatar.AvatarCacheServer preferenceService pref.Service entityEventsService store.EntityEventsService + folderPermissionsService accesscontrol.FolderPermissionsService + dashboardPermissionsService accesscontrol.DashboardPermissionsService } type ServerOptions struct { @@ -177,12 +178,14 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi pluginsUpdateChecker *updatechecker.PluginsService, searchUsersService searchusers.Service, dataSourcesService datasources.DataSourceService, secretsService secrets.Service, queryDataService *query.Service, ldapGroups ldap.Groups, teamGuardian teamguardian.TeamGuardian, serviceaccountsService serviceaccounts.Service, - authInfoService login.AuthInfoService, permissionsServices accesscontrol.PermissionsServices, storageService store.HTTPStorageService, + authInfoService login.AuthInfoService, storageService store.HTTPStorageService, notificationService *notifications.NotificationService, dashboardService dashboards.DashboardService, dashboardProvisioningService dashboards.DashboardProvisioningService, folderService dashboards.FolderService, datasourcePermissionsService permissions.DatasourcePermissionsService, alertNotificationService *alerting.AlertNotificationService, dashboardsnapshotsService *dashboardsnapshots.Service, commentsService *comments.Service, pluginSettings *pluginSettings.Service, avatarCacheServer *avatar.AvatarCacheServer, preferenceService pref.Service, entityEventsService store.EntityEventsService, + teamsPermissionsService accesscontrol.TeamPermissionsService, folderPermissionsService accesscontrol.FolderPermissionsService, + dashboardPermissionsService accesscontrol.DashboardPermissionsService, ) (*HTTPServer, error) { web.Env = cfg.Env m := web.New() @@ -250,14 +253,15 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi folderService: folderService, DatasourcePermissionsService: datasourcePermissionsService, commentsService: commentsService, - teamPermissionsService: permissionsServices.GetTeamService(), + teamPermissionsService: teamsPermissionsService, AlertNotificationService: alertNotificationService, DashboardsnapshotsService: dashboardsnapshotsService, PluginSettings: pluginSettings, - permissionServices: permissionsServices, AvatarCacheServer: avatarCacheServer, preferenceService: preferenceService, entityEventsService: entityEventsService, + folderPermissionsService: folderPermissionsService, + dashboardPermissionsService: dashboardPermissionsService, } if hs.Listener != nil { hs.log.Debug("Using provided listener") diff --git a/pkg/api/metrics_test.go b/pkg/api/metrics_test.go index c7acbe032df..de14e9d9da9 100644 --- a/pkg/api/metrics_test.go +++ b/pkg/api/metrics_test.go @@ -202,7 +202,7 @@ func TestAPIEndpoint_Metrics_QueryMetricsFromDashboard(t *testing.T) { secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - ds := datasources.ProvideService(nil, secretsService, secretsStore, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + ds := datasources.ProvideService(nil, secretsService, secretsStore, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) setInitCtxSignedInViewer(sc.initCtx) sc.hs.queryDataService = query.ProvideService( diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go index 544b0997e74..6e0016868c2 100644 --- a/pkg/api/pluginproxy/ds_proxy_test.go +++ b/pkg/api/pluginproxy/ds_proxy_test.go @@ -131,7 +131,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { t.Run("When matching route path", func(t *testing.T) { ctx, req := setUp() - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/v4/some/method", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer) require.NoError(t, err) @@ -144,7 +144,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { t.Run("When matching route path and has dynamic url", func(t *testing.T) { ctx, req := setUp() - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/common/some/method", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer) require.NoError(t, err) proxy.matchedRoute = routes[3] @@ -156,7 +156,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { t.Run("When matching route path with no url", func(t *testing.T) { ctx, req := setUp() - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer) require.NoError(t, err) proxy.matchedRoute = routes[4] @@ -167,7 +167,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { t.Run("When matching route path and has dynamic body", func(t *testing.T) { ctx, req := setUp() - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/body", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer) require.NoError(t, err) proxy.matchedRoute = routes[5] @@ -181,7 +181,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { t.Run("Validating request", func(t *testing.T) { t.Run("plugin route with valid role", func(t *testing.T) { ctx, _ := setUp() - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/v4/some/method", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer) require.NoError(t, err) err = proxy.validateRequest() @@ -190,7 +190,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { t.Run("plugin route with admin role and user is editor", func(t *testing.T) { ctx, _ := setUp() - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/admin", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer) require.NoError(t, err) err = proxy.validateRequest() @@ -200,7 +200,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { t.Run("plugin route with admin role and user is admin", func(t *testing.T) { ctx, _ := setUp() ctx.SignedInUser.OrgRole = models.ROLE_ADMIN - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/admin", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer) require.NoError(t, err) err = proxy.validateRequest() @@ -290,7 +290,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { }, } - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken1", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer) require.NoError(t, err) ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, cfg) @@ -306,7 +306,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { req, err := http.NewRequest("GET", "http://localhost/asd", nil) require.NoError(t, err) client = newFakeHTTPClient(t, json2) - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken2", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer) require.NoError(t, err) ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[1], dsInfo, cfg) @@ -323,7 +323,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { require.NoError(t, err) client = newFakeHTTPClient(t, []byte{}) - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken1", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer) require.NoError(t, err) ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, cfg) @@ -346,7 +346,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{BuildVersion: "5.3.0"}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer) require.NoError(t, err) req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) @@ -373,7 +373,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { var routes []*plugins.Route secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer) require.NoError(t, err) @@ -398,7 +398,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { var routes []*plugins.Route secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer) require.NoError(t, err) @@ -427,7 +427,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { var pluginRoutes []*plugins.Route secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) proxy, err := NewDataSourceProxy(ds, pluginRoutes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer) require.NoError(t, err) @@ -451,7 +451,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { var routes []*plugins.Route secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/to/folder/", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer) require.NoError(t, err) req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) @@ -501,7 +501,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { var routes []*plugins.Route secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/to/folder/", &setting.Cfg{}, httpClientProvider, &mockAuthToken, dsService, tracer) require.NoError(t, err) req, err = http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) @@ -637,7 +637,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) { var routes []*plugins.Route secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer) require.NoError(t, err) @@ -656,7 +656,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) { var routes []*plugins.Route secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer) require.NoError(t, err) @@ -671,7 +671,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) { var routes []*plugins.Route secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer) require.NoError(t, err) @@ -694,7 +694,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) { var routes []*plugins.Route secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer) require.NoError(t, err) @@ -720,7 +720,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) { var routes []*plugins.Route secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/%2Ftest%2Ftest%2F", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer) require.NoError(t, err) @@ -745,7 +745,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) { var routes []*plugins.Route secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/%2Ftest%2Ftest%2F", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer) require.NoError(t, err) @@ -771,7 +771,7 @@ func TestNewDataSourceProxy_InvalidURL(t *testing.T) { var routes []*plugins.Route secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) _, err = NewDataSourceProxy(&ds, routes, &ctx, "api/method", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer) require.Error(t, err) assert.True(t, strings.HasPrefix(err.Error(), `validation of data source URL "://host/root" failed`)) @@ -793,7 +793,7 @@ func TestNewDataSourceProxy_ProtocolLessURL(t *testing.T) { var routes []*plugins.Route secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) _, err = NewDataSourceProxy(&ds, routes, &ctx, "api/method", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer) require.NoError(t, err) @@ -837,7 +837,7 @@ func TestNewDataSourceProxy_MSSQL(t *testing.T) { var routes []*plugins.Route secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) p, err := NewDataSourceProxy(&ds, routes, &ctx, "api/method", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer) if tc.err == nil { require.NoError(t, err) @@ -865,7 +865,7 @@ func getDatasourceProxiedRequest(t *testing.T, ctx *models.ReqContext, cfg *sett var routes []*plugins.Route secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) proxy, err := NewDataSourceProxy(ds, routes, ctx, "", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer) require.NoError(t, err) req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) @@ -993,7 +993,7 @@ func runDatasourceAuthTest(t *testing.T, secretsService secrets.Service, secrets require.NoError(t, err) var routes []*plugins.Route - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) proxy, err := NewDataSourceProxy(test.datasource, routes, ctx, "", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer) require.NoError(t, err) @@ -1037,7 +1037,7 @@ func Test_PathCheck(t *testing.T) { ctx, _ := setUp() secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) proxy, err := NewDataSourceProxy(&models.DataSource{}, routes, ctx, "b", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer) require.NoError(t, err) diff --git a/pkg/api/team_members.go b/pkg/api/team_members.go index f0a7576da64..834b40a33bc 100644 --- a/pkg/api/team_members.go +++ b/pkg/api/team_members.go @@ -176,7 +176,7 @@ func (hs *HTTPServer) RemoveTeamMember(c *models.ReqContext) response.Response { // addOrUpdateTeamMember adds or updates a team member. // // Stubbable by tests. -var addOrUpdateTeamMember = func(ctx context.Context, resourcePermissionService accesscontrol.PermissionsService, userID, orgID, teamID int64, permission string) error { +var addOrUpdateTeamMember = func(ctx context.Context, resourcePermissionService accesscontrol.TeamPermissionsService, userID, orgID, teamID int64, permission string) error { teamIDString := strconv.FormatInt(teamID, 10) if _, err := resourcePermissionService.SetUserPermission(ctx, orgID, accesscontrol.User{ID: userID}, teamIDString, permission); err != nil { return fmt.Errorf("failed setting permissions for user %d in team %d: %w", userID, teamID, err) diff --git a/pkg/api/team_test.go b/pkg/api/team_test.go index 09395b036af..dd4c463d858 100644 --- a/pkg/api/team_test.go +++ b/pkg/api/team_test.go @@ -104,7 +104,7 @@ func TestTeamAPIEndpoint(t *testing.T) { teamName := "team foo" addTeamMemberCalled := 0 - addOrUpdateTeamMember = func(ctx context.Context, resourcePermissionService accesscontrol.PermissionsService, userID, orgID, teamID int64, + addOrUpdateTeamMember = func(ctx context.Context, resourcePermissionService accesscontrol.TeamPermissionsService, userID, orgID, teamID int64, permission string) error { addTeamMemberCalled++ return nil diff --git a/pkg/server/wire.go b/pkg/server/wire.go index e0d7cf8ee2b..4aa4ccdfc4e 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -34,6 +34,8 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager" "github.com/grafana/grafana/pkg/plugins/manager/loader" "github.com/grafana/grafana/pkg/plugins/plugincontext" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/auth/jwt" "github.com/grafana/grafana/pkg/services/cleanup" @@ -250,6 +252,12 @@ var wireBasicSet = wire.NewSet( cmreg.ProvideRegistry, cuectx.ProvideCUEContext, cuectx.ProvideThemaLibrary, + ossaccesscontrol.ProvideTeamPermissions, + wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), + ossaccesscontrol.ProvideFolderPermissions, + wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), + ossaccesscontrol.ProvideDashboardPermissions, + wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), ) var wireSet = wire.NewSet( diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index dd047cd5d6b..4f4e06b03b2 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -81,10 +81,10 @@ var wireExtsBasicSet = wire.NewSet( wire.Bind(new(ldap.Groups), new(*ldap.OSSGroups)), permissions.ProvideDatasourcePermissionsService, wire.Bind(new(permissions.DatasourcePermissionsService), new(*permissions.OSSDatasourcePermissionsService)), - ossaccesscontrol.ProvidePermissionsServices, - wire.Bind(new(accesscontrol.PermissionsServices), new(*ossaccesscontrol.PermissionsServices)), usagestatssvcs.ProvideUsageStatsProvidersRegistry, wire.Bind(new(registry.UsageStatsProvidersRegistry), new(*usagestatssvcs.UsageStatsProvidersRegistry)), + ossaccesscontrol.ProvideDatasourcePermissionsService, + wire.Bind(new(accesscontrol.DatasourcePermissionsService), new(*ossaccesscontrol.DatasourcePermissionsService)), ) var wireExtsSet = wire.NewSet( diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index 4330c8bacbb..f53ac3c8796 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -43,11 +43,21 @@ type PermissionsProvider interface { GetUserPermissions(ctx context.Context, query GetUserPermissionsQuery) ([]*Permission, error) } -type PermissionsServices interface { - GetTeamService() PermissionsService - GetFolderService() PermissionsService - GetDashboardService() PermissionsService - GetDataSourceService() PermissionsService +type TeamPermissionsService interface { + GetPermissions(ctx context.Context, user *models.SignedInUser, resourceID string) ([]ResourcePermission, error) + SetUserPermission(ctx context.Context, orgID int64, user User, resourceID, permission string) (*ResourcePermission, error) +} + +type FolderPermissionsService interface { + PermissionsService +} + +type DashboardPermissionsService interface { + PermissionsService +} + +type DatasourcePermissionsService interface { + PermissionsService } type PermissionsService interface { diff --git a/pkg/services/accesscontrol/mock/permissions_services_mock.go b/pkg/services/accesscontrol/mock/permissions_services_mock.go deleted file mode 100644 index 01b545ab997..00000000000 --- a/pkg/services/accesscontrol/mock/permissions_services_mock.go +++ /dev/null @@ -1,39 +0,0 @@ -package mock - -import ( - "github.com/grafana/grafana/pkg/services/accesscontrol" -) - -var _ accesscontrol.PermissionsServices = new(PermissionsServicesMock) - -func NewPermissionsServicesMock() *PermissionsServicesMock { - return &PermissionsServicesMock{ - Teams: &MockPermissionsService{}, - Folders: &MockPermissionsService{}, - Dashboards: &MockPermissionsService{}, - Datasources: &MockPermissionsService{}, - } -} - -type PermissionsServicesMock struct { - Teams *MockPermissionsService - Folders *MockPermissionsService - Dashboards *MockPermissionsService - Datasources *MockPermissionsService -} - -func (p PermissionsServicesMock) GetTeamService() accesscontrol.PermissionsService { - return p.Teams -} - -func (p PermissionsServicesMock) GetFolderService() accesscontrol.PermissionsService { - return p.Folders -} - -func (p PermissionsServicesMock) GetDashboardService() accesscontrol.PermissionsService { - return p.Dashboards -} - -func (p PermissionsServicesMock) GetDataSourceService() accesscontrol.PermissionsService { - return p.Datasources -} diff --git a/pkg/services/accesscontrol/mock/service_mock.go b/pkg/services/accesscontrol/mock/service_mock.go index 085257689c7..b57797b6f92 100644 --- a/pkg/services/accesscontrol/mock/service_mock.go +++ b/pkg/services/accesscontrol/mock/service_mock.go @@ -11,6 +11,10 @@ import ( var _ accesscontrol.PermissionsService = new(MockPermissionsService) +func NewMockedPermissionsService() *MockPermissionsService { + return &MockPermissionsService{} +} + type MockPermissionsService struct { mock.Mock } diff --git a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go index a1208cb90fc..6bfecbee92f 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go @@ -15,52 +15,8 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -func ProvidePermissionsServices( - cfg *setting.Cfg, router routing.RouteRegister, sql *sqlstore.SQLStore, - ac accesscontrol.AccessControl, store resourcepermissions.Store, -) (*PermissionsServices, error) { - teamPermissions, err := ProvideTeamPermissions(cfg, router, sql, ac, store) - if err != nil { - return nil, err - } - folderPermissions, err := ProvideFolderPermissions(cfg, router, sql, ac, store) - if err != nil { - return nil, err - } - dashboardPermissions, err := ProvideDashboardPermissions(cfg, router, sql, ac, store) - if err != nil { - return nil, err - } - - return &PermissionsServices{ - teams: teamPermissions, - folder: folderPermissions, - dashboard: dashboardPermissions, - datasources: provideEmptyPermissionsService(), - }, nil -} - -type PermissionsServices struct { - teams accesscontrol.PermissionsService - folder accesscontrol.PermissionsService - dashboard accesscontrol.PermissionsService - datasources accesscontrol.PermissionsService -} - -func (s *PermissionsServices) GetTeamService() accesscontrol.PermissionsService { - return s.teams -} - -func (s *PermissionsServices) GetFolderService() accesscontrol.PermissionsService { - return s.folder -} - -func (s *PermissionsServices) GetDashboardService() accesscontrol.PermissionsService { - return s.dashboard -} - -func (s *PermissionsServices) GetDataSourceService() accesscontrol.PermissionsService { - return s.datasources +type TeamPermissionsService struct { + *resourcepermissions.Service } var ( @@ -80,7 +36,7 @@ var ( func ProvideTeamPermissions( cfg *setting.Cfg, router routing.RouteRegister, sql *sqlstore.SQLStore, ac accesscontrol.AccessControl, store resourcepermissions.Store, -) (*resourcepermissions.Service, error) { +) (*TeamPermissionsService, error) { options := resourcepermissions.Options{ Resource: "teams", ResourceAttribute: "id", @@ -135,7 +91,15 @@ func ProvideTeamPermissions( }, } - return resourcepermissions.New(options, cfg, router, ac, store, sql) + srv, err := resourcepermissions.New(options, cfg, router, ac, store, sql) + if err != nil { + return nil, err + } + return &TeamPermissionsService{srv}, nil +} + +type DashboardPermissionsService struct { + *resourcepermissions.Service } var DashboardViewActions = []string{dashboards.ActionDashboardsRead} @@ -145,7 +109,7 @@ var DashboardAdminActions = append(DashboardEditActions, []string{dashboards.Act func ProvideDashboardPermissions( cfg *setting.Cfg, router routing.RouteRegister, sql *sqlstore.SQLStore, ac accesscontrol.AccessControl, store resourcepermissions.Store, -) (*resourcepermissions.Service, error) { +) (*DashboardPermissionsService, error) { getDashboard := func(ctx context.Context, orgID int64, resourceID string) (*models.Dashboard, error) { query := &models.GetDashboardQuery{Uid: resourceID, OrgId: orgID} if err := sql.GetDashboard(ctx, query); err != nil { @@ -199,7 +163,15 @@ func ProvideDashboardPermissions( RoleGroup: "Dashboards", } - return resourcepermissions.New(options, cfg, router, ac, store, sql) + srv, err := resourcepermissions.New(options, cfg, router, ac, store, sql) + if err != nil { + return nil, err + } + return &DashboardPermissionsService{srv}, nil +} + +type FolderPermissionsService struct { + *resourcepermissions.Service } var FolderViewActions = []string{dashboards.ActionFoldersRead} @@ -209,7 +181,7 @@ var FolderAdminActions = append(FolderEditActions, []string{dashboards.ActionFol func ProvideFolderPermissions( cfg *setting.Cfg, router routing.RouteRegister, sql *sqlstore.SQLStore, accesscontrol accesscontrol.AccessControl, store resourcepermissions.Store, -) (*resourcepermissions.Service, error) { +) (*FolderPermissionsService, error) { options := resourcepermissions.Options{ Resource: "folders", ResourceAttribute: "uid", @@ -239,38 +211,41 @@ func ProvideFolderPermissions( WriterRoleName: "Folder permission writer", RoleGroup: "Folders", } - - return resourcepermissions.New(options, cfg, router, accesscontrol, store, sql) + srv, err := resourcepermissions.New(options, cfg, router, accesscontrol, store, sql) + if err != nil { + return nil, err + } + return &FolderPermissionsService{srv}, nil } -func provideEmptyPermissionsService() accesscontrol.PermissionsService { - return &emptyPermissionsService{} +func ProvideDatasourcePermissionsService() *DatasourcePermissionsService { + return &DatasourcePermissionsService{} } -var _ accesscontrol.PermissionsService = new(emptyPermissionsService) +var _ accesscontrol.DatasourcePermissionsService = new(DatasourcePermissionsService) -type emptyPermissionsService struct{} +type DatasourcePermissionsService struct{} -func (e emptyPermissionsService) GetPermissions(ctx context.Context, user *models.SignedInUser, resourceID string) ([]accesscontrol.ResourcePermission, error) { +func (e DatasourcePermissionsService) GetPermissions(ctx context.Context, user *models.SignedInUser, resourceID string) ([]accesscontrol.ResourcePermission, error) { return nil, nil } -func (e emptyPermissionsService) SetUserPermission(ctx context.Context, orgID int64, user accesscontrol.User, resourceID, permission string) (*accesscontrol.ResourcePermission, error) { +func (e DatasourcePermissionsService) SetUserPermission(ctx context.Context, orgID int64, user accesscontrol.User, resourceID, permission string) (*accesscontrol.ResourcePermission, error) { return nil, nil } -func (e emptyPermissionsService) SetTeamPermission(ctx context.Context, orgID, teamID int64, resourceID, permission string) (*accesscontrol.ResourcePermission, error) { +func (e DatasourcePermissionsService) SetTeamPermission(ctx context.Context, orgID, teamID int64, resourceID, permission string) (*accesscontrol.ResourcePermission, error) { return nil, nil } -func (e emptyPermissionsService) SetBuiltInRolePermission(ctx context.Context, orgID int64, builtInRole string, resourceID string, permission string) (*accesscontrol.ResourcePermission, error) { +func (e DatasourcePermissionsService) SetBuiltInRolePermission(ctx context.Context, orgID int64, builtInRole string, resourceID string, permission string) (*accesscontrol.ResourcePermission, error) { return nil, nil } -func (e emptyPermissionsService) SetPermissions(ctx context.Context, orgID int64, resourceID string, commands ...accesscontrol.SetResourcePermissionCommand) ([]accesscontrol.ResourcePermission, error) { +func (e DatasourcePermissionsService) SetPermissions(ctx context.Context, orgID int64, resourceID string, commands ...accesscontrol.SetResourcePermissionCommand) ([]accesscontrol.ResourcePermission, error) { return nil, nil } -func (e emptyPermissionsService) MapActions(permission accesscontrol.ResourcePermission) string { +func (e DatasourcePermissionsService) MapActions(permission accesscontrol.ResourcePermission) string { return "" } diff --git a/pkg/services/accesscontrol/resourcepermissions/api.go b/pkg/services/accesscontrol/resourcepermissions/api.go index a391c204217..53fbbf55399 100644 --- a/pkg/services/accesscontrol/resourcepermissions/api.go +++ b/pkg/services/accesscontrol/resourcepermissions/api.go @@ -65,9 +65,15 @@ func (a *api) registerEndpoints() { readEvaluator, writeEvaluator := a.getEvaluators(actionRead, actionWrite, scope) r.Get("/description", auth(disable, accesscontrol.EvalPermission(actionRead)), routing.Wrap(a.getDescription)) r.Get("/:resourceID", inheritanceSolver, auth(disable, readEvaluator), routing.Wrap(a.getPermissions)) - r.Post("/:resourceID/users/:userID", inheritanceSolver, auth(disable, writeEvaluator), routing.Wrap(a.setUserPermission)) - r.Post("/:resourceID/teams/:teamID", inheritanceSolver, auth(disable, writeEvaluator), routing.Wrap(a.setTeamPermission)) - r.Post("/:resourceID/builtInRoles/:builtInRole", inheritanceSolver, auth(disable, writeEvaluator), routing.Wrap(a.setBuiltinRolePermission)) + if a.service.options.Assignments.Users { + r.Post("/:resourceID/users/:userID", inheritanceSolver, auth(disable, writeEvaluator), routing.Wrap(a.setUserPermission)) + } + if a.service.options.Assignments.Teams { + r.Post("/:resourceID/teams/:teamID", inheritanceSolver, auth(disable, writeEvaluator), routing.Wrap(a.setTeamPermission)) + } + if a.service.options.Assignments.BuiltInRoles { + r.Post("/:resourceID/builtInRoles/:builtInRole", inheritanceSolver, auth(disable, writeEvaluator), routing.Wrap(a.setBuiltinRolePermission)) + } }) } diff --git a/pkg/services/dashboards/manager/dashboard_service.go b/pkg/services/dashboards/manager/dashboard_service.go index f8bc2c3618c..619b4b909df 100644 --- a/pkg/services/dashboards/manager/dashboard_service.go +++ b/pkg/services/dashboards/manager/dashboard_service.go @@ -35,13 +35,14 @@ type DashboardServiceImpl struct { dashboardStore m.Store dashAlertExtractor alerting.DashAlertExtractor features featuremgmt.FeatureToggles - folderPermissions accesscontrol.PermissionsService - dashboardPermissions accesscontrol.PermissionsService + folderPermissions accesscontrol.FolderPermissionsService + dashboardPermissions accesscontrol.DashboardPermissionsService } func ProvideDashboardService( cfg *setting.Cfg, store m.Store, dashAlertExtractor alerting.DashAlertExtractor, - features featuremgmt.FeatureToggles, permissionsServices accesscontrol.PermissionsServices, + features featuremgmt.FeatureToggles, folderPermissionsService accesscontrol.FolderPermissionsService, + dashboardPermissionsService accesscontrol.DashboardPermissionsService, ) *DashboardServiceImpl { return &DashboardServiceImpl{ cfg: cfg, @@ -49,8 +50,8 @@ func ProvideDashboardService( dashboardStore: store, dashAlertExtractor: dashAlertExtractor, features: features, - folderPermissions: permissionsServices.GetFolderService(), - dashboardPermissions: permissionsServices.GetDashboardService(), + folderPermissions: folderPermissionsService, + dashboardPermissions: dashboardPermissionsService, } } diff --git a/pkg/services/dashboards/manager/dashboard_service_integration_test.go b/pkg/services/dashboards/manager/dashboard_service_integration_test.go index dfd700b8bad..3fc3662bd48 100644 --- a/pkg/services/dashboards/manager/dashboard_service_integration_test.go +++ b/pkg/services/dashboards/manager/dashboard_service_integration_test.go @@ -862,7 +862,9 @@ func callSaveWithResult(t *testing.T, cmd models.SaveDashboardCommand, sqlStore cfg.IsFeatureToggleEnabled = featuremgmt.WithFeatures().IsEnabled service := ProvideDashboardService( cfg, dashboardStore, &dummyDashAlertExtractor{}, - featuremgmt.WithFeatures(), accesscontrolmock.NewPermissionsServicesMock(), + featuremgmt.WithFeatures(), + accesscontrolmock.NewMockedPermissionsService(), + accesscontrolmock.NewMockedPermissionsService(), ) res, err := service.SaveDashboard(context.Background(), &dto, false) require.NoError(t, err) @@ -877,7 +879,9 @@ func callSaveWithError(cmd models.SaveDashboardCommand, sqlStore *sqlstore.SQLSt cfg.IsFeatureToggleEnabled = featuremgmt.WithFeatures().IsEnabled service := ProvideDashboardService( cfg, dashboardStore, &dummyDashAlertExtractor{}, - featuremgmt.WithFeatures(), accesscontrolmock.NewPermissionsServicesMock(), + featuremgmt.WithFeatures(), + accesscontrolmock.NewMockedPermissionsService(), + accesscontrolmock.NewMockedPermissionsService(), ) _, err := service.SaveDashboard(context.Background(), &dto, false) return err @@ -910,7 +914,8 @@ func saveTestDashboard(t *testing.T, title string, orgID, folderID int64, sqlSto cfg.IsFeatureToggleEnabled = featuremgmt.WithFeatures().IsEnabled service := ProvideDashboardService( cfg, dashboardStore, &dummyDashAlertExtractor{}, - featuremgmt.WithFeatures(), accesscontrolmock.NewPermissionsServicesMock(), + featuremgmt.WithFeatures(), + accesscontrolmock.NewMockedPermissionsService(), accesscontrolmock.NewMockedPermissionsService(), ) res, err := service.SaveDashboard(context.Background(), &dto, false) require.NoError(t, err) @@ -944,7 +949,8 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore *sqlstore. cfg.IsFeatureToggleEnabled = featuremgmt.WithFeatures().IsEnabled service := ProvideDashboardService( cfg, dashboardStore, &dummyDashAlertExtractor{}, - featuremgmt.WithFeatures(), accesscontrolmock.NewPermissionsServicesMock(), + featuremgmt.WithFeatures(), + accesscontrolmock.NewMockedPermissionsService(), accesscontrolmock.NewMockedPermissionsService(), ) res, err := service.SaveDashboard(context.Background(), &dto, false) require.NoError(t, err) diff --git a/pkg/services/dashboards/manager/folder_service.go b/pkg/services/dashboards/manager/folder_service.go index cca5041390a..3e517d3bf73 100644 --- a/pkg/services/dashboards/manager/folder_service.go +++ b/pkg/services/dashboards/manager/folder_service.go @@ -23,13 +23,13 @@ type FolderServiceImpl struct { dashboardStore dashboards.Store searchService *search.SearchService features featuremgmt.FeatureToggles - permissions accesscontrol.PermissionsService + permissions accesscontrol.FolderPermissionsService sqlStore sqlstore.Store } func ProvideFolderService( cfg *setting.Cfg, dashboardService dashboards.DashboardService, dashboardStore dashboards.Store, - searchService *search.SearchService, features featuremgmt.FeatureToggles, permissionsServices accesscontrol.PermissionsServices, + searchService *search.SearchService, features featuremgmt.FeatureToggles, folderPermissionsService accesscontrol.FolderPermissionsService, ac accesscontrol.AccessControl, sqlStore sqlstore.Store, ) *FolderServiceImpl { ac.RegisterScopeAttributeResolver(dashboards.NewFolderNameScopeResolver(dashboardStore)) @@ -42,7 +42,7 @@ func ProvideFolderService( dashboardStore: dashboardStore, searchService: searchService, features: features, - permissions: permissionsServices.GetFolderService(), + permissions: folderPermissionsService, sqlStore: sqlStore, } } diff --git a/pkg/services/dashboards/manager/folder_service_test.go b/pkg/services/dashboards/manager/folder_service_test.go index 1cc03e0cb4a..a65c488df29 100644 --- a/pkg/services/dashboards/manager/folder_service_test.go +++ b/pkg/services/dashboards/manager/folder_service_test.go @@ -32,13 +32,14 @@ func TestProvideFolderService(t *testing.T) { cfg := setting.NewCfg() features := featuremgmt.WithFeatures() cfg.IsFeatureToggleEnabled = features.IsEnabled - permissionsServices := acmock.NewPermissionsServicesMock() - dashboardService := ProvideDashboardService(cfg, store, nil, features, permissionsServices) + folderPermissions := acmock.NewMockedPermissionsService() + dashboardPermissions := acmock.NewMockedPermissionsService() + dashboardService := ProvideDashboardService(cfg, store, nil, features, folderPermissions, dashboardPermissions) ac := acmock.New() ProvideFolderService( cfg, &dashboards.FakeDashboardService{DashboardService: dashboardService}, - store, nil, features, permissionsServices, ac, mockstore.NewSQLStoreMock(), + store, nil, features, folderPermissions, ac, mockstore.NewSQLStoreMock(), ) require.Len(t, ac.Calls.RegisterAttributeScopeResolver, 2) @@ -51,8 +52,9 @@ func TestFolderService(t *testing.T) { cfg := setting.NewCfg() features := featuremgmt.WithFeatures() cfg.IsFeatureToggleEnabled = features.IsEnabled - permissionsServices := acmock.NewPermissionsServicesMock() - dashboardService := ProvideDashboardService(cfg, store, nil, features, permissionsServices) + folderPermissions := acmock.NewMockedPermissionsService() + dashboardPermissions := acmock.NewMockedPermissionsService() + dashboardService := ProvideDashboardService(cfg, store, nil, features, folderPermissions, dashboardPermissions) mockStore := mockstore.NewSQLStoreMock() service := FolderServiceImpl{ @@ -62,7 +64,7 @@ func TestFolderService(t *testing.T) { dashboardStore: store, searchService: nil, features: features, - permissions: permissionsServices.GetFolderService(), + permissions: folderPermissions, sqlStore: mockStore, } diff --git a/pkg/services/datasources/service/datasource_service.go b/pkg/services/datasources/service/datasource_service.go index 572ea48c1ca..8cbab36d30a 100644 --- a/pkg/services/datasources/service/datasource_service.go +++ b/pkg/services/datasources/service/datasource_service.go @@ -35,7 +35,7 @@ type Service struct { SecretsService secrets.Service cfg *setting.Cfg features featuremgmt.FeatureToggles - permissionsService accesscontrol.PermissionsService + permissionsService accesscontrol.DatasourcePermissionsService ac accesscontrol.AccessControl ptc proxyTransportCache @@ -53,7 +53,7 @@ type cachedRoundTripper struct { func ProvideService( store *sqlstore.SQLStore, secretsService secrets.Service, secretsStore kvstore.SecretsKVStore, cfg *setting.Cfg, - features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl, permissionsServices accesscontrol.PermissionsServices, + features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl, datasourcePermissionsService accesscontrol.DatasourcePermissionsService, ) *Service { s := &Service{ SQLStore: store, @@ -64,7 +64,7 @@ func ProvideService( }, cfg: cfg, features: features, - permissionsService: permissionsServices.GetDataSourceService(), + permissionsService: datasourcePermissionsService, ac: ac, } diff --git a/pkg/services/datasources/service/datasource_service_test.go b/pkg/services/datasources/service/datasource_service_test.go index 120c6e54c7e..6cb930bd714 100644 --- a/pkg/services/datasources/service/datasource_service_test.go +++ b/pkg/services/datasources/service/datasource_service_test.go @@ -198,7 +198,7 @@ func TestService_GetHttpTransport(t *testing.T) { secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) rt1, err := dsService.GetHTTPTransport(context.Background(), &ds, provider) require.NoError(t, err) @@ -232,7 +232,7 @@ func TestService_GetHttpTransport(t *testing.T) { secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) ds := models.DataSource{ Id: 1, @@ -280,7 +280,7 @@ func TestService_GetHttpTransport(t *testing.T) { secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) ds := models.DataSource{ Id: 1, @@ -325,7 +325,7 @@ func TestService_GetHttpTransport(t *testing.T) { secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) ds := models.DataSource{ Id: 1, @@ -367,7 +367,7 @@ func TestService_GetHttpTransport(t *testing.T) { secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) ds := models.DataSource{ Id: 1, @@ -399,7 +399,7 @@ func TestService_GetHttpTransport(t *testing.T) { secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) ds := models.DataSource{ Id: 1, @@ -465,7 +465,7 @@ func TestService_GetHttpTransport(t *testing.T) { secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) ds := models.DataSource{ Id: 1, @@ -499,7 +499,7 @@ func TestService_GetHttpTransport(t *testing.T) { secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) ds := models.DataSource{ Type: models.DS_ES, @@ -535,7 +535,7 @@ func TestService_getTimeout(t *testing.T) { secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) for _, tc := range testCases { ds := &models.DataSource{ @@ -576,7 +576,7 @@ func TestService_HTTPClientOptions(t *testing.T) { secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(nil, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewMockedPermissionsService()) opts, err := dsService.httpClientOptions(context.Background(), &ds) require.NoError(t, err) @@ -594,7 +594,7 @@ func TestService_HTTPClientOptions(t *testing.T) { secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(nil, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewMockedPermissionsService()) opts, err := dsService.httpClientOptions(context.Background(), &ds) require.NoError(t, err) @@ -614,7 +614,7 @@ func TestService_HTTPClientOptions(t *testing.T) { secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(nil, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewMockedPermissionsService()) _, err := dsService.httpClientOptions(context.Background(), &ds) assert.Error(t, err) @@ -633,7 +633,7 @@ func TestService_HTTPClientOptions(t *testing.T) { secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(nil, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewMockedPermissionsService()) opts, err := dsService.httpClientOptions(context.Background(), &ds) require.NoError(t, err) @@ -652,7 +652,7 @@ func TestService_HTTPClientOptions(t *testing.T) { secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(nil, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewMockedPermissionsService()) opts, err := dsService.httpClientOptions(context.Background(), &ds) require.NoError(t, err) @@ -675,7 +675,7 @@ func TestService_HTTPClientOptions(t *testing.T) { secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(nil, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, secretsStore, cfg, features, acmock.New(), acmock.NewMockedPermissionsService()) _, err := dsService.httpClientOptions(context.Background(), &ds) assert.Error(t, err) @@ -696,7 +696,7 @@ func TestService_HTTPClientOptions(t *testing.T) { secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) opts, err := dsService.httpClientOptions(context.Background(), &ds) require.NoError(t, err) @@ -719,7 +719,7 @@ func TestService_GetDecryptedValues(t *testing.T) { secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(nil, secretsService, secretsStore, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, secretsStore, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) jsonData := map[string]string{ "password": "securePassword", @@ -744,7 +744,7 @@ func TestService_GetDecryptedValues(t *testing.T) { secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := ProvideService(nil, secretsService, secretsStore, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + dsService := ProvideService(nil, secretsService, secretsStore, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) jsonData := map[string]string{ "password": "securePassword", diff --git a/pkg/services/guardian/accesscontrol_guardian.go b/pkg/services/guardian/accesscontrol_guardian.go index 5e3dd3b33ff..6c2b7f2b046 100644 --- a/pkg/services/guardian/accesscontrol_guardian.go +++ b/pkg/services/guardian/accesscontrol_guardian.go @@ -22,29 +22,33 @@ var _ DashboardGuardian = new(AccessControlDashboardGuardian) func NewAccessControlDashboardGuardian( ctx context.Context, dashboardId int64, user *models.SignedInUser, - store sqlstore.Store, ac accesscontrol.AccessControl, permissionsServices accesscontrol.PermissionsServices, + store sqlstore.Store, ac accesscontrol.AccessControl, + folderPermissionsService accesscontrol.FolderPermissionsService, + dashboardPermissionsService accesscontrol.DashboardPermissionsService, ) *AccessControlDashboardGuardian { return &AccessControlDashboardGuardian{ - ctx: ctx, - log: log.New("dashboard.permissions"), - dashboardID: dashboardId, - user: user, - store: store, - ac: ac, - permissionServices: permissionsServices, + ctx: ctx, + log: log.New("dashboard.permissions"), + dashboardID: dashboardId, + user: user, + store: store, + ac: ac, + folderPermissionsService: folderPermissionsService, + dashboardPermissionsService: dashboardPermissionsService, } } type AccessControlDashboardGuardian struct { - ctx context.Context - log log.Logger - dashboardID int64 - dashboard *models.Dashboard - parentFolderUID string - user *models.SignedInUser - store sqlstore.Store - ac accesscontrol.AccessControl - permissionServices accesscontrol.PermissionsServices + ctx context.Context + log log.Logger + dashboardID int64 + dashboard *models.Dashboard + parentFolderUID string + user *models.SignedInUser + store sqlstore.Store + ac accesscontrol.AccessControl + folderPermissionsService accesscontrol.FolderPermissionsService + dashboardPermissionsService accesscontrol.DashboardPermissionsService } func (a *AccessControlDashboardGuardian) CanSave() (bool, error) { @@ -169,9 +173,11 @@ func (a *AccessControlDashboardGuardian) GetAcl() ([]*models.DashboardAclInfoDTO return nil, err } - svc := a.permissionServices.GetDashboardService() + var svc accesscontrol.PermissionsService if a.dashboard.IsFolder { - svc = a.permissionServices.GetFolderService() + svc = a.folderPermissionsService + } else { + svc = a.dashboardPermissionsService } permissions, err := svc.GetPermissions(a.ctx, a.user, strconv.FormatInt(a.dashboard.Id, 10)) diff --git a/pkg/services/guardian/accesscontrol_guardian_test.go b/pkg/services/guardian/accesscontrol_guardian_test.go index c780df1cd17..ef0ffadba26 100644 --- a/pkg/services/guardian/accesscontrol_guardian_test.go +++ b/pkg/services/guardian/accesscontrol_guardian_test.go @@ -559,10 +559,10 @@ func TestAccessControlDashboardGuardian_GetHiddenACL(t *testing.T) { t.Run(tt.desc, func(t *testing.T) { guardian, _ := setupAccessControlGuardianTest(t, "1", nil) - mocked := accesscontrolmock.NewPermissionsServicesMock() - guardian.permissionServices = mocked - mocked.Dashboards.On("MapActions", mock.Anything).Return("View") - mocked.Dashboards.On("GetPermissions", mock.Anything, mock.Anything, mock.Anything).Return(tt.permissions, nil) + mocked := accesscontrolmock.NewMockedPermissionsService() + guardian.dashboardPermissionsService = mocked + mocked.On("MapActions", mock.Anything).Return("View") + mocked.On("GetPermissions", mock.Anything, mock.Anything, mock.Anything).Return(tt.permissions, nil) cfg := setting.NewCfg() cfg.HiddenUsers = tt.hiddenUsers permissions, err := guardian.GetHiddenACL(cfg) @@ -595,8 +595,10 @@ func setupAccessControlGuardianTest(t *testing.T, uid string, permissions []*acc }) require.NoError(t, err) ac := accesscontrolmock.New().WithPermissions(permissions) - services, err := ossaccesscontrol.ProvidePermissionsServices(setting.NewCfg(), routing.NewRouteRegister(), store, ac, database.ProvideService(store)) + folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions(setting.NewCfg(), routing.NewRouteRegister(), store, ac, database.ProvideService(store)) + require.NoError(t, err) + dashboardPermissions, err := ossaccesscontrol.ProvideDashboardPermissions(setting.NewCfg(), routing.NewRouteRegister(), store, ac, database.ProvideService(store)) require.NoError(t, err) - return NewAccessControlDashboardGuardian(context.Background(), dash.Id, &models.SignedInUser{OrgId: 1}, store, ac, services), dash + return NewAccessControlDashboardGuardian(context.Background(), dash.Id, &models.SignedInUser{OrgId: 1}, store, ac, folderPermissions, dashboardPermissions), dash } diff --git a/pkg/services/guardian/provider.go b/pkg/services/guardian/provider.go index 3318fb45abb..f026e923b4b 100644 --- a/pkg/services/guardian/provider.go +++ b/pkg/services/guardian/provider.go @@ -5,16 +5,18 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/sqlstore" ) type Provider struct{} -func ProvideService(store *sqlstore.SQLStore, ac accesscontrol.AccessControl, permissionsServices accesscontrol.PermissionsServices, features featuremgmt.FeatureToggles) *Provider { +func ProvideService( + store *sqlstore.SQLStore, ac accesscontrol.AccessControl, + folderPermissionsService accesscontrol.FolderPermissionsService, dashboardPermissionsService accesscontrol.DashboardPermissionsService, +) *Provider { if !ac.IsDisabled() { // TODO: Fix this hack, see https://github.com/grafana/grafana-enterprise/issues/2935 - InitAcessControlGuardian(store, ac, permissionsServices) + InitAccessControlGuardian(store, ac, folderPermissionsService, dashboardPermissionsService) } else { InitLegacyGuardian(store) } @@ -27,8 +29,11 @@ func InitLegacyGuardian(store sqlstore.Store) { } } -func InitAcessControlGuardian(store sqlstore.Store, ac accesscontrol.AccessControl, permissionsServices accesscontrol.PermissionsServices) { +func InitAccessControlGuardian( + store sqlstore.Store, ac accesscontrol.AccessControl, folderPermissionsService accesscontrol.FolderPermissionsService, + dashboardPermissionsService accesscontrol.DashboardPermissionsService, +) { New = func(ctx context.Context, dashId int64, orgId int64, user *models.SignedInUser) DashboardGuardian { - return NewAccessControlDashboardGuardian(ctx, dashId, user, store, ac, permissionsServices) + return NewAccessControlDashboardGuardian(ctx, dashId, user, store, ac, folderPermissionsService, dashboardPermissionsService) } } diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 8021c262aa0..be150681140 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -205,9 +205,11 @@ func createDashboard(t *testing.T, sqlStore *sqlstore.SQLStore, user models.Sign features := featuremgmt.WithFeatures() cfg := setting.NewCfg() cfg.IsFeatureToggleEnabled = features.IsEnabled + folderPermissions := acmock.NewMockedPermissionsService() + dashboardPermissions := acmock.NewMockedPermissionsService() service := dashboardservice.ProvideDashboardService( cfg, dashboardStore, dashAlertExtractor, - features, acmock.NewPermissionsServicesMock(), + features, folderPermissions, dashboardPermissions, ) dashboard, err := service.SaveDashboard(context.Background(), dashItem, true) require.NoError(t, err) @@ -222,17 +224,18 @@ func createFolderWithACL(t *testing.T, sqlStore *sqlstore.SQLStore, title string cfg := setting.NewCfg() features := featuremgmt.WithFeatures() cfg.IsFeatureToggleEnabled = features.IsEnabled - permissionsServices := acmock.NewPermissionsServicesMock() + folderPermissions := acmock.NewMockedPermissionsService() + dashboardPermissions := acmock.NewMockedPermissionsService() dashboardStore := database.ProvideDashboardStore(sqlStore) d := dashboardservice.ProvideDashboardService( cfg, dashboardStore, nil, - features, permissionsServices, + features, folderPermissions, dashboardPermissions, ) ac := acmock.New() s := dashboardservice.ProvideFolderService( cfg, d, dashboardStore, nil, - features, permissionsServices, ac, nil, + features, folderPermissions, ac, nil, ) t.Logf("Creating folder with title and UID %q", title) folder, err := s.CreateFolder(context.Background(), &user, user.OrgId, title, title) @@ -324,9 +327,12 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo features := featuremgmt.WithFeatures() cfg := setting.NewCfg() cfg.IsFeatureToggleEnabled = features.IsEnabled + folderPermissions := acmock.NewMockedPermissionsService() + dashboardPermissions := acmock.NewMockedPermissionsService() + dashboardService := dashboardservice.ProvideDashboardService( cfg, dashboardStore, nil, - features, acmock.NewPermissionsServicesMock(), + features, folderPermissions, dashboardPermissions, ) ac := acmock.New() service := LibraryElementService{ @@ -334,7 +340,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo SQLStore: sqlStore, folderService: dashboardservice.ProvideFolderService( cfg, dashboardService, dashboardStore, nil, - features, acmock.NewPermissionsServicesMock(), ac, nil, + features, folderPermissions, ac, nil, ), } diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index a2493c8e1c7..f87fe357ceb 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -1372,7 +1372,7 @@ func createDashboard(t *testing.T, sqlStore *sqlstore.SQLStore, user *models.Sig cfg.IsFeatureToggleEnabled = featuremgmt.WithFeatures().IsEnabled service := dashboardservice.ProvideDashboardService( cfg, dashboardStore, dashAlertService, - featuremgmt.WithFeatures(), acmock.NewPermissionsServicesMock(), + featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), acmock.NewMockedPermissionsService(), ) dashboard, err := service.SaveDashboard(context.Background(), dashItem, true) require.NoError(t, err) @@ -1384,14 +1384,15 @@ func createFolderWithACL(t *testing.T, sqlStore *sqlstore.SQLStore, title string items []folderACLItem) *models.Folder { t.Helper() + ac := acmock.New() cfg := setting.NewCfg() cfg.IsFeatureToggleEnabled = featuremgmt.WithFeatures().IsEnabled features := featuremgmt.WithFeatures() - permissionsServices := acmock.NewPermissionsServicesMock() + folderPermissions := acmock.NewMockedPermissionsService() + dashboardPermissions := acmock.NewMockedPermissionsService() dashboardStore := database.ProvideDashboardStore(sqlStore) - d := dashboardservice.ProvideDashboardService(cfg, dashboardStore, nil, features, permissionsServices) - ac := acmock.New() - s := dashboardservice.ProvideFolderService(cfg, d, dashboardStore, nil, features, permissionsServices, ac, nil) + d := dashboardservice.ProvideDashboardService(cfg, dashboardStore, nil, features, folderPermissions, dashboardPermissions) + s := dashboardservice.ProvideFolderService(cfg, d, dashboardStore, nil, features, folderPermissions, ac, nil) t.Logf("Creating folder with title and UID %q", title) folder, err := s.CreateFolder(context.Background(), user, user.OrgId, title, title) @@ -1484,17 +1485,18 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo dashboardStore := database.ProvideDashboardStore(sqlStore) features := featuremgmt.WithFeatures() - permissionsServices := acmock.NewPermissionsServicesMock() + folderPermissions := acmock.NewMockedPermissionsService() + dashboardPermissions := acmock.NewMockedPermissionsService() dashboardService := dashboardservice.ProvideDashboardService( cfg, dashboardStore, &alerting.DashAlertExtractorService{}, - features, permissionsServices, + features, folderPermissions, dashboardPermissions, ) ac := acmock.New() folderService := dashboardservice.ProvideFolderService( cfg, dashboardService, dashboardStore, nil, - features, permissionsServices, ac, nil, + features, folderPermissions, ac, nil, ) elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService) diff --git a/pkg/services/ngalert/tests/util.go b/pkg/services/ngalert/tests/util.go index 69f79d427ae..ed5915fa071 100644 --- a/pkg/services/ngalert/tests/util.go +++ b/pkg/services/ngalert/tests/util.go @@ -51,15 +51,16 @@ func SetupTestEnv(t *testing.T, baseInterval time.Duration) (*ngalert.AlertNG, * ac := acmock.New() features := featuremgmt.WithFeatures() - permissionsServices := acmock.NewPermissionsServicesMock() + folderPermissions := acmock.NewMockedPermissionsService() + dashboardPermissions := acmock.NewMockedPermissionsService() dashboardService := dashboardservice.ProvideDashboardService( cfg, dashboardStore, nil, - features, permissionsServices, + features, folderPermissions, dashboardPermissions, ) folderService := dashboardservice.ProvideFolderService( cfg, dashboardService, dashboardStore, nil, - features, permissionsServices, ac, nil, + features, folderPermissions, ac, nil, ) ng, err := ngalert.ProvideService( diff --git a/pkg/services/query/query_test.go b/pkg/services/query/query_test.go index b5e05ed5e35..d4f1d7b87ad 100644 --- a/pkg/services/query/query_test.go +++ b/pkg/services/query/query_test.go @@ -71,7 +71,7 @@ func setup(t *testing.T) *testContext { ss := kvstore.SetupTestService(t) ssvc := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - ds := datasources.ProvideService(nil, ssvc, ss, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + ds := datasources.ProvideService(nil, ssvc, ss, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService()) return &testContext{ pluginContext: pc, diff --git a/pkg/tsdb/legacydata/service/service_test.go b/pkg/tsdb/legacydata/service/service_test.go index 24457784b95..2c26896cea9 100644 --- a/pkg/tsdb/legacydata/service/service_test.go +++ b/pkg/tsdb/legacydata/service/service_test.go @@ -41,7 +41,8 @@ func TestHandleRequest(t *testing.T) { } secretsStore := kvstore.SetupTestService(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) - dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewPermissionsServicesMock()) + datasourcePermissions := acmock.NewMockedPermissionsService() + dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), datasourcePermissions) s := ProvideService(client, nil, dsService) ds := &models.DataSource{Id: 12, Type: "unregisteredType", JsonData: simplejson.New()} From 2bd9e9aca5ce057884abe4bf20795d61fe94079c Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Tue, 10 May 2022 15:05:48 +0100 Subject: [PATCH 116/440] AzureMonitor: Add support for selecting multiple options when using the equals and not equals dimension filters (#48650) * Add support for multiselect - Add filters param to Dimensions - Update existing tests - Add MultiSelect component - Add helper function to determine valid options - Update labels hook to account for custom values - Update go type - Add function to build valid filters string * Additional go tests - Ensure query targets are built correctly * Update DimensionFields frontend test - Corrently rerender components - Additional test for multiple labels selection - Better selection of options in react-select components * Fix lint issue * Reset filters when operator or dimension changes * Terminology * Update test * Add backend migration - Update types (deprecate Filter field) - Add migration logic - Update tests - Update dimension filters buliding * Add migration test code * Simplify some logic * Add frontend deprecation notice * Add frontend migration logic and migration tests * Update setting of filter values * Update DimensionFields test * Fix linting issues * PR comment updates - Remove unnecessary if/else condition - Don't set filter default value as queries should be migrated - Add comment explaining why sw operator only accepts one value - Remove unnecessary test for merging of old and new filters * Nit on terminology Co-authored-by: Andres Martinez Gotor * Rename migrations for clarity Co-authored-by: Andres Martinez Gotor --- .../metrics/azuremonitor-datasource.go | 6 +- .../metrics/azuremonitor-datasource_test.go | 35 +++- pkg/tsdb/azuremonitor/metrics/migrations.go | 43 +++++ .../azuremonitor/metrics/migrations_test.go | 62 +++++++ pkg/tsdb/azuremonitor/types/types.go | 23 ++- .../azure_monitor/azure_monitor_datasource.ts | 4 +- .../DimensionFields.test.tsx | 159 ++++++++++++++++-- .../MetricsQueryEditor/DimensionFields.tsx | 62 +++++-- .../MetricsQueryEditor/setQueryValue.ts | 7 +- .../types/query.ts | 4 + .../utils/migrateQuery.test.ts | 84 ++++++++- .../utils/migrateQuery.ts | 56 ++++-- 12 files changed, 482 insertions(+), 63 deletions(-) create mode 100644 pkg/tsdb/azuremonitor/metrics/migrations.go create mode 100644 pkg/tsdb/azuremonitor/metrics/migrations_test.go diff --git a/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go index 3a5ba27f2b2..e8ccd0458b4 100644 --- a/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go +++ b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource.go @@ -85,6 +85,8 @@ func (e *AzureMonitorDatasource) buildQueries(queries []backend.DataQuery, dsInf MetricDefinition: azJSONModel.MetricDefinition, ResourceName: azJSONModel.ResourceName, } + + azJSONModel.DimensionFilters = MigrateDimensionFilters(azJSONModel.DimensionFilters) azureURL := ub.BuildMetricsURL() resourceName := azJSONModel.ResourceName @@ -129,10 +131,10 @@ func (e *AzureMonitorDatasource) buildQueries(queries []backend.DataQuery, dsInf dimSB.WriteString(fmt.Sprintf("%s eq '%s'", dimension, dimensionFilter)) } else { for i, filter := range azJSONModel.DimensionFilters { - if filter.Operator != "eq" && filter.Filter == "*" { + if len(filter.Filters) == 0 { dimSB.WriteString(fmt.Sprintf("%s eq '*'", filter.Dimension)) } else { - dimSB.WriteString(filter.String()) + dimSB.WriteString(filter.ConstructFiltersString()) } if i != len(azJSONModel.DimensionFilters)-1 { dimSB.WriteString(" and ") diff --git a/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource_test.go b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource_test.go index 98a021aa429..33be35745c4 100644 --- a/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource_test.go +++ b/pkg/tsdb/azuremonitor/metrics/azuremonitor-datasource_test.go @@ -32,7 +32,8 @@ func TestAzureMonitorBuildQueries(t *testing.T) { fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local) duration, _ := time.ParseDuration("400s") - + wildcardFilter := "*" + testFilter := "test" tests := []struct { name string azureMonitorVariedProperties map[string]interface{} @@ -101,7 +102,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) { name: "legacy query without resourceURI and has dimensionFilter*s* property with one dimension", azureMonitorVariedProperties: map[string]interface{}{ "timeGrain": "PT1M", - "dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "eq", Filter: "*"}}, + "dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "eq", Filter: &wildcardFilter}}, "top": "30", }, queryInterval: duration, @@ -112,7 +113,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) { name: "legacy query without resourceURI and has dimensionFilter*s* property with two dimensions", azureMonitorVariedProperties: map[string]interface{}{ "timeGrain": "PT1M", - "dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "eq", Filter: "*"}, {Dimension: "tier", Operator: "eq", Filter: "*"}}, + "dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "eq", Filter: &wildcardFilter}, {Dimension: "tier", Operator: "eq", Filter: &wildcardFilter}}, "top": "30", }, queryInterval: duration, @@ -134,7 +135,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) { name: "has dimensionFilter*s* property with not equals operator", azureMonitorVariedProperties: map[string]interface{}{ "timeGrain": "PT1M", - "dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "ne", Filter: "test"}}, + "dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "ne", Filter: &wildcardFilter, Filters: []string{"test"}}}, "top": "30", }, queryInterval: duration, @@ -145,7 +146,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) { name: "has dimensionFilter*s* property with startsWith operator", azureMonitorVariedProperties: map[string]interface{}{ "timeGrain": "PT1M", - "dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "sw", Filter: "test"}}, + "dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "sw", Filter: &testFilter}}, "top": "30", }, queryInterval: duration, @@ -156,13 +157,35 @@ func TestAzureMonitorBuildQueries(t *testing.T) { name: "correctly sets dimension operator to eq (irrespective of operator) when filter value is '*'", azureMonitorVariedProperties: map[string]interface{}{ "timeGrain": "PT1M", - "dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "sw", Filter: "*"}, {Dimension: "tier", Operator: "ne", Filter: "*"}}, + "dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "sw", Filter: &wildcardFilter}, {Dimension: "tier", Operator: "ne", Filter: &wildcardFilter}}, "top": "30", }, queryInterval: duration, expectedInterval: "PT1M", azureMonitorQueryTarget: "%24filter=blob+eq+%27%2A%27+and+tier+eq+%27%2A%27&aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute-virtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=30", }, + { + name: "correctly constructs target when multiple filter values are provided for the 'eq' operator", + azureMonitorVariedProperties: map[string]interface{}{ + "timeGrain": "PT1M", + "dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "eq", Filter: &wildcardFilter, Filters: []string{"test", "test2"}}}, + "top": "30", + }, + queryInterval: duration, + expectedInterval: "PT1M", + azureMonitorQueryTarget: "%24filter=blob+eq+%27test%27+or+blob+eq+%27test2%27&aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute-virtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=30", + }, + { + name: "correctly constructs target when multiple filter values are provided for ne 'eq' operator", + azureMonitorVariedProperties: map[string]interface{}{ + "timeGrain": "PT1M", + "dimensionFilters": []types.AzureMonitorDimensionFilter{{Dimension: "blob", Operator: "ne", Filter: &wildcardFilter, Filters: []string{"test", "test2"}}}, + "top": "30", + }, + queryInterval: duration, + expectedInterval: "PT1M", + azureMonitorQueryTarget: "%24filter=blob+ne+%27test%27+and+blob+ne+%27test2%27&aggregation=Average&api-version=2018-01-01&interval=PT1M&metricnames=Percentage+CPU&metricnamespace=Microsoft.Compute-virtualMachines×pan=2018-03-15T13%3A00%3A00Z%2F2018-03-15T13%3A34%3A00Z&top=30", + }, } commonAzureModelProps := map[string]interface{}{ diff --git a/pkg/tsdb/azuremonitor/metrics/migrations.go b/pkg/tsdb/azuremonitor/metrics/migrations.go new file mode 100644 index 00000000000..a144a892862 --- /dev/null +++ b/pkg/tsdb/azuremonitor/metrics/migrations.go @@ -0,0 +1,43 @@ +package metrics + +import ( + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" +) + +func MigrateDimensionFilters(filters []types.AzureMonitorDimensionFilter) []types.AzureMonitorDimensionFilter { + var newFilters []types.AzureMonitorDimensionFilter + for _, filter := range filters { + newFilter := filter + // Ignore the deprecation check as this is a migration + // nolint:staticcheck + newFilter.Filter = nil + // If there is no old field and the new field is specified - append as this is valid + // nolint:staticcheck + if filter.Filter == nil && filter.Filters != nil { + newFilters = append(newFilters, newFilter) + } else { + // nolint:staticcheck + oldFilter := *filter.Filter + // If there is an old filter and no new ones then construct the new array and append + if filter.Filters == nil && oldFilter != "*" { + newFilter.Filters = []string{oldFilter} + // If both the new and old fields are specified (edge case) then construct the appropriate values + } else { + hasFilter := false + oldFilters := filter.Filters + for _, filterValue := range oldFilters { + if filterValue == oldFilter { + hasFilter = true + break + } + } + if !hasFilter && oldFilter != "*" { + oldFilters = append(oldFilters, oldFilter) + newFilter.Filters = oldFilters + } + } + newFilters = append(newFilters, newFilter) + } + } + return newFilters +} diff --git a/pkg/tsdb/azuremonitor/metrics/migrations_test.go b/pkg/tsdb/azuremonitor/metrics/migrations_test.go new file mode 100644 index 00000000000..56b7508dbbe --- /dev/null +++ b/pkg/tsdb/azuremonitor/metrics/migrations_test.go @@ -0,0 +1,62 @@ +package metrics + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" +) + +func TestDimensionFiltersMigration(t *testing.T) { + wildcard := "*" + testFilter := "testFilter" + additionalTestFilter := "testFilter2" + tests := []struct { + name string + dimensionFilters []types.AzureMonitorDimensionFilter + expectedDimensionFilters []types.AzureMonitorDimensionFilter + }{ + { + name: "will return new format unchanged", + dimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filters: []string{"testFilter"}}}, + expectedDimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filters: []string{"testFilter"}}}, + }, + { + name: "correctly updates old format with wildcard", + dimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filter: &wildcard}}, + expectedDimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq"}}, + }, + { + name: "correctly updates old format with a value", + dimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filter: &testFilter}}, + expectedDimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filters: []string{testFilter}}}, + }, + { + name: "correctly ignores wildcard if filters has a value", + dimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filter: &wildcard, Filters: []string{testFilter}}}, + expectedDimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filters: []string{testFilter}}}, + }, + { + name: "correctly merges values if filters has a value (ignores duplicates)", + dimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filter: &testFilter, Filters: []string{testFilter}}}, + expectedDimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filters: []string{testFilter}}}, + }, + { + name: "correctly merges values if filters has a value", + dimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filter: &additionalTestFilter, Filters: []string{testFilter}}}, + expectedDimensionFilters: []types.AzureMonitorDimensionFilter{{Dimension: "testDimension", Operator: "eq", Filters: []string{testFilter, additionalTestFilter}}}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filters := MigrateDimensionFilters(tt.dimensionFilters) + + if diff := cmp.Diff(tt.expectedDimensionFilters, filters, cmpopts.IgnoreUnexported(simplejson.Json{})); diff != "" { + t.Errorf("Result mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/pkg/tsdb/azuremonitor/types/types.go b/pkg/tsdb/azuremonitor/types/types.go index 1a9b4d60271..5213bf5399c 100644 --- a/pkg/tsdb/azuremonitor/types/types.go +++ b/pkg/tsdb/azuremonitor/types/types.go @@ -5,6 +5,7 @@ import ( "net/http" "net/url" "regexp" + "strings" "time" "github.com/grafana/grafana-azure-sdk-go/azcredentials" @@ -139,17 +140,23 @@ type AzureMonitorJSONQuery struct { // AzureMonitorDimensionFilter is the model for the frontend sent for azureMonitor metric // queries like "BlobType", "eq", "*" type AzureMonitorDimensionFilter struct { - Dimension string `json:"dimension"` - Operator string `json:"operator"` - Filter string `json:"filter"` + Dimension string `json:"dimension"` + Operator string `json:"operator"` + Filters []string `json:"filters,omitempty"` + // Deprecated: To support multiselection, filters are passed in a slice now. Also migrated in frontend. + Filter *string `json:"filter,omitempty"` } -func (a AzureMonitorDimensionFilter) String() string { - filter := "*" - if a.Filter != "" { - filter = a.Filter +func (a AzureMonitorDimensionFilter) ConstructFiltersString() string { + var filterStrings []string + for _, filter := range a.Filters { + filterStrings = append(filterStrings, fmt.Sprintf("%v %v '%v'", a.Dimension, a.Operator, filter)) + } + if a.Operator == "eq" { + return strings.Join(filterStrings, " or ") + } else { + return strings.Join(filterStrings, " and ") } - return fmt.Sprintf("%v %v '%v'", a.Dimension, a.Operator, filter) } // LogJSONQuery is the frontend JSON query model for an Azure Log Analytics query. diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts index fbc474e63ac..6dd613faaef 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts @@ -105,11 +105,11 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend f.dimension && f.dimension !== 'None') .map((f) => { - const filter = templateSrv.replace(f.filter ?? '', scopedVars); + const filters = f.filters?.map((filter) => templateSrv.replace(filter ?? '', scopedVars)); return { dimension: templateSrv.replace(f.dimension, scopedVars), operator: f.operator || 'eq', - filter: filter || '*', // send * when empty + filters: filters || [], }; }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/DimensionFields.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/DimensionFields.test.tsx index b26f3cde2b9..b4c6e16cf31 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/DimensionFields.test.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/DimensionFields.test.tsx @@ -1,6 +1,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; +import { openMenu } from 'react-select-event'; import { selectOptionInTest } from '@grafana/ui'; @@ -18,17 +19,17 @@ const variableOptionGroup = { const user = userEvent.setup(); describe('Azure Monitor QueryEditor', () => { - const mockPanelData = createMockPanelData(); const mockDatasource = createMockDatasource(); it('should render a dimension filter', async () => { let mockQuery = createMockQuery(); + const mockPanelData = createMockPanelData(); const onQueryChange = jest.fn(); const dimensionOptions = [ { label: 'Test Dimension 1', value: 'TestDimension1' }, { label: 'Test Dimension 2', value: 'TestDimension2' }, ]; - render( + const { rerender } = render( { mockQuery = appendDimensionFilter(mockQuery); expect(onQueryChange).toHaveBeenCalledWith({ ...mockQuery, - azureMonitor: { ...mockQuery.azureMonitor, dimensionFilters: [{ dimension: '', operator: 'eq', filter: '*' }] }, + azureMonitor: { + ...mockQuery.azureMonitor, + dimensionFilters: [{ dimension: '', operator: 'eq', filters: [] }], + }, }); - render( + rerender( { ...mockQuery, azureMonitor: { ...mockQuery.azureMonitor, - dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filter: '*' }], + dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: [] }], }, }); expect(screen.queryByText('Test Dimension 1')).toBeInTheDocument(); @@ -74,16 +78,17 @@ describe('Azure Monitor QueryEditor', () => { it('correctly filters out dimensions when selected', async () => { let mockQuery = createMockQuery(); + const mockPanelData = createMockPanelData(); mockQuery.azureMonitor = { ...mockQuery.azureMonitor, - dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filter: '*' }], + dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: [] }], }; const onQueryChange = jest.fn(); const dimensionOptions = [ { label: 'Test Dimension 1', value: 'TestDimension1' }, { label: 'Test Dimension 2', value: 'TestDimension2' }, ]; - render( + const { rerender } = render( { const addDimension = await screen.findByText('Add new dimension'); await user.click(addDimension); mockQuery = appendDimensionFilter(mockQuery); - render( + rerender( { it('correctly displays dimension labels', async () => { let mockQuery = createMockQuery(); + const mockPanelData = createMockPanelData(); mockQuery.azureMonitor = { ...mockQuery.azureMonitor, - dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filter: '*' }], + dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: [] }], }; mockPanelData.series = [ @@ -150,7 +156,7 @@ describe('Azure Monitor QueryEditor', () => { dimensionOptions={dimensionOptions} /> ); - const labelSelect = await screen.findByText('Select value'); + const labelSelect = await screen.findByText('Select value(s)'); await user.click(labelSelect); const options = await screen.findAllByLabelText('Select option'); expect(options).toHaveLength(1); @@ -159,9 +165,10 @@ describe('Azure Monitor QueryEditor', () => { it('correctly updates dimension labels', async () => { let mockQuery = createMockQuery(); + const mockPanelData = createMockPanelData(); mockQuery.azureMonitor = { ...mockQuery.azureMonitor, - dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filter: 'testlabel' }], + dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: ['testlabel'] }], }; mockPanelData.series = [ @@ -178,7 +185,7 @@ describe('Azure Monitor QueryEditor', () => { ]; const onQueryChange = jest.fn(); const dimensionOptions = [{ label: 'Test Dimension 1', value: 'TestDimension1' }]; - render( + const { rerender } = render( { /> ); await screen.findByText('testlabel'); - const labelClear = await screen.findByLabelText('select-clear-value'); + const labelClear = await screen.findByLabelText('Remove testlabel'); await user.click(labelClear); - mockQuery = setDimensionFilterValue(mockQuery, 0, 'filter', ''); + mockQuery = setDimensionFilterValue(mockQuery, 0, 'filters', []); expect(onQueryChange).toHaveBeenCalledWith({ ...mockQuery, azureMonitor: { ...mockQuery.azureMonitor, - dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filter: '' }], + dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: [] }], }, }); mockPanelData.series = [ @@ -214,7 +221,7 @@ describe('Azure Monitor QueryEditor', () => { ], }, ]; - render( + rerender( { dimensionOptions={dimensionOptions} /> ); - const labelSelect = await screen.findByText('Select value'); - await user.click(labelSelect); + const labelSelect = await screen.getByLabelText('dimension-labels-select'); + await openMenu(labelSelect); const options = await screen.findAllByLabelText('Select option'); expect(options).toHaveLength(2); expect(options[0]).toHaveTextContent('testlabel'); expect(options[1]).toHaveTextContent('testlabel2'); }); + + it('correctly selects multiple dimension labels', async () => { + let mockQuery = createMockQuery(); + const mockPanelData = createMockPanelData(); + mockPanelData.series = [ + { + ...mockPanelData.series[0], + fields: [ + { + ...mockPanelData.series[0].fields[0], + name: 'Test Dimension 1', + labels: { testdimension1: 'testlabel' }, + }, + ], + }, + { + ...mockPanelData.series[0], + fields: [ + { + ...mockPanelData.series[0].fields[0], + name: 'Test Dimension 1', + labels: { testdimension1: 'testlabel2' }, + }, + ], + }, + ]; + const onQueryChange = jest.fn(); + const dimensionOptions = [{ label: 'Test Dimension 1', value: 'TestDimension1' }]; + mockQuery = appendDimensionFilter(mockQuery, 'TestDimension1'); + const { rerender } = render( + {}} + dimensionOptions={dimensionOptions} + /> + ); + const labelSelect = await screen.getByLabelText('dimension-labels-select'); + await user.click(labelSelect); + await openMenu(labelSelect); + await screen.getByText('testlabel'); + await screen.getByText('testlabel2'); + await selectOptionInTest(labelSelect, 'testlabel'); + mockQuery = setDimensionFilterValue(mockQuery, 0, 'filters', ['testlabel']); + expect(onQueryChange).toHaveBeenCalledWith({ + ...mockQuery, + azureMonitor: { + ...mockQuery.azureMonitor, + dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: ['testlabel'] }], + }, + }); + mockPanelData.series = [ + { + ...mockPanelData.series[0], + fields: [ + { + ...mockPanelData.series[0].fields[0], + name: 'Test Dimension 1', + labels: { testdimension1: 'testlabel' }, + }, + ], + }, + ]; + rerender( + {}} + dimensionOptions={dimensionOptions} + /> + ); + const labelSelect2 = await screen.getByLabelText('dimension-labels-select'); + await openMenu(labelSelect2); + const refreshedOptions = await screen.findAllByLabelText('Select options menu'); + expect(refreshedOptions).toHaveLength(1); + expect(refreshedOptions[0]).toHaveTextContent('testlabel2'); + await selectOptionInTest(labelSelect2, 'testlabel2'); + mockQuery = setDimensionFilterValue(mockQuery, 0, 'filters', ['testlabel', 'testlabel2']); + expect(onQueryChange).toHaveBeenCalledWith({ + ...mockQuery, + azureMonitor: { + ...mockQuery.azureMonitor, + dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: ['testlabel', 'testlabel2'] }], + }, + }); + mockPanelData.series = [ + { + ...mockPanelData.series[0], + fields: [ + { + ...mockPanelData.series[0].fields[0], + name: 'Test Dimension 1', + labels: { testdimension1: 'testlabel' }, + }, + ], + }, + { + ...mockPanelData.series[0], + fields: [ + { + ...mockPanelData.series[0].fields[0], + name: 'Test Dimension 1', + labels: { testdimension1: 'testlabel2' }, + }, + ], + }, + ]; + }); }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/DimensionFields.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/DimensionFields.tsx index e7027918ddc..110fdcc2837 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/DimensionFields.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/DimensionFields.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { SelectableValue, DataFrame, PanelData } from '@grafana/data'; -import { Button, Select, HorizontalGroup, VerticalGroup } from '@grafana/ui'; +import { Button, Select, HorizontalGroup, VerticalGroup, MultiSelect } from '@grafana/ui'; import { AzureMetricDimension, AzureMonitorOption, AzureMonitorQuery, AzureQueryEditorFieldProps } from '../../types'; import { Field } from '../Field'; @@ -44,7 +44,11 @@ const useDimensionLabels = (data: PanelData | undefined, query: AzureMonitorQuer } setDimensionLabels((prevLabels) => { const newLabels: DimensionLabels = {}; - for (const label of Object.keys(labelsObj)) { + const currentLabels = Object.keys(labelsObj); + if (currentLabels.length === 0) { + return prevLabels; + } + for (const label of currentLabels) { if (prevLabels[label] && labelsObj[label].size < prevLabels[label].size) { newLabels[label] = prevLabels[label]; } else { @@ -100,7 +104,7 @@ const DimensionFields: React.FC = ({ data, query, dimensio }; const onFilterInputChange = (index: number, v: SelectableValue | null) => { - onFieldChange(index, 'filter', v?.value ?? ''); + onFieldChange(index, 'filters', [v?.value ?? '']); }; const getValidDimensionOptions = (selectedDimension: string) => { @@ -118,6 +122,18 @@ const DimensionFields: React.FC = ({ data, query, dimensio })); }; + const getValidMultiSelectOptions = (selectedFilters: string[] | undefined, dimension: string) => { + const labelOptions = getValidFilterOptions(undefined, dimension); + if (selectedFilters) { + for (const filter of selectedFilters) { + if (!labelOptions.find((label) => label.value === filter)) { + labelOptions.push({ value: filter, label: filter }); + } + } + } + return labelOptions; + }; + const getValidOperators = (selectedOperator: string) => { if (dimensionOperators.find((operator: SelectableValue) => operator.value === selectedOperator)) { return dimensionOperators; @@ -125,6 +141,14 @@ const DimensionFields: React.FC = ({ data, query, dimensio return [...dimensionOperators, ...(selectedOperator ? [{ label: selectedOperator, value: selectedOperator }] : [])]; }; + const onMultiSelectFilterChange = (index: number, v: Array>) => { + onFieldChange( + index, + 'filters', + v.map((item) => item.value || '') + ); + }; + return ( @@ -145,16 +169,28 @@ const DimensionFields: React.FC = ({ data, query, dimensio onChange={(v) => onFieldChange(index, 'operator', v.value ?? '')} allowCustomValue /> - onFilterInputChange(index, v)} + isClearable + /> + )} + + + + + ) : null; +}; + +const getStyles = stylesFactory((theme: GrafanaTheme) => { + return { + modal: css` + width: 500px; + `, + content: css` + margin-bottom: ${theme.spacing.lg}; + `, + }; +}); diff --git a/public/app/features/search/page/components/SearchResultsTable.tsx b/public/app/features/search/page/components/SearchResultsTable.tsx index 22cc6b758f5..4b0733778d4 100644 --- a/public/app/features/search/page/components/SearchResultsTable.tsx +++ b/public/app/features/search/page/components/SearchResultsTable.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/jsx-no-undef */ import { css } from '@emotion/css'; import React, { useMemo } from 'react'; import { useTable, Column, TableOptions, Cell, useAbsoluteLayout } from 'react-table'; diff --git a/public/app/features/search/types.ts b/public/app/features/search/types.ts index 1ee78da5174..28619ba28a8 100644 --- a/public/app/features/search/types.ts +++ b/public/app/features/search/types.ts @@ -108,3 +108,7 @@ export interface SearchQueryParams { layout?: SearchLayout | null; folder?: string | null; } + +// new Search Types +export type OnDeleteSelectedItems = (folders: string[], dashboards: string[]) => void; +export type OnMoveSelectedItems = (selectedDashboards: string[], folder: FolderInfo | null) => void; From dc33e09b241bdb824fe767a2f19be04e2b94d379 Mon Sep 17 00:00:00 2001 From: Yuriy Tseretyan Date: Wed, 11 May 2022 09:44:31 -0400 Subject: [PATCH 121/440] simplify getting a slice of keys (#48889) --- pkg/services/ngalert/schedule/schedule.go | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/pkg/services/ngalert/schedule/schedule.go b/pkg/services/ngalert/schedule/schedule.go index 04ab92e7aff..23e0c97e61c 100644 --- a/pkg/services/ngalert/schedule/schedule.go +++ b/pkg/services/ngalert/schedule/schedule.go @@ -700,26 +700,11 @@ func (r *alertRuleRegistry) del(key models.AlertRuleKey) (*alertRuleInfo, bool) return info, ok } -func (r *alertRuleRegistry) iter() <-chan models.AlertRuleKey { - c := make(chan models.AlertRuleKey) - - f := func() { - r.mu.Lock() - defer r.mu.Unlock() - - for k := range r.alertRuleInfo { - c <- k - } - close(c) - } - go f() - - return c -} - func (r *alertRuleRegistry) keyMap() map[models.AlertRuleKey]struct{} { - definitionsIDs := make(map[models.AlertRuleKey]struct{}) - for k := range r.iter() { + r.mu.Lock() + defer r.mu.Unlock() + definitionsIDs := make(map[models.AlertRuleKey]struct{}, len(r.alertRuleInfo)) + for k := range r.alertRuleInfo { definitionsIDs[k] = struct{}{} } return definitionsIDs From 99156b40bd142f46349fb1f1d751f6feee9f87ff Mon Sep 17 00:00:00 2001 From: Yuriy Tseretyan Date: Wed, 11 May 2022 10:04:50 -0400 Subject: [PATCH 122/440] Alerting: Move alertRuleRegistry to its own file (#48890) * move alertRuleRegistry to its own file * move tests to separate file --- pkg/services/ngalert/schedule/registry.go | 113 +++++++++++++++++ .../ngalert/schedule/registry_test.go | 118 ++++++++++++++++++ pkg/services/ngalert/schedule/schedule.go | 103 --------------- .../ngalert/schedule/schedule_unit_test.go | 107 ---------------- 4 files changed, 231 insertions(+), 210 deletions(-) create mode 100644 pkg/services/ngalert/schedule/registry.go create mode 100644 pkg/services/ngalert/schedule/registry_test.go diff --git a/pkg/services/ngalert/schedule/registry.go b/pkg/services/ngalert/schedule/registry.go new file mode 100644 index 00000000000..1d550c82738 --- /dev/null +++ b/pkg/services/ngalert/schedule/registry.go @@ -0,0 +1,113 @@ +package schedule + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/grafana/grafana/pkg/services/ngalert/models" +) + +type alertRuleRegistry struct { + mu sync.Mutex + alertRuleInfo map[models.AlertRuleKey]*alertRuleInfo +} + +// getOrCreateInfo gets rule routine information from registry by the key. If it does not exist, it creates a new one. +// Returns a pointer to the rule routine information and a flag that indicates whether it is a new struct or not. +func (r *alertRuleRegistry) getOrCreateInfo(context context.Context, key models.AlertRuleKey) (*alertRuleInfo, bool) { + r.mu.Lock() + defer r.mu.Unlock() + + info, ok := r.alertRuleInfo[key] + if !ok { + info = newAlertRuleInfo(context) + r.alertRuleInfo[key] = info + } + return info, !ok +} + +// get returns the channel for the specific alert rule +// if the key does not exist returns an error +func (r *alertRuleRegistry) get(key models.AlertRuleKey) (*alertRuleInfo, error) { + r.mu.Lock() + defer r.mu.Unlock() + + info, ok := r.alertRuleInfo[key] + if !ok { + return nil, fmt.Errorf("%v key not found", key) + } + return info, nil +} + +func (r *alertRuleRegistry) exists(key models.AlertRuleKey) bool { + r.mu.Lock() + defer r.mu.Unlock() + + _, ok := r.alertRuleInfo[key] + return ok +} + +// del removes pair that has specific key from alertRuleInfo. +// Returns 2-tuple where the first element is value of the removed pair +// and the second element indicates whether element with the specified key existed. +func (r *alertRuleRegistry) del(key models.AlertRuleKey) (*alertRuleInfo, bool) { + r.mu.Lock() + defer r.mu.Unlock() + info, ok := r.alertRuleInfo[key] + if ok { + delete(r.alertRuleInfo, key) + } + return info, ok +} + +func (r *alertRuleRegistry) keyMap() map[models.AlertRuleKey]struct{} { + r.mu.Lock() + defer r.mu.Unlock() + definitionsIDs := make(map[models.AlertRuleKey]struct{}, len(r.alertRuleInfo)) + for k := range r.alertRuleInfo { + definitionsIDs[k] = struct{}{} + } + return definitionsIDs +} + +type alertRuleInfo struct { + evalCh chan *evaluation + updateCh chan struct{} + ctx context.Context + stop context.CancelFunc +} + +func newAlertRuleInfo(parent context.Context) *alertRuleInfo { + ctx, cancel := context.WithCancel(parent) + return &alertRuleInfo{evalCh: make(chan *evaluation), updateCh: make(chan struct{}), ctx: ctx, stop: cancel} +} + +// eval signals the rule evaluation routine to perform the evaluation of the rule. Does nothing if the loop is stopped +func (a *alertRuleInfo) eval(t time.Time, version int64) bool { + select { + case a.evalCh <- &evaluation{ + scheduledAt: t, + version: version, + }: + return true + case <-a.ctx.Done(): + return false + } +} + +// update signals the rule evaluation routine to update the internal state. Does nothing if the loop is stopped +func (a *alertRuleInfo) update() bool { + select { + case a.updateCh <- struct{}{}: + return true + case <-a.ctx.Done(): + return false + } +} + +type evaluation struct { + scheduledAt time.Time + version int64 +} diff --git a/pkg/services/ngalert/schedule/registry_test.go b/pkg/services/ngalert/schedule/registry_test.go new file mode 100644 index 00000000000..f5a2eb991de --- /dev/null +++ b/pkg/services/ngalert/schedule/registry_test.go @@ -0,0 +1,118 @@ +package schedule + +import ( + "context" + "math/rand" + "runtime" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestSchedule_alertRuleInfo(t *testing.T) { + t.Run("when rule evaluation is not stopped", func(t *testing.T) { + t.Run("Update should send to updateCh", func(t *testing.T) { + r := newAlertRuleInfo(context.Background()) + resultCh := make(chan bool) + go func() { + resultCh <- r.update() + }() + select { + case <-r.updateCh: + require.True(t, <-resultCh) + case <-time.After(5 * time.Second): + t.Fatal("No message was received on update channel") + } + }) + t.Run("eval should send to evalCh", func(t *testing.T) { + r := newAlertRuleInfo(context.Background()) + expected := time.Now() + resultCh := make(chan bool) + version := rand.Int63() + go func() { + resultCh <- r.eval(expected, version) + }() + select { + case ctx := <-r.evalCh: + require.Equal(t, version, ctx.version) + require.Equal(t, expected, ctx.scheduledAt) + require.True(t, <-resultCh) + case <-time.After(5 * time.Second): + t.Fatal("No message was received on eval channel") + } + }) + t.Run("eval should exit when context is cancelled", func(t *testing.T) { + r := newAlertRuleInfo(context.Background()) + resultCh := make(chan bool) + go func() { + resultCh <- r.eval(time.Now(), rand.Int63()) + }() + runtime.Gosched() + r.stop() + select { + case result := <-resultCh: + require.False(t, result) + case <-time.After(5 * time.Second): + t.Fatal("No message was received on eval channel") + } + }) + }) + t.Run("when rule evaluation is stopped", func(t *testing.T) { + t.Run("Update should do nothing", func(t *testing.T) { + r := newAlertRuleInfo(context.Background()) + r.stop() + require.False(t, r.update()) + }) + t.Run("eval should do nothing", func(t *testing.T) { + r := newAlertRuleInfo(context.Background()) + r.stop() + require.False(t, r.eval(time.Now(), rand.Int63())) + }) + t.Run("stop should do nothing", func(t *testing.T) { + r := newAlertRuleInfo(context.Background()) + r.stop() + r.stop() + }) + }) + t.Run("should be thread-safe", func(t *testing.T) { + r := newAlertRuleInfo(context.Background()) + wg := sync.WaitGroup{} + go func() { + for { + select { + case <-r.evalCh: + time.Sleep(time.Microsecond) + case <-r.updateCh: + time.Sleep(time.Microsecond) + case <-r.ctx.Done(): + return + } + } + }() + + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + for i := 0; i < 20; i++ { + max := 3 + if i <= 10 { + max = 2 + } + switch rand.Intn(max) + 1 { + case 1: + r.update() + case 2: + r.eval(time.Now(), rand.Int63()) + case 3: + r.stop() + } + } + wg.Done() + }() + } + + wg.Wait() + }) +} diff --git a/pkg/services/ngalert/schedule/schedule.go b/pkg/services/ngalert/schedule/schedule.go index 23e0c97e61c..f890e5c48c3 100644 --- a/pkg/services/ngalert/schedule/schedule.go +++ b/pkg/services/ngalert/schedule/schedule.go @@ -647,109 +647,6 @@ func (sch *schedule) saveAlertStates(ctx context.Context, states []*state.State) } } -type alertRuleRegistry struct { - mu sync.Mutex - alertRuleInfo map[models.AlertRuleKey]*alertRuleInfo -} - -// getOrCreateInfo gets rule routine information from registry by the key. If it does not exist, it creates a new one. -// Returns a pointer to the rule routine information and a flag that indicates whether it is a new struct or not. -func (r *alertRuleRegistry) getOrCreateInfo(context context.Context, key models.AlertRuleKey) (*alertRuleInfo, bool) { - r.mu.Lock() - defer r.mu.Unlock() - - info, ok := r.alertRuleInfo[key] - if !ok { - info = newAlertRuleInfo(context) - r.alertRuleInfo[key] = info - } - return info, !ok -} - -// get returns the channel for the specific alert rule -// if the key does not exist returns an error -func (r *alertRuleRegistry) get(key models.AlertRuleKey) (*alertRuleInfo, error) { - r.mu.Lock() - defer r.mu.Unlock() - - info, ok := r.alertRuleInfo[key] - if !ok { - return nil, fmt.Errorf("%v key not found", key) - } - return info, nil -} - -func (r *alertRuleRegistry) exists(key models.AlertRuleKey) bool { - r.mu.Lock() - defer r.mu.Unlock() - - _, ok := r.alertRuleInfo[key] - return ok -} - -// del removes pair that has specific key from alertRuleInfo. -// Returns 2-tuple where the first element is value of the removed pair -// and the second element indicates whether element with the specified key existed. -func (r *alertRuleRegistry) del(key models.AlertRuleKey) (*alertRuleInfo, bool) { - r.mu.Lock() - defer r.mu.Unlock() - info, ok := r.alertRuleInfo[key] - if ok { - delete(r.alertRuleInfo, key) - } - return info, ok -} - -func (r *alertRuleRegistry) keyMap() map[models.AlertRuleKey]struct{} { - r.mu.Lock() - defer r.mu.Unlock() - definitionsIDs := make(map[models.AlertRuleKey]struct{}, len(r.alertRuleInfo)) - for k := range r.alertRuleInfo { - definitionsIDs[k] = struct{}{} - } - return definitionsIDs -} - -type alertRuleInfo struct { - evalCh chan *evaluation - updateCh chan struct{} - ctx context.Context - stop context.CancelFunc -} - -func newAlertRuleInfo(parent context.Context) *alertRuleInfo { - ctx, cancel := context.WithCancel(parent) - return &alertRuleInfo{evalCh: make(chan *evaluation), updateCh: make(chan struct{}), ctx: ctx, stop: cancel} -} - -// eval signals the rule evaluation routine to perform the evaluation of the rule. Does nothing if the loop is stopped -func (a *alertRuleInfo) eval(t time.Time, version int64) bool { - select { - case a.evalCh <- &evaluation{ - scheduledAt: t, - version: version, - }: - return true - case <-a.ctx.Done(): - return false - } -} - -// update signals the rule evaluation routine to update the internal state. Does nothing if the loop is stopped -func (a *alertRuleInfo) update() bool { - select { - case a.updateCh <- struct{}{}: - return true - case <-a.ctx.Done(): - return false - } -} - -type evaluation struct { - scheduledAt time.Time - version int64 -} - // overrideCfg is only used on tests. func (sch *schedule) overrideCfg(cfg SchedulerCfg) { sch.clock = cfg.C diff --git a/pkg/services/ngalert/schedule/schedule_unit_test.go b/pkg/services/ngalert/schedule/schedule_unit_test.go index 5882a49dc12..5935921a9d0 100644 --- a/pkg/services/ngalert/schedule/schedule_unit_test.go +++ b/pkg/services/ngalert/schedule/schedule_unit_test.go @@ -8,7 +8,6 @@ import ( "fmt" "math/rand" "net/url" - "runtime" "sync" "testing" "time" @@ -817,112 +816,6 @@ func TestSchedule_ruleRoutine(t *testing.T) { }) } -func TestSchedule_alertRuleInfo(t *testing.T) { - t.Run("when rule evaluation is not stopped", func(t *testing.T) { - t.Run("Update should send to updateCh", func(t *testing.T) { - r := newAlertRuleInfo(context.Background()) - resultCh := make(chan bool) - go func() { - resultCh <- r.update() - }() - select { - case <-r.updateCh: - require.True(t, <-resultCh) - case <-time.After(5 * time.Second): - t.Fatal("No message was received on update channel") - } - }) - t.Run("eval should send to evalCh", func(t *testing.T) { - r := newAlertRuleInfo(context.Background()) - expected := time.Now() - resultCh := make(chan bool) - version := rand.Int63() - go func() { - resultCh <- r.eval(expected, version) - }() - select { - case ctx := <-r.evalCh: - require.Equal(t, version, ctx.version) - require.Equal(t, expected, ctx.scheduledAt) - require.True(t, <-resultCh) - case <-time.After(5 * time.Second): - t.Fatal("No message was received on eval channel") - } - }) - t.Run("eval should exit when context is cancelled", func(t *testing.T) { - r := newAlertRuleInfo(context.Background()) - resultCh := make(chan bool) - go func() { - resultCh <- r.eval(time.Now(), rand.Int63()) - }() - runtime.Gosched() - r.stop() - select { - case result := <-resultCh: - require.False(t, result) - case <-time.After(5 * time.Second): - t.Fatal("No message was received on eval channel") - } - }) - }) - t.Run("when rule evaluation is stopped", func(t *testing.T) { - t.Run("Update should do nothing", func(t *testing.T) { - r := newAlertRuleInfo(context.Background()) - r.stop() - require.False(t, r.update()) - }) - t.Run("eval should do nothing", func(t *testing.T) { - r := newAlertRuleInfo(context.Background()) - r.stop() - require.False(t, r.eval(time.Now(), rand.Int63())) - }) - t.Run("stop should do nothing", func(t *testing.T) { - r := newAlertRuleInfo(context.Background()) - r.stop() - r.stop() - }) - }) - t.Run("should be thread-safe", func(t *testing.T) { - r := newAlertRuleInfo(context.Background()) - wg := sync.WaitGroup{} - go func() { - for { - select { - case <-r.evalCh: - time.Sleep(time.Microsecond) - case <-r.updateCh: - time.Sleep(time.Microsecond) - case <-r.ctx.Done(): - return - } - } - }() - - for i := 0; i < 10; i++ { - wg.Add(1) - go func() { - for i := 0; i < 20; i++ { - max := 3 - if i <= 10 { - max = 2 - } - switch rand.Intn(max) + 1 { - case 1: - r.update() - case 2: - r.eval(time.Now(), rand.Int63()) - case 3: - r.stop() - } - } - wg.Done() - }() - } - - wg.Wait() - }) -} - func TestSchedule_UpdateAlertRule(t *testing.T) { t.Run("when rule exists", func(t *testing.T) { t.Run("it should call Update", func(t *testing.T) { From 233a96d818a1e894d00db8ff28fc9bc6fe4b3f7f Mon Sep 17 00:00:00 2001 From: Connor Lindsey Date: Wed, 11 May 2022 07:39:02 -0700 Subject: [PATCH 123/440] Tracing: Add ability to write trace to metrics query (#48824) * Tracing: Add ability to write trace to metrics query --- docs/sources/datasources/jaeger.md | 9 +++++ docs/sources/datasources/tempo.md | 9 +++++ docs/sources/datasources/zipkin.md | 9 +++++ .../TraceToMetrics/TraceToMetricsSettings.tsx | 33 ++++++++++++++++--- .../explore/TraceView/createSpanLink.test.ts | 23 +++++++++++++ .../explore/TraceView/createSpanLink.tsx | 3 +- 6 files changed, 80 insertions(+), 6 deletions(-) diff --git a/docs/sources/datasources/jaeger.md b/docs/sources/datasources/jaeger.md index a740f617744..58579bb194f 100644 --- a/docs/sources/datasources/jaeger.md +++ b/docs/sources/datasources/jaeger.md @@ -40,6 +40,15 @@ This is a configuration for the [trace to logs feature]({{< relref "../explore/t ![Trace to logs settings](/static/img/docs/explore/trace-to-logs-settings-8-2.png 'Screenshot of the trace to logs settings') +### Trace to metrics + +> **Note:** This feature is behind the `traceToMetrics` feature toggle. + +To configure trace to metrics, select the target Prometheus data source and enter the desired query. + +-- **Data source -** Target data source. +-- **Query -** Query that runs when navigating from a trace to the metrics data source. + ### Node Graph This is a configuration for the beta Node Graph visualization. The Node Graph is shown after the trace view is loaded and is disabled by default. diff --git a/docs/sources/datasources/tempo.md b/docs/sources/datasources/tempo.md index be78cf56908..bc0afeb7f90 100644 --- a/docs/sources/datasources/tempo.md +++ b/docs/sources/datasources/tempo.md @@ -39,6 +39,15 @@ This is a configuration for the [trace to logs feature]({{< relref "../explore/t {{< figure src="/static/img/docs/explore/traces-to-logs-settings-8-2.png" class="docs-image--no-shadow" caption="Screenshot of the trace to logs settings" >}} +### Trace to metrics + +> **Note:** This feature is behind the `traceToMetrics` feature toggle. + +To configure trace to metrics, select the target Prometheus data source and enter the desired query. + +-- **Data source -** Target data source. +-- **Query -** Query that runs when navigating from a trace to the metrics data source. + ### Service Graph This is a configuration for the Service Graph feature. diff --git a/docs/sources/datasources/zipkin.md b/docs/sources/datasources/zipkin.md index 867952bfb91..8d9766a73c9 100644 --- a/docs/sources/datasources/zipkin.md +++ b/docs/sources/datasources/zipkin.md @@ -40,6 +40,15 @@ This is a configuration for the [trace to logs feature]({{< relref "../explore/t ![Trace to logs settings](/static/img/docs/explore/trace-to-logs-settings-8-2.png 'Screenshot of the trace to logs settings') +### Trace to metrics + +> **Note:** This feature is behind the `traceToMetrics` feature toggle. + +To configure trace to metrics, select the target Prometheus data source and enter the desired query. + +-- **Data source -** Target data source. +-- **Query -** Query that runs when navigating from a trace to the metrics data source. + ### Node Graph This is a configuration for the beta Node Graph visualization. The Node Graph is shown after the trace view is loaded and is disabled by default. diff --git a/public/app/core/components/TraceToMetrics/TraceToMetricsSettings.tsx b/public/app/core/components/TraceToMetrics/TraceToMetricsSettings.tsx index fc450315580..4a0d5733f7c 100644 --- a/public/app/core/components/TraceToMetrics/TraceToMetricsSettings.tsx +++ b/public/app/core/components/TraceToMetrics/TraceToMetricsSettings.tsx @@ -8,10 +8,11 @@ import { updateDatasourcePluginJsonDataOption, } from '@grafana/data'; import { DataSourcePicker } from '@grafana/runtime'; -import { Button, InlineField, InlineFieldRow, useStyles } from '@grafana/ui'; +import { Button, InlineField, InlineFieldRow, Input, useStyles } from '@grafana/ui'; export interface TraceToMetricsOptions { datasourceUid?: string; + query: string; } export interface TraceToMetricsData extends DataSourceJsonData { @@ -49,10 +50,10 @@ export function TraceToMetricsSettings({ options, onOptionsChange }: Props) { {options.jsonData.tracesToMetrics?.datasourceUid ? ( ) : null} + + + + { + updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToMetrics', { + ...options.jsonData.tracesToMetrics, + query: e.currentTarget.value, + }); + }} + /> + +
    ); } diff --git a/public/app/features/explore/TraceView/createSpanLink.test.ts b/public/app/features/explore/TraceView/createSpanLink.test.ts index 89d2e05c16a..2145c2ee331 100644 --- a/public/app/features/explore/TraceView/createSpanLink.test.ts +++ b/public/app/features/explore/TraceView/createSpanLink.test.ts @@ -1,6 +1,7 @@ import { DataSourceInstanceSettings, MutableDataFrame } from '@grafana/data'; import { setDataSourceSrv, setTemplateSrv } from '@grafana/runtime'; import { TraceSpan } from '@jaegertracing/jaeger-ui-components'; +import { TraceToMetricsOptions } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; import { TraceToLogsOptions } from '../../../core/components/TraceToLogs/TraceToLogsSettings'; @@ -399,6 +400,7 @@ describe('createSpanLinkFactory', () => { splitOpenFn, traceToMetricsOptions: { datasourceUid: 'prom1', + query: 'customQuery', }, }); expect(createLink).toBeDefined(); @@ -406,6 +408,27 @@ describe('createSpanLinkFactory', () => { const links = createLink!(createTraceSpan()); const linkDef = links?.metricLinks?.[0]; + expect(linkDef).toBeDefined(); + expect(linkDef!.href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"prom1","queries":[{"expr":"customQuery","refId":""}],"panelsState":{}}' + )}` + ); + }); + + it('uses default query if no query specified', () => { + const splitOpenFn = jest.fn(); + const createLink = createSpanLinkFactory({ + splitOpenFn, + traceToMetricsOptions: { + datasourceUid: 'prom1', + } as TraceToMetricsOptions, + }); + expect(createLink).toBeDefined(); + + const links = createLink!(createTraceSpan()); + const linkDef = links?.metricLinks?.[0]; + expect(linkDef).toBeDefined(); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( diff --git a/public/app/features/explore/TraceView/createSpanLink.tsx b/public/app/features/explore/TraceView/createSpanLink.tsx index 9eb6e5f075a..4681390b80b 100644 --- a/public/app/features/explore/TraceView/createSpanLink.tsx +++ b/public/app/features/explore/TraceView/createSpanLink.tsx @@ -150,6 +150,7 @@ function legacyCreateSpanLinkFactory( // Get metrics links if (metricsDataSourceSettings && traceToMetricsOptions) { + const defaultQuery = `histogram_quantile(0.5, sum(rate(tempo_spanmetrics_latency_bucket{operation="${span.operationName}"}[5m])) by (le))`; const dataLink: DataLink = { title: metricsDataSourceSettings.name, url: '', @@ -157,7 +158,7 @@ function legacyCreateSpanLinkFactory( datasourceUid: metricsDataSourceSettings.uid, datasourceName: metricsDataSourceSettings.name, query: { - expr: `histogram_quantile(0.5, sum(rate(tempo_spanmetrics_latency_bucket{operation="${span.operationName}"}[5m])) by (le))`, + expr: traceToMetricsOptions.query ?? defaultQuery, refId: '', }, }, From d31d300ce170b1c4464335b442c942989591ff56 Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Wed, 11 May 2022 17:22:43 +0200 Subject: [PATCH 124/440] Accesscontrol: Rename scope permissions:delegate (#48898) --- .../custom-role-actions-scopes.md | 18 +-- .../access-control/manage-rbac-roles.md | 2 +- docs/sources/http_api/access_control.md | 104 +++++++++--------- pkg/services/accesscontrol/evaluator_test.go | 2 +- public/api-merged.json | 22 ++-- public/api-spec.json | 22 ++-- 6 files changed, 85 insertions(+), 85 deletions(-) diff --git a/docs/sources/enterprise/access-control/custom-role-actions-scopes.md b/docs/sources/enterprise/access-control/custom-role-actions-scopes.md index 6aaaec62969..edcf3272db4 100644 --- a/docs/sources/enterprise/access-control/custom-role-actions-scopes.md +++ b/docs/sources/enterprise/access-control/custom-role-actions-scopes.md @@ -94,22 +94,22 @@ The following list contains role-based access control actions. | `reports:delete` | `reports:*`
    `reports:id:*` | Delete reports. | | `reports:read` | `reports:*` | List all available reports or get a specific report. | | `reports:send` | `reports:*` | Send a report email. | -| `roles.builtin:add` | `permissions:delegate` | Create a built-in role assignment. | +| `roles.builtin:add` | `permissions:type:delegate` | Create a built-in role assignment. | | `roles.builtin:list` | `roles:*` | List built-in role assignments. | -| `roles.builtin:remove` | `permissions:delegate` | Delete a built-in role assignment. | -| `roles:delete` | `permissions:delegate` | Delete a custom role. | +| `roles.builtin:remove` | `permissions:type:delegate` | Delete a built-in role assignment. | +| `roles:delete` | `permissions:type:delegate` | Delete a custom role. | | `roles:list` | `roles:*` | List available roles without permissions. | | `roles:read` | `roles:*`
    `roles:uid:*` | Read a specific role with its permissions. | -| `roles:write` | `permissions:delegate` | Create or update a custom role. | +| `roles:write` | `permissions:type:delegate` | Create or update a custom role. | | `server.stats:read` | n/a | Read Grafana instance statistics. | | `settings:read` | `settings:*`
    `settings:auth.saml:*`
    `settings:auth.saml:enabled` (property level) | Read the [Grafana configuration settings]({{< relref "../../administration/configuration/_index.md" >}}) | | `settings:write` | `settings:*`
    `settings:auth.saml:*`
    `settings:auth.saml:enabled` (property level) | Update any Grafana configuration settings that can be [updated at runtime]({{< relref "../../enterprise/settings-updates/_index.md" >}}). | | `status:accesscontrol` | `services:accesscontrol` | Get access-control enabled status. | | `teams.permissions:read` | `teams:*`
    `teams:id:*` | Read members and External Group Synchronization setup for teams. | | `teams.permissions:write` | `teams:*`
    `teams:id:*` | Add, remove and update members and manage External Group Synchronization setup for teams. | -| `teams.roles:add` | `permissions:delegate` | Assign a role to a team. | +| `teams.roles:add` | `permissions:type:delegate` | Assign a role to a team. | | `teams.roles:list` | `teams:*` | List roles assigned directly to a team. | -| `teams.roles:remove` | `permissions:delegate` | Unassign a role from a team. | +| `teams.roles:remove` | `permissions:type:delegate` | Unassign a role from a team. | | `teams:create` | n/a | Create teams. | | `teams:delete` | `teams:*`
    `teams:id:*` | Delete one or more teams. | | `teams:read` | `teams:*`
    `teams:id:*` | Read one or more teams and team preferences. | @@ -121,9 +121,9 @@ The following list contains role-based access control actions. | `users.permissions:update` | `global.users:*`
    `global.users:id:*` | Update a user’s organization-level permissions. | | `users.quotas:list` | `global.users:*`
    `global.users:id:*` | List a user’s quotas. | | `users.quotas:update` | `global.users:*`
    `global.users:id:*` | Update a user’s quotas. | -| `users.roles:add` | `permissions:delegate` | Assign a role to a user. | +| `users.roles:add` | `permissions:type:delegate` | Assign a role to a user. | | `users.roles:list` | `users:*` | List roles assigned directly to a user. | -| `users.roles:remove` | `permissions:delegate` | Unassign a role from a user. | +| `users.roles:remove` | `permissions:type:delegate` | Unassign a role from a user. | | `users.teams:read` | `global.users:*`
    `global.users:id:*` | Read a user’s teams. | | `users:create` | n/a | Create a user. | | `users:delete` | `global.users:*`
    `global.users:id:*` | Delete a user. | @@ -146,7 +146,7 @@ The following list contains role-based access control scopes. | `folders:*`
    `folders:uid:*` | Restrict an action to a set of folders. For example, `folders:*` matches any folder, and `folders:uid:1` matches the folder whose UID is `1`. | | `global.users:*`
    `global.users:id:*` | Restrict an action to a set of global users. For example, `global.users:*` matches any user and `global.users:id:1` matches the user whose ID is `1`. | | `orgs:*`
    `orgs:id:*` | Restrict an action to a set of organizations. For example, `orgs:*` matches any organization and `orgs:id:1` matches the organization whose ID is `1`. | -| `permissions:delegate` | The scope is only applicable for roles associated with the Access Control itself and indicates that you can delegate your permissions only, or a subset of it, by creating a new role or making an assignment. | +| `permissions:type:delegate` | The scope is only applicable for roles associated with the Access Control itself and indicates that you can delegate your permissions only, or a subset of it, by creating a new role or making an assignment. | | `provisioners:*` | Restrict an action to a set of provisioners. For example, `provisioners:*` matches any provisioner, and `provisioners:accesscontrol` matches the role-based access control [provisioner]({{< relref "./custom-role-actions-scopes" >}}). | | `reports:*`
    `reports:id:*` | Restrict an action to a set of reports. For example, `reports:*` matches any report and `reports:id:1` matches the report whose ID is `1`. | | `roles:*`
    `roles:uid:*` | Restrict an action to a set of roles. For example, `roles:*` matches any role and `roles:uid:randomuid` matches only the role whose UID is `randomuid`. | diff --git a/docs/sources/enterprise/access-control/manage-rbac-roles.md b/docs/sources/enterprise/access-control/manage-rbac-roles.md index 39d1968201e..9d18e05f457 100644 --- a/docs/sources/enterprise/access-control/manage-rbac-roles.md +++ b/docs/sources/enterprise/access-control/manage-rbac-roles.md @@ -152,7 +152,7 @@ Create a custom role when basic roles and fixed roles do not meet your permissio - [Enable role provisioning]({{< relref "./enable-rbac-and-provisioning#enable-rbac" >}}). - Ensure that you have permissions to create a custom role. - By default, the Grafana Admin role has permission to create custom roles. - - A Grafana Admin can delegate the custom role privilege to another user by creating a custom role with the relevant permissions and adding the `permissions:delegate` scope. + - A Grafana Admin can delegate the custom role privilege to another user by creating a custom role with the relevant permissions and adding the `permissions:type:delegate` scope. ### Create custom roles using provisioning diff --git a/docs/sources/http_api/access_control.md b/docs/sources/http_api/access_control.md index bd32d8355c2..a9ff4c9eab7 100644 --- a/docs/sources/http_api/access_control.md +++ b/docs/sources/http_api/access_control.md @@ -219,12 +219,12 @@ Creates a new custom role and maps given permissions to that role. Note that rol #### Required permissions -`permission:delegate` scope ensures that users can only create custom roles with the same, or a subset of permissions which the user has. +`permissions:type:delegate` scope ensures that users can only create custom roles with the same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won't be able to create a custom role which allows to do that. This is done to prevent escalation of privileges. -| Action | Scope | -| ----------- | -------------------- | -| roles:write | permissions:delegate | +| Action | Scope | +| ----------- | ------------------------- | +| roles:write | permissions:type:delegate | #### Example request @@ -245,7 +245,7 @@ Content-Type: application/json "permissions": [ { "action": "roles:delete", - "scope": "permissions:delegate" + "scope": "permissions:type:delegate" } ] } @@ -290,7 +290,7 @@ Content-Type: application/json; charset=UTF-8 "permissions": [ { "action": "roles:delete", - "scope": "permissions:delegate", + "scope": "permissions:type:delegate", "updated": "2021-05-13T23:19:46+02:00", "created": "2021-05-13T23:19:46+02:00" } @@ -317,12 +317,12 @@ Update the role with the given UID, and it's permissions with the given UID. The #### Required permissions -`permission:delegate` scope ensures that users can only update custom roles with the same, or a subset of permissions which the user has. +`permissions:type:delegate` scope ensures that users can only update custom roles with the same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won't be able to update a custom role which allows to do that. This is done to prevent escalation of privileges. -| Action | Scope | -| ----------- | -------------------- | -| roles:write | permissions:delegate | +| Action | Scope | +| ----------- | ------------------------- | +| roles:write | permissions:type:delegate | #### Example request @@ -342,11 +342,11 @@ Content-Type: application/json "permissions": [ { "action": "roles:delete", - "scope": "permissions:delegate" + "scope": "permissions:type:delegate" }, { "action": "roles:write", - "scope": "permissions:delegate" + "scope": "permissions:type:delegate" } ] } @@ -388,13 +388,13 @@ Content-Type: application/json; charset=UTF-8 "permissions":[ { "action":"roles:delete", - "scope":"permissions:delegate", + "scope":"permissions:type:delegate", "updated":"2021-08-06T18:27:40+02:00", "created":"2021-08-06T18:27:40+02:00" }, { "action":"roles:write", - "scope":"permissions:delegate", + "scope":"permissions:type:delegate", "updated":"2021-08-06T18:27:41+02:00", "created":"2021-08-06T18:27:41+02:00" } @@ -423,12 +423,12 @@ Delete a role with the given UID, and it's permissions. If the role is assigned #### Required permissions -`permission:delegate` scope ensures that users can only delete a custom role with the same, or a subset of permissions which the user has. +`permissions:type:delegate` scope ensures that users can only delete a custom role with the same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won't be able to delete a custom role which allows to do that. -| Action | Scope | -| ------------ | -------------------- | -| roles:delete | permissions:delegate | +| Action | Scope | +| ------------ | ------------------------- | +| roles:delete | permissions:type:delegate | #### Example request @@ -574,12 +574,12 @@ For bulk updates consider #### Required permissions -`permission:delegate` scope ensures that users can only assign roles which have same, or a subset of permissions which the user has. +`permissions:type:delegate` scope ensures that users can only assign roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won't be able to assign a role which will allow to do that. This is done to prevent escalation of privileges. -| Action | Scope | -| --------------- | -------------------- | -| users.roles:add | permissions:delegate | +| Action | Scope | +| --------------- | ------------------------- | +| users.roles:add | permissions:type:delegate | #### Example request @@ -632,12 +632,12 @@ For bulk updates consider #### Required permissions -`permission:delegate` scope ensures that users can only unassign roles which have same, or a subset of permissions which the user has. +`permissions:type:delegate` scope ensures that users can only unassign roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won't be able to unassign a role which will allow to do that. This is done to prevent escalation of privileges. -| Action | Scope | -| ------------------ | -------------------- | -| users.roles:remove | permissions:delegate | +| Action | Scope | +| ------------------ | ------------------------- | +| users.roles:remove | permissions:type:delegate | #### Query parameters @@ -686,13 +686,13 @@ instead. #### Required permissions -`permission:delegate` scope ensures that users can only assign or unassign roles which have same, or a subset of permissions which the user has. +`permissions:type:delegate` scope ensures that users can only assign or unassign roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won't be able to assign or unassign a role which will allow to do that. This is done to prevent escalation of privileges. -| Action | Scope | -| ------------------ | -------------------- | -| users.roles:add | permissions:delegate | -| users.roles:remove | permissions:delegate | +| Action | Scope | +| ------------------ | ------------------------- | +| users.roles:add | permissions:type:delegate | +| users.roles:remove | permissions:type:delegate | #### Example request @@ -802,12 +802,12 @@ For bulk updates consider [Set team role assignments]({{< ref "#set-team-role-as #### Required permissions -`permission:delegate` scope ensures that users can only assign roles which have same, or a subset of permissions which the user has. +`permissions:type:delegate` scope ensures that users can only assign roles which have same, or a subset of permissions which the user has. For example, if a user does not have the permissions required to create users, they won't be able to assign a role that contains these permissions. This is done to prevent escalation of privileges. -| Action | Scope | -| --------------- | -------------------- | -| teams.roles:add | permissions:delegate | +| Action | Scope | +| --------------- | ------------------------- | +| teams.roles:add | permissions:type:delegate | #### Example request @@ -857,12 +857,12 @@ For bulk updates consider [Set team role assignments]({{< ref "#set-team-role-as #### Required permissions -`permission:delegate` scope ensures that users can only unassign roles which have same, or a subset of permissions which the user has. +`permissions:type:delegate` scope ensures that users can only unassign roles which have same, or a subset of permissions which the user has. For example, if a user does not have the permissions required to create users, they won't be able to assign a role that contains these permissions. This is done to prevent escalation of privileges.``` -| Action | Scope | -| ------------------ | -------------------- | -| teams.roles:remove | permissions:delegate | +| Action | Scope | +| ------------------ | ------------------------- | +| teams.roles:remove | permissions:type:delegate | #### Example request @@ -905,13 +905,13 @@ instead. #### Required permissions -`permission:delegate` scope ensures that users can only assign or unassign roles which have same, or a subset of permissions which the user has. +`permissions:type:delegate` scope ensures that users can only assign or unassign roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won't be able to assign or unassign a role to a team which will allow to do that. This is done to prevent escalation of privileges. -| Action | Scope | -| ------------------ | -------------------- | -| teams.roles:add | permissions:delegate | -| teams.roles:remove | permissions:delegate | +| Action | Scope | +| ------------------ | ------------------------- | +| teams.roles:add | permissions:type:delegate | +| teams.roles:remove | permissions:type:delegate | #### Example request @@ -1045,12 +1045,12 @@ Creates a new built-in role assignment. #### Required permissions -`permission:delegate` scope ensures that users can only create built-in role assignments with the roles which have same, or a subset of permissions which the user has. +`permissions:type:delegate` scope ensures that users can only create built-in role assignments with the roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won't be able to create a built-in role assignment which will allow to do that. This is done to prevent escalation of privileges. -| Action | Scope | -| ----------------- | -------------------- | -| roles.builtin:add | permissions:delegate | +| Action | Scope | +| ----------------- | ------------------------- | +| roles.builtin:add | permissions:type:delegate | #### Example request @@ -1103,12 +1103,12 @@ Deletes a built-in role assignment (for one of _Viewer_, _Editor_, _Admin_, or _ #### Required permissions -`permission:delegate` scope ensures that users can only remove built-in role assignments with the roles which have same, or a subset of permissions which the user has. +`permissions:type:delegate` scope ensures that users can only remove built-in role assignments with the roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won't be able to remove a built-in role assignment which allows to do that. -| Action | Scope | -| -------------------- | -------------------- | -| roles.builtin:remove | permissions:delegate | +| Action | Scope | +| -------------------- | ------------------------- | +| roles.builtin:remove | permissions:type:delegate | #### Example request diff --git a/pkg/services/accesscontrol/evaluator_test.go b/pkg/services/accesscontrol/evaluator_test.go index 82df04ffe9b..b15c260e14b 100644 --- a/pkg/services/accesscontrol/evaluator_test.go +++ b/pkg/services/accesscontrol/evaluator_test.go @@ -275,7 +275,7 @@ func TestAny_Evaluate(t *testing.T) { EvalPermission("report:write", Scope("reports", "10")), ), permissions: map[string][]string{ - "permissions:write": {"permissions:delegate"}, + "permissions:write": {"permissions:type:delegate"}, }, expected: false, }, diff --git a/public/api-merged.json b/public/api-merged.json index 8cc8b20cc78..eb54cbca70c 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -38,7 +38,7 @@ } }, "post": { - "description": "You need to have a permission with action `roles.builtin:add` and scope `permissions:delegate`. `permission:delegate` scope ensures that users can only create built-in role assignments with the roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to create a built-in role assignment which will allow to do that. This is done to prevent escalation of privileges.", + "description": "You need to have a permission with action `roles.builtin:add` and scope `permissions:type:delegate`. `permissions:type:delegate` scope ensures that users can only create built-in role assignments with the roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to create a built-in role assignment which will allow to do that. This is done to prevent escalation of privileges.", "tags": ["access_control", "enterprise"], "summary": "Create a built-in role assignment.", "operationId": "addBuiltinRole", @@ -71,7 +71,7 @@ }, "/access-control/builtin-roles/{builtinRole}/roles/{roleUID}": { "delete": { - "description": "Deletes a built-in role assignment (for one of Viewer, Editor, Admin, or Grafana Admin) to the role with the provided UID.\n\nYou need to have a permission with action `roles.builtin:remove` and scope `permissions:delegate`. `permission:delegate` scope ensures that users can only remove built-in role assignments with the roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to remove a built-in role assignment which allows to do that.", + "description": "Deletes a built-in role assignment (for one of Viewer, Editor, Admin, or Grafana Admin) to the role with the provided UID.\n\nYou need to have a permission with action `roles.builtin:remove` and scope `permissions:type:delegate`. `permissions:type:delegate` scope ensures that users can only remove built-in role assignments with the roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to remove a built-in role assignment which allows to do that.", "tags": ["access_control", "enterprise"], "summary": "Remove a built-in role assignment.", "operationId": "removeBuiltinRole", @@ -136,7 +136,7 @@ } }, "post": { - "description": "Creates a new custom role and maps given permissions to that role. Note that roles with the same prefix as Fixed Roles can’t be created.\n\nYou need to have a permission with action `roles:write` and scope `permissions:delegate`. `permission:delegate`` scope ensures that users can only create custom roles with the same, or a subset of permissions which the user has.\nFor example, if a user does not have required permissions for creating users, they won’t be able to create a custom role which allows to do that. This is done to prevent escalation of privileges.", + "description": "Creates a new custom role and maps given permissions to that role. Note that roles with the same prefix as Fixed Roles can’t be created.\n\nYou need to have a permission with action `roles:write` and scope `permissions:type:delegate`. `permissions:type:delegate`` scope ensures that users can only create custom roles with the same, or a subset of permissions which the user has.\nFor example, if a user does not have required permissions for creating users, they won’t be able to create a custom role which allows to do that. This is done to prevent escalation of privileges.", "tags": ["access_control", "enterprise"], "summary": "Create a new custom role.", "operationId": "createRoleWithPermissions", @@ -195,7 +195,7 @@ } }, "put": { - "description": "You need to have a permission with action `roles:write` and scope `permissions:delegate`. `permission:delegate`` scope ensures that users can only create custom roles with the same, or a subset of permissions which the user has.", + "description": "You need to have a permission with action `roles:write` and scope `permissions:type:delegate`. `permissions:type:delegate`` scope ensures that users can only create custom roles with the same, or a subset of permissions which the user has.", "tags": ["access_control", "enterprise"], "summary": "Update a custom role.", "operationId": "updateRoleWithPermissions", @@ -236,7 +236,7 @@ } }, "delete": { - "description": "Delete a role with the given UID, and it’s permissions. If the role is assigned to a built-in role, the deletion operation will fail, unless force query param is set to true, and in that case all assignments will also be deleted.\n\nYou need to have a permission with action `roles:delete` and scope `permissions:delegate`. `permission:delegate` scope ensures that users can only delete a custom role with the same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to delete a custom role which allows to do that.", + "description": "Delete a role with the given UID, and it’s permissions. If the role is assigned to a built-in role, the deletion operation will fail, unless force query param is set to true, and in that case all assignments will also be deleted.\n\nYou need to have a permission with action `roles:delete` and scope `permissions:type:delegate`. `permissions:type:delegate` scope ensures that users can only delete a custom role with the same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to delete a custom role which allows to do that.", "tags": ["access_control", "enterprise"], "summary": "Delete a custom role.", "operationId": "deleteCustomRole", @@ -319,7 +319,7 @@ } }, "put": { - "description": "You need to have a permission with action `teams.roles:add` and `teams.roles:remove` and scope `permissions:delegate` for each.", + "description": "You need to have a permission with action `teams.roles:add` and `teams.roles:remove` and scope `permissions:type:delegate` for each.", "tags": ["access_control", "enterprise"], "summary": "Update team role.", "operationId": "setTeamRoles", @@ -352,7 +352,7 @@ } }, "post": { - "description": "You need to have a permission with action `teams.roles:add` and scope `permissions:delegate`.", + "description": "You need to have a permission with action `teams.roles:add` and scope `permissions:type:delegate`.", "tags": ["access_control", "enterprise"], "summary": "Add team role.", "operationId": "addTeamRole", @@ -396,7 +396,7 @@ }, "/access-control/teams/{teamId}/roles/{roleUID}": { "delete": { - "description": "You need to have a permission with action `teams.roles:remove` and scope `permissions:delegate`.", + "description": "You need to have a permission with action `teams.roles:remove` and scope `permissions:type:delegate`.", "tags": ["access_control", "enterprise"], "summary": "Remove team role.", "operationId": "removeTeamRole", @@ -468,7 +468,7 @@ } }, "put": { - "description": "Update the user’s role assignments to match the provided set of UIDs. This will remove any assigned roles that aren’t in the request and add roles that are in the set but are not already assigned to the user.\nIf you want to add or remove a single role, consider using Add a user role assignment or Remove a user role assignment instead.\n\nYou need to have a permission with action `users.roles:add` and `users.roles:remove` and scope `permissions:delegate` for each. `permission:delegate` scope ensures that users can only assign or unassign roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to assign or unassign a role which will allow to do that. This is done to prevent escalation of privileges.", + "description": "Update the user’s role assignments to match the provided set of UIDs. This will remove any assigned roles that aren’t in the request and add roles that are in the set but are not already assigned to the user.\nIf you want to add or remove a single role, consider using Add a user role assignment or Remove a user role assignment instead.\n\nYou need to have a permission with action `users.roles:add` and `users.roles:remove` and scope `permissions:type:delegate` for each. `permissions:type:delegate` scope ensures that users can only assign or unassign roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to assign or unassign a role which will allow to do that. This is done to prevent escalation of privileges.", "tags": ["access_control", "enterprise"], "summary": "Set user role assignments.", "operationId": "setUserRoles", @@ -501,7 +501,7 @@ } }, "post": { - "description": "Assign a role to a specific user. For bulk updates consider Set user role assignments.\n\nYou need to have a permission with action `users.roles:add` and scope `permissions:delegate`. `permission:delegate` scope ensures that users can only assign roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to assign a role which will allow to do that. This is done to prevent escalation of privileges.", + "description": "Assign a role to a specific user. For bulk updates consider Set user role assignments.\n\nYou need to have a permission with action `users.roles:add` and scope `permissions:type:delegate`. `permissions:type:delegate` scope ensures that users can only assign roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to assign a role which will allow to do that. This is done to prevent escalation of privileges.", "tags": ["access_control", "enterprise"], "summary": "Add a user role assignment.", "operationId": "addUserRole", @@ -542,7 +542,7 @@ }, "/access-control/users/{user_id}/roles/{roleUID}": { "delete": { - "description": "Revoke a role from a user. For bulk updates consider Set user role assignments.\n\nYou need to have a permission with action `users.roles:remove` and scope `permissions:delegate`. `permission:delegate` scope ensures that users can only unassign roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to unassign a role which will allow to do that. This is done to prevent escalation of privileges.", + "description": "Revoke a role from a user. For bulk updates consider Set user role assignments.\n\nYou need to have a permission with action `users.roles:remove` and scope `permissions:type:delegate`. `permissions:type:delegate` scope ensures that users can only unassign roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to unassign a role which will allow to do that. This is done to prevent escalation of privileges.", "tags": ["access_control", "enterprise"], "summary": "Remove a user role assignment.", "operationId": "removeUserRole", diff --git a/public/api-spec.json b/public/api-spec.json index 32d01405a32..f4bb5c87778 100644 --- a/public/api-spec.json +++ b/public/api-spec.json @@ -38,7 +38,7 @@ } }, "post": { - "description": "You need to have a permission with action `roles.builtin:add` and scope `permissions:delegate`. `permission:delegate` scope ensures that users can only create built-in role assignments with the roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to create a built-in role assignment which will allow to do that. This is done to prevent escalation of privileges.", + "description": "You need to have a permission with action `roles.builtin:add` and scope `permissions:type:delegate`. `permissions:type:delegate` scope ensures that users can only create built-in role assignments with the roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to create a built-in role assignment which will allow to do that. This is done to prevent escalation of privileges.", "tags": ["access_control", "enterprise"], "summary": "Create a built-in role assignment.", "operationId": "addBuiltinRole", @@ -71,7 +71,7 @@ }, "/access-control/builtin-roles/{builtinRole}/roles/{roleUID}": { "delete": { - "description": "Deletes a built-in role assignment (for one of Viewer, Editor, Admin, or Grafana Admin) to the role with the provided UID.\n\nYou need to have a permission with action `roles.builtin:remove` and scope `permissions:delegate`. `permission:delegate` scope ensures that users can only remove built-in role assignments with the roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to remove a built-in role assignment which allows to do that.", + "description": "Deletes a built-in role assignment (for one of Viewer, Editor, Admin, or Grafana Admin) to the role with the provided UID.\n\nYou need to have a permission with action `roles.builtin:remove` and scope `permissions:type:delegate`. `permissions:type:delegate` scope ensures that users can only remove built-in role assignments with the roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to remove a built-in role assignment which allows to do that.", "tags": ["access_control", "enterprise"], "summary": "Remove a built-in role assignment.", "operationId": "removeBuiltinRole", @@ -136,7 +136,7 @@ } }, "post": { - "description": "Creates a new custom role and maps given permissions to that role. Note that roles with the same prefix as Fixed Roles can’t be created.\n\nYou need to have a permission with action `roles:write` and scope `permissions:delegate`. `permission:delegate`` scope ensures that users can only create custom roles with the same, or a subset of permissions which the user has.\nFor example, if a user does not have required permissions for creating users, they won’t be able to create a custom role which allows to do that. This is done to prevent escalation of privileges.", + "description": "Creates a new custom role and maps given permissions to that role. Note that roles with the same prefix as Fixed Roles can’t be created.\n\nYou need to have a permission with action `roles:write` and scope `permissions:type:delegate`. `permissions:type:delegate`` scope ensures that users can only create custom roles with the same, or a subset of permissions which the user has.\nFor example, if a user does not have required permissions for creating users, they won’t be able to create a custom role which allows to do that. This is done to prevent escalation of privileges.", "tags": ["access_control", "enterprise"], "summary": "Create a new custom role.", "operationId": "createRoleWithPermissions", @@ -195,7 +195,7 @@ } }, "put": { - "description": "You need to have a permission with action `roles:write` and scope `permissions:delegate`. `permission:delegate`` scope ensures that users can only create custom roles with the same, or a subset of permissions which the user has.", + "description": "You need to have a permission with action `roles:write` and scope `permissions:type:delegate`. `permissions:type:delegate`` scope ensures that users can only create custom roles with the same, or a subset of permissions which the user has.", "tags": ["access_control", "enterprise"], "summary": "Update a custom role.", "operationId": "updateRoleWithPermissions", @@ -236,7 +236,7 @@ } }, "delete": { - "description": "Delete a role with the given UID, and it’s permissions. If the role is assigned to a built-in role, the deletion operation will fail, unless force query param is set to true, and in that case all assignments will also be deleted.\n\nYou need to have a permission with action `roles:delete` and scope `permissions:delegate`. `permission:delegate` scope ensures that users can only delete a custom role with the same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to delete a custom role which allows to do that.", + "description": "Delete a role with the given UID, and it’s permissions. If the role is assigned to a built-in role, the deletion operation will fail, unless force query param is set to true, and in that case all assignments will also be deleted.\n\nYou need to have a permission with action `roles:delete` and scope `permissions:type:delegate`. `permissions:type:delegate` scope ensures that users can only delete a custom role with the same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to delete a custom role which allows to do that.", "tags": ["access_control", "enterprise"], "summary": "Delete a custom role.", "operationId": "deleteCustomRole", @@ -319,7 +319,7 @@ } }, "put": { - "description": "You need to have a permission with action `teams.roles:add` and `teams.roles:remove` and scope `permissions:delegate` for each.", + "description": "You need to have a permission with action `teams.roles:add` and `teams.roles:remove` and scope `permissions:type:delegate` for each.", "tags": ["access_control", "enterprise"], "summary": "Update team role.", "operationId": "setTeamRoles", @@ -352,7 +352,7 @@ } }, "post": { - "description": "You need to have a permission with action `teams.roles:add` and scope `permissions:delegate`.", + "description": "You need to have a permission with action `teams.roles:add` and scope `permissions:type:delegate`.", "tags": ["access_control", "enterprise"], "summary": "Add team role.", "operationId": "addTeamRole", @@ -396,7 +396,7 @@ }, "/access-control/teams/{teamId}/roles/{roleUID}": { "delete": { - "description": "You need to have a permission with action `teams.roles:remove` and scope `permissions:delegate`.", + "description": "You need to have a permission with action `teams.roles:remove` and scope `permissions:type:delegate`.", "tags": ["access_control", "enterprise"], "summary": "Remove team role.", "operationId": "removeTeamRole", @@ -468,7 +468,7 @@ } }, "put": { - "description": "Update the user’s role assignments to match the provided set of UIDs. This will remove any assigned roles that aren’t in the request and add roles that are in the set but are not already assigned to the user.\nIf you want to add or remove a single role, consider using Add a user role assignment or Remove a user role assignment instead.\n\nYou need to have a permission with action `users.roles:add` and `users.roles:remove` and scope `permissions:delegate` for each. `permission:delegate` scope ensures that users can only assign or unassign roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to assign or unassign a role which will allow to do that. This is done to prevent escalation of privileges.", + "description": "Update the user’s role assignments to match the provided set of UIDs. This will remove any assigned roles that aren’t in the request and add roles that are in the set but are not already assigned to the user.\nIf you want to add or remove a single role, consider using Add a user role assignment or Remove a user role assignment instead.\n\nYou need to have a permission with action `users.roles:add` and `users.roles:remove` and scope `permissions:type:delegate` for each. `permissions:type:delegate` scope ensures that users can only assign or unassign roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to assign or unassign a role which will allow to do that. This is done to prevent escalation of privileges.", "tags": ["access_control", "enterprise"], "summary": "Set user role assignments.", "operationId": "setUserRoles", @@ -501,7 +501,7 @@ } }, "post": { - "description": "Assign a role to a specific user. For bulk updates consider Set user role assignments.\n\nYou need to have a permission with action `users.roles:add` and scope `permissions:delegate`. `permission:delegate` scope ensures that users can only assign roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to assign a role which will allow to do that. This is done to prevent escalation of privileges.", + "description": "Assign a role to a specific user. For bulk updates consider Set user role assignments.\n\nYou need to have a permission with action `users.roles:add` and scope `permissions:type:delegate`. `permissions:type:delegate` scope ensures that users can only assign roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to assign a role which will allow to do that. This is done to prevent escalation of privileges.", "tags": ["access_control", "enterprise"], "summary": "Add a user role assignment.", "operationId": "addUserRole", @@ -542,7 +542,7 @@ }, "/access-control/users/{user_id}/roles/{roleUID}": { "delete": { - "description": "Revoke a role from a user. For bulk updates consider Set user role assignments.\n\nYou need to have a permission with action `users.roles:remove` and scope `permissions:delegate`. `permission:delegate` scope ensures that users can only unassign roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to unassign a role which will allow to do that. This is done to prevent escalation of privileges.", + "description": "Revoke a role from a user. For bulk updates consider Set user role assignments.\n\nYou need to have a permission with action `users.roles:remove` and scope `permissions:type:delegate`. `permissions:type:delegate` scope ensures that users can only unassign roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to unassign a role which will allow to do that. This is done to prevent escalation of privileges.", "tags": ["access_control", "enterprise"], "summary": "Remove a user role assignment.", "operationId": "removeUserRole", From 3a32a73459ab10c7e36099cad2406e6ad8dfda4a Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 11 May 2022 08:32:13 -0700 Subject: [PATCH 125/440] Search: use bluge index for frontend search (playground) (#48847) --- package.json | 3 +- .../app/features/search/page/SearchPage.tsx | 205 +++++------ .../search/page/components/ActionRow.tsx | 10 +- .../page/components/ConfirmDeleteModal.tsx | 2 +- .../search/page/components/FolderSection.tsx | 177 ++++++++++ .../search/page/components/FolderView.tsx | 127 +++++++ .../page/components/SearchResultsGrid.tsx | 104 ++++++ .../page/components/SearchResultsTable.tsx | 120 +++---- .../search/page/components/columns.tsx | 249 ++++++-------- public/app/features/search/service/backend.ts | 213 ------------ public/app/features/search/service/bluge.ts | 133 ++++++++ .../features/search/service/minisearcher.ts | 320 ------------------ .../features/search/service/searcher.test.ts | 56 --- .../app/features/search/service/searcher.ts | 4 +- public/app/features/search/service/types.ts | 61 +++- .../grafana/components/QueryEditor.tsx | 76 ++++- .../app/plugins/datasource/grafana/types.ts | 2 +- yarn.lock | 32 +- 18 files changed, 918 insertions(+), 976 deletions(-) create mode 100644 public/app/features/search/page/components/FolderSection.tsx create mode 100644 public/app/features/search/page/components/FolderView.tsx create mode 100644 public/app/features/search/page/components/SearchResultsGrid.tsx delete mode 100644 public/app/features/search/service/backend.ts create mode 100644 public/app/features/search/service/bluge.ts delete mode 100644 public/app/features/search/service/minisearcher.ts delete mode 100644 public/app/features/search/service/searcher.test.ts diff --git a/package.json b/package.json index 428cbca42a9..a9d18adebe7 100644 --- a/package.json +++ b/package.json @@ -156,6 +156,7 @@ "@types/react-transition-group": "4.4.4", "@types/react-virtualized-auto-sizer": "1.0.1", "@types/react-window": "1.8.5", + "@types/react-window-infinite-loader": "^1", "@types/redux-mock-store": "1.0.3", "@types/reselect": "2.2.0", "@types/semver": "7.3.9", @@ -327,7 +328,6 @@ "logfmt": "^1.3.2", "lru-cache": "7.9.0", "memoize-one": "6.0.0", - "minisearch": "5.0.0-beta1", "moment": "2.29.2", "moment-timezone": "0.5.34", "monaco-editor": "^0.31.1", @@ -369,6 +369,7 @@ "react-use": "17.3.2", "react-virtualized-auto-sizer": "1.0.6", "react-window": "1.8.6", + "react-window-infinite-loader": "^1.0.7", "redux": "4.1.2", "redux-thunk": "2.4.1", "regenerator-runtime": "0.13.9", diff --git a/public/app/features/search/page/SearchPage.tsx b/public/app/features/search/page/SearchPage.tsx index 55fb1a3a11b..72b75fd6497 100644 --- a/public/app/features/search/page/SearchPage.tsx +++ b/public/app/features/search/page/SearchPage.tsx @@ -1,25 +1,24 @@ import { css } from '@emotion/css'; import React, { useState } from 'react'; -import { useAsync } from 'react-use'; +import { useAsync, useDebounce } from 'react-use'; import AutoSizer from 'react-virtualized-auto-sizer'; -import { FixedSizeGrid } from 'react-window'; -import { DataFrameView, GrafanaTheme2, NavModelItem } from '@grafana/data'; +import { GrafanaTheme2, NavModelItem } from '@grafana/data'; import { config } from '@grafana/runtime'; import { Input, useStyles2, Spinner, InlineSwitch, InlineFieldRow, InlineField, Button } from '@grafana/ui'; import Page from 'app/core/components/Page/Page'; import { TermCount } from 'app/core/components/TagFilter/TagFilter'; import { PreviewsSystemRequirements } from '../components/PreviewsSystemRequirements'; -import { SearchCard } from '../components/SearchCard'; import { useSearchQuery } from '../hooks/useSearchQuery'; -import { getGrafanaSearcher, QueryFilters, QueryResult } from '../service'; -import { getTermCounts } from '../service/backend'; -import { DashboardSearchItemType, DashboardSectionItem, SearchLayout } from '../types'; +import { getGrafanaSearcher, SearchQuery } from '../service'; +import { SearchLayout } from '../types'; import { ActionRow, getValidQueryLayout } from './components/ActionRow'; +import { FolderView } from './components/FolderView'; import { ManageActions } from './components/ManageActions'; -import { SearchResultsTable } from './components/SearchResultsTable'; +import { SearchResultsGrid } from './components/SearchResultsGrid'; +import { SearchResultsTable, SearchResultsProps } from './components/SearchResultsTable'; import { newSearchSelection, updateSearchSelection } from './selection'; const node: NavModelItem = { @@ -38,16 +37,30 @@ export default function SearchPage() { const [showManage, setShowManage] = useState(false); // grid vs list view const [searchSelection, setSearchSelection] = useState(newSearchSelection()); + const layout = getValidQueryLayout(query); + const isFolders = layout === SearchLayout.Folders; const results = useAsync(() => { - const { query: searchQuery, tag: tags, datasource } = query; - - const filters: QueryFilters = { - tags, - datasource, + let qstr = query.query as string; + if (!qstr?.length) { + qstr = '*'; + } + const q: SearchQuery = { + query: qstr, + tags: query.tag as string[], + ds_uid: query.datasource as string, }; - return getGrafanaSearcher().search(searchQuery, tags.length || datasource ? filters : undefined); - }, [query]); + console.log('DO QUERY', q); + return getGrafanaSearcher().search(q); + }, [query, layout]); + + const [inputValue, setInputValue] = useState(''); + const onSearchQueryChange = (e: React.ChangeEvent) => { + e.preventDefault(); + setInputValue(e.currentTarget.value); + }; + + useDebounce(() => onQueryChange(inputValue), 200, [inputValue]); if (!config.featureToggles.panelTitleSearch) { return
    Unsupported
    ; @@ -55,20 +68,12 @@ export default function SearchPage() { // This gets the possible tags from within the query results const getTagOptions = (): Promise => { - const tags = results.value?.body.fields.find((f) => f.name === 'tags'); - - if (tags) { - return Promise.resolve(getTermCounts(tags)); - } - return Promise.resolve([]); - }; - - const onSearchQueryChange = (event: React.ChangeEvent) => { - onQueryChange(event.currentTarget.value); - }; - - const onTagChange = (tags: string[]) => { - onTagFilterChange(tags); + const q: SearchQuery = { + query: query.query ?? '*', + tags: query.tag, + ds_uid: query.datasource, + }; + return getGrafanaSearcher().tags(q); }; const onTagSelected = (tag: string) => { @@ -83,16 +88,14 @@ export default function SearchPage() { setSearchSelection(updateSearchSelection(searchSelection, !current, kind, [uid])); }; - const layout = getValidQueryLayout(query); - const showPreviews = layout === SearchLayout.Grid && config.featureToggles.dashboardPreviews; - const renderResults = () => { - if (results.loading) { - return ; - } + const value = results.value; + + if ((!value || !value.totalRows) && !isFolders) { + if (results.loading && !value) { + return ; + } - const df = results.value?.body; - if (!df || !df.length) { return (
    No results found for your query.
    @@ -117,103 +120,58 @@ export default function SearchPage() { ); } - return ( - - {({ width, height }) => { - if (showPreviews) { - const view = new DataFrameView(df); + const selection = showManage ? searchSelection.isSelected : undefined; + if (layout === SearchLayout.Folders) { + return ; + } - // Hacked to reuse existing SearchCard (and old DashboardSectionItem) - const itemProps = { - editable: showManage, - onToggleChecked: (item: any) => { - const d = item as DashboardSectionItem; - const t = d.type === DashboardSearchItemType.DashFolder ? 'folder' : 'dashboard'; - toggleSelection(t, d.uid!); - }, - onTagSelected, + return ( +
    + + {({ width, height }) => { + const props: SearchResultsProps = { + response: value!, + selection, + selectionToggle: toggleSelection, + width: width, + height: height, + onTagSelected: onTagSelected, + onDatasourceChange: query.datasource ? onDatasourceChange : undefined, }; - const numColumns = Math.ceil(width / 320); - const cellWidth = width / numColumns; - const cellHeight = (cellWidth - 64) * 0.75 + 56 + 8; - const numRows = Math.ceil(df.length / numColumns); - return ( - - {({ columnIndex, rowIndex, style }) => { - const index = rowIndex * numColumns + columnIndex; - const item = view.get(index); - const kind = item.kind ?? 'dashboard'; - const facade: DashboardSectionItem = { - uid: item.uid, - title: item.name, - url: item.url, - uri: item.url, - type: kind === 'folder' ? DashboardSearchItemType.DashFolder : DashboardSearchItemType.DashDB, - id: 666, // do not use me! - isStarred: false, - tags: item.tags ?? [], - checked: searchSelection.isSelected(kind, item.uid), - }; + if (layout === SearchLayout.Grid) { + return ; + } - // The wrapper div is needed as the inner SearchItem has margin-bottom spacing - // And without this wrapper there is no room for that margin - return item ? ( -
  • - -
  • - ) : null; - }} -
    - ); - } - - return ( - <> - - - ); - }} -
    + return ; + }} + +
    ); }; return ( - + : null} /> setShowManage(!showManage)} /> -
    -
    {Boolean(searchSelection.items.size > 0) ? ( @@ -235,14 +193,13 @@ export default function SearchPage() { /> )} - {showPreviews && ( + {layout === SearchLayout.Grid && ( onLayoutChange(SearchLayout.List)} /> )} - {renderResults()}
    @@ -250,6 +207,9 @@ export default function SearchPage() { } const getStyles = (theme: GrafanaTheme2) => ({ + searchInput: css` + margin-bottom: 6px; + `, unsupported: css` padding: 10px; display: flex; @@ -258,17 +218,6 @@ const getStyles = (theme: GrafanaTheme2) => ({ height: 100%; font-size: 18px; `, - virtualizedGridItemWrapper: css` - padding: 4px; - `, - wrapper: css` - display: flex; - flex-direction: column; - - > ul { - list-style: none; - } - `, noResults: css` padding: ${theme.v1.spacing.md}; background: ${theme.v1.colors.bg2}; diff --git a/public/app/features/search/page/components/ActionRow.tsx b/public/app/features/search/page/components/ActionRow.tsx index d79af608095..92a7cc63e6c 100644 --- a/public/app/features/search/page/components/ActionRow.tsx +++ b/public/app/features/search/page/components/ActionRow.tsx @@ -31,13 +31,19 @@ interface Props { } export function getValidQueryLayout(q: DashboardQuery): SearchLayout { + const layout = q.layout ?? SearchLayout.Folders; + // Folders is not valid when a query exists - if (q.layout === SearchLayout.Folders) { + if (layout === SearchLayout.Folders) { if (q.query || q.sort) { return SearchLayout.List; } } - return q.layout; + + if (layout === SearchLayout.Grid && !config.featureToggles.dashboardPreviews) { + return SearchLayout.List; + } + return layout; } export const ActionRow: FC = ({ diff --git a/public/app/features/search/page/components/ConfirmDeleteModal.tsx b/public/app/features/search/page/components/ConfirmDeleteModal.tsx index 8058f641fbc..c1269a93fdf 100644 --- a/public/app/features/search/page/components/ConfirmDeleteModal.tsx +++ b/public/app/features/search/page/components/ConfirmDeleteModal.tsx @@ -19,7 +19,7 @@ export const ConfirmDeleteModal: FC = ({ results, onDeleteItems, isOpen, const styles = getStyles(theme); const dashboards = Array.from(results.get('dashboard') ?? []); - const folders = Array.from(results.get('folders') ?? []); + const folders = Array.from(results.get('folder') ?? []); const folderCount = folders.length; const dashCount = dashboards.length; diff --git a/public/app/features/search/page/components/FolderSection.tsx b/public/app/features/search/page/components/FolderSection.tsx new file mode 100644 index 00000000000..263cb7f320d --- /dev/null +++ b/public/app/features/search/page/components/FolderSection.tsx @@ -0,0 +1,177 @@ +import { css, cx } from '@emotion/css'; +import React, { FC } from 'react'; +import { useAsync, useLocalStorage } from 'react-use'; + +import { GrafanaTheme } from '@grafana/data'; +import { Checkbox, CollapsableSection, Icon, stylesFactory, useTheme } from '@grafana/ui'; +import { getSectionStorageKey } from 'app/features/search/utils'; +import { useUniqueId } from 'app/plugins/datasource/influxdb/components/useUniqueId'; + +import { SearchItem } from '../..'; +import { getGrafanaSearcher } from '../../service'; +import { DashboardSearchItemType, DashboardSectionItem } from '../../types'; +import { SelectionChecker, SelectionToggle } from '../selection'; + +export interface DashboardSection { + kind: string; // folder | query! + uid: string; + title: string; + selected?: boolean; // not used ? keyboard + url?: string; + icon?: string; +} + +interface SectionHeaderProps { + selection?: SelectionChecker; + selectionToggle?: SelectionToggle; + onTagSelected: (tag: string) => void; + section: DashboardSection; +} + +export const FolderSection: FC = ({ section, selectionToggle, onTagSelected, selection }) => { + const editable = selectionToggle != null; + const theme = useTheme(); + const styles = getSectionHeaderStyles(theme, section.selected, editable); + const [sectionExpanded, setSectionExpanded] = useLocalStorage(getSectionStorageKey(section.title), false); + + const results = useAsync(async () => { + if (!sectionExpanded) { + return Promise.resolve([] as DashboardSectionItem[]); + } + let query = { + query: '*', + kind: ['dashboard'], + location: section.uid, + }; + if (section.title === 'Starred') { + // TODO + } else if (section.title === 'Recent') { + // TODO + } + const raw = await getGrafanaSearcher().search(query); + const v = raw.view.map( + (item) => + ({ + uid: item.uid, + title: item.name, + url: item.url, + uri: item.url, + type: item.kind === 'folder' ? DashboardSearchItemType.DashFolder : DashboardSearchItemType.DashDB, + id: 666, // do not use me! + isStarred: false, + tags: item.tags ?? [], + checked: selection ? selection(item.kind, item.uid) : false, + } as DashboardSectionItem) + ); + console.log('HERE!'); + return v; + }, [sectionExpanded, section]); + + const onSectionExpand = () => { + setSectionExpanded(!sectionExpanded); + console.log('TODO!! section', section.title, section); + }; + + const id = useUniqueId(); + const labelId = `section-header-label-${id}`; + + let icon = section.icon; + if (!icon) { + icon = sectionExpanded ? 'folder-open' : 'folder'; + } + + return ( + + {selectionToggle && selection && ( +
    console.log(v)} className={styles.checkbox}> + +
    + )} + +
    + +
    + +
    + {section.title} + {section.url && ( + + | Go to folder + + )} +
    + + } + > + {results.value && ( +
      + {results.value.map((v) => ( + + ))} +
    + )} +
    + ); +}; + +const getSectionHeaderStyles = stylesFactory((theme: GrafanaTheme, selected = false, editable: boolean) => { + const { sm } = theme.spacing; + return { + wrapper: cx( + css` + align-items: center; + font-size: ${theme.typography.size.base}; + padding: 12px; + border-bottom: none; + color: ${theme.colors.textWeak}; + z-index: 1; + + &:hover, + &.selected { + color: ${theme.colors.text}; + } + + &:hover, + &:focus-visible, + &:focus-within { + a { + opacity: 1; + } + } + `, + 'pointer', + { selected } + ), + checkbox: css` + padding: 0 ${sm} 0 0; + `, + icon: css` + padding: 0 ${sm} 0 ${editable ? 0 : sm}; + `, + text: css` + flex-grow: 1; + line-height: 24px; + `, + link: css` + padding: 2px 10px 0; + color: ${theme.colors.textWeak}; + opacity: 0; + transition: opacity 150ms ease-in-out; + `, + separator: css` + margin-right: 6px; + `, + content: css` + padding-top: 0px; + padding-bottom: 0px; + `, + }; +}); diff --git a/public/app/features/search/page/components/FolderView.tsx b/public/app/features/search/page/components/FolderView.tsx new file mode 100644 index 00000000000..7b950428054 --- /dev/null +++ b/public/app/features/search/page/components/FolderView.tsx @@ -0,0 +1,127 @@ +import { css } from '@emotion/css'; +import React from 'react'; +import { useAsync } from 'react-use'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; +import { Spinner, useStyles2 } from '@grafana/ui'; + +import { getGrafanaSearcher } from '../../service'; +import { SearchResultsProps } from '../components/SearchResultsTable'; + +import { DashboardSection, FolderSection } from './FolderSection'; + +export const FolderView = ({ + selection, + selectionToggle, + onTagSelected, +}: Pick) => { + const styles = useStyles2(getStyles); + + const results = useAsync(async () => { + const rsp = await getGrafanaSearcher().search({ + query: '*', + kind: ['folder'], + }); + const folders: DashboardSection[] = [ + { title: 'Recent', icon: 'clock', kind: 'query-recent', uid: '__recent' }, + { title: 'Starred', icon: 'star', kind: 'query-star', uid: '__starred' }, + { title: 'General', url: '/dashboards', kind: 'folder', uid: 'general' }, // not sure why this is not in the index + ]; + for (const row of rsp.view) { + folders.push({ + title: row.name, + url: row.url, + uid: row.uid, + kind: row.kind, + }); + } + + return folders; + }, []); + + if (results.loading) { + return ; + } + if (!results.value) { + return
    ?
    ; + } + + return ( +
    + {results.value.map((section) => { + return ( +
    + {section.title && ( + + )} +
    + ); + })} +
    + ); +}; + +const getStyles = (theme: GrafanaTheme2) => { + const { md, sm } = theme.v1.spacing; + + return { + virtualizedGridItemWrapper: css` + padding: 4px; + `, + wrapper: css` + display: flex; + flex-direction: column; + + > ul { + list-style: none; + } + `, + section: css` + display: flex; + flex-direction: column; + background: ${theme.v1.colors.panelBg}; + border-bottom: solid 1px ${theme.v1.colors.border2}; + `, + sectionItems: css` + margin: 0 24px 0 32px; + `, + spinner: css` + display: flex; + justify-content: center; + align-items: center; + min-height: 100px; + `, + gridContainer: css` + display: grid; + gap: ${sm}; + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + margin-bottom: ${md}; + `, + resultsContainer: css` + position: relative; + flex-grow: 10; + margin-bottom: ${md}; + background: ${theme.v1.colors.bg1}; + border: 1px solid ${theme.v1.colors.border1}; + border-radius: 3px; + height: 100%; + `, + noResults: css` + padding: ${md}; + background: ${theme.v1.colors.bg2}; + font-style: italic; + margin-top: ${theme.v1.spacing.md}; + `, + listModeWrapper: css` + position: relative; + height: 100%; + padding: ${md}; + `, + }; +}; diff --git a/public/app/features/search/page/components/SearchResultsGrid.tsx b/public/app/features/search/page/components/SearchResultsGrid.tsx new file mode 100644 index 00000000000..296e105ba35 --- /dev/null +++ b/public/app/features/search/page/components/SearchResultsGrid.tsx @@ -0,0 +1,104 @@ +import { css } from '@emotion/css'; +import React from 'react'; +import { FixedSizeGrid } from 'react-window'; +import InfiniteLoader from 'react-window-infinite-loader'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { useStyles2 } from '@grafana/ui'; + +import { SearchCard } from '../../components/SearchCard'; +import { DashboardSearchItemType, DashboardSectionItem } from '../../types'; + +import { SearchResultsProps } from './SearchResultsTable'; + +export const SearchResultsGrid = ({ + response, + width, + height, + selection, + selectionToggle, + onTagSelected, + onDatasourceChange, +}: SearchResultsProps) => { + const styles = useStyles2(getStyles); + + // Hacked to reuse existing SearchCard (and old DashboardSectionItem) + const itemProps = { + editable: selection != null, + onToggleChecked: (item: any) => { + const d = item as DashboardSectionItem; + const t = d.type === DashboardSearchItemType.DashFolder ? 'folder' : 'dashboard'; + if (selectionToggle) { + selectionToggle(t, d.uid!); + } + }, + onTagSelected, + }; + + const itemCount = response.totalRows ?? response.view.length; + + const view = response.view; + const numColumns = Math.ceil(width / 320); + const cellWidth = width / numColumns; + const cellHeight = (cellWidth - 64) * 0.75 + 56 + 8; + const numRows = Math.ceil(itemCount / numColumns); + return ( + + {({ onItemsRendered, ref }) => ( + + {({ columnIndex, rowIndex, style }) => { + const index = rowIndex * numColumns + columnIndex; + if (index >= view.length) { + return null; + } + + const item = view.get(index); + const kind = item.kind ?? 'dashboard'; + const facade: DashboardSectionItem = { + uid: item.uid, + title: item.name, + url: item.url, + uri: item.url, + type: kind === 'folder' ? DashboardSearchItemType.DashFolder : DashboardSearchItemType.DashDB, + id: 666, // do not use me! + isStarred: false, + tags: item.tags ?? [], + checked: selection ? selection(kind, item.uid) : false, + }; + + // The wrapper div is needed as the inner SearchItem has margin-bottom spacing + // And without this wrapper there is no room for that margin + return item ? ( +
  • + +
  • + ) : null; + }} +
    + )} +
    + ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + virtualizedGridItemWrapper: css` + padding: 4px; + `, + wrapper: css` + display: flex; + flex-direction: column; + + > ul { + list-style: none; + } + `, +}); diff --git a/public/app/features/search/page/components/SearchResultsTable.tsx b/public/app/features/search/page/components/SearchResultsTable.tsx index 4b0733778d4..ce766448035 100644 --- a/public/app/features/search/page/components/SearchResultsTable.tsx +++ b/public/app/features/search/page/components/SearchResultsTable.tsx @@ -3,92 +3,61 @@ import { css } from '@emotion/css'; import React, { useMemo } from 'react'; import { useTable, Column, TableOptions, Cell, useAbsoluteLayout } from 'react-table'; import { FixedSizeList } from 'react-window'; +import InfiniteLoader from 'react-window-infinite-loader'; -import { DataFrame, DataFrameView, DataSourceRef, Field, GrafanaTheme2 } from '@grafana/data'; +import { Field, GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '@grafana/ui'; import { TableCell } from '@grafana/ui/src/components/Table/TableCell'; import { getTableStyles } from '@grafana/ui/src/components/Table/styles'; -import { LocationInfo } from '../../service'; -import { SearchLayout } from '../../types'; +import { QueryResponse } from '../../service'; import { SelectionChecker, SelectionToggle } from '../selection'; import { generateColumns } from './columns'; -type Props = { - data: DataFrame; +export type SearchResultsProps = { + response: QueryResponse; width: number; height: number; selection?: SelectionChecker; selectionToggle?: SelectionToggle; - layout: SearchLayout; - tags: string[]; - onTagFilterChange: (tags: string[]) => void; - onDatasourceChange: (datasource?: string) => void; + onTagSelected: (tag: string) => void; + onDatasourceChange?: (datasource?: string) => void; }; export type TableColumn = Column & { field?: Field; }; -export interface FieldAccess { - uid: string; // the item UID - kind: string; // panel, dashboard, folder - name: string; - description: string; - url: string; // link to value (unique) - type: string; // graph - tags: string[]; - location: LocationInfo[]; // the folder name - score: number; - - // Count info - panelCount: number; - datasource: DataSourceRef[]; -} - -const skipHREF = new Set(['column-checkbox', 'column-datasource']); +const skipHREF = new Set(['column-checkbox', 'column-datasource', 'column-location']); +const HEADER_HEIGHT = 36; // pixels export const SearchResultsTable = ({ - data, + response, width, height, - tags, selection, selectionToggle, - layout, - onTagFilterChange, + onTagSelected, onDatasourceChange, -}: Props) => { +}: SearchResultsProps) => { const styles = useStyles2(getStyles); const tableStyles = useStyles2(getTableStyles); const memoizedData = useMemo(() => { - if (!data.fields.length) { + if (!response?.view?.dataFrame.fields.length) { return []; } // as we only use this to fake the length of our data set for react-table we need to make sure we always return an array // filled with values at each index otherwise we'll end up trying to call accessRow for null|undefined value in // https://github.com/tannerlinsley/react-table/blob/7be2fc9d8b5e223fc998af88865ae86a88792fdb/src/hooks/useTable.js#L585 - return Array(data.length).fill(0); - }, [data]); + return Array(response.totalRows).fill(0); + }, [response]); // React-table column definitions - const access = useMemo(() => new DataFrameView(data), [data]); const memoizedColumns = useMemo(() => { - const isDashboardList = layout === SearchLayout.Folders; - return generateColumns( - access, - isDashboardList, - width, - selection, - selectionToggle, - styles, - tags, - onTagFilterChange, - onDatasourceChange - ); - }, [layout, access, width, styles, tags, selection, selectionToggle, onTagFilterChange, onDatasourceChange]); + return generateColumns(response, width, selection, selectionToggle, styles, onTagSelected, onDatasourceChange); + }, [response, width, styles, selection, selectionToggle, onTagSelected, onDatasourceChange]); const options: TableOptions<{}> = useMemo( () => ({ @@ -105,8 +74,7 @@ export const SearchResultsTable = ({ const row = rows[rowIndex]; prepareRow(row); - const url = access.fields.url?.values.get(rowIndex); - + const url = response.view.fields.url?.values.get(rowIndex); return (
    {row.cells.map((cell: Cell, index: number) => { @@ -122,7 +90,6 @@ export const SearchResultsTable = ({ if (skipHREF.has(cell.column.id)) { return body; } - return ( {body} @@ -132,11 +99,15 @@ export const SearchResultsTable = ({
    ); }, - [rows, prepareRow, access.fields.url?.values, styles.rowContainer, styles.cellWrapper, tableStyles] + [rows, prepareRow, response.view.fields.url?.values, styles.rowContainer, styles.cellWrapper, tableStyles] ); + if (!rows.length) { + return
    No data
    ; + } + return ( -
    +
    {headerGroups.map((headerGroup) => { const { key, ...headerGroupProps } = headerGroup.getHeaderGroupProps(); @@ -157,19 +128,25 @@ export const SearchResultsTable = ({
    - {rows.length > 0 ? ( - - {RenderRow} - - ) : ( -
    No data
    - )} + + {({ onItemsRendered, ref }) => ( + + {RenderRow} + + )} +
    ); @@ -189,9 +166,6 @@ const getStyles = (theme: GrafanaTheme2) => { table: css` width: 100%; `, - tableBody: css` - overflow: 'hidden auto'; - `, cellIcon: css` display: flex; align-items: center; @@ -210,7 +184,7 @@ const getStyles = (theme: GrafanaTheme2) => { `, headerRow: css` background-color: ${theme.colors.background.secondary}; - height: 36px; + height: ${HEADER_HEIGHT}px; align-items: center; `, rowContainer: css` @@ -218,6 +192,12 @@ const getStyles = (theme: GrafanaTheme2) => { &:hover { background-color: ${rowHoverBg}; } + + &:not(:hover) div[role='cell'] { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } `, typeIcon: css` margin-left: 5px; diff --git a/public/app/features/search/page/components/columns.tsx b/public/app/features/search/page/components/columns.tsx index 7abdc5aea81..e5ea9f5ff79 100644 --- a/public/app/features/search/page/components/columns.tsx +++ b/public/app/features/search/page/components/columns.tsx @@ -1,39 +1,43 @@ +import { css, cx } from '@emotion/css'; import React from 'react'; import SVG from 'react-inlinesvg'; -import { DataFrameView, DataSourceRef, Field } from '@grafana/data'; +import { Field } from '@grafana/data'; import { config, getDataSourceSrv } from '@grafana/runtime'; import { Checkbox, Icon, IconName, TagList } from '@grafana/ui'; import { DefaultCell } from '@grafana/ui/src/components/Table/DefaultCell'; -import { LocationInfo } from '../../service'; +import { QueryResponse, SearchResultMeta } from '../../service'; import { SelectionChecker, SelectionToggle } from '../selection'; -import { FieldAccess, TableColumn } from './SearchResultsTable'; +import { TableColumn } from './SearchResultsTable'; + +const TYPE_COLUMN_WIDTH = 130; +const DATASOURCE_COLUMN_WIDTH = 200; +const LOCATION_COLUMN_WIDTH = 200; +const TAGS_COLUMN_WIDTH = 200; export const generateColumns = ( - data: DataFrameView, - isDashboardList: boolean, + response: QueryResponse, availableWidth: number, selection: SelectionChecker | undefined, selectionToggle: SelectionToggle | undefined, styles: { [key: string]: string }, - tags: string[], - onTagFilterChange: (tags: string[]) => void, - onDatasourceChange: (datasource?: string) => void + onTagSelected: (tag: string) => void, + onDatasourceChange?: (datasource?: string) => void ): TableColumn[] => { const columns: TableColumn[] = []; - const uidField = data.fields.uid!; - const kindField = data.fields.kind!; - const access = data.fields; + const access = response.view.fields; + const uidField = access.uid; + const kindField = access.kind; - availableWidth -= 8; // ??? let width = 50; if (selection && selectionToggle) { width = 30; columns.push({ id: `column-checkbox`, + width, Header: () => (
    ), - width, Cell: (p) => { const uid = uidField.values.get(p.row.index); const kind = kindField ? kindField.values.get(p.row.index) : 'dashboard'; // HACK for now @@ -71,55 +74,33 @@ export const generateColumns = ( } // Name column - width = Math.max(availableWidth * 0.2, 200); + width = Math.max(availableWidth * 0.2, 300); columns.push({ - Cell: DefaultCell, + Cell: (p) => { + const name = access.name.values.get(p.row.index); + return ( +
    + {name} +
    + ); + }, id: `column-name`, field: access.name!, Header: 'Name', - accessor: (row: any, i: number) => { - const name = access.name!.values.get(i); - return name; - }, width, }); availableWidth -= width; - const TYPE_COLUMN_WIDTH = 130; - const DATASOURCE_COLUMN_WIDTH = 200; - const INFO_COLUMN_WIDTH = 100; - const LOCATION_COLUMN_WIDTH = 200; - const TAGS_COLUMN_WIDTH = 200; - width = TYPE_COLUMN_WIDTH; - if (isDashboardList) { - columns.push({ - Cell: DefaultCell, - id: `column-type`, - field: access.name!, - Header: 'Type', - accessor: (row: any, i: number) => { - return ( -
    - - Dashboard -
    - ); - }, - width, - }); - availableWidth -= width; - } else { - columns.push(makeTypeColumn(access.kind, access.type, width, styles.typeText, styles.typeIcon)); - availableWidth -= width; - } + columns.push(makeTypeColumn(access.kind, access.panel_type, width, styles.typeText, styles.typeIcon)); + availableWidth -= width; // Show datasources if we have any - if (access.datasource && hasFieldValue(access.datasource)) { + if (access.ds_uid && onDatasourceChange) { width = DATASOURCE_COLUMN_WIDTH; columns.push( makeDataSourceColumn( - access.datasource, + access.ds_uid, width, styles.typeIcon, styles.datasourceItem, @@ -131,71 +112,51 @@ export const generateColumns = ( } // Show tags if we have any - if (access.tags && hasFieldValue(access.tags)) { + if (access.tags) { width = TAGS_COLUMN_WIDTH; - columns.push(makeTagsColumn(access.tags, width, styles.tagList, tags, onTagFilterChange)); + columns.push(makeTagsColumn(access.tags, width, styles.tagList, onTagSelected)); availableWidth -= width; } - if (isDashboardList) { - width = Math.max(availableWidth, INFO_COLUMN_WIDTH); + width = Math.max(availableWidth, LOCATION_COLUMN_WIDTH); + const meta = response.view.dataFrame.meta?.custom as SearchResultMeta; + if (meta?.locationInfo) { columns.push({ - Cell: DefaultCell, - id: `column-info`, - field: access.url!, - Header: 'Info', - accessor: (row: any, i: number) => { - const panelCount = access.panelCount?.values.get(i); - return
    {panelCount != null && Panels: {panelCount}}
    ; + Cell: (p) => { + const parts = (access.location?.values.get(p.row.index) ?? '').split('/'); + return ( +
    + ); }, - width: width, - }); - } else { - width = Math.max(availableWidth, LOCATION_COLUMN_WIDTH); - columns.push({ - Cell: DefaultCell, id: `column-location`, field: access.location ?? access.url, Header: 'Location', - accessor: (row: any, i: number) => { - const location = access.location?.values.get(i) as LocationInfo[]; - if (location) { - return ( -
    - {location.map((v, id) => ( - { - e.preventDefault(); - alert('CLICK: ' + v.name); - }} - > - {v.name} - - ))} -
    - ); - } - return null; - }, - width: width, + width, }); } return columns; }; -function hasFieldValue(field: Field): boolean { - for (let i = 0; i < field.values.length; i++) { - const v = field.values.get(i); - if (v && v.length) { - return true; - } - } - return false; -} - function getIconForKind(v: string): IconName { if (v === 'dashboard') { return 'apps'; @@ -207,52 +168,51 @@ function getIconForKind(v: string): IconName { } function makeDataSourceColumn( - field: Field, + field: Field, width: number, iconClass: string, datasourceItemClass: string, invalidDatasourceItemClass: string, onDatasourceChange: (datasource?: string) => void ): TableColumn { + const srv = getDataSourceSrv(); return { - Cell: DefaultCell, id: `column-datasource`, field, Header: 'Data source', - accessor: (row: any, i: number) => { - const dslist = field.values.get(i); - if (dslist?.length) { - const srv = getDataSourceSrv(); - return ( -
    - {dslist.map((v, i) => { - const settings = srv.getInstanceSettings(v); - const icon = settings?.meta?.info?.logos?.small; - if (icon) { - return ( - { - e.stopPropagation(); - e.preventDefault(); - onDatasourceChange(settings.uid); - }} - > - - {settings.name} - - ); - } + Cell: (p) => { + const dslist = field.values.get(p.row.index); + if (!dslist?.length) { + return null; + } + return ( +
    + {dslist.map((v, i) => { + const settings = srv.getInstanceSettings(v); + const icon = settings?.meta?.info?.logos?.small; + if (icon) { return ( - - {v.type} + { + e.stopPropagation(); + e.preventDefault(); + onDatasourceChange(settings.uid); + }} + > + + {settings.name} ); - })} -
    - ); - } - return null; + } + return ( + + {v} + + ); + })} +
    + ); }, width, }; @@ -303,7 +263,6 @@ function makeTypeColumn( break; } } - return (
    @@ -319,27 +278,23 @@ function makeTagsColumn( field: Field, width: number, tagListClass: string, - currentTagFilter: string[], - onTagFilterChange: (tags: string[]) => void + onTagSelected: (tag: string) => void ): TableColumn { - const updateTagFilter = (tag: string) => { - if (!currentTagFilter.includes(tag)) { - onTagFilterChange([...currentTagFilter, tag]); - } - }; - return { - Cell: DefaultCell, - id: `column-tags`, - field: field, - Header: 'Tags', - accessor: (row: any, i: number) => { - const tags = field.values.get(i); + Cell: (p) => { + const tags = field.values.get(p.row.index); if (tags) { - return ; + return ( +
    + +
    + ); } return null; }, + id: `column-tags`, + field: field, + Header: 'Tags', width, }; } diff --git a/public/app/features/search/service/backend.ts b/public/app/features/search/service/backend.ts deleted file mode 100644 index bd391ef95f7..00000000000 --- a/public/app/features/search/service/backend.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { lastValueFrom } from 'rxjs'; - -import { - ArrayVector, - DataFrame, - DataFrameType, - DataFrameView, - Field, - FieldType, - getDisplayProcessor, - Vector, -} from '@grafana/data'; -import { config, getDataSourceSrv } from '@grafana/runtime'; -import { TermCount } from 'app/core/components/TagFilter/TagFilter'; -import { GrafanaDatasource } from 'app/plugins/datasource/grafana/datasource'; -import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types'; - -import { QueryFilters } from './types'; - -import { QueryResult } from '.'; - -// The raw restuls from query server -export interface RawIndexData { - folder?: DataFrame; - dashboard?: DataFrame; - panel?: DataFrame; -} - -export type rawIndexSupplier = () => Promise; - -export async function getRawIndexData(): Promise { - const ds = (await getDataSourceSrv().get('-- Grafana --')) as GrafanaDatasource; - const rsp = await lastValueFrom( - ds.query({ - targets: [ - { refId: 'A', queryType: GrafanaQueryType.Search }, // gets all data - ], - } as any) - ); - - const data: RawIndexData = {}; - for (const f of rsp.data) { - const frame = f as DataFrame; - for (const field of frame.fields) { - // Parse tags/ds from JSON string - if (field.name === 'tags' || field.name === 'datasource') { - const values = field.values.toArray().map((v) => { - if (v?.length) { - try { - const arr = JSON.parse(v); - return arr.length ? arr : undefined; - } catch {} - } - return undefined; - }); - field.type = FieldType.other; // []string - field.values = new ArrayVector(values); - } - - field.display = getDisplayProcessor({ field, theme: config.theme2 }); - } - frame.meta = { - type: DataFrameType.DirectoryListing, - }; - - switch (frame.name) { - case 'dashboards': - data.dashboard = frame; - break; - case 'panels': - data.panel = frame; - break; - case 'folders': - data.folder = frame; - break; - } - } - return data; -} - -export function buildStatsTable(field?: Field): DataFrame { - if (!field) { - return { length: 0, fields: [] }; - } - - const counts = new Map(); - for (let i = 0; i < field.values.length; i++) { - const k = field.values.get(i); - const v = counts.get(k) ?? 0; - counts.set(k, v + 1); - } - - // Sort largest first - counts[Symbol.iterator] = function* () { - yield* [...this.entries()].sort((a, b) => b[1] - a[1]); - }; - - const keys: any[] = []; - const vals: number[] = []; - - for (let [k, v] of counts) { - keys.push(k); - vals.push(v); - } - - return { - fields: [ - { ...field, values: new ArrayVector(keys) }, - { name: 'Count', type: FieldType.number, values: new ArrayVector(vals), config: {} }, - ], - length: keys.length, - }; -} - -export function getTermCounts(field?: Field): TermCount[] { - if (!field) { - return []; - } - - const counts = new Map(); - for (let i = 0; i < field.values.length; i++) { - const k = field.values.get(i); - if (k == null || !k.length) { - continue; - } - if (Array.isArray(k)) { - for (const sub of k) { - const v = counts.get(sub) ?? 0; - counts.set(sub, v + 1); - } - } else { - const v = counts.get(k) ?? 0; - counts.set(k, v + 1); - } - } - - // Sort largest first - counts[Symbol.iterator] = function* () { - yield* [...this.entries()].sort((a, b) => b[1] - a[1]); - }; - - const terms: TermCount[] = []; - for (let [term, count] of counts) { - terms.push({ - term, - count, - }); - } - - return terms; -} - -export function filterFrame(frame: DataFrame, filter?: QueryFilters): DataFrame { - if (!filter) { - return frame; - } - const view = new DataFrameView(frame); - const keep: number[] = []; - - const ds = filter.datasource ? view.fields.datasource : undefined; - const tags = filter.tags?.length ? view.fields.tags : undefined; - - let ok = true; - for (let i = 0; i < view.length; i++) { - ok = true; - - if (tags) { - const v = tags.values.get(i); - if (!v) { - ok = false; - } else { - for (const t of filter.tags!) { - if (!v.includes(t)) { - ok = false; - break; - } - } - } - } - - if (ok && ds && filter.datasource) { - ok = false; - const v = ds.values.get(i); - if (v) { - for (const d of v) { - if (d.uid === filter.datasource) { - ok = true; - break; - } - } - } - } - - if (ok) { - keep.push(i); - } - } - - return { - meta: frame.meta, - name: frame.name, - fields: frame.fields.map((f) => ({ ...f, values: filterValues(keep, f.values) })), - length: keep.length, - }; -} - -function filterValues(keep: number[], raw: Vector): Vector { - const values = new Array(keep.length); - for (let i = 0; i < keep.length; i++) { - values[i] = raw.get(keep[i]); - } - return new ArrayVector(values); -} diff --git a/public/app/features/search/service/bluge.ts b/public/app/features/search/service/bluge.ts new file mode 100644 index 00000000000..0104d163e5f --- /dev/null +++ b/public/app/features/search/service/bluge.ts @@ -0,0 +1,133 @@ +import { lastValueFrom } from 'rxjs'; + +import { ArrayVector, DataFrame, DataFrameView, getDisplayProcessor } from '@grafana/data'; +import { config, getDataSourceSrv } from '@grafana/runtime'; +import { TermCount } from 'app/core/components/TagFilter/TagFilter'; +import { GrafanaDatasource } from 'app/plugins/datasource/grafana/datasource'; +import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types'; + +import { DashboardQueryResult, GrafanaSearcher, QueryResponse, SearchQuery, SearchResultMeta } from '.'; + +export class BlugeSearcher implements GrafanaSearcher { + async search(query: SearchQuery): Promise { + if (query.facet?.length) { + throw 'facets not supported!'; + } + return doSearchQuery(query); + } + + async list(location: string): Promise { + return doSearchQuery({ query: `list:${location ?? ''}` }); + } + + async tags(query: SearchQuery): Promise { + const ds = (await getDataSourceSrv().get('-- Grafana --')) as GrafanaDatasource; + const target = { + ...query, + refId: 'A', + queryType: GrafanaQueryType.Search, + query: query.query ?? '*', + facet: [{ field: 'tag' }], + limit: 1, // 0 would be better, but is ignored by the backend + }; + + const data = ( + await lastValueFrom( + ds.query({ + targets: [target], + } as any) + ) + ).data as DataFrame[]; + for (const frame of data) { + if (frame.fields[0].name === 'tag') { + return getTermCountsFrom(frame); + } + } + return []; + } +} + +const firstPageSize = 50; +const nextPageSizes = 100; + +export async function doSearchQuery(query: SearchQuery): Promise { + const ds = (await getDataSourceSrv().get('-- Grafana --')) as GrafanaDatasource; + const target = { + ...query, + refId: 'A', + queryType: GrafanaQueryType.Search, + query: query.query ?? '*', + limit: firstPageSize, + }; + const rsp = await lastValueFrom( + ds.query({ + targets: [target], + } as any) + ); + + const first = (rsp.data?.[0] as DataFrame) ?? { fields: [], length: 0 }; + for (const field of first.fields) { + field.display = getDisplayProcessor({ field, theme: config.theme2 }); + } + + const meta = first.meta?.custom as SearchResultMeta; + const view = new DataFrameView(first); + return { + totalRows: meta.count ?? first.length, + view, + loadMoreItems: async (startIndex: number, stopIndex: number): Promise => { + console.log('LOAD NEXT PAGE', { startIndex, stopIndex, length: view.dataFrame.length }); + const from = view.dataFrame.length; + const limit = stopIndex - from; + if (limit < 0) { + return; + } + const frame = ( + await lastValueFrom( + ds.query({ + targets: [{ ...target, refId: 'Page', facet: undefined, from, limit: Math.max(limit, nextPageSizes) }], + } as any) + ) + ).data?.[0] as DataFrame; + + if (!frame) { + console.log('no results', frame); + return; + } + if (frame.fields.length !== view.dataFrame.fields.length) { + console.log('invalid shape', frame, view.dataFrame); + return; + } + + // Append the raw values to the same array buffer + const length = frame.length + view.dataFrame.length; + for (let i = 0; i < frame.fields.length; i++) { + const values = (view.dataFrame.fields[i].values as ArrayVector).buffer; + values.push(...frame.fields[i].values.toArray()); + } + view.dataFrame.length = length; + + // Add all the location lookup info + const submeta = frame.meta?.custom as SearchResultMeta; + if (submeta?.locationInfo && meta) { + for (const [key, value] of Object.entries(submeta.locationInfo)) { + meta.locationInfo[key] = value; + } + } + return; + }, + isItemLoaded: (index: number): boolean => { + return index < view.dataFrame.length; + }, + }; +} + +function getTermCountsFrom(frame: DataFrame): TermCount[] { + const keys = frame.fields[0].values; + const vals = frame.fields[1].values; + const counts: TermCount[] = []; + for (let i = 0; i < frame.length; i++) { + counts.push({ term: keys.get(i), count: vals.get(i) }); + } + return counts; +} diff --git a/public/app/features/search/service/minisearcher.ts b/public/app/features/search/service/minisearcher.ts deleted file mode 100644 index 194d9e90ee9..00000000000 --- a/public/app/features/search/service/minisearcher.ts +++ /dev/null @@ -1,320 +0,0 @@ -import { isArray, isString } from 'lodash'; -import MiniSearch from 'minisearch'; - -import { ArrayVector, DataFrame, DataSourceRef, Field, FieldType, getDisplayProcessor, Vector } from '@grafana/data'; -import { config } from '@grafana/runtime'; - -import { filterFrame, getRawIndexData, RawIndexData, rawIndexSupplier } from './backend'; -import { GrafanaSearcher, QueryFilters, QueryResponse } from './types'; - -import { LocationInfo } from '.'; - -export type SearchResultKind = keyof RawIndexData; - -interface InputDoc { - kind: SearchResultKind; - index: number; - - // Fields - id?: Vector; - url?: Vector; - uid?: Vector; - name?: Vector; - folder?: Vector; - description?: Vector; - dashboardID?: Vector; - location?: Vector; - datasource?: Vector; - type?: Vector; - tags?: Vector; // JSON strings? -} - -interface CompositeKey { - kind: SearchResultKind; - index: number; -} - -// This implements search in the frontend using the minisearch library -export class MiniSearcher implements GrafanaSearcher { - lookup = new Map(); - data: RawIndexData = {}; - index?: MiniSearch; - - constructor(private supplier: rawIndexSupplier = getRawIndexData) { - // waits for first request to load data - } - - private async initIndex() { - const data = await this.supplier(); - - const searcher = new MiniSearch({ - idField: '__id', - fields: ['name', 'description', 'tags', 'type', 'tags'], // fields to index for full-text search - searchOptions: { - boost: { - name: 3, - description: 1, - }, - // boost dashboard matches first - boostDocument: (documentId: any, term: string) => { - const kind = documentId.kind; - if (kind === 'dashboard') { - return 1.4; - } - if (kind === 'folder') { - return 1.2; - } - return 1; - }, - prefix: true, - fuzzy: (term) => (term.length > 4 ? 0.2 : false), - }, - extractField: (doc, name) => { - // return a composite key for the id - if (name === '__id') { - return { - kind: doc.kind, - index: doc.index, - } as any; - } - const values = (doc as any)[name] as Vector; - if (!values) { - return ''; - } - const value = values.get(doc.index); - if (isString(value)) { - return value as string; - } - if (isArray(value)) { - return value.join(' '); - } - return JSON.stringify(value); - }, - }); - - const lookup = new Map(); - for (const [key, frame] of Object.entries(data)) { - const kind = key as SearchResultKind; - const input = getInputDoc(kind, frame); - lookup.set(kind, input); - for (let i = 0; i < frame.length; i++) { - input.index = i; - searcher.add(input); - } - } - - // Construct the URL field for each panel - const folderIDToIndex = new Map(); - const folder = lookup.get('folder'); - const dashboard = lookup.get('dashboard'); - const panel = lookup.get('panel'); - if (folder?.id) { - for (let i = 0; i < folder.id?.length; i++) { - folderIDToIndex.set(folder.id.get(i), i); - } - } - - if (dashboard?.id && panel?.dashboardID && dashboard.url) { - let location: LocationInfo[][] = new Array(dashboard.id.length); - const dashIDToIndex = new Map(); - for (let i = 0; i < dashboard.id?.length; i++) { - dashIDToIndex.set(dashboard.id.get(i), i); - const folderId = dashboard.folder?.get(i); - if (folderId != null) { - const index = folderIDToIndex.get(folderId); - const name = folder?.name?.get(index!); - if (name) { - location[i] = [ - { - kind: 'folder', - name, - }, - ]; - } - } - } - dashboard.location = new ArrayVector(location); // folder name - - location = new Array(panel.dashboardID.length); - const urls: string[] = new Array(location.length); - for (let i = 0; i < panel.dashboardID.length; i++) { - const dashboardID = panel.dashboardID.get(i); - const index = dashIDToIndex.get(dashboardID); - if (index != null) { - const idx = panel.id?.get(i); - urls[i] = dashboard.url.get(index) + '?viewPanel=' + idx; - - const parent = dashboard.location.get(index) ?? []; - const name = dashboard.name?.get(index) ?? '?'; - location[i] = [...parent, { kind: 'dashboard', name }]; - } - } - panel.url = new ArrayVector(urls); - panel.location = new ArrayVector(location); - } - - this.index = searcher; - this.data = data; - this.lookup = lookup; - } - - async search(query: string, filter?: QueryFilters): Promise { - if (!this.index) { - await this.initIndex(); - } - - // empty query can return everything - if (!query && this.data.dashboard) { - return { - body: filterFrame(this.data.dashboard, filter), - }; - } - - const found = this.index!.search(query); - - // frame fields - const uid: string[] = []; - const url: string[] = []; - const kind: string[] = []; - const type: string[] = []; - const name: string[] = []; - const tags: string[][] = []; - const location: LocationInfo[][] = []; - const datasource: DataSourceRef[][] = []; - const info: any[] = []; - const score: number[] = []; - - for (const res of found) { - const key = res.id as CompositeKey; - const index = key.index; - const input = this.lookup.get(key.kind); - if (!input) { - continue; - } - - if (filter && !shouldKeep(filter, input, index)) { - continue; - } - - uid.push(input.uid?.get(index)!); - url.push(input.url?.get(index) ?? '?'); - location.push(input.location?.get(index) as any); - datasource.push(input.datasource?.get(index) as any); - tags.push(input.tags?.get(index) as any); - kind.push(key.kind); - name.push(input.name?.get(index) ?? '?'); - type.push(input.type?.get(index)!); - info.push(res.match); // ??? - score.push(res.score); - } - const fields: Field[] = [ - { name: 'uid', config: {}, type: FieldType.string, values: new ArrayVector(uid) }, - { name: 'kind', config: {}, type: FieldType.string, values: new ArrayVector(kind) }, - { name: 'name', config: {}, type: FieldType.string, values: new ArrayVector(name) }, - { - name: 'url', - config: {}, - type: FieldType.string, - values: new ArrayVector(url), - }, - { name: 'type', config: {}, type: FieldType.string, values: new ArrayVector(type) }, - { name: 'info', config: {}, type: FieldType.other, values: new ArrayVector(info) }, - { name: 'tags', config: {}, type: FieldType.other, values: new ArrayVector(tags) }, - { name: 'location', config: {}, type: FieldType.other, values: new ArrayVector(location) }, - { name: 'datasource', config: {}, type: FieldType.other, values: new ArrayVector(datasource) }, - { name: 'score', config: {}, type: FieldType.number, values: new ArrayVector(score) }, - ]; - for (const field of fields) { - field.display = getDisplayProcessor({ field, theme: config.theme2 }); - } - return { - body: { - fields, - length: kind.length, - }, - }; - } -} - -function shouldKeep(filter: QueryFilters, doc: InputDoc, index: number): boolean { - if (filter.tags) { - const tags = doc.tags?.get(index); - if (!tags?.length) { - return false; - } - for (const t of filter.tags) { - if (!tags.includes(t)) { - return false; - } - } - } - - let keep = true; - // Any is OK - if (filter.datasource) { - keep = false; - const dss = doc.datasource?.get(index); - if (dss) { - for (const ds of dss) { - if (ds.uid === filter.datasource) { - keep = true; - break; - } - } - } - } - return keep; -} - -function getInputDoc(kind: SearchResultKind, frame: DataFrame): InputDoc { - const input: InputDoc = { - kind, - index: 0, - }; - for (const field of frame.fields) { - switch (field.name) { - case 'name': - case 'Name': - input.name = field.values; - break; - case 'Description': - case 'Description': - input.description = field.values; - break; - case 'url': - case 'URL': - input.url = field.values; - break; - case 'uid': - case 'UID': - input.uid = field.values; - break; - case 'id': - case 'ID': - input.id = field.values; - break; - case 'Tags': - case 'tags': - input.tags = field.values; - break; - case 'DashboardID': - case 'dashboardID': - input.dashboardID = field.values; - break; - case 'Type': - case 'type': - input.type = field.values; - break; - case 'folderID': - case 'FolderID': - input.folder = field.values; - break; - case 'datasource': - case 'dsList': - case 'DSList': - input.datasource = field.values; - break; - } - } - return input; -} diff --git a/public/app/features/search/service/searcher.test.ts b/public/app/features/search/service/searcher.test.ts deleted file mode 100644 index 26742a1bc3e..00000000000 --- a/public/app/features/search/service/searcher.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { toDataFrame } from '@grafana/data'; - -import { rawIndexSupplier } from './backend'; -import { MiniSearcher } from './minisearcher'; - -jest.mock('@grafana/data', () => ({ - ...jest.requireActual('@grafana/data'), - getDisplayProcessor: jest - .fn() - .mockName('mockedGetDisplayProcesser') - .mockImplementation(() => ({})), -})); - -describe('simple search', () => { - it('should support frontend search', async () => { - const supplier: rawIndexSupplier = () => - Promise.resolve({ - dashboard: toDataFrame([ - { Name: 'A name (dash)', Description: 'A descr (dash)' }, - { Name: 'B name (dash)', Description: 'B descr (dash)' }, - ]), - panel: toDataFrame([ - { Name: 'A name (panels)', Description: 'A descr (panels)' }, - { Name: 'B name (panels)', Description: 'B descr (panels)' }, - ]), - }); - - const searcher = new MiniSearcher(supplier); - let results = await searcher.search('name'); - expect(results.body.fields[2].values.toArray()).toMatchInlineSnapshot(` - Array [ - "A name (dash)", - "B name (dash)", - "A name (panels)", - "B name (panels)", - ] - `); - - results = await searcher.search('B'); - expect(results.body.fields[2].values.toArray()).toMatchInlineSnapshot(` - Array [ - "B name (dash)", - "B name (panels)", - ] - `); - - // All fields must have display set - for (const field of results.body.fields) { - expect(field.display).toBeDefined(); - } - - // Empty search has defined values - results = await searcher.search(''); - expect(results.body.fields.length).toBeGreaterThan(0); - }); -}); diff --git a/public/app/features/search/service/searcher.ts b/public/app/features/search/service/searcher.ts index a1cea005ade..ff6675339d6 100644 --- a/public/app/features/search/service/searcher.ts +++ b/public/app/features/search/service/searcher.ts @@ -1,11 +1,11 @@ -import { MiniSearcher } from './minisearcher'; +import { BlugeSearcher } from './bluge'; import { GrafanaSearcher } from './types'; let searcher: GrafanaSearcher | undefined = undefined; export function getGrafanaSearcher(): GrafanaSearcher { if (!searcher) { - searcher = new MiniSearcher(); + searcher = new BlugeSearcher(); } return searcher!; } diff --git a/public/app/features/search/service/types.ts b/public/app/features/search/service/types.ts index 51ee071a1b3..adff98d3467 100644 --- a/public/app/features/search/service/types.ts +++ b/public/app/features/search/service/types.ts @@ -1,32 +1,67 @@ -import { DataFrame, DataSourceRef } from '@grafana/data'; +import { DataFrameView } from '@grafana/data'; +import { TermCount } from 'app/core/components/TagFilter/TagFilter'; -export interface QueryResult { +export interface FacetField { + field: string; + count?: number; +} + +export interface SearchQuery { + query?: string; + location?: string; + sort?: string; + ds_uid?: string; + tags?: string[]; + kind?: string[]; + uid?: string[]; + id?: number[]; + facet?: FacetField[]; + explain?: boolean; + accessInfo?: boolean; + hasPreview?: string; // theme + limit?: number; + from?: number; +} + +export interface DashboardQueryResult { kind: string; // panel, dashboard, folder name: string; uid: string; - description?: string; url: string; // link to value (unique) - tags?: string[]; - location?: LocationInfo[]; // the folder name - datasource?: DataSourceRef[]; + panel_type: string; + tags: string[]; + location: string; // url that can be split + ds_uid: string[]; score?: number; } export interface LocationInfo { - kind: 'folder' | 'dashboard'; + kind: string; name: string; + url: string; } -export interface QueryFilters { - kind?: string; // limit to a single type - tags?: string[]; // match all tags - datasource?: string; // limit to a single datasource +export interface SearchResultMeta { + count: number; + max_score: number; + locationInfo: Record; } export interface QueryResponse { - body: DataFrame; + view: DataFrameView; + + /** Supports lazy loading. This will mutate the `view` object above, adding rows as needed */ + loadMoreItems: (startIndex: number, stopIndex: number) => Promise; + + /** Checks if a row in the view needs to be added */ + isItemLoaded: (index: number) => boolean; + + /** the total query results size */ + totalRows: number; } export interface GrafanaSearcher { - search: (query: string, filter?: QueryFilters) => Promise; + search: (query: SearchQuery) => Promise; + list: (location: string) => Promise; + tags: (query: SearchQuery) => Promise; } diff --git a/public/app/plugins/datasource/grafana/components/QueryEditor.tsx b/public/app/plugins/datasource/grafana/components/QueryEditor.tsx index 50c09bf0efa..a561c21eb63 100644 --- a/public/app/plugins/datasource/grafana/components/QueryEditor.tsx +++ b/public/app/plugins/datasource/grafana/components/QueryEditor.tsx @@ -9,7 +9,8 @@ import { DataFrame, } from '@grafana/data'; import { config, getBackendSrv, getDataSourceSrv } from '@grafana/runtime'; -import { InlineField, Select, Alert, Input, InlineFieldRow } from '@grafana/ui'; +import { InlineField, Select, Alert, Input, InlineFieldRow, CodeEditor } from '@grafana/ui'; +import { SearchQuery } from 'app/features/search/service'; import { GrafanaDatasource } from '../datasource'; import { defaultQuery, GrafanaQuery, GrafanaQueryType } from '../types'; @@ -351,20 +352,69 @@ export class QueryEditor extends PureComponent { this.checkAndUpdateValue('query', e.target.value); }; + onSaveSearchJSON = (rawSearchJSON: string) => { + try { + const json = JSON.parse(rawSearchJSON) as GrafanaQuery; + json.queryType = GrafanaQueryType.Search; + this.props.onChange(json); + this.props.onRunQuery(); + } catch (ex) { + console.log('UNABLE TO parse search', rawSearchJSON, ex); + } + }; + renderSearch() { - let { query } = this.props.query; + let query = (this.props.query ?? {}) as SearchQuery; + const emptySearchQuery: SearchQuery = { + query: '*', + location: '', // general, etc + ds_uid: '', + sort: 'score desc', + tags: [], + kind: ['dashboard', 'folder'], + uid: [], + id: [], + explain: true, + accessInfo: true, + facet: [{ field: 'kind' }, { field: 'tag' }, { field: 'location' }], + hasPreview: 'dark', + from: 0, + limit: 20, + }; + + const json = JSON.stringify(query ?? {}, null, 2); + for (const [key, val] of Object.entries(emptySearchQuery)) { + if ((query as any)[key] == null) { + (query as any)[key] = val; + } + } + return ( - - - - - + <> + + This interface to the grafana search API is experimental, and subject to change at any time without notice + + + + + + + + ); } diff --git a/public/app/plugins/datasource/grafana/types.ts b/public/app/plugins/datasource/grafana/types.ts index 694f3197413..1773d251f97 100644 --- a/public/app/plugins/datasource/grafana/types.ts +++ b/public/app/plugins/datasource/grafana/types.ts @@ -23,7 +23,7 @@ export interface GrafanaQuery extends DataQuery { buffer?: number; path?: string; // for list and read query?: string; // for query endpoint -} +} // NOTE, query will have more field!!! export const defaultQuery: GrafanaQuery = { refId: 'A', diff --git a/yarn.lock b/yarn.lock index dbe448494a5..bf108f066f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10879,7 +10879,17 @@ __metadata: languageName: node linkType: hard -"@types/react-window@npm:1.8.5": +"@types/react-window-infinite-loader@npm:^1": + version: 1.0.6 + resolution: "@types/react-window-infinite-loader@npm:1.0.6" + dependencies: + "@types/react": "*" + "@types/react-window": "*" + checksum: d4648dfb44614e4f0137d7b77eb1868b0c5252f451a78edfc4520e508157ce7687d4b7d9efd6df8f01e72e0d92224338b8c8d934220f32a3081b528599a25829 + languageName: node + linkType: hard + +"@types/react-window@npm:*, @types/react-window@npm:1.8.5": version: 1.8.5 resolution: "@types/react-window@npm:1.8.5" dependencies: @@ -20969,6 +20979,7 @@ __metadata: "@types/react-transition-group": 4.4.4 "@types/react-virtualized-auto-sizer": 1.0.1 "@types/react-window": 1.8.5 + "@types/react-window-infinite-loader": ^1 "@types/redux-mock-store": 1.0.3 "@types/reselect": 2.2.0 "@types/semver": 7.3.9 @@ -21067,7 +21078,6 @@ __metadata: lru-cache: 7.9.0 memoize-one: 6.0.0 mini-css-extract-plugin: 2.6.0 - minisearch: 5.0.0-beta1 moment: 2.29.2 moment-timezone: 0.5.34 monaco-editor: ^0.31.1 @@ -21122,6 +21132,7 @@ __metadata: react-use: 17.3.2 react-virtualized-auto-sizer: 1.0.6 react-window: 1.8.6 + react-window-infinite-loader: ^1.0.7 redux: 4.1.2 redux-mock-store: 1.5.4 redux-thunk: 2.4.1 @@ -26632,13 +26643,6 @@ __metadata: languageName: node linkType: hard -"minisearch@npm:5.0.0-beta1": - version: 5.0.0-beta1 - resolution: "minisearch@npm:5.0.0-beta1" - checksum: 7c5ba8b2d1b52df0724e69183306b4204ae4cdc0102813da7f12a8f90a7b4efe7add4c54814ec1bb63f8bd5be599373af9055e0d68ceeef3d225f0a2c7637699 - languageName: node - linkType: hard - "minizlib@npm:^1.3.3": version: 1.3.3 resolution: "minizlib@npm:1.3.3" @@ -31894,6 +31898,16 @@ __metadata: languageName: node linkType: hard +"react-window-infinite-loader@npm:^1.0.7": + version: 1.0.7 + resolution: "react-window-infinite-loader@npm:1.0.7" + peerDependencies: + react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 + react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 + checksum: 5c11a8958274f79a44672e9b76ca9bfe2ba09d9f6d1c747827f0a2c3da96e423bb059d033ffd866efa94ed8989b405ee7b9c002b11281ca0697eb7ed73caf85e + languageName: node + linkType: hard + "react-window@npm:1.8.6": version: 1.8.6 resolution: "react-window@npm:1.8.6" From 906484b809a1e4df219fc7dd3b7fe8c34e31fda9 Mon Sep 17 00:00:00 2001 From: selvavm Date: Thu, 12 May 2022 05:46:41 +0530 Subject: [PATCH 126/440] Transformation: Added variance and standard deviation (#48844) * Transformation: Added variance and standard deviation for sample and population. Modified mean calculation approach * Transformation: Removed existing mean calculation * Transformation: Added testcases for variance and Standard deviation * Update docs/sources/panels/calculation-types.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> --- docs/sources/panels/calculation-types.md | 42 ++++++++------- .../src/transformations/fieldReducer.test.ts | 15 +++++- .../src/transformations/fieldReducer.ts | 52 ++++++++++++++++++- 3 files changed, 88 insertions(+), 21 deletions(-) diff --git a/docs/sources/panels/calculation-types.md b/docs/sources/panels/calculation-types.md index 4e586af014e..df2198d7fab 100644 --- a/docs/sources/panels/calculation-types.md +++ b/docs/sources/panels/calculation-types.md @@ -6,23 +6,27 @@ weight = 1100 # Reference: Calculation types -Refer to the following list of calculations you can perform in Grafana. You can find these calculations in the **Transform** tab and in the bar gauge, gauge, and stat visualizations. +You can perform the following calculations in Grafana. Navigate to the **Transform** tab and in the bar gauge, gauge, and stat visualizations. -| Calculation | Description | -| :----------------- | :-------------------------------------------------------- | -| All nulls | True when all values are null | -| All zeros | True when all values are 0 | -| Change count | Number of times the field's value changes | -| Count | Number of values in a field | -| Delta | Cumulative change in value, only counts increments | -| Difference | Difference between first and last value of a field | -| Difference percent | Percentage change between first and last value of a field | -| Distinct count | Number of unique values in a field | -| First (not null) | First, not null value in a field | -| Max | Maximum value of a field | -| Mean | Mean value of all values in a field | -| Min | Minimum value of a field | -| Min (above zero) | Minimum, positive value of a field | -| Range | Difference between maximum and minimum values of a field | -| Step | Minimal interval between values of a field | -| Total | Sum of all values in a field | +| Calculation | Description | +| :------------------------------ | :---------------------------------------------------------------- | +| All nulls | True when all values are null | +| All zeros | True when all values are 0 | +| Change count | Number of times the field's value changes | +| Count | Number of values in a field | +| Delta | Cumulative change in value, only counts increments | +| Difference | Difference between first and last value of a field | +| Difference percent | Percentage change between first and last value of a field | +| Distinct count | Number of unique values in a field | +| First (not null) | First, not null value in a field | +| Max | Maximum value of a field | +| Mean | Mean value of all values in a field | +| Variance (Population) | Variance (based on population) of all values in a field | +| Standard deviation (Population) | Standard deviation (based on population) of all values in a field | +| Variance (Sample) | Variance (based on sample) of all values in a field | +| Standard deviation (Sample) | Standard deviation (based on sample) of all values in a field | +| Min | Minimum value of a field | +| Min (above zero) | Minimum, positive value of a field | +| Range | Difference between maximum and minimum values of a field | +| Step | Minimal interval between values of a field | +| Total | Sum of all values in a field | diff --git a/packages/grafana-data/src/transformations/fieldReducer.test.ts b/packages/grafana-data/src/transformations/fieldReducer.test.ts index 34de7c94f90..63bc01a1af8 100644 --- a/packages/grafana-data/src/transformations/fieldReducer.test.ts +++ b/packages/grafana-data/src/transformations/fieldReducer.test.ts @@ -56,13 +56,26 @@ describe('Stats Calculators', () => { it('should calculate basic stats', () => { const stats = reduceField({ field: basicTable.fields[0], - reducers: ['first', 'last', 'mean', 'count'], + reducers: [ + 'first', + 'last', + 'mean', + 'count', + 'Variance (Population)', + 'Variance (Sample)', + 'Standard deviation (Population)', + 'Standard deviation (Sample)', + ], }); expect(stats.first).toEqual(10); expect(stats.last).toEqual(20); expect(stats.mean).toEqual(15); expect(stats.count).toEqual(2); + expect(stats.variancePopulation).toEqual(25); + expect(stats.varianceSample).toEqual(50); + expect(stats.stddevPopulation).toEqual(5); + expect(stats.stddevSample).toBeCloseTo(7.0710678, 5); }); it('should support a single stat also', () => { diff --git a/packages/grafana-data/src/transformations/fieldReducer.ts b/packages/grafana-data/src/transformations/fieldReducer.ts index c593ac47346..79d76dea190 100644 --- a/packages/grafana-data/src/transformations/fieldReducer.ts +++ b/packages/grafana-data/src/transformations/fieldReducer.ts @@ -10,6 +10,10 @@ export enum ReducerID { min = 'min', logmin = 'logmin', mean = 'mean', + variancePopulation = 'variancePopulation', + stddevPopulation = 'stddevPopulation', + varianceSample = 'varianceSample', + stddevSample = 'stddevSample', last = 'last', first = 'first', count = 'count', @@ -152,6 +156,30 @@ export const fieldReducers = new Registry(() => [ { id: ReducerID.min, name: 'Min', description: 'Minimum Value', standard: true }, { id: ReducerID.max, name: 'Max', description: 'Maximum Value', standard: true }, { id: ReducerID.mean, name: 'Mean', description: 'Average Value', standard: true, aliasIds: ['avg'] }, + { + id: ReducerID.variancePopulation, + name: 'Variance (Population)', + description: 'Variance (based on population) of all values in a field', + standard: true, + }, + { + id: ReducerID.stddevPopulation, + name: 'Standard deviation (Population)', + description: 'Standard deviation (based on population) of all values in a field', + standard: true, + }, + { + id: ReducerID.varianceSample, + name: 'Variance (Sample)', + description: 'Variance (based on sample) of all values in a field', + standard: true, + }, + { + id: ReducerID.stddevSample, + name: 'Standard deviation (Sample)', + description: 'Standard deviation (based on sample) of all values in a field', + standard: true, + }, { id: ReducerID.sum, name: 'Total', @@ -256,6 +284,10 @@ export function doStandardCalcs(field: Field, ignoreNulls: boolean, nullAsZero: min: Number.MAX_VALUE, logmin: Number.MAX_VALUE, mean: null, + variancePopulation: null, + stddevPopulation: null, + varianceSample: null, + stddevSample: null, last: null, first: null, lastNotNull: null, @@ -274,6 +306,8 @@ export function doStandardCalcs(field: Field, ignoreNulls: boolean, nullAsZero: previousDeltaUp: true, } as FieldCalcs; + let squareSum = 0; + const data = field.values; calcs.count = data.length; @@ -343,6 +377,10 @@ export function doStandardCalcs(field: Field, ignoreNulls: boolean, nullAsZero: if (currentValue < calcs.logmin && currentValue > 0) { calcs.logmin = currentValue; } + + let _oldMean = calcs.mean; + calcs.mean += (currentValue - _oldMean) / calcs.nonNullCount; + squareSum += (currentValue - _oldMean) * (currentValue - calcs.mean); } if (currentValue !== 0) { @@ -366,7 +404,19 @@ export function doStandardCalcs(field: Field, ignoreNulls: boolean, nullAsZero: } if (calcs.nonNullCount > 0) { - calcs.mean = calcs.sum! / calcs.nonNullCount; + calcs.variancePopulation = squareSum / calcs.nonNullCount; + } + + if (calcs.nonNullCount > 0) { + calcs.stddevPopulation = Math.sqrt(calcs.variancePopulation); + } + + if (calcs.nonNullCount > 0) { + calcs.varianceSample = squareSum / (calcs.nonNullCount - 1); + } + + if (calcs.nonNullCount > 0) { + calcs.stddevSample = Math.sqrt(calcs.varianceSample); } if (calcs.allIsNull) { From a1217ef8dada46ceaa7266f5991b470ce38be96a Mon Sep 17 00:00:00 2001 From: Adam Simpson Date: Wed, 11 May 2022 22:54:43 -0400 Subject: [PATCH 127/440] AzureMonitor: begin moving metrics query editor to use @grafana/experimental (#48878) * feat: migrate Azure Metrics Query Editor Field to experimental UI * feat: ensure tests pass with experimental feature flag enabled * feat: avoid duplicate unit tests for experimental MetricsQueryEditor --- .../components/Field.tsx | 11 +- .../MetricsQueryEditor.test.tsx | 374 +++++++++--------- .../MetricsQueryEditor.tsx | 230 ++++++++--- .../QueryEditor/QueryEditor.test.tsx | 12 + 4 files changed, 383 insertions(+), 244 deletions(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/Field.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/Field.tsx index a40992d93a6..52e2fa818bc 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/Field.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/Field.tsx @@ -1,10 +1,19 @@ import React from 'react'; +import { EditorField } from '@grafana/experimental'; +import { config } from '@grafana/runtime'; import { InlineField } from '@grafana/ui'; import { Props as InlineFieldProps } from '@grafana/ui/src/components/Forms/InlineField'; +interface Props extends InlineFieldProps { + label: string; +} + const DEFAULT_LABEL_WIDTH = 18; -export const Field = (props: InlineFieldProps) => { +export const Field = (props: Props) => { + if (config.featureToggles.azureMonitorExperimentalUI) { + return ; + } return ; }; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/MetricsQueryEditor.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/MetricsQueryEditor.test.tsx index 9fa39966131..74dd628c824 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/MetricsQueryEditor.test.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/MetricsQueryEditor.test.tsx @@ -2,6 +2,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; +import { config } from '@grafana/runtime'; import { selectOptionInTest } from '@grafana/ui'; import createMockDatasource from '../../__mocks__/datasource'; @@ -22,6 +23,15 @@ const variableOptionGroup = { options: [], }; +const tests = [ + { + id: 'azure-monitor-metrics-query-editor-with-resource-picker', + }, + { + id: 'azure-monitor-metrics-query-editor-with-experimental-ui', + }, +]; + export function createMockResourcePickerData() { const mockDatasource = new ResourcePickerData(createMockInstanceSetttings()); @@ -36,209 +46,215 @@ export function createMockResourcePickerData() { return mockDatasource; } -describe('MetricsQueryEditor', () => { - const originalScrollIntoView = window.HTMLElement.prototype.scrollIntoView; - beforeEach(() => { - window.HTMLElement.prototype.scrollIntoView = function () {}; - }); - afterEach(() => { - window.HTMLElement.prototype.scrollIntoView = originalScrollIntoView; - }); - const mockPanelData = createMockPanelData(); +for (const t of tests) { + describe(`MetricsQueryEditor: ${t.id}`, () => { + const originalScrollIntoView = window.HTMLElement.prototype.scrollIntoView; + const mockPanelData = createMockPanelData(); - it('should render', async () => { - const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); + beforeEach(() => { + window.HTMLElement.prototype.scrollIntoView = function () {}; + config.featureToggles.azureMonitorExperimentalUI = + t.id === 'azure-monitor-metrics-query-editor-with-experimental-ui'; + }); + afterEach(() => { + window.HTMLElement.prototype.scrollIntoView = originalScrollIntoView; + config.featureToggles.azureMonitorExperimentalUI = false; + }); - render( - {}} - setError={() => {}} - /> - ); + it('should render', async () => { + const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); - expect(await screen.findByTestId('azure-monitor-metrics-query-editor-with-resource-picker')).toBeInTheDocument(); - }); + render( + {}} + setError={() => {}} + /> + ); - it('should change resource when a resource is selected in the ResourcePicker', async () => { - const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); - const query = createMockQuery(); - delete query?.azureMonitor?.resourceUri; - const onChange = jest.fn(); + expect(await screen.findByTestId(t.id)).toBeInTheDocument(); + }); - render( - {}} - /> - ); + it('should change resource when a resource is selected in the ResourcePicker', async () => { + const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); + const query = createMockQuery(); + delete query?.azureMonitor?.resourceUri; + const onChange = jest.fn(); - const resourcePickerButton = await screen.findByRole('button', { name: 'Select a resource' }); - expect(resourcePickerButton).toBeInTheDocument(); - expect(screen.queryByRole('button', { name: 'Expand Primary Subscription' })).not.toBeInTheDocument(); - resourcePickerButton.click(); + render( + {}} + /> + ); - const subscriptionButton = await screen.findByRole('button', { name: 'Expand Primary Subscription' }); - expect(subscriptionButton).toBeInTheDocument(); - expect(screen.queryByRole('button', { name: 'Expand A Great Resource Group' })).not.toBeInTheDocument(); - subscriptionButton.click(); + const resourcePickerButton = await screen.findByRole('button', { name: 'Select a resource' }); + expect(resourcePickerButton).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Expand Primary Subscription' })).not.toBeInTheDocument(); + resourcePickerButton.click(); - const resourceGroupButton = await screen.findByRole('button', { name: 'Expand A Great Resource Group' }); - expect(resourceGroupButton).toBeInTheDocument(); - expect(screen.queryByLabelText('web-server')).not.toBeInTheDocument(); - resourceGroupButton.click(); + const subscriptionButton = await screen.findByRole('button', { name: 'Expand Primary Subscription' }); + expect(subscriptionButton).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Expand A Great Resource Group' })).not.toBeInTheDocument(); + subscriptionButton.click(); - const checkbox = await screen.findByLabelText('web-server'); - expect(checkbox).toBeInTheDocument(); - expect(checkbox).not.toBeChecked(); - await userEvent.click(checkbox); - expect(checkbox).toBeChecked(); - await userEvent.click(await screen.findByRole('button', { name: 'Apply' })); + const resourceGroupButton = await screen.findByRole('button', { name: 'Expand A Great Resource Group' }); + expect(resourceGroupButton).toBeInTheDocument(); + expect(screen.queryByLabelText('web-server')).not.toBeInTheDocument(); + resourceGroupButton.click(); - expect(onChange).toBeCalledTimes(1); - expect(onChange).toBeCalledWith( - expect.objectContaining({ - azureMonitor: expect.objectContaining({ - resourceUri: - '/subscriptions/def-456/resourceGroups/dev-3/providers/Microsoft.Compute/virtualMachines/web-server', - }), - }) - ); - }); + const checkbox = await screen.findByLabelText('web-server'); + expect(checkbox).toBeInTheDocument(); + expect(checkbox).not.toBeChecked(); + await userEvent.click(checkbox); + expect(checkbox).toBeChecked(); + await userEvent.click(await screen.findByRole('button', { name: 'Apply' })); - it('should reset metric namespace, metric name, and aggregation fields after selecting a new resource when a valid query has already been set', async () => { - const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); - const query = createMockQuery(); - const onChange = jest.fn(); + expect(onChange).toBeCalledTimes(1); + expect(onChange).toBeCalledWith( + expect.objectContaining({ + azureMonitor: expect.objectContaining({ + resourceUri: + '/subscriptions/def-456/resourceGroups/dev-3/providers/Microsoft.Compute/virtualMachines/web-server', + }), + }) + ); + }); - render( - {}} - /> - ); + it('should reset metric namespace, metric name, and aggregation fields after selecting a new resource when a valid query has already been set', async () => { + const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); + const query = createMockQuery(); + const onChange = jest.fn(); - const resourcePickerButton = await screen.findByRole('button', { name: /grafanastaging/ }); + render( + {}} + /> + ); - expect(screen.getByText('Microsoft.Compute/virtualMachines')).toBeInTheDocument(); - expect(screen.getByText('Metric A')).toBeInTheDocument(); - expect(screen.getByText('Average')).toBeInTheDocument(); + const resourcePickerButton = await screen.findByRole('button', { name: /grafanastaging/ }); - expect(resourcePickerButton).toBeInTheDocument(); - expect(screen.queryByRole('button', { name: 'Expand Primary Subscription' })).not.toBeInTheDocument(); - resourcePickerButton.click(); + expect(screen.getByText('Microsoft.Compute/virtualMachines')).toBeInTheDocument(); + expect(screen.getByText('Metric A')).toBeInTheDocument(); + expect(screen.getByText('Average')).toBeInTheDocument(); - const subscriptionButton = await screen.findByRole('button', { name: 'Expand Dev Subscription' }); - expect(subscriptionButton).toBeInTheDocument(); - expect(screen.queryByRole('button', { name: 'Expand Development 3' })).not.toBeInTheDocument(); - subscriptionButton.click(); + expect(resourcePickerButton).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Expand Primary Subscription' })).not.toBeInTheDocument(); + resourcePickerButton.click(); - const resourceGroupButton = await screen.findByRole('button', { name: 'Expand Development 3' }); - expect(resourceGroupButton).toBeInTheDocument(); - expect(screen.queryByLabelText('db-server')).not.toBeInTheDocument(); - resourceGroupButton.click(); + const subscriptionButton = await screen.findByRole('button', { name: 'Expand Dev Subscription' }); + expect(subscriptionButton).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Expand Development 3' })).not.toBeInTheDocument(); + subscriptionButton.click(); - const checkbox = await screen.findByLabelText('db-server'); - expect(checkbox).toBeInTheDocument(); - expect(checkbox).not.toBeChecked(); - await userEvent.click(checkbox); - expect(checkbox).toBeChecked(); - await userEvent.click(await screen.findByRole('button', { name: 'Apply' })); + const resourceGroupButton = await screen.findByRole('button', { name: 'Expand Development 3' }); + expect(resourceGroupButton).toBeInTheDocument(); + expect(screen.queryByLabelText('db-server')).not.toBeInTheDocument(); + resourceGroupButton.click(); - expect(onChange).toBeCalledTimes(1); - expect(onChange).toBeCalledWith( - expect.objectContaining({ - azureMonitor: expect.objectContaining({ - resourceUri: - '/subscriptions/def-456/resourceGroups/dev-3/providers/Microsoft.Compute/virtualMachines/db-server', - metricNamespace: undefined, - metricName: undefined, + const checkbox = await screen.findByLabelText('db-server'); + expect(checkbox).toBeInTheDocument(); + expect(checkbox).not.toBeChecked(); + await userEvent.click(checkbox); + expect(checkbox).toBeChecked(); + await userEvent.click(await screen.findByRole('button', { name: 'Apply' })); + + expect(onChange).toBeCalledTimes(1); + expect(onChange).toBeCalledWith( + expect.objectContaining({ + azureMonitor: expect.objectContaining({ + resourceUri: + '/subscriptions/def-456/resourceGroups/dev-3/providers/Microsoft.Compute/virtualMachines/db-server', + metricNamespace: undefined, + metricName: undefined, + aggregation: undefined, + timeGrain: '', + dimensionFilters: [], + }), + }) + ); + }); + + it('should change the metric name when selected', async () => { + const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); + const onChange = jest.fn(); + const mockQuery = createMockQuery(); + mockDatasource.azureMonitorDatasource.getMetricNames = jest.fn().mockResolvedValue([ + { + value: 'metric-a', + text: 'Metric A', + }, + { + value: 'metric-b', + text: 'Metric B', + }, + ]); + + render( + {}} + /> + ); + + const metrics = await screen.findByLabelText('Metric'); + expect(metrics).toBeInTheDocument(); + await selectOptionInTest(metrics, 'Metric B'); + + expect(onChange).toHaveBeenLastCalledWith({ + ...mockQuery, + azureMonitor: { + ...mockQuery.azureMonitor, + metricName: 'metric-b', aggregation: undefined, timeGrain: '', - dimensionFilters: [], - }), - }) - ); - }); + }, + }); + }); - it('should change the metric name when selected', async () => { - const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); - const onChange = jest.fn(); - const mockQuery = createMockQuery(); - mockDatasource.azureMonitorDatasource.getMetricNames = jest.fn().mockResolvedValue([ - { - value: 'metric-a', - text: 'Metric A', - }, - { - value: 'metric-b', - text: 'Metric B', - }, - ]); + it('should change the aggregation type when selected', async () => { + const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); + const onChange = jest.fn(); + const mockQuery = createMockQuery(); - render( - {}} - /> - ); + render( + {}} + /> + ); - const metrics = await screen.findByLabelText('Metric'); - expect(metrics).toBeInTheDocument(); - await selectOptionInTest(metrics, 'Metric B'); + const aggregation = await screen.findByLabelText('Aggregation'); + expect(aggregation).toBeInTheDocument(); + await selectOptionInTest(aggregation, 'Maximum'); - expect(onChange).toHaveBeenLastCalledWith({ - ...mockQuery, - azureMonitor: { - ...mockQuery.azureMonitor, - metricName: 'metric-b', - aggregation: undefined, - timeGrain: '', - }, + expect(onChange).toHaveBeenLastCalledWith({ + ...mockQuery, + azureMonitor: { + ...mockQuery.azureMonitor, + aggregation: 'Maximum', + }, + }); }); }); - - it('should change the aggregation type when selected', async () => { - const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); - const onChange = jest.fn(); - const mockQuery = createMockQuery(); - - render( - {}} - /> - ); - - const aggregation = await screen.findByLabelText('Aggregation'); - expect(aggregation).toBeInTheDocument(); - await selectOptionInTest(aggregation, 'Maximum'); - - expect(onChange).toHaveBeenLastCalledWith({ - ...mockQuery, - azureMonitor: { - ...mockQuery.azureMonitor, - aggregation: 'Maximum', - }, - }); - }); -}); +} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/MetricsQueryEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/MetricsQueryEditor.tsx index f7693b36585..a0ce1ff4a52 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/MetricsQueryEditor.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/MetricsQueryEditor.tsx @@ -1,6 +1,8 @@ import React from 'react'; import { PanelData } from '@grafana/data/src/types'; +import { EditorRows, EditorRow, EditorFieldGroup } from '@grafana/experimental'; +import { config } from '@grafana/runtime'; import { InlineFieldRow } from '@grafana/ui'; import type Datasource from '../../datasource'; @@ -38,83 +40,183 @@ const MetricsQueryEditor: React.FC = ({ const metricsMetadata = useMetricMetadata(query, datasource, onChange); const metricNamespaces = useMetricNamespaces(query, datasource, onChange, setError); const metricNames = useMetricNames(query, datasource, onChange, setError); - return ( -
    - - - + if (config.featureToggles.azureMonitorExperimentalUI) { + return ( + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } else { + return ( +
    + + + + + + + + + + + + + + - - - - - - - - - -
    - ); +
    + ); + } }; export default MetricsQueryEditor; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor.test.tsx index 9b602d04215..766abdc1085 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor.test.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor.test.tsx @@ -108,4 +108,16 @@ describe('Azure Monitor QueryEditor', () => { config.featureToggles.azureMonitorExperimentalUI = originalConfigValue; }); + + it('should not render the experimental QueryHeader when feature toggle is disabled', async () => { + const mockDatasource = createMockDatasource(); + const mockQuery = { + ...createMockQuery(), + queryType: AzureQueryType.AzureMonitor, + }; + + render( {}} onRunQuery={() => {}} />); + + await waitFor(() => expect(screen.queryByTestId('azure-monitor-experimental-header')).not.toBeInTheDocument()); + }); }); From 44e7602ad2f11f78f9703ac8bd06ff827f4e4d16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Thu, 12 May 2022 08:56:09 +0200 Subject: [PATCH 128/440] loki: backend-mode: add "limit" dataframe meta attribute (#48894) --- .../loki/backendResultTransformer.test.ts | 31 +++++++++++++++++++ .../loki/backendResultTransformer.ts | 1 + 2 files changed, 32 insertions(+) diff --git a/public/app/plugins/datasource/loki/backendResultTransformer.test.ts b/public/app/plugins/datasource/loki/backendResultTransformer.test.ts index 1b713005141..098b7d276c3 100644 --- a/public/app/plugins/datasource/loki/backendResultTransformer.test.ts +++ b/public/app/plugins/datasource/loki/backendResultTransformer.test.ts @@ -91,6 +91,37 @@ describe('loki backendResultTransformer', () => { expect(result).toEqual(expected); }); + it('applies maxLines correctly', () => { + const response: DataQueryResponse = { data: [cloneDeep(inputFrame)] }; + + const frame1: DataFrame = transformBackendResult( + response, + [ + { + refId: 'A', + expr: LOKI_EXPR, + }, + ], + [] + ).data[0]; + + expect(frame1.meta?.limit).toBeUndefined(); + + const frame2 = transformBackendResult( + response, + [ + { + refId: 'A', + expr: LOKI_EXPR, + maxLines: 42, + }, + ], + [] + ).data[0]; + + expect(frame2.meta?.limit).toBe(42); + }); + it('processed derived fields correctly', () => { const input: DataFrame = { length: 1, diff --git a/public/app/plugins/datasource/loki/backendResultTransformer.ts b/public/app/plugins/datasource/loki/backendResultTransformer.ts index 93504c1caef..29c9b7a0f3b 100644 --- a/public/app/plugins/datasource/loki/backendResultTransformer.ts +++ b/public/app/plugins/datasource/loki/backendResultTransformer.ts @@ -27,6 +27,7 @@ function processStreamFrame( ): DataFrame { const meta: QueryResultMeta = { preferredVisualisationType: 'logs', + limit: query?.maxLines, searchWords: query !== undefined ? getHighlighterExpressionsFromQuery(formatQuery(query.expr)) : undefined, custom: { // used by logs_model From e1a9ce4cc4a90a619b40e3435221a1851fb0362c Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Thu, 12 May 2022 05:34:09 -0400 Subject: [PATCH 129/440] Prometheus: Move existing query logic to new buffered package (#48668) --- .../prometheus/{ => buffered}/framing_test.go | 26 ++++- .../{ => buffered}/promclient/cache.go | 0 .../{ => buffered}/promclient/cache_test.go | 2 +- .../{ => buffered}/promclient/provider.go | 0 .../promclient/provider_azure.go | 0 .../promclient/provider_azure_test.go | 0 .../promclient/provider_test.go | 2 +- .../{ => buffered}/prometeus_bench_test.go | 4 +- .../{ => buffered}/time_series_query.go | 91 ++++++++++++----- .../{ => buffered}/time_series_query_test.go | 98 +++++++++---------- pkg/tsdb/prometheus/{ => buffered}/types.go | 18 ++-- pkg/tsdb/prometheus/prometheus.go | 65 ++++-------- 12 files changed, 166 insertions(+), 140 deletions(-) rename pkg/tsdb/prometheus/{ => buffered}/framing_test.go (79%) rename pkg/tsdb/prometheus/{ => buffered}/promclient/cache.go (100%) rename pkg/tsdb/prometheus/{ => buffered}/promclient/cache_test.go (97%) rename pkg/tsdb/prometheus/{ => buffered}/promclient/provider.go (100%) rename pkg/tsdb/prometheus/{ => buffered}/promclient/provider_azure.go (100%) rename pkg/tsdb/prometheus/{ => buffered}/promclient/provider_azure_test.go (100%) rename pkg/tsdb/prometheus/{ => buffered}/promclient/provider_test.go (98%) rename pkg/tsdb/prometheus/{ => buffered}/prometeus_bench_test.go (97%) rename pkg/tsdb/prometheus/{ => buffered}/time_series_query.go (83%) rename pkg/tsdb/prometheus/{ => buffered}/time_series_query_test.go (90%) rename pkg/tsdb/prometheus/{ => buffered}/types.go (81%) diff --git a/pkg/tsdb/prometheus/framing_test.go b/pkg/tsdb/prometheus/buffered/framing_test.go similarity index 79% rename from pkg/tsdb/prometheus/framing_test.go rename to pkg/tsdb/prometheus/buffered/framing_test.go index c2ddadd0dac..1bc75c102c7 100644 --- a/pkg/tsdb/prometheus/framing_test.go +++ b/pkg/tsdb/prometheus/buffered/framing_test.go @@ -1,4 +1,4 @@ -package prometheus +package buffered import ( "bytes" @@ -15,7 +15,9 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/experimental" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/tsdb/intervalv2" "github.com/prometheus/client_golang/api" apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" ) @@ -33,9 +35,9 @@ func TestMatrixResponses(t *testing.T) { for _, test := range tt { t.Run(test.name, func(t *testing.T) { - queryFileName := filepath.Join("testdata", test.filepath+".query.json") - responseFileName := filepath.Join("testdata", test.filepath+".result.json") - goldenFileName := filepath.Join("testdata", test.filepath+".result.golden.txt") + queryFileName := filepath.Join("../testdata", test.filepath+".query.json") + responseFileName := filepath.Join("../testdata", test.filepath+".result.json") + goldenFileName := filepath.Join("../testdata", test.filepath+".result.golden.txt") query, err := loadStoredPrometheusQuery(queryFileName) require.NoError(t, err) @@ -131,6 +133,20 @@ func runQuery(response []byte, query PrometheusQuery) (*backend.QueryDataRespons return nil, err } - s := Service{tracer: tracer} + s := Buffered{ + intervalCalculator: intervalv2.NewCalculator(), + tracer: tracer, + TimeInterval: "15s", + log: &fakeLogger{}, + } return s.runQueries(context.Background(), api, []*PrometheusQuery{&query}) } + +type fakeLogger struct { + log.Logger +} + +func (fl *fakeLogger) Debug(testMessage string, ctx ...interface{}) {} +func (fl *fakeLogger) Info(testMessage string, ctx ...interface{}) {} +func (fl *fakeLogger) Warn(testMessage string, ctx ...interface{}) {} +func (fl *fakeLogger) Error(testMessage string, ctx ...interface{}) {} diff --git a/pkg/tsdb/prometheus/promclient/cache.go b/pkg/tsdb/prometheus/buffered/promclient/cache.go similarity index 100% rename from pkg/tsdb/prometheus/promclient/cache.go rename to pkg/tsdb/prometheus/buffered/promclient/cache.go diff --git a/pkg/tsdb/prometheus/promclient/cache_test.go b/pkg/tsdb/prometheus/buffered/promclient/cache_test.go similarity index 97% rename from pkg/tsdb/prometheus/promclient/cache_test.go rename to pkg/tsdb/prometheus/buffered/promclient/cache_test.go index 2c27f6c3fea..d1cbd7b8f5d 100644 --- a/pkg/tsdb/prometheus/promclient/cache_test.go +++ b/pkg/tsdb/prometheus/buffered/promclient/cache_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "github.com/grafana/grafana/pkg/tsdb/prometheus/promclient" + "github.com/grafana/grafana/pkg/tsdb/prometheus/buffered/promclient" apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" diff --git a/pkg/tsdb/prometheus/promclient/provider.go b/pkg/tsdb/prometheus/buffered/promclient/provider.go similarity index 100% rename from pkg/tsdb/prometheus/promclient/provider.go rename to pkg/tsdb/prometheus/buffered/promclient/provider.go diff --git a/pkg/tsdb/prometheus/promclient/provider_azure.go b/pkg/tsdb/prometheus/buffered/promclient/provider_azure.go similarity index 100% rename from pkg/tsdb/prometheus/promclient/provider_azure.go rename to pkg/tsdb/prometheus/buffered/promclient/provider_azure.go diff --git a/pkg/tsdb/prometheus/promclient/provider_azure_test.go b/pkg/tsdb/prometheus/buffered/promclient/provider_azure_test.go similarity index 100% rename from pkg/tsdb/prometheus/promclient/provider_azure_test.go rename to pkg/tsdb/prometheus/buffered/promclient/provider_azure_test.go diff --git a/pkg/tsdb/prometheus/promclient/provider_test.go b/pkg/tsdb/prometheus/buffered/promclient/provider_test.go similarity index 98% rename from pkg/tsdb/prometheus/promclient/provider_test.go rename to pkg/tsdb/prometheus/buffered/promclient/provider_test.go index b62af351c39..cc340e3d330 100644 --- a/pkg/tsdb/prometheus/promclient/provider_test.go +++ b/pkg/tsdb/prometheus/buffered/promclient/provider_test.go @@ -5,7 +5,7 @@ import ( "net/http" "testing" - "github.com/grafana/grafana/pkg/tsdb/prometheus/promclient" + "github.com/grafana/grafana/pkg/tsdb/prometheus/buffered/promclient" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" diff --git a/pkg/tsdb/prometheus/prometeus_bench_test.go b/pkg/tsdb/prometheus/buffered/prometeus_bench_test.go similarity index 97% rename from pkg/tsdb/prometheus/prometeus_bench_test.go rename to pkg/tsdb/prometheus/buffered/prometeus_bench_test.go index b7365e30c44..122dc19bb0d 100644 --- a/pkg/tsdb/prometheus/prometeus_bench_test.go +++ b/pkg/tsdb/prometheus/buffered/prometeus_bench_test.go @@ -1,4 +1,4 @@ -package prometheus +package buffered import ( "context" @@ -24,7 +24,7 @@ func BenchmarkJson(b *testing.B) { tracer, err := tracing.InitializeTracerForTest() require.NoError(b, err) - s := Service{tracer: tracer} + s := Buffered{tracer: tracer, log: &fakeLogger{}} b.ResetTimer() for n := 0; n < b.N; n++ { diff --git a/pkg/tsdb/prometheus/time_series_query.go b/pkg/tsdb/prometheus/buffered/time_series_query.go similarity index 83% rename from pkg/tsdb/prometheus/time_series_query.go rename to pkg/tsdb/prometheus/buffered/time_series_query.go index d768738d8a5..e0a7fad20f1 100644 --- a/pkg/tsdb/prometheus/time_series_query.go +++ b/pkg/tsdb/prometheus/buffered/time_series_query.go @@ -1,10 +1,11 @@ -package prometheus +package buffered import ( "context" "encoding/json" "fmt" "math" + "regexp" "sort" "strconv" "strings" @@ -12,7 +13,14 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/infra/httpclient" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/intervalv2" + "github.com/grafana/grafana/pkg/tsdb/prometheus/buffered/promclient" + "github.com/grafana/grafana/pkg/util/maputil" apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" "github.com/prometheus/common/model" "go.opentelemetry.io/otel/attribute" @@ -41,23 +49,57 @@ const ( const legendFormatAuto = "__auto" -type TimeSeriesQueryType string - -const ( - RangeQueryType TimeSeriesQueryType = "range" - InstantQueryType TimeSeriesQueryType = "instant" - ExemplarQueryType TimeSeriesQueryType = "exemplar" +var ( + legendFormat = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`) + safeRes = 11000 ) -func (s *Service) runQueries(ctx context.Context, client apiv1.API, queries []*PrometheusQuery) (*backend.QueryDataResponse, error) { +type Buffered struct { + intervalCalculator intervalv2.Calculator + tracer tracing.Tracer + getClient clientGetter + log log.Logger + ID int64 + URL string + TimeInterval string +} + +func New(httpClientProvider httpclient.Provider, cfg *setting.Cfg, features featuremgmt.FeatureToggles, tracer tracing.Tracer, settings backend.DataSourceInstanceSettings, plog log.Logger) (*Buffered, error) { + var jsonData map[string]interface{} + if err := json.Unmarshal(settings.JSONData, &jsonData); err != nil { + return nil, fmt.Errorf("error reading settings: %w", err) + } + + timeInterval, err := maputil.GetStringOptional(jsonData, "timeInterval") + if err != nil { + return nil, err + } + + p := promclient.NewProvider(settings, jsonData, httpClientProvider, cfg, features, plog) + pc, err := promclient.NewProviderCache(p) + if err != nil { + return nil, err + } + return &Buffered{ + intervalCalculator: intervalv2.NewCalculator(), + tracer: tracer, + log: plog, + getClient: pc.GetClient, + TimeInterval: timeInterval, + ID: settings.ID, + URL: settings.URL, + }, nil +} + +func (b *Buffered) runQueries(ctx context.Context, client apiv1.API, queries []*PrometheusQuery) (*backend.QueryDataResponse, error) { result := backend.QueryDataResponse{ Responses: backend.Responses{}, } for _, query := range queries { - plog.Debug("Sending query", "start", query.Start, "end", query.End, "step", query.Step, "query", query.Expr) + b.log.Debug("Sending query", "start", query.Start, "end", query.End, "step", query.Step, "query", query.Expr) - ctx, span := s.tracer.Start(ctx, "datasource.prometheus") + ctx, span := b.tracer.Start(ctx, "datasource.prometheus") span.SetAttributes("expr", query.Expr, attribute.Key("expr").String(query.Expr)) span.SetAttributes("start_unixnano", query.Start, attribute.Key("start_unixnano").Int64(query.Start.UnixNano())) span.SetAttributes("stop_unixnano", query.End, attribute.Key("stop_unixnano").Int64(query.End.UnixNano())) @@ -75,7 +117,7 @@ func (s *Service) runQueries(ctx context.Context, client apiv1.API, queries []*P if query.RangeQuery { rangeResponse, _, err := client.QueryRange(ctx, query.Expr, timeRange) if err != nil { - plog.Error("Range query failed", "query", query.Expr, "err", err) + b.log.Error("Range query failed", "query", query.Expr, "err", err) result.Responses[query.RefId] = backend.DataResponse{Error: err} continue } @@ -85,7 +127,7 @@ func (s *Service) runQueries(ctx context.Context, client apiv1.API, queries []*P if query.InstantQuery { instantResponse, _, err := client.Query(ctx, query.Expr, query.End) if err != nil { - plog.Error("Instant query failed", "query", query.Expr, "err", err) + b.log.Error("Instant query failed", "query", query.Expr, "err", err) result.Responses[query.RefId] = backend.DataResponse{Error: err} continue } @@ -97,7 +139,7 @@ func (s *Service) runQueries(ctx context.Context, client apiv1.API, queries []*P if query.ExemplarQuery { exemplarResponse, err := client.QueryExemplars(ctx, query.Expr, timeRange.Start, timeRange.End) if err != nil { - plog.Error("Exemplar query failed", "query", query.Expr, "err", err) + b.log.Error("Exemplar query failed", "query", query.Expr, "err", err) } else { response[ExemplarQueryType] = exemplarResponse } @@ -121,13 +163,13 @@ func (s *Service) runQueries(ctx context.Context, client apiv1.API, queries []*P return &result, nil } -func (s *Service) executeTimeSeriesQuery(ctx context.Context, req *backend.QueryDataRequest, dsInfo *DatasourceInfo) (*backend.QueryDataResponse, error) { - client, err := dsInfo.getClient(req.Headers) +func (b *Buffered) ExecuteTimeSeriesQuery(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + client, err := b.getClient(req.Headers) if err != nil { return nil, err } - queries, err := s.parseTimeSeriesQuery(req, dsInfo) + queries, err := b.parseTimeSeriesQuery(req) if err != nil { result := backend.QueryDataResponse{ Responses: backend.Responses{}, @@ -135,7 +177,7 @@ func (s *Service) executeTimeSeriesQuery(ctx context.Context, req *backend.Query return &result, err } - return s.runQueries(ctx, client, queries) + return b.runQueries(ctx, client, queries) } func formatLegend(metric model.Metric, query *PrometheusQuery) string { @@ -167,7 +209,7 @@ func formatLegend(metric model.Metric, query *PrometheusQuery) string { return legend } -func (s *Service) parseTimeSeriesQuery(queryContext *backend.QueryDataRequest, dsInfo *DatasourceInfo) ([]*PrometheusQuery, error) { +func (b *Buffered) parseTimeSeriesQuery(queryContext *backend.QueryDataRequest) ([]*PrometheusQuery, error) { qs := []*PrometheusQuery{} for _, query := range queryContext.Queries { model := &QueryModel{} @@ -176,14 +218,14 @@ func (s *Service) parseTimeSeriesQuery(queryContext *backend.QueryDataRequest, d return nil, err } //Final interval value - interval, err := calculatePrometheusInterval(model, dsInfo, query, s.intervalCalculator) + interval, err := calculatePrometheusInterval(model, b.TimeInterval, query, b.intervalCalculator) if err != nil { return nil, err } // Interpolate variables in expr timeRange := query.TimeRange.To.Sub(query.TimeRange.From) - expr := interpolateVariables(model, interval, timeRange, s.intervalCalculator, dsInfo.TimeInterval) + expr := interpolateVariables(model, interval, timeRange, b.intervalCalculator, b.TimeInterval) rangeQuery := model.RangeQuery if !model.InstantQuery && !model.RangeQuery { // In older dashboards, we were not setting range query param and !range && !instant was run as range query @@ -232,8 +274,7 @@ func parseTimeSeriesResponse(value map[TimeSeriesQueryType]interface{}, query *P case []apiv1.ExemplarQueryResult: nextFrames = exemplarToDataFrames(v, query, nextFrames) default: - plog.Error("Query returned unexpected result type", "type", v, "query", query.Expr) - continue + return nil, fmt.Errorf("unexpected result type: %s query: %s", v, query.Expr) } frames = append(frames, nextFrames...) @@ -242,7 +283,7 @@ func parseTimeSeriesResponse(value map[TimeSeriesQueryType]interface{}, query *P return frames, nil } -func calculatePrometheusInterval(model *QueryModel, dsInfo *DatasourceInfo, query backend.DataQuery, intervalCalculator intervalv2.Calculator) (time.Duration, error) { +func calculatePrometheusInterval(model *QueryModel, timeInterval string, query backend.DataQuery, intervalCalculator intervalv2.Calculator) (time.Duration, error) { queryInterval := model.Interval //If we are using variable for interval/step, we will replace it with calculated interval @@ -250,7 +291,7 @@ func calculatePrometheusInterval(model *QueryModel, dsInfo *DatasourceInfo, quer queryInterval = "" } - minInterval, err := intervalv2.GetIntervalFrom(dsInfo.TimeInterval, queryInterval, model.IntervalMS, 15*time.Second) + minInterval, err := intervalv2.GetIntervalFrom(timeInterval, queryInterval, model.IntervalMS, 15*time.Second) if err != nil { return time.Duration(0), err } @@ -264,7 +305,7 @@ func calculatePrometheusInterval(model *QueryModel, dsInfo *DatasourceInfo, quer if model.Interval == varRateInterval || model.Interval == varRateIntervalAlt { // Rate interval is final and is not affected by resolution - return calculateRateInterval(adjustedInterval, dsInfo.TimeInterval, intervalCalculator), nil + return calculateRateInterval(adjustedInterval, timeInterval, intervalCalculator), nil } else { intervalFactor := model.IntervalFactor if intervalFactor == 0 { diff --git a/pkg/tsdb/prometheus/time_series_query_test.go b/pkg/tsdb/prometheus/buffered/time_series_query_test.go similarity index 90% rename from pkg/tsdb/prometheus/time_series_query_test.go rename to pkg/tsdb/prometheus/buffered/time_series_query_test.go index 6b1cdc6f7a0..f4bef8555ba 100644 --- a/pkg/tsdb/prometheus/time_series_query_test.go +++ b/pkg/tsdb/prometheus/buffered/time_series_query_test.go @@ -1,4 +1,4 @@ -package prometheus +package buffered import ( "math" @@ -79,7 +79,7 @@ func TestPrometheus_timeSeriesQuery_formatLeged(t *testing.T) { } func TestPrometheus_timeSeriesQuery_parseTimeSeriesQuery(t *testing.T) { - service := Service{ + service := Buffered{ intervalCalculator: intervalv2.NewCalculator(), } @@ -108,8 +108,8 @@ func TestPrometheus_timeSeriesQuery_parseTimeSeriesQuery(t *testing.T) { }, } - dsInfo := &DatasourceInfo{} - models, err := service.parseTimeSeriesQuery(query, dsInfo) + service.TimeInterval = "15s" + models, err := service.parseTimeSeriesQuery(query) require.NoError(t, err) require.Equal(t, false, models[0].ExemplarQuery) }) @@ -126,8 +126,8 @@ func TestPrometheus_timeSeriesQuery_parseTimeSeriesQuery(t *testing.T) { "refId": "A" }`, timeRange) - dsInfo := &DatasourceInfo{} - models, err := service.parseTimeSeriesQuery(query, dsInfo) + service.TimeInterval = "15s" + models, err := service.parseTimeSeriesQuery(query) require.NoError(t, err) require.Equal(t, time.Second*30, models[0].Step) }) @@ -145,8 +145,8 @@ func TestPrometheus_timeSeriesQuery_parseTimeSeriesQuery(t *testing.T) { "refId": "A" }`, timeRange) - dsInfo := &DatasourceInfo{} - models, err := service.parseTimeSeriesQuery(query, dsInfo) + service.TimeInterval = "15s" + models, err := service.parseTimeSeriesQuery(query) require.NoError(t, err) require.Equal(t, time.Second*15, models[0].Step) }) @@ -164,8 +164,8 @@ func TestPrometheus_timeSeriesQuery_parseTimeSeriesQuery(t *testing.T) { "refId": "A" }`, timeRange) - dsInfo := &DatasourceInfo{} - models, err := service.parseTimeSeriesQuery(query, dsInfo) + service.TimeInterval = "15s" + models, err := service.parseTimeSeriesQuery(query) require.NoError(t, err) require.Equal(t, time.Minute*20, models[0].Step) }) @@ -183,8 +183,8 @@ func TestPrometheus_timeSeriesQuery_parseTimeSeriesQuery(t *testing.T) { "refId": "A" }`, timeRange) - dsInfo := &DatasourceInfo{} - models, err := service.parseTimeSeriesQuery(query, dsInfo) + service.TimeInterval = "15s" + models, err := service.parseTimeSeriesQuery(query) require.NoError(t, err) require.Equal(t, time.Minute*2, models[0].Step) }) @@ -202,10 +202,8 @@ func TestPrometheus_timeSeriesQuery_parseTimeSeriesQuery(t *testing.T) { "refId": "A" }`, timeRange) - dsInfo := &DatasourceInfo{ - TimeInterval: "240s", - } - models, err := service.parseTimeSeriesQuery(query, dsInfo) + service.TimeInterval = "240s" + models, err := service.parseTimeSeriesQuery(query) require.NoError(t, err) require.Equal(t, time.Minute*4, models[0].Step) }) @@ -223,8 +221,8 @@ func TestPrometheus_timeSeriesQuery_parseTimeSeriesQuery(t *testing.T) { "refId": "A" }`, timeRange) - dsInfo := &DatasourceInfo{} - models, err := service.parseTimeSeriesQuery(query, dsInfo) + service.TimeInterval = "15s" + models, err := service.parseTimeSeriesQuery(query) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [2m]})", models[0].Expr) }) @@ -242,8 +240,8 @@ func TestPrometheus_timeSeriesQuery_parseTimeSeriesQuery(t *testing.T) { "refId": "A" }`, timeRange) - dsInfo := &DatasourceInfo{} - models, err := service.parseTimeSeriesQuery(query, dsInfo) + service.TimeInterval = "15s" + models, err := service.parseTimeSeriesQuery(query) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [2m]})", models[0].Expr) }) @@ -261,8 +259,8 @@ func TestPrometheus_timeSeriesQuery_parseTimeSeriesQuery(t *testing.T) { "refId": "A" }`, timeRange) - dsInfo := &DatasourceInfo{} - models, err := service.parseTimeSeriesQuery(query, dsInfo) + service.TimeInterval = "15s" + models, err := service.parseTimeSeriesQuery(query) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [120000]})", models[0].Expr) }) @@ -280,8 +278,8 @@ func TestPrometheus_timeSeriesQuery_parseTimeSeriesQuery(t *testing.T) { "refId": "A" }`, timeRange) - dsInfo := &DatasourceInfo{} - models, err := service.parseTimeSeriesQuery(query, dsInfo) + service.TimeInterval = "15s" + models, err := service.parseTimeSeriesQuery(query) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [120000]}) + rate(ALERTS{job=\"test\" [2m]})", models[0].Expr) }) @@ -299,8 +297,8 @@ func TestPrometheus_timeSeriesQuery_parseTimeSeriesQuery(t *testing.T) { "refId": "A" }`, timeRange) - dsInfo := &DatasourceInfo{} - models, err := service.parseTimeSeriesQuery(query, dsInfo) + service.TimeInterval = "15s" + models, err := service.parseTimeSeriesQuery(query) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [120000]}) + rate(ALERTS{job=\"test\" [2m]})", models[0].Expr) }) @@ -318,8 +316,8 @@ func TestPrometheus_timeSeriesQuery_parseTimeSeriesQuery(t *testing.T) { "refId": "A" }`, timeRange) - dsInfo := &DatasourceInfo{} - models, err := service.parseTimeSeriesQuery(query, dsInfo) + service.TimeInterval = "15s" + models, err := service.parseTimeSeriesQuery(query) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [172800s]})", models[0].Expr) }) @@ -337,8 +335,8 @@ func TestPrometheus_timeSeriesQuery_parseTimeSeriesQuery(t *testing.T) { "refId": "A" }`, timeRange) - dsInfo := &DatasourceInfo{} - models, err := service.parseTimeSeriesQuery(query, dsInfo) + service.TimeInterval = "15s" + models, err := service.parseTimeSeriesQuery(query) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [172800]})", models[0].Expr) }) @@ -356,8 +354,8 @@ func TestPrometheus_timeSeriesQuery_parseTimeSeriesQuery(t *testing.T) { "refId": "A" }`, timeRange) - dsInfo := &DatasourceInfo{} - models, err := service.parseTimeSeriesQuery(query, dsInfo) + service.TimeInterval = "15s" + models, err := service.parseTimeSeriesQuery(query) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [172800s]})", models[0].Expr) }) @@ -375,8 +373,8 @@ func TestPrometheus_timeSeriesQuery_parseTimeSeriesQuery(t *testing.T) { "refId": "A" }`, timeRange) - dsInfo := &DatasourceInfo{} - models, err := service.parseTimeSeriesQuery(query, dsInfo) + service.TimeInterval = "15s" + models, err := service.parseTimeSeriesQuery(query) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [0]})", models[0].Expr) }) @@ -394,8 +392,8 @@ func TestPrometheus_timeSeriesQuery_parseTimeSeriesQuery(t *testing.T) { "refId": "A" }`, timeRange) - dsInfo := &DatasourceInfo{} - models, err := service.parseTimeSeriesQuery(query, dsInfo) + service.TimeInterval = "15s" + models, err := service.parseTimeSeriesQuery(query) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [1]})", models[0].Expr) }) @@ -413,8 +411,8 @@ func TestPrometheus_timeSeriesQuery_parseTimeSeriesQuery(t *testing.T) { "refId": "A" }`, timeRange) - dsInfo := &DatasourceInfo{} - models, err := service.parseTimeSeriesQuery(query, dsInfo) + service.TimeInterval = "15s" + models, err := service.parseTimeSeriesQuery(query) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [172800000]})", models[0].Expr) }) @@ -432,8 +430,8 @@ func TestPrometheus_timeSeriesQuery_parseTimeSeriesQuery(t *testing.T) { "refId": "A" }`, timeRange) - dsInfo := &DatasourceInfo{} - models, err := service.parseTimeSeriesQuery(query, dsInfo) + service.TimeInterval = "15s" + models, err := service.parseTimeSeriesQuery(query) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [20]})", models[0].Expr) }) @@ -452,8 +450,8 @@ func TestPrometheus_timeSeriesQuery_parseTimeSeriesQuery(t *testing.T) { "refId": "A" }`, timeRange) - dsInfo := &DatasourceInfo{} - models, err := service.parseTimeSeriesQuery(query, dsInfo) + service.TimeInterval = "15s" + models, err := service.parseTimeSeriesQuery(query) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [5m15s]})", models[0].Expr) }) @@ -472,8 +470,8 @@ func TestPrometheus_timeSeriesQuery_parseTimeSeriesQuery(t *testing.T) { "refId": "A" }`, timeRange) - dsInfo := &DatasourceInfo{} - models, err := service.parseTimeSeriesQuery(query, dsInfo) + service.TimeInterval = "15s" + models, err := service.parseTimeSeriesQuery(query) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [1m0s]})", models[0].Expr) require.Equal(t, 1*time.Minute, models[0].Step) @@ -493,8 +491,8 @@ func TestPrometheus_timeSeriesQuery_parseTimeSeriesQuery(t *testing.T) { "range": true }`, timeRange) - dsInfo := &DatasourceInfo{} - models, err := service.parseTimeSeriesQuery(query, dsInfo) + service.TimeInterval = "15s" + models, err := service.parseTimeSeriesQuery(query) require.NoError(t, err) require.Equal(t, true, models[0].RangeQuery) }) @@ -514,8 +512,8 @@ func TestPrometheus_timeSeriesQuery_parseTimeSeriesQuery(t *testing.T) { "instant": true }`, timeRange) - dsInfo := &DatasourceInfo{} - models, err := service.parseTimeSeriesQuery(query, dsInfo) + service.TimeInterval = "15s" + models, err := service.parseTimeSeriesQuery(query) require.NoError(t, err) require.Equal(t, true, models[0].RangeQuery) require.Equal(t, true, models[0].InstantQuery) @@ -534,8 +532,8 @@ func TestPrometheus_timeSeriesQuery_parseTimeSeriesQuery(t *testing.T) { "refId": "A" }`, timeRange) - dsInfo := &DatasourceInfo{} - models, err := service.parseTimeSeriesQuery(query, dsInfo) + service.TimeInterval = "15s" + models, err := service.parseTimeSeriesQuery(query) require.NoError(t, err) require.Equal(t, true, models[0].RangeQuery) }) diff --git a/pkg/tsdb/prometheus/types.go b/pkg/tsdb/prometheus/buffered/types.go similarity index 81% rename from pkg/tsdb/prometheus/types.go rename to pkg/tsdb/prometheus/buffered/types.go index 92784411b13..8dec9d19dca 100644 --- a/pkg/tsdb/prometheus/types.go +++ b/pkg/tsdb/prometheus/buffered/types.go @@ -1,4 +1,4 @@ -package prometheus +package buffered import ( "time" @@ -6,14 +6,6 @@ import ( apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" ) -type DatasourceInfo struct { - ID int64 - URL string - TimeInterval string - - getClient clientGetter -} - type clientGetter func(map[string]string) (apiv1.API, error) type PrometheusQuery struct { @@ -47,3 +39,11 @@ type QueryModel struct { IntervalFactor int64 `json:"intervalFactor"` UtcOffsetSec int64 `json:"utcOffsetSec"` } + +type TimeSeriesQueryType string + +const ( + RangeQueryType TimeSeriesQueryType = "range" + InstantQueryType TimeSeriesQueryType = "instant" + ExemplarQueryType TimeSeriesQueryType = "exemplar" +) diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index 71c1726e996..49b2b9a78eb 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "regexp" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" @@ -15,34 +14,28 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/tsdb/intervalv2" - "github.com/grafana/grafana/pkg/tsdb/prometheus/promclient" - "github.com/grafana/grafana/pkg/util/maputil" + "github.com/grafana/grafana/pkg/tsdb/prometheus/buffered" apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" ) -var ( - plog = log.New("tsdb.prometheus") - legendFormat = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`) - safeRes = 11000 -) +var plog = log.New("tsdb.prometheus") type Service struct { - intervalCalculator intervalv2.Calculator - im instancemgmt.InstanceManager - tracer tracing.Tracer + im instancemgmt.InstanceManager +} + +type instance struct { + Buffered *buffered.Buffered } func ProvideService(httpClientProvider httpclient.Provider, cfg *setting.Cfg, features featuremgmt.FeatureToggles, tracer tracing.Tracer) *Service { plog.Debug("initializing") return &Service{ - intervalCalculator: intervalv2.NewCalculator(), - im: datasource.NewInstanceManager(newInstanceSettings(httpClientProvider, cfg, features)), - tracer: tracer, + im: datasource.NewInstanceManager(newInstanceSettings(httpClientProvider, cfg, features, tracer)), } } -func newInstanceSettings(httpClientProvider httpclient.Provider, cfg *setting.Cfg, features featuremgmt.FeatureToggles) datasource.InstanceFactoryFunc { +func newInstanceSettings(httpClientProvider httpclient.Provider, cfg *setting.Cfg, features featuremgmt.FeatureToggles, tracer tracing.Tracer) datasource.InstanceFactoryFunc { return func(settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { var jsonData map[string]interface{} err := json.Unmarshal(settings.JSONData, &jsonData) @@ -50,25 +43,14 @@ func newInstanceSettings(httpClientProvider httpclient.Provider, cfg *setting.Cf return nil, fmt.Errorf("error reading settings: %w", err) } - p := promclient.NewProvider(settings, jsonData, httpClientProvider, cfg, features, plog) - pc, err := promclient.NewProviderCache(p) + buf, err := buffered.New(httpClientProvider, cfg, features, tracer, settings, plog) if err != nil { return nil, err } - timeInterval, err := maputil.GetStringOptional(jsonData, "timeInterval") - if err != nil { - return nil, err - } - - mdl := DatasourceInfo{ - ID: settings.ID, - URL: settings.URL, - TimeInterval: timeInterval, - getClient: pc.GetClient, - } - - return mdl, nil + return instance{ + Buffered: buf, + }, nil } } @@ -77,32 +59,21 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) return &backend.QueryDataResponse{}, fmt.Errorf("query contains no queries") } - q := req.Queries[0] - dsInfo, err := s.getDSInfo(req.PluginContext) + i, err := s.getInstance(req.PluginContext) if err != nil { return nil, err } - var result *backend.QueryDataResponse - switch q.QueryType { - case "timeSeriesQuery": - fallthrough - default: - result, err = s.executeTimeSeriesQuery(ctx, req, dsInfo) - } - - return result, err + return i.Buffered.ExecuteTimeSeriesQuery(ctx, req) } -func (s *Service) getDSInfo(pluginCtx backend.PluginContext) (*DatasourceInfo, error) { +func (s *Service) getInstance(pluginCtx backend.PluginContext) (*instance, error) { i, err := s.im.Get(pluginCtx) if err != nil { return nil, err } - - instance := i.(DatasourceInfo) - - return &instance, nil + in := i.(instance) + return &in, nil } // IsAPIError returns whether err is or wraps a Prometheus error. From 58fa119270f66223a01bbddbcd9ffe9ca9b292a1 Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Thu, 12 May 2022 11:46:56 +0200 Subject: [PATCH 130/440] sort user permissions by scope (#48928) --- pkg/services/accesscontrol/database/database.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/services/accesscontrol/database/database.go b/pkg/services/accesscontrol/database/database.go index 85f025cfbd4..ec5ef5dbe1c 100644 --- a/pkg/services/accesscontrol/database/database.go +++ b/pkg/services/accesscontrol/database/database.go @@ -48,6 +48,10 @@ func (s *AccessControlStore) GetUserPermissions(ctx context.Context, query acces } } + q += ` + ORDER BY permission.scope + ` + if err := sess.SQL(q, params...).Find(&result); err != nil { return err } From c8a0e52a59b975c18cc2d173372a15f832c88e25 Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Thu, 12 May 2022 12:41:20 +0100 Subject: [PATCH 131/440] Search: Improve tab navigation (#48932) --- .../src/components/Table/TableCell.tsx | 4 +++- .../page/components/SearchResultsTable.tsx | 22 +++++-------------- .../search/page/components/columns.tsx | 13 +++++------ 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/packages/grafana-ui/src/components/Table/TableCell.tsx b/packages/grafana-ui/src/components/Table/TableCell.tsx index 9ba4ed6cc3a..2ebb78e89f5 100644 --- a/packages/grafana-ui/src/components/Table/TableCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableCell.tsx @@ -10,9 +10,10 @@ export interface Props { onCellFilterAdded?: TableFilterActionCallback; columnIndex: number; columnCount: number; + userProps?: object; } -export const TableCell: FC = ({ cell, tableStyles, onCellFilterAdded, columnIndex, columnCount }) => { +export const TableCell: FC = ({ cell, tableStyles, onCellFilterAdded, columnIndex, columnCount, userProps }) => { const cellProps = cell.getCellProps(); const field = (cell.column as any as GrafanaTableColumn).field; @@ -38,5 +39,6 @@ export const TableCell: FC = ({ cell, tableStyles, onCellFilterAdded, col onCellFilterAdded, cellProps, innerWidth, + userProps, }) as React.ReactElement; }; diff --git a/public/app/features/search/page/components/SearchResultsTable.tsx b/public/app/features/search/page/components/SearchResultsTable.tsx index ce766448035..b6e3173f3c7 100644 --- a/public/app/features/search/page/components/SearchResultsTable.tsx +++ b/public/app/features/search/page/components/SearchResultsTable.tsx @@ -29,7 +29,6 @@ export type TableColumn = Column & { field?: Field; }; -const skipHREF = new Set(['column-checkbox', 'column-datasource', 'column-location']); const HEADER_HEIGHT = 36; // pixels export const SearchResultsTable = ({ @@ -78,28 +77,21 @@ export const SearchResultsTable = ({ return (
    {row.cells.map((cell: Cell, index: number) => { - const body = ( + return ( ); - if (skipHREF.has(cell.column.id)) { - return body; - } - return ( - - {body} - - ); })}
    ); }, - [rows, prepareRow, response.view.fields.url?.values, styles.rowContainer, styles.cellWrapper, tableStyles] + [rows, prepareRow, response.view.fields.url?.values, styles.rowContainer, tableStyles] ); if (!rows.length) { @@ -171,11 +163,9 @@ const getStyles = (theme: GrafanaTheme2) => { align-items: center; `, cellWrapper: css` - div { - border-right: none; - &:hover { - box-shadow: none; - } + border-right: none; + &:hover { + box-shadow: none; } `, headerCell: css` diff --git a/public/app/features/search/page/components/columns.tsx b/public/app/features/search/page/components/columns.tsx index e5ea9f5ff79..52944030918 100644 --- a/public/app/features/search/page/components/columns.tsx +++ b/public/app/features/search/page/components/columns.tsx @@ -79,9 +79,9 @@ export const generateColumns = ( Cell: (p) => { const name = access.name.values.get(p.row.index); return ( - + ); }, id: `column-name`, @@ -92,7 +92,7 @@ export const generateColumns = ( availableWidth -= width; width = TYPE_COLUMN_WIDTH; - columns.push(makeTypeColumn(access.kind, access.panel_type, width, styles.typeText, styles.typeIcon)); + columns.push(makeTypeColumn(access.kind, access.panel_type, width, styles)); availableWidth -= width; // Show datasources if we have any @@ -222,8 +222,7 @@ function makeTypeColumn( kindField: Field, typeField: Field, width: number, - typeTextClass: string, - iconClass: string + styles: Record ): TableColumn { return { Cell: DefaultCell, @@ -264,8 +263,8 @@ function makeTypeColumn( } } return ( -
    - +
    + {txt}
    ); From 369fcc5e9ae1f51ec63af0dccafb061a30665aa3 Mon Sep 17 00:00:00 2001 From: Yuriy Tseretyan Date: Thu, 12 May 2022 09:55:05 -0400 Subject: [PATCH 132/440] Alerting: scheduler to use short version of model for alert rule (#48916) * scheduler to use a short version of alert rule model --- pkg/services/ngalert/models/alert_rule.go | 18 ++++++++++++++++++ pkg/services/ngalert/schedule/fetcher.go | 6 +++--- pkg/services/ngalert/store/alert_rule.go | 21 ++++++++++++--------- pkg/services/ngalert/store/testing.go | 11 +++++++++-- 4 files changed, 42 insertions(+), 14 deletions(-) diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index 6bf3c088421..12fde52f4c6 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -113,6 +113,13 @@ type AlertRule struct { Labels map[string]string } +type SchedulableAlertRule struct { + UID string `xorm:"uid"` + OrgID int64 `xorm:"org_id"` + IntervalSeconds int64 + Version int64 +} + type LabelOption func(map[string]string) func WithoutInternalLabels() LabelOption { @@ -169,6 +176,11 @@ func (alertRule *AlertRule) GetKey() AlertRuleKey { return AlertRuleKey{OrgID: alertRule.OrgID, UID: alertRule.UID} } +// GetKey returns the alert definitions identifier +func (alertRule *SchedulableAlertRule) GetKey() AlertRuleKey { + return AlertRuleKey{OrgID: alertRule.OrgID, UID: alertRule.UID} +} + // PreSave sets default values and loads the updated model for each alert query. func (alertRule *AlertRule) PreSave(timeNow func() time.Time) error { for i, q := range alertRule.Data { @@ -242,6 +254,12 @@ type ListAlertRulesQuery struct { Result []*AlertRule } +type GetAlertRulesForSchedulingQuery struct { + ExcludeOrgIDs []int64 + + Result []*SchedulableAlertRule +} + // ListNamespaceAlertRulesQuery is the query for listing namespace alert rules type ListNamespaceAlertRulesQuery struct { OrgID int64 diff --git a/pkg/services/ngalert/schedule/fetcher.go b/pkg/services/ngalert/schedule/fetcher.go index 9f20f4ac23d..7a2cf1d55b8 100644 --- a/pkg/services/ngalert/schedule/fetcher.go +++ b/pkg/services/ngalert/schedule/fetcher.go @@ -7,14 +7,14 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/models" ) -func (sch *schedule) getAlertRules(ctx context.Context, disabledOrgs []int64) []*models.AlertRule { +func (sch *schedule) getAlertRules(ctx context.Context, disabledOrgs []int64) []*models.SchedulableAlertRule { start := time.Now() defer func() { sch.metrics.GetAlertRulesDuration.Observe(time.Since(start).Seconds()) }() - q := models.ListAlertRulesQuery{ - ExcludeOrgs: disabledOrgs, + q := models.GetAlertRulesForSchedulingQuery{ + ExcludeOrgIDs: disabledOrgs, } err := sch.ruleStore.GetAlertRulesForScheduling(ctx, &q) if err != nil { diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index bc6a0f97c00..0402942bc8e 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -37,7 +37,7 @@ type RuleStore interface { DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUID ...string) error DeleteAlertInstancesByRuleUID(ctx context.Context, orgID int64, ruleUID string) error GetAlertRuleByUID(ctx context.Context, query *ngmodels.GetAlertRuleByUIDQuery) error - GetAlertRulesForScheduling(ctx context.Context, query *ngmodels.ListAlertRulesQuery) error + GetAlertRulesForScheduling(ctx context.Context, query *ngmodels.GetAlertRulesForSchedulingQuery) error ListAlertRules(ctx context.Context, query *ngmodels.ListAlertRulesQuery) error // GetRuleGroups returns the unique rule groups across all organizations. GetRuleGroups(ctx context.Context, query *ngmodels.ListRuleGroupsQuery) error @@ -344,16 +344,19 @@ func (st DBstore) GetNamespaceByTitle(ctx context.Context, namespace string, org return folder, nil } -// GetAlertRulesForScheduling returns alert rule info (identifier, interval, version state) -// that is useful for it's scheduling. -func (st DBstore) GetAlertRulesForScheduling(ctx context.Context, query *ngmodels.ListAlertRulesQuery) error { +// GetAlertRulesForScheduling returns a short version of all alert rules except those that belong to an excluded list of organizations +func (st DBstore) GetAlertRulesForScheduling(ctx context.Context, query *ngmodels.GetAlertRulesForSchedulingQuery) error { return st.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { - alerts := make([]*ngmodels.AlertRule, 0) - q := "SELECT uid, org_id, interval_seconds, version FROM alert_rule" - if len(query.ExcludeOrgs) > 0 { - q = fmt.Sprintf("%s WHERE org_id NOT IN (%s)", q, strings.Join(strings.Split(strings.Trim(fmt.Sprint(query.ExcludeOrgs), "[]"), " "), ",")) + alerts := make([]*ngmodels.SchedulableAlertRule, 0) + q := sess.Table("alert_rule") + if len(query.ExcludeOrgIDs) > 0 { + excludeOrgs := make([]interface{}, 0, len(query.ExcludeOrgIDs)) + for _, orgID := range query.ExcludeOrgIDs { + excludeOrgs = append(excludeOrgs, orgID) + } + q = q.NotIn("org_id", excludeOrgs...) } - if err := sess.SQL(q).Find(&alerts); err != nil { + if err := q.Find(&alerts); err != nil { return err } query.Result = alerts diff --git a/pkg/services/ngalert/store/testing.go b/pkg/services/ngalert/store/testing.go index 1a3af60bc30..a6cad95bcfb 100644 --- a/pkg/services/ngalert/store/testing.go +++ b/pkg/services/ngalert/store/testing.go @@ -153,7 +153,7 @@ func (f *FakeRuleStore) GetAlertRuleByUID(_ context.Context, q *models.GetAlertR } // For now, we're not implementing namespace filtering. -func (f *FakeRuleStore) GetAlertRulesForScheduling(_ context.Context, q *models.ListAlertRulesQuery) error { +func (f *FakeRuleStore) GetAlertRulesForScheduling(_ context.Context, q *models.GetAlertRulesForSchedulingQuery) error { f.mtx.Lock() defer f.mtx.Unlock() f.RecordedOps = append(f.RecordedOps, *q) @@ -161,7 +161,14 @@ func (f *FakeRuleStore) GetAlertRulesForScheduling(_ context.Context, q *models. return err } for _, rules := range f.Rules { - q.Result = append(q.Result, rules...) + for _, rule := range rules { + q.Result = append(q.Result, &models.SchedulableAlertRule{ + UID: rule.UID, + OrgID: rule.OrgID, + IntervalSeconds: rule.IntervalSeconds, + Version: rule.Version, + }) + } } return nil } From 6c9cf4843fdec3a2bb2b0ceb336a88b25c0032b4 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Thu, 12 May 2022 16:32:29 +0200 Subject: [PATCH 133/440] AzureMonitor: Style improvements to ResourcePicker (#48875) Co-authored-by: Kevin Yu Co-authored-by: Isabella Siu Co-authored-by: Sarah Zinger --- .../LogsQueryEditor/LogsQueryEditor.tsx | 36 +++++++------- .../MetricsQueryEditor.test.tsx | 2 +- .../MetricsQueryEditor.tsx | 17 +++++-- .../ResourceField/ResourceField.tsx | 49 +++++++------------ .../components/ResourcePicker/styles.ts | 14 ++++++ 5 files changed, 64 insertions(+), 54 deletions(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx index d81ac96920b..1e77997c5c4 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { Alert, InlineFieldRow } from '@grafana/ui'; +import { Alert } from '@grafana/ui'; import Datasource from '../../datasource'; import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery } from '../../types'; @@ -35,24 +35,22 @@ const LogsQueryEditor: React.FC = ({ return (
    - - - + ); - const resourcePickerButton = await screen.findByRole('button', { name: /grafanastaging/ }); + const resourcePickerButton = await screen.findByRole('button', { name: /grafana/ }); expect(screen.getByText('Microsoft.Compute/virtualMachines')).toBeInTheDocument(); expect(screen.getByText('Metric A')).toBeInTheDocument(); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/MetricsQueryEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/MetricsQueryEditor.tsx index a0ce1ff4a52..22f85cedc63 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/MetricsQueryEditor.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/NewMetricsQueryEditor/MetricsQueryEditor.tsx @@ -1,9 +1,10 @@ +import { css } from '@emotion/css'; import React from 'react'; import { PanelData } from '@grafana/data/src/types'; import { EditorRows, EditorRow, EditorFieldGroup } from '@grafana/experimental'; import { config } from '@grafana/runtime'; -import { InlineFieldRow } from '@grafana/ui'; +import { InlineFieldRow, useStyles2 } from '@grafana/ui'; import type Datasource from '../../datasource'; import type { AzureMonitorQuery, AzureMonitorOption, AzureMonitorErrorish } from '../../types'; @@ -37,6 +38,8 @@ const MetricsQueryEditor: React.FC = ({ onChange, setError, }) => { + const styles = useStyles2(getStyles); + const metricsMetadata = useMetricMetadata(query, datasource, onChange); const metricNamespaces = useMetricNamespaces(query, datasource, onChange, setError); const metricNames = useMetricNames(query, datasource, onChange, setError); @@ -141,7 +144,7 @@ const MetricsQueryEditor: React.FC = ({ } else { return (
    - + = ({ /> - + = ({ setError={setError} /> - + = ({ } }; +const getStyles = () => ({ + row: css({ + rowGap: 0, + }), +}); + export default MetricsQueryEditor; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourceField/ResourceField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourceField/ResourceField.tsx index 2a2bb98b879..20e5bbc01aa 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourceField/ResourceField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourceField/ResourceField.tsx @@ -1,16 +1,15 @@ -import { css } from '@emotion/css'; +import { cx } from '@emotion/css'; import React, { useCallback, useEffect, useState } from 'react'; -import { GrafanaTheme2 } from '@grafana/data'; import { Button, Icon, Modal, useStyles2 } from '@grafana/ui'; import Datasource from '../../datasource'; import { AzureQueryEditorFieldProps, AzureMonitorQuery, AzureResourceSummaryItem } from '../../types'; import { Field } from '../Field'; import ResourcePicker from '../ResourcePicker'; +import getStyles from '../ResourcePicker/styles'; import { ResourceRowType } from '../ResourcePicker/types'; import { parseResourceURI } from '../ResourcePicker/utils'; -import { Space } from '../Space'; function parseResourceDetails(resourceURI: string) { const parsed = parseResourceURI(resourceURI); @@ -80,7 +79,7 @@ const ResourceField: React.FC = ({ - @@ -128,37 +127,27 @@ interface FormattedResourceProps { } const FormattedResource = ({ resource }: FormattedResourceProps) => { + const styles = useStyles2(getStyles); + + if (resource.resourceName) { + return ( + + {resource.resourceName} + + ); + } + if (resource.resourceGroupName) { + return ( + + {resource.resourceGroupName} + + ); + } return ( {resource.subscriptionName} - {resource.resourceGroupName && ( - <> - - {resource.resourceGroupName} - - )} - {resource.resourceName && ( - <> - - {resource.resourceName} - - )} ); }; -const Separator = () => ( - <> - - {'/'} - - -); - export default ResourceField; - -const getStyles = (theme: GrafanaTheme2) => ({ - modal: css({ - width: theme.breakpoints.values.lg, - }), -}); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/styles.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/styles.ts index 51db0d61820..ecc2992500b 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/styles.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/styles.ts @@ -66,6 +66,16 @@ const getStyles = (theme: GrafanaTheme2) => ({ whiteSpace: 'nowrap', }), + resourceField: css({ + maxWidth: theme.spacing(36), + overflow: 'hidden', + }), + + resourceFieldButton: css({ + padding: '7px', + textAlign: 'left', + }), + nestedRowCheckbox: css({ zIndex: 0, }), @@ -88,6 +98,10 @@ const getStyles = (theme: GrafanaTheme2) => ({ margin: '4px 0', fontStyle: 'italic', }), + + modal: css({ + width: theme.breakpoints.values.lg, + }), }); export default getStyles; From 186ba26b59d301cc977abbbe8a2334901bcf8f81 Mon Sep 17 00:00:00 2001 From: Yuriy Tseretyan Date: Thu, 12 May 2022 10:42:31 -0400 Subject: [PATCH 134/440] Alerting: refactor rule API to create rule group in a single place (#48915) * extract method toGettableRuleGroupConfig --- pkg/services/ngalert/api/api_ruler.go | 80 ++++++++++++--------------- 1 file changed, 35 insertions(+), 45 deletions(-) diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index c122a19c1be..8e2ff91d57b 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -226,9 +226,6 @@ func (srv RulerSrv) RouteGetRulesGroupConfig(c *models.ReqContext) response.Resp return ErrResp(http.StatusInternalServerError, err, "failed to get group alert rules") } - var ruleGroupInterval model.Duration - ruleNodes := make([]apimodels.GettableExtendedRuleNode, 0, len(q.Result)) - hasAccess := func(evaluator accesscontrol.Evaluator) bool { return accesscontrol.HasAccess(srv.ac, c)(accesscontrol.ReqSignedIn, evaluator) } @@ -238,20 +235,16 @@ func (srv RulerSrv) RouteGetRulesGroupConfig(c *models.ReqContext) response.Resp return ErrResp(http.StatusInternalServerError, err, "failed to get group alert rules") } + groupRules := make([]*ngmodels.AlertRule, 0, len(q.Result)) for _, r := range q.Result { if !authorizeDatasourceAccessForRule(r, hasAccess) { continue } - ruleGroupInterval = model.Duration(time.Duration(r.IntervalSeconds) * time.Second) - ruleNodes = append(ruleNodes, toGettableExtendedRuleNode(*r, namespace.Id, provenanceRecords)) + groupRules = append(groupRules, r) } result := apimodels.RuleGroupConfigResponse{ - GettableRuleGroupConfig: apimodels.GettableRuleGroupConfig{ - Name: ruleGroup, - Interval: ruleGroupInterval, - Rules: ruleNodes, - }, + GettableRuleGroupConfig: toGettableRuleGroupConfig(ruleGroup, groupRules, namespace.Id, provenanceRecords), } return response.JSON(http.StatusAccepted, result) } @@ -293,7 +286,7 @@ func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response return ErrResp(http.StatusInternalServerError, err, "failed to get alert rules") } - configs := make(map[string]map[string]apimodels.GettableRuleGroupConfig) + configs := make(map[string]map[string][]*ngmodels.AlertRule) hasAccess := func(evaluator accesscontrol.Evaluator) bool { return accesscontrol.HasAccess(srv.ac, c)(accesscontrol.ReqSignedIn, evaluator) @@ -308,44 +301,25 @@ func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response if !authorizeDatasourceAccessForRule(r, hasAccess) { continue } - folder, ok := namespaceMap[r.NamespaceUID] + namespaceCfgs, ok := configs[r.NamespaceUID] if !ok { - srv.log.Error("namespace not visible to the user", "user", c.SignedInUser.UserId, "namespace", r.NamespaceUID, "rule", r.UID) + namespaceCfgs = make(map[string][]*ngmodels.AlertRule) + configs[r.NamespaceUID] = namespaceCfgs + } + group := namespaceCfgs[r.RuleGroup] + group = append(group, r) + namespaceCfgs[r.RuleGroup] = group + } + + for namespaceUID, m := range configs { + folder, ok := namespaceMap[namespaceUID] + if !ok { + srv.log.Error("namespace not visible to the user", "user", c.SignedInUser.UserId, "namespace", namespaceUID) continue } namespace := folder.Title - _, ok = configs[namespace] - if !ok { - ruleGroupInterval := model.Duration(time.Duration(r.IntervalSeconds) * time.Second) - configs[namespace] = make(map[string]apimodels.GettableRuleGroupConfig) - configs[namespace][r.RuleGroup] = apimodels.GettableRuleGroupConfig{ - Name: r.RuleGroup, - Interval: ruleGroupInterval, - Rules: []apimodels.GettableExtendedRuleNode{ - toGettableExtendedRuleNode(*r, folder.Id, provenanceRecords), - }, - } - } else { - ruleGroupConfig, ok := configs[namespace][r.RuleGroup] - if !ok { - ruleGroupInterval := model.Duration(time.Duration(r.IntervalSeconds) * time.Second) - configs[namespace][r.RuleGroup] = apimodels.GettableRuleGroupConfig{ - Name: r.RuleGroup, - Interval: ruleGroupInterval, - Rules: []apimodels.GettableExtendedRuleNode{ - toGettableExtendedRuleNode(*r, folder.Id, provenanceRecords), - }, - } - } else { - ruleGroupConfig.Rules = append(ruleGroupConfig.Rules, toGettableExtendedRuleNode(*r, folder.Id, provenanceRecords)) - configs[namespace][r.RuleGroup] = ruleGroupConfig - } - } - } - - for namespace, m := range configs { - for _, ruleGroupConfig := range m { - result[namespace] = append(result[namespace], ruleGroupConfig) + for groupName, groupRules := range m { + result[namespace] = append(result[namespace], toGettableRuleGroupConfig(groupName, groupRules, folder.Id, provenanceRecords)) } } return response.JSON(http.StatusOK, result) @@ -526,6 +500,22 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, namespace *mod return response.JSON(http.StatusAccepted, util.DynMap{"message": "rule group updated successfully"}) } +func toGettableRuleGroupConfig(groupName string, rules []*ngmodels.AlertRule, namespaceID int64, provenanceRecords map[string]ngmodels.Provenance) apimodels.GettableRuleGroupConfig { + ruleNodes := make([]apimodels.GettableExtendedRuleNode, 0, len(rules)) + var interval time.Duration + if len(rules) > 0 { + interval = time.Duration(rules[0].IntervalSeconds) * time.Second + } + for _, r := range rules { + ruleNodes = append(ruleNodes, toGettableExtendedRuleNode(*r, namespaceID, provenanceRecords)) + } + return apimodels.GettableRuleGroupConfig{ + Name: groupName, + Interval: model.Duration(interval), + Rules: ruleNodes, + } +} + func toGettableExtendedRuleNode(r ngmodels.AlertRule, namespaceID int64, provenanceRecords map[string]ngmodels.Provenance) apimodels.GettableExtendedRuleNode { provenance := ngmodels.ProvenanceNone if prov, exists := provenanceRecords[r.ResourceID()]; exists { From 6cbaa18cf64c38e289015259e09fe76857c3ebfe Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Thu, 12 May 2022 11:09:58 -0400 Subject: [PATCH 135/440] Prometheus: Add golden JSON tests (#48941) --- pkg/tsdb/prometheus/buffered/framing_test.go | 20 +++- .../range_infinity.result.golden.json | 63 ++++++++++ .../testdata/range_missing.result.golden.json | 56 +++++++++ .../testdata/range_nan.result.golden.json | 56 +++++++++ .../testdata/range_simple.result.golden.json | 112 ++++++++++++++++++ 5 files changed, 305 insertions(+), 2 deletions(-) create mode 100644 pkg/tsdb/prometheus/testdata/range_infinity.result.golden.json create mode 100644 pkg/tsdb/prometheus/testdata/range_missing.result.golden.json create mode 100644 pkg/tsdb/prometheus/testdata/range_nan.result.golden.json create mode 100644 pkg/tsdb/prometheus/testdata/range_simple.result.golden.json diff --git a/pkg/tsdb/prometheus/buffered/framing_test.go b/pkg/tsdb/prometheus/buffered/framing_test.go index 1bc75c102c7..40a2ead8fb5 100644 --- a/pkg/tsdb/prometheus/buffered/framing_test.go +++ b/pkg/tsdb/prometheus/buffered/framing_test.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "io" + "io/ioutil" "net/http" "os" "path/filepath" @@ -22,6 +23,8 @@ import ( apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" ) +var update = false + func TestMatrixResponses(t *testing.T) { tt := []struct { name string @@ -37,7 +40,7 @@ func TestMatrixResponses(t *testing.T) { t.Run(test.name, func(t *testing.T) { queryFileName := filepath.Join("../testdata", test.filepath+".query.json") responseFileName := filepath.Join("../testdata", test.filepath+".result.json") - goldenFileName := filepath.Join("../testdata", test.filepath+".result.golden.txt") + goldenFileName := filepath.Join("../testdata", test.filepath+".result.golden") query, err := loadStoredPrometheusQuery(queryFileName) require.NoError(t, err) @@ -52,7 +55,20 @@ func TestMatrixResponses(t *testing.T) { dr, found := result.Responses["A"] require.True(t, found) - require.NoError(t, experimental.CheckGoldenDataResponse(goldenFileName, &dr, true)) + actual, err := json.MarshalIndent(&dr, "", " ") + require.NoError(t, err) + + // nolint:gosec + // We can ignore the gosec G304 because this is a test with static defined paths + expected, err := ioutil.ReadFile(goldenFileName + ".json") + if err != nil || update { + err = os.WriteFile(goldenFileName+".json", actual, 0600) + require.NoError(t, err) + } + + require.JSONEq(t, string(expected), string(actual)) + + require.NoError(t, experimental.CheckGoldenDataResponse(goldenFileName+".txt", &dr, update)) }) } } diff --git a/pkg/tsdb/prometheus/testdata/range_infinity.result.golden.json b/pkg/tsdb/prometheus/testdata/range_infinity.result.golden.json new file mode 100644 index 00000000000..5327904e05b --- /dev/null +++ b/pkg/tsdb/prometheus/testdata/range_infinity.result.golden.json @@ -0,0 +1,63 @@ +{ + "frames": [ + { + "schema": { + "name": "1 / 0", + "meta": { + "custom": { + "resultType": "matrix" + }, + "executedQueryString": "Expr: 1 / 0\nStep: 1s" + }, + "fields": [ + { + "name": "Time", + "type": "time", + "typeInfo": { + "frame": "time.Time" + }, + "config": { + "interval": 1000 + } + }, + { + "name": "Value", + "type": "number", + "typeInfo": { + "frame": "float64", + "nullable": true + }, + "labels": {}, + "config": { + "displayNameFromDS": "1 / 0" + } + } + ] + }, + "data": { + "values": [ + [ + 1641889530000, + 1641889531000, + 1641889532000 + ], + [ + null, + null, + null + ] + ], + "entities": [ + null, + { + "Inf": [ + 0, + 1, + 2 + ] + } + ] + } + } + ] +} \ No newline at end of file diff --git a/pkg/tsdb/prometheus/testdata/range_missing.result.golden.json b/pkg/tsdb/prometheus/testdata/range_missing.result.golden.json new file mode 100644 index 00000000000..99155255d2c --- /dev/null +++ b/pkg/tsdb/prometheus/testdata/range_missing.result.golden.json @@ -0,0 +1,56 @@ +{ + "frames": [ + { + "schema": { + "name": "go_goroutines{job=\"prometheus\"}", + "meta": { + "custom": { + "resultType": "matrix" + }, + "executedQueryString": "Expr: test1\nStep: 1s" + }, + "fields": [ + { + "name": "Time", + "type": "time", + "typeInfo": { + "frame": "time.Time" + }, + "config": { + "interval": 1000 + } + }, + { + "name": "Value", + "type": "number", + "typeInfo": { + "frame": "float64", + "nullable": true + }, + "labels": { + "__name__": "go_goroutines", + "job": "prometheus" + }, + "config": { + "displayNameFromDS": "go_goroutines{job=\"prometheus\"}" + } + } + ] + }, + "data": { + "values": [ + [ + 1641889533000, + 1641889534000, + 1641889537000 + ], + [ + 21, + 32, + 43 + ] + ] + } + } + ] +} \ No newline at end of file diff --git a/pkg/tsdb/prometheus/testdata/range_nan.result.golden.json b/pkg/tsdb/prometheus/testdata/range_nan.result.golden.json new file mode 100644 index 00000000000..d33e61b5923 --- /dev/null +++ b/pkg/tsdb/prometheus/testdata/range_nan.result.golden.json @@ -0,0 +1,56 @@ +{ + "frames": [ + { + "schema": { + "name": "{handler=\"/api/v1/query_range\", job=\"prometheus\"}", + "meta": { + "custom": { + "resultType": "matrix" + }, + "executedQueryString": "Expr: \nStep: 1s" + }, + "fields": [ + { + "name": "Time", + "type": "time", + "typeInfo": { + "frame": "time.Time" + }, + "config": { + "interval": 1000 + } + }, + { + "name": "Value", + "type": "number", + "typeInfo": { + "frame": "float64", + "nullable": true + }, + "labels": { + "handler": "/api/v1/query_range", + "job": "prometheus" + }, + "config": { + "displayNameFromDS": "{handler=\"/api/v1/query_range\", job=\"prometheus\"}" + } + } + ] + }, + "data": { + "values": [ + [ + 1641889530000, + 1641889531000, + 1641889532000 + ], + [ + null, + null, + null + ] + ] + } + } + ] +} \ No newline at end of file diff --git a/pkg/tsdb/prometheus/testdata/range_simple.result.golden.json b/pkg/tsdb/prometheus/testdata/range_simple.result.golden.json new file mode 100644 index 00000000000..462e1a46a4d --- /dev/null +++ b/pkg/tsdb/prometheus/testdata/range_simple.result.golden.json @@ -0,0 +1,112 @@ +{ + "frames": [ + { + "schema": { + "name": "prometheus_http_requests_total{code=\"200\", handler=\"/api/v1/query_range\", job=\"prometheus\"}", + "meta": { + "custom": { + "resultType": "matrix" + }, + "executedQueryString": "Expr: \nStep: 1s" + }, + "fields": [ + { + "name": "Time", + "type": "time", + "typeInfo": { + "frame": "time.Time" + }, + "config": { + "interval": 1000 + } + }, + { + "name": "Value", + "type": "number", + "typeInfo": { + "frame": "float64", + "nullable": true + }, + "labels": { + "__name__": "prometheus_http_requests_total", + "code": "200", + "handler": "/api/v1/query_range", + "job": "prometheus" + }, + "config": { + "displayNameFromDS": "prometheus_http_requests_total{code=\"200\", handler=\"/api/v1/query_range\", job=\"prometheus\"}" + } + } + ] + }, + "data": { + "values": [ + [ + 1641889530123, + 1641889531123, + 1641889532123 + ], + [ + 21, + 32, + 43 + ] + ] + } + }, + { + "schema": { + "name": "prometheus_http_requests_total{code=\"400\", handler=\"/api/v1/query_range\", job=\"prometheus\"}", + "meta": { + "custom": { + "resultType": "matrix" + }, + "executedQueryString": "Expr: \nStep: 1s" + }, + "fields": [ + { + "name": "Time", + "type": "time", + "typeInfo": { + "frame": "time.Time" + }, + "config": { + "interval": 1000 + } + }, + { + "name": "Value", + "type": "number", + "typeInfo": { + "frame": "float64", + "nullable": true + }, + "labels": { + "__name__": "prometheus_http_requests_total", + "code": "400", + "handler": "/api/v1/query_range", + "job": "prometheus" + }, + "config": { + "displayNameFromDS": "prometheus_http_requests_total{code=\"400\", handler=\"/api/v1/query_range\", job=\"prometheus\"}" + } + } + ] + }, + "data": { + "values": [ + [ + 1641889530123, + 1641889531123, + 1641889532123 + ], + [ + 54, + 65, + 76 + ] + ] + } + } + ] +} \ No newline at end of file From 1c679e814b1e031a351c3d2c066eb85d8633150d Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Thu, 12 May 2022 17:15:18 +0200 Subject: [PATCH 136/440] AccessControl: Only return action and scope for user permissions and make them unique (#48939) * Only return action and scope for user permissions and make them unique --- pkg/services/accesscontrol/accesscontrol.go | 3 ++- pkg/services/accesscontrol/database/database.go | 8 ++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index f53ac3c8796..a1a918809db 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -21,7 +21,7 @@ type AccessControl interface { // Evaluate evaluates access to the given resources. Evaluate(ctx context.Context, user *models.SignedInUser, evaluator Evaluator) (bool, error) - // GetUserPermissions returns user permissions. + // GetUserPermissions returns user permissions with only action and scope fields set. GetUserPermissions(ctx context.Context, user *models.SignedInUser, options Options) ([]*Permission, error) // GetUserRoles returns user roles. @@ -40,6 +40,7 @@ type AccessControl interface { } type PermissionsProvider interface { + // GetUserPermissions returns user permissions with only action and scope fields set. GetUserPermissions(ctx context.Context, query GetUserPermissionsQuery) ([]*Permission, error) } diff --git a/pkg/services/accesscontrol/database/database.go b/pkg/services/accesscontrol/database/database.go index ec5ef5dbe1c..b69c7b8c858 100644 --- a/pkg/services/accesscontrol/database/database.go +++ b/pkg/services/accesscontrol/database/database.go @@ -26,13 +26,9 @@ func (s *AccessControlStore) GetUserPermissions(ctx context.Context, query acces filter, params := userRolesFilter(query.OrgID, query.UserID, query.Roles) // TODO: optimize this - q := `SELECT - permission.id, - permission.role_id, + q := `SELECT DISTINCT permission.action, - permission.scope, - permission.updated, - permission.created + permission.scope FROM permission INNER JOIN role ON role.id = permission.role_id ` + filter From ab91af202cf4b83e15d2a8b5c356abff2bcce513 Mon Sep 17 00:00:00 2001 From: Chrysa Dikonimaki Date: Thu, 12 May 2022 22:11:35 +0200 Subject: [PATCH 137/440] Convert `packages/grafana-ui/src/components/Logs/LogMessageAnsi.test.tsx` to RTL (#48918) * convert LogLabels tests toRTL * convert LogMessageAnsi tests to RTL * convert LogMessageAnsi to RTL fix * remove comment from LogMessageAnsi tests --- .betterer.results | 3 -- .../components/Logs/LogMessageAnsi.test.tsx | 47 +++++++++---------- 2 files changed, 21 insertions(+), 29 deletions(-) diff --git a/.betterer.results b/.betterer.results index dabdf567604..e7c5b4af315 100644 --- a/.betterer.results +++ b/.betterer.results @@ -26,9 +26,6 @@ exports[`no enzyme tests`] = { "packages/grafana-ui/src/components/Graph/GraphTooltip/MultiModeGraphTooltip.test.tsx:1865444105": [ [0, 17, 13, "RegExp match", "2409514259"] ], - "packages/grafana-ui/src/components/Logs/LogMessageAnsi.test.tsx:1630730648": [ - [0, 19, 13, "RegExp match", "2409514259"] - ], "packages/grafana-ui/src/components/Logs/LogRowContextProvider.test.tsx:2719724375": [ [0, 17, 13, "RegExp match", "2409514259"] ], diff --git a/packages/grafana-ui/src/components/Logs/LogMessageAnsi.test.tsx b/packages/grafana-ui/src/components/Logs/LogMessageAnsi.test.tsx index 8bafbb9725a..cfc197459b6 100644 --- a/packages/grafana-ui/src/components/Logs/LogMessageAnsi.test.tsx +++ b/packages/grafana-ui/src/components/Logs/LogMessageAnsi.test.tsx @@ -1,4 +1,4 @@ -import { shallow } from 'enzyme'; +import { render, screen, within } from '@testing-library/react'; import React from 'react'; import { createTheme } from '@grafana/data'; @@ -7,44 +7,39 @@ import { UnThemedLogMessageAnsi as LogMessageAnsi } from './LogMessageAnsi'; describe('', () => { it('renders string without ANSI codes', () => { - const wrapper = shallow(); + render(); - expect(wrapper.find('span').exists()).toBe(false); - expect(wrapper.text()).toBe('Lorem ipsum'); + expect(screen.queryByTestId('ansiLogLine')).not.toBeInTheDocument(); + expect(screen.queryByText('Lorem ipsum')).toBeInTheDocument(); }); it('renders string with ANSI codes', () => { const value = 'Lorem \u001B[31mipsum\u001B[0m et dolor'; - const wrapper = shallow(); + render(); - expect(wrapper.find('span')).toHaveLength(1); - expect(wrapper.find('span').first().prop('style')).toMatchObject( - expect.objectContaining({ - color: expect.any(String), - }) - ); - expect(wrapper.find('span').first().text()).toBe('ipsum'); + expect(screen.queryByTestId('ansiLogLine')).toBeInTheDocument(); + expect(screen.getAllByTestId('ansiLogLine')).toHaveLength(1); + expect(screen.getAllByTestId('ansiLogLine').at(0)).toHaveAttribute('style', expect.stringMatching('color')); + + const { getByText } = within(screen.getAllByTestId('ansiLogLine').at(0)!); + expect(getByText('ipsum')).toBeInTheDocument(); }); it('renders string with ANSI codes with correctly converted css classnames', () => { const value = 'Lorem \u001B[1;32mIpsum'; - const wrapper = shallow(); + render(); - expect(wrapper.find('span')).toHaveLength(1); - expect(wrapper.find('span').first().prop('style')).toMatchObject( - expect.objectContaining({ - fontWeight: expect.any(String), - }) - ); + expect(screen.queryByTestId('ansiLogLine')).toBeInTheDocument(); + expect(screen.getAllByTestId('ansiLogLine')).toHaveLength(1); + + expect(screen.getAllByTestId('ansiLogLine').at(0)).toHaveAttribute('style', expect.stringMatching('font-weight')); }); it('renders string with ANSI dim code with appropriate themed color', () => { const value = 'Lorem \u001B[1;2mIpsum'; const theme = createTheme(); - const wrapper = shallow(); + render(); - expect(wrapper.find('span')).toHaveLength(1); - expect(wrapper.find('span').first().prop('style')).toMatchObject( - expect.objectContaining({ - color: theme.colors.text.secondary, - }) - ); + expect(screen.queryByTestId('ansiLogLine')).toBeInTheDocument(); + expect(screen.getAllByTestId('ansiLogLine')).toHaveLength(1); + + expect(screen.getAllByTestId('ansiLogLine').at(0)).toHaveStyle({ color: theme.colors.text.secondary }); }); }); From a51c2774b8e77cafc0100a3882d77039c189e301 Mon Sep 17 00:00:00 2001 From: tomxwang <289758716@qq.com> Date: Fri, 13 May 2022 04:16:22 +0800 Subject: [PATCH 138/440] convert UsersActionBar tests to RTL (#48737) --- .betterer.results | 3 - .../features/users/UsersActionBar.test.tsx | 23 ++-- public/app/features/users/UsersActionBar.tsx | 2 +- .../UsersActionBar.test.tsx.snap | 114 ------------------ 4 files changed, 14 insertions(+), 128 deletions(-) delete mode 100644 public/app/features/users/__snapshots__/UsersActionBar.test.tsx.snap diff --git a/.betterer.results b/.betterer.results index e7c5b4af315..5cdf0fe5ed9 100644 --- a/.betterer.results +++ b/.betterer.results @@ -242,9 +242,6 @@ exports[`no enzyme tests`] = { "public/app/features/teams/TeamSettings.test.tsx:2043271249": [ [0, 19, 13, "RegExp match", "2409514259"] ], - "public/app/features/users/UsersActionBar.test.tsx:3740177621": [ - [0, 19, 13, "RegExp match", "2409514259"] - ], "public/app/features/users/UsersListPage.test.tsx:3908145117": [ [0, 19, 13, "RegExp match", "2409514259"] ], diff --git a/public/app/features/users/UsersActionBar.test.tsx b/public/app/features/users/UsersActionBar.test.tsx index a9e9c4b4490..1e8d2d44a73 100644 --- a/public/app/features/users/UsersActionBar.test.tsx +++ b/public/app/features/users/UsersActionBar.test.tsx @@ -1,4 +1,4 @@ -import { shallow } from 'enzyme'; +import { render, screen } from '@testing-library/react'; import React from 'react'; import { mockToolkitActionCreator } from 'test/core/redux/mocks'; @@ -26,37 +26,40 @@ const setup = (propOverrides?: object) => { Object.assign(props, propOverrides); - return shallow(); + const { rerender } = render(); + + return { rerender, props }; }; describe('Render', () => { it('should render component', () => { - const wrapper = setup(); + setup(); - expect(wrapper).toMatchSnapshot(); + expect(screen.getByTestId('users-action-bar')).toBeInTheDocument(); }); it('should render pending invites button', () => { - const wrapper = setup({ + setup({ pendingInvitesCount: 5, }); - expect(wrapper).toMatchSnapshot(); + expect(screen.getByRole('radio', { name: 'Pending Invites (5)' })).toBeInTheDocument(); }); it('should show invite button', () => { - const wrapper = setup({ + setup({ canInvite: true, }); - expect(wrapper).toMatchSnapshot(); + expect(screen.getByRole('link', { name: 'Invite' })).toHaveAttribute('href', 'org/users/invite'); }); it('should show external user management button', () => { - const wrapper = setup({ + setup({ externalUserMngLinkUrl: 'some/url', + externalUserMngLinkName: 'someUrl', }); - expect(wrapper).toMatchSnapshot(); + expect(screen.getByRole('link', { name: 'someUrl' })).toHaveAttribute('href', 'some/url'); }); }); diff --git a/public/app/features/users/UsersActionBar.tsx b/public/app/features/users/UsersActionBar.tsx index 05b1166b2c1..55fe3b046f3 100644 --- a/public/app/features/users/UsersActionBar.tsx +++ b/public/app/features/users/UsersActionBar.tsx @@ -40,7 +40,7 @@ export class UsersActionBar extends PureComponent { const canAddToOrg = contextSrv.hasAccess(AccessControlAction.UsersCreate, canInvite); return ( -
    +
    -
    - -
    - - Invite - -
    -`; - -exports[`Render should render pending invites button 1`] = ` -
    -
    - -
    -
    - -
    - - Invite - -
    -`; - -exports[`Render should show external user management button 1`] = ` -
    -
    - -
    - - Invite - - -
    -`; - -exports[`Render should show invite button 1`] = ` -
    -
    - -
    - - Invite - -
    -`; From 555867135be7403c46b80b3da77c2690d17a5c1f Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Fri, 13 May 2022 09:26:34 +0200 Subject: [PATCH 139/440] Access control: Using RBAC to filter users in list view that you have read access to (#47963) * Add SQL filter for global user search * Remove scope requirements from endpoints Co-authored-by: Gabriel MABILLE Co-authored-by: Ieva Co-authored-by: Karl Persson --- pkg/api/api.go | 6 +++--- pkg/models/user.go | 13 +++++++------ pkg/services/comments/handlers.go | 8 +++++++- pkg/services/searchusers/searchusers.go | 9 ++++++++- pkg/services/sqlstore/user.go | 10 ++++++++++ pkg/services/sqlstore/user_test.go | 22 ++++++++++++++++++++++ 6 files changed, 57 insertions(+), 11 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 6f06156f0ae..2931c9027de 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -176,8 +176,8 @@ func (hs *HTTPServer) registerRoutes() { apiRoute.Group("/users", func(usersRoute routing.RouteRegister) { userIDScope := ac.Scope("global.users", "id", ac.Parameter(":id")) - usersRoute.Get("/", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead, ac.ScopeGlobalUsersAll)), routing.Wrap(hs.searchUsersService.SearchUsers)) - usersRoute.Get("/search", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead, ac.ScopeGlobalUsersAll)), routing.Wrap(hs.searchUsersService.SearchUsersWithPaging)) + usersRoute.Get("/", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead)), routing.Wrap(hs.searchUsersService.SearchUsers)) + usersRoute.Get("/search", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead)), routing.Wrap(hs.searchUsersService.SearchUsersWithPaging)) usersRoute.Get("/:id", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead, userIDScope)), routing.Wrap(hs.GetUserByID)) usersRoute.Get("/:id/teams", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersTeamRead, userIDScope)), routing.Wrap(hs.GetUserTeams)) usersRoute.Get("/:id/orgs", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead, userIDScope)), routing.Wrap(hs.GetUserOrgList)) @@ -277,7 +277,7 @@ func (hs *HTTPServer) registerRoutes() { orgsRoute.Put("/", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ActionOrgsWrite)), routing.Wrap(hs.UpdateOrg)) orgsRoute.Put("/address", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ActionOrgsWrite)), routing.Wrap(hs.UpdateOrgAddress)) orgsRoute.Delete("/", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ActionOrgsDelete)), routing.Wrap(hs.DeleteOrgByID)) - orgsRoute.Get("/users", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersRead, ac.ScopeUsersAll)), routing.Wrap(hs.GetOrgUsers)) + orgsRoute.Get("/users", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersRead)), routing.Wrap(hs.GetOrgUsers)) orgsRoute.Post("/users", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersAdd, ac.ScopeUsersAll)), routing.Wrap(hs.AddOrgUser)) orgsRoute.Patch("/users/:userId", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersRoleUpdate, userIDScope)), routing.Wrap(hs.UpdateOrgUser)) orgsRoute.Delete("/users/:userId", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersRemove, userIDScope)), routing.Wrap(hs.RemoveOrgUser)) diff --git a/pkg/models/user.go b/pkg/models/user.go index b2419637364..9ed0996020b 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -141,12 +141,13 @@ type GetUserProfileQuery struct { } type SearchUsersQuery struct { - OrgId int64 - Query string - Page int - Limit int - AuthModule string - Filters []Filter + SignedInUser *SignedInUser + OrgId int64 + Query string + Page int + Limit int + AuthModule string + Filters []Filter IsDisabled *bool diff --git a/pkg/services/comments/handlers.go b/pkg/services/comments/handlers.go index 8773f19b4ff..c98fd691944 100644 --- a/pkg/services/comments/handlers.go +++ b/pkg/services/comments/handlers.go @@ -146,7 +146,13 @@ func (s *Service) Get(ctx context.Context, orgID int64, signedInUser *models.Sig } // NOTE: probably replace with comment and user table join. - query := &models.SearchUsersQuery{Query: "", Filters: []models.Filter{NewIDFilter(userIds)}, Page: 0, Limit: len(userIds)} + query := &models.SearchUsersQuery{ + Query: "", + Page: 0, + Limit: len(userIds), + SignedInUser: signedInUser, + Filters: []models.Filter{NewIDFilter(userIds)}, + } if err := s.sqlStore.SearchUsers(ctx, query); err != nil { return nil, err } diff --git a/pkg/services/searchusers/searchusers.go b/pkg/services/searchusers/searchusers.go index fbed2051dae..78b59735c1d 100644 --- a/pkg/services/searchusers/searchusers.go +++ b/pkg/services/searchusers/searchusers.go @@ -61,7 +61,14 @@ func (s *OSSService) SearchUser(c *models.ReqContext) (*models.SearchUsersQuery, } } - query := &models.SearchUsersQuery{Query: searchQuery, Filters: filters, Page: page, Limit: perPage} + query := &models.SearchUsersQuery{ + // added SignedInUser to the query, as to only list the users that the user has permission to read + SignedInUser: c.SignedInUser, + Query: searchQuery, + Filters: filters, + Page: page, + Limit: perPage, + } if err := s.sqlStore.SearchUsers(c.Req.Context(), query); err != nil { return nil, err } diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 2a029912a37..51b21360002 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -620,6 +620,16 @@ func (ss *SQLStore) SearchUsers(ctx context.Context, query *models.SearchUsersQu whereParams = append(whereParams, query.OrgId) } + // user only sees the users for which it has read permissions + if !ac.IsDisabled(ss.Cfg) { + acFilter, err := ac.Filter(query.SignedInUser, "u.id", "global.users:id:", ac.ActionUsersRead) + if err != nil { + return err + } + whereConditions = append(whereConditions, acFilter.Where) + whereParams = append(whereParams, acFilter.Args...) + } + if query.Query != "" { whereConditions = append(whereConditions, "(email "+dialect.LikeStr()+" ? OR name "+dialect.LikeStr()+" ? OR login "+dialect.LikeStr()+" ?)") whereParams = append(whereParams, queryWithWildcards, queryWithWildcards, queryWithWildcards) diff --git a/pkg/services/sqlstore/user_test.go b/pkg/services/sqlstore/user_test.go index bd971fa6f66..7c02e9cfe68 100644 --- a/pkg/services/sqlstore/user_test.go +++ b/pkg/services/sqlstore/user_test.go @@ -9,7 +9,9 @@ import ( "testing" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -336,6 +338,26 @@ func TestUserDataAccess(t *testing.T) { require.Len(t, permQuery.Result, 0) }) + t.Run("Testing DB - return list of users that the SignedInUser has permission to read", func(t *testing.T) { + ss := InitTestDB(t, InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagAccesscontrol}}) + createFiveTestUsers(t, ss, func(i int) *models.CreateUserCommand { + return &models.CreateUserCommand{ + Email: fmt.Sprint("user", i, "@test.com"), + Name: fmt.Sprint("user", i), + Login: fmt.Sprint("loginuser", i), + } + }) + + testUser := &models.SignedInUser{ + OrgId: 1, + Permissions: map[int64]map[string][]string{1: {"users:read": {"global.users:id:1", "global.users:id:3"}}}, + } + query := models.SearchUsersQuery{SignedInUser: testUser} + err := ss.SearchUsers(context.Background(), &query) + assert.Nil(t, err) + assert.Len(t, query.Result.Users, 2) + }) + ss = InitTestDB(t) t.Run("Testing DB - enable all users", func(t *testing.T) { From 60bc3e4e5c57bd77e4193d096e5e3c00366ff8c1 Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Fri, 13 May 2022 10:30:26 +0200 Subject: [PATCH 140/440] AccessControl: Let users with data source create permissions list non-core plugins (#48897) * Only require create and permissions for new data source page * Let users with permissions to create data sources list non-core plugins * Keep the admin check as fallback when using rbac as well --- pkg/api/plugins.go | 9 +++++++-- pkg/services/datasources/accesscontrol.go | 1 - public/app/features/datasources/DataSourcesListPage.tsx | 4 +--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 48f938b83ea..466093c8ac1 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -13,6 +13,9 @@ import ( "sort" "strings" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" @@ -32,8 +35,10 @@ func (hs *HTTPServer) GetPluginList(c *models.ReqContext) response.Response { embeddedFilter := c.Query("embedded") coreFilter := c.Query("core") - // For users with viewer role we only return core plugins - if !c.HasRole(models.ROLE_ADMIN) { + // When using access control anyone that can create a data source should be able to list all data sources installed + // Fallback to only letting admins list non-core plugins + hasAccess := accesscontrol.HasAccess(hs.AccessControl, c) + if !hasAccess(accesscontrol.ReqOrgAdmin, accesscontrol.EvalPermission(datasources.ActionCreate)) || c.HasRole(models.ROLE_ADMIN) { coreFilter = "1" } diff --git a/pkg/services/datasources/accesscontrol.go b/pkg/services/datasources/accesscontrol.go index 30dc2d1d213..4b16ab1479c 100644 --- a/pkg/services/datasources/accesscontrol.go +++ b/pkg/services/datasources/accesscontrol.go @@ -37,7 +37,6 @@ var ( NewPageAccess = accesscontrol.EvalAll( accesscontrol.EvalPermission(ActionRead), accesscontrol.EvalPermission(ActionCreate), - accesscontrol.EvalPermission(ActionWrite), ) // EditPageAccess is used to protect the "Configure > Data sources > Edit" page access diff --git a/public/app/features/datasources/DataSourcesListPage.tsx b/public/app/features/datasources/DataSourcesListPage.tsx index ca8c5d3ec20..f03e7ede1af 100644 --- a/public/app/features/datasources/DataSourcesListPage.tsx +++ b/public/app/features/datasources/DataSourcesListPage.tsx @@ -60,9 +60,7 @@ export class DataSourcesListPage extends PureComponent { const { dataSources, dataSourcesCount, navModel, layoutMode, searchQuery, setDataSourcesSearchQuery, hasFetched } = this.props; - const canCreateDataSource = - contextSrv.hasPermission(AccessControlAction.DataSourcesCreate) && - contextSrv.hasPermission(AccessControlAction.DataSourcesWrite); + const canCreateDataSource = contextSrv.hasPermission(AccessControlAction.DataSourcesCreate); const linkButton = { href: 'datasources/new', From 90b7a763288b649f514388410e45dc5def6ea34b Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Fri, 13 May 2022 11:40:04 +0100 Subject: [PATCH 141/440] Dashboards: Remove "delete dashboard" button for new dashboards (#48947) --- public/app/features/dashboard/state/initDashboard.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index c49b5f95628..5a092c25adf 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -212,6 +212,7 @@ export function getNewDashboardModelData(urlFolderId?: string | null): any { meta: { canStar: false, canShare: false, + canDelete: false, isNew: true, folderId: 0, }, From 75760e90b4ea920cb00c7bb9bfb467367cb7c364 Mon Sep 17 00:00:00 2001 From: Maria Alexandra <239999+axelavargas@users.noreply.github.com> Date: Fri, 13 May 2022 13:42:47 +0200 Subject: [PATCH 142/440] Search(Playground): Sync data when Move and Delete Dashboards (#48944) Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com> --- .../app/features/search/page/SearchPage.tsx | 10 +++++++- .../page/components/ConfirmDeleteModal.tsx | 6 ++--- .../search/page/components/ManageActions.tsx | 24 +++++-------------- .../page/components/MoveToFolderModal.tsx | 12 ++++++---- public/app/features/search/types.ts | 3 +-- 5 files changed, 26 insertions(+), 29 deletions(-) diff --git a/public/app/features/search/page/SearchPage.tsx b/public/app/features/search/page/SearchPage.tsx index 72b75fd6497..66b5a8c6ac7 100644 --- a/public/app/features/search/page/SearchPage.tsx +++ b/public/app/features/search/page/SearchPage.tsx @@ -88,6 +88,14 @@ export default function SearchPage() { setSearchSelection(updateSearchSelection(searchSelection, !current, kind, [uid])); }; + // function to update items when dashboards or folders are moved or deleted + const onChangeItemsList = async () => { + // clean up search selection + setSearchSelection(newSearchSelection()); + // trigger again the search to the backend + onQueryChange(inputValue); + }; + const renderResults = () => { const value = results.value; @@ -174,7 +182,7 @@ export default function SearchPage() { {Boolean(searchSelection.items.size > 0) ? ( - + ) : ( { diff --git a/public/app/features/search/page/components/ConfirmDeleteModal.tsx b/public/app/features/search/page/components/ConfirmDeleteModal.tsx index c1269a93fdf..7850be6819b 100644 --- a/public/app/features/search/page/components/ConfirmDeleteModal.tsx +++ b/public/app/features/search/page/components/ConfirmDeleteModal.tsx @@ -5,10 +5,10 @@ import { GrafanaTheme } from '@grafana/data'; import { ConfirmModal, stylesFactory, useTheme } from '@grafana/ui'; import { deleteFoldersAndDashboards } from 'app/features/manage-dashboards/state/actions'; -import { OnDeleteSelectedItems } from '../../types'; +import { OnMoveOrDeleleSelectedItems } from '../../types'; interface Props { - onDeleteItems: OnDeleteSelectedItems; + onDeleteItems: OnMoveOrDeleleSelectedItems; results: Map>; isOpen: boolean; onDismiss: () => void; @@ -40,8 +40,8 @@ export const ConfirmDeleteModal: FC = ({ results, onDeleteItems, isOpen, const deleteItems = () => { deleteFoldersAndDashboards(folders, dashboards).then(() => { + onDeleteItems(); onDismiss(); - onDeleteItems(folders, dashboards); }); }; diff --git a/public/app/features/search/page/components/ManageActions.tsx b/public/app/features/search/page/components/ManageActions.tsx index 8c4bac310b9..a9e808222f0 100644 --- a/public/app/features/search/page/components/ManageActions.tsx +++ b/public/app/features/search/page/components/ManageActions.tsx @@ -2,9 +2,10 @@ import React, { useState } from 'react'; import { Button, Checkbox, HorizontalGroup, useStyles2 } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; -import { FolderDTO, FolderInfo } from 'app/types'; +import { FolderDTO } from 'app/types'; import { GENERAL_FOLDER_UID } from '../../constants'; +import { OnMoveOrDeleleSelectedItems } from '../../types'; import { getStyles } from './ActionRow'; import { ConfirmDeleteModal } from './ConfirmDeleteModal'; @@ -13,9 +14,10 @@ import { MoveToFolderModal } from './MoveToFolderModal'; type Props = { items: Map>; folder?: FolderDTO; // when we are loading in folder page + onChange: OnMoveOrDeleleSelectedItems; }; -export function ManageActions({ items, folder }: Props) { +export function ManageActions({ items, folder, onChange }: Props) { const styles = useStyles2(getStyles); const canSave = folder?.canSave; @@ -45,20 +47,6 @@ export function ManageActions({ items, folder }: Props) { alert('TODO, toggle all....'); }; - //Todo: update item lists that were moved - const onMoveItems = (selectedDashboards: string[], folder: FolderInfo | null) => { - console.log({ selectedDashboards }); - console.log({ folder }); - console.log('items were moved in the backend'); - }; - - //Todo: update item lists that were deleted - const onDeleteItems = (folders: string[], dashboards: string[]) => { - console.log({ folders }); - console.log({ dashboards }); - console.log('items were moved in the backend'); - }; - return (
    @@ -83,13 +71,13 @@ export function ManageActions({ items, folder }: Props) {
    setIsDeleteModalOpen(false)} /> setIsMoveModalOpen(false)} diff --git a/public/app/features/search/page/components/MoveToFolderModal.tsx b/public/app/features/search/page/components/MoveToFolderModal.tsx index 26e672673e0..3675097ec7e 100644 --- a/public/app/features/search/page/components/MoveToFolderModal.tsx +++ b/public/app/features/search/page/components/MoveToFolderModal.tsx @@ -8,10 +8,10 @@ import { useAppNotification } from 'app/core/copy/appNotification'; import { moveDashboards } from 'app/features/manage-dashboards/state/actions'; import { FolderInfo } from 'app/types'; -import { OnMoveSelectedItems } from '../../types'; +import { OnMoveOrDeleleSelectedItems } from '../../types'; interface Props { - onMoveItems: OnMoveSelectedItems; + onMoveItems: OnMoveOrDeleleSelectedItems; results: Map>; isOpen: boolean; onDismiss: () => void; @@ -23,11 +23,12 @@ export const MoveToFolderModal: FC = ({ results, onMoveItems, isOpen, onD const styles = getStyles(theme); const notifyApp = useAppNotification(); const selectedDashboards = Array.from(results.get('dashboard') ?? []); + const [moving, setMoving] = useState(false); const moveTo = () => { if (folder && selectedDashboards.length) { const folderTitle = folder.title ?? 'General'; - + setMoving(true); moveDashboards(selectedDashboards, folder).then((result: any) => { if (result.successCount > 0) { const ending = result.successCount === 1 ? '' : 's'; @@ -40,9 +41,10 @@ export const MoveToFolderModal: FC = ({ results, onMoveItems, isOpen, onD notifyApp.error('Error', `Dashboard already belongs to folder ${folderTitle}`); } else { //update the list - onMoveItems(selectedDashboards, folder); + onMoveItems(); } + setMoving(false); onDismiss(); }); } @@ -66,7 +68,7 @@ export const MoveToFolderModal: FC = ({ results, onMoveItems, isOpen, onD
    -