From 76e1946bd7e923124c5f6c1430ea40b9036e68e4 Mon Sep 17 00:00:00 2001 From: Hamas Shafiq Date: Tue, 24 Jan 2023 19:21:48 +0000 Subject: [PATCH 001/172] Tempo: [TraceQL] Do not override the `status` tag name (#62030) --- .../plugins/datasource/tempo/traceql/autocomplete.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/public/app/plugins/datasource/tempo/traceql/autocomplete.ts b/public/app/plugins/datasource/tempo/traceql/autocomplete.ts index 3ba40c6b7cd..f7ea65a181b 100644 --- a/public/app/plugins/datasource/tempo/traceql/autocomplete.ts +++ b/public/app/plugins/datasource/tempo/traceql/autocomplete.ts @@ -92,15 +92,6 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP this.registerInteractionCommandId = id; } - private overrideTagName(tagName: string): string { - switch (tagName) { - case 'status': - return 'status.code'; - default: - return tagName; - } - } - private async getTagValues(tagName: string): Promise>> { let tagValues: Array>; @@ -148,8 +139,7 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP type: 'OPERATOR', })); case 'SPANSET_IN_VALUE': - const tagName = this.overrideTagName(situation.tagName); - const tagValues = await this.getTagValues(tagName); + const tagValues = await this.getTagValues(situation.tagName); const items: Completion[] = []; const getInsertionText = (val: SelectableValue): string => { From af1a264f859b8eed3cc0be1e1367dc75bcc95699 Mon Sep 17 00:00:00 2001 From: sam boyer Date: Tue, 24 Jan 2023 15:07:17 -0500 Subject: [PATCH 002/172] dashboards: Mark dashboards team as kind owner (#62013) * dashboards: Mark dashboards team as kind owner * Update .github/CODEOWNERS Co-authored-by: Ivan Ortega Alba Co-authored-by: Ivan Ortega Alba --- .github/CODEOWNERS | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4544d4f52c9..252f7feac62 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -535,9 +535,12 @@ lerna.json @grafana/frontend-ops /pkg/infra/httpclient/httpclientprovider/sigv4_middleware.go @grafana/grafana-partnerships-team /pkg/infra/httpclient/httpclientprovider/sigv4_middleware_test.go @grafana/grafana-partnerships-team +# Kind definitions +/kinds/dashboard @grafana/dashboards-squad +/kinds/ @grafana/grafana-as-code + # Kind system and code generation embed.go @grafana/grafana-as-code -/kinds/ @grafana/grafana-as-code /pkg/kinds/ @grafana/grafana-as-code /pkg/cuectx/ @grafana/grafana-as-code /pkg/registry/ @grafana/grafana-as-code From 0f93548ac4d3786df3871c2c57a3b5f647a0ad09 Mon Sep 17 00:00:00 2001 From: Christopher Moyer <35463610+chri2547@users.noreply.github.com> Date: Tue, 24 Jan 2023 14:32:14 -0600 Subject: [PATCH 003/172] Docs: corrects broken link to v9.0 docs (#62035) corrects broken link to v9.0 docs --- docs/sources/datasources/azure-monitor/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/datasources/azure-monitor/_index.md b/docs/sources/datasources/azure-monitor/_index.md index 227b4414e27..175fba96596 100644 --- a/docs/sources/datasources/azure-monitor/_index.md +++ b/docs/sources/datasources/azure-monitor/_index.md @@ -155,4 +155,4 @@ Until Grafana v8.0, you could query the same Azure Application Insights data usi These queries were deprecated in Grafana v7.5. In Grafana v8.0, Application Insights and Insights Analytics were made read-only in favor of querying this data through Metrics and Logs. These query methods were completely removed in Grafana v9.0. -If you're upgrading from a Grafana version prior to v9.0 and relied on Application Insights and Analytics queries, refer to the [Grafana v9.0 documentation](/v9.0/datasources/azuremonitor/deprecated-application-insights/) for help migrating these queries to Metrics and Logs queries. +If you're upgrading from a Grafana version prior to v9.0 and relied on Application Insights and Analytics queries, refer to the [Grafana v9.0 documentation](/docs/grafana/v9.0/datasources/azuremonitor/deprecated-application-insights/) for help migrating these queries to Metrics and Logs queries. From fbfb79afce12e5645f528691d39e24f07d11dd5d Mon Sep 17 00:00:00 2001 From: Aashish Illa Date: Wed, 25 Jan 2023 02:41:00 +0530 Subject: [PATCH 004/172] Explore: Changed references to DataQuery and DataSourceRef (#62034) --- .../explore/AddToDashboard/addToDashboard.test.ts | 4 ++-- .../features/explore/AddToDashboard/addToDashboard.ts | 3 ++- .../app/features/explore/AddToDashboard/index.test.tsx | 3 +-- public/app/features/explore/Explore.tsx | 2 +- public/app/features/explore/Logs.tsx | 2 +- public/app/features/explore/LogsNavigation.tsx | 3 ++- public/app/features/explore/QueryRows.test.tsx | 2 +- public/app/features/explore/QueryRows.tsx | 3 ++- .../explore/RichHistory/RichHistoryCard.test.tsx | 3 ++- .../features/explore/RichHistory/RichHistoryCard.tsx | 3 ++- public/app/features/explore/TraceView/TraceView.tsx | 2 +- .../app/features/explore/TraceView/createSpanLink.tsx | 2 +- public/app/features/explore/spec/helper/setup.tsx | 10 ++-------- public/app/features/explore/state/datasource.test.ts | 3 ++- public/app/features/explore/state/explorePane.ts | 3 +-- public/app/features/explore/state/history.ts | 3 ++- public/app/features/explore/state/main.ts | 3 ++- public/app/features/explore/state/query.test.ts | 2 +- public/app/features/explore/state/query.ts | 2 +- public/app/features/explore/state/utils.ts | 2 +- public/app/features/explore/utils/decorators.ts | 2 +- 21 files changed, 31 insertions(+), 31 deletions(-) diff --git a/public/app/features/explore/AddToDashboard/addToDashboard.test.ts b/public/app/features/explore/AddToDashboard/addToDashboard.test.ts index 8c98f81feb5..299d5c8f183 100644 --- a/public/app/features/explore/AddToDashboard/addToDashboard.test.ts +++ b/public/app/features/explore/AddToDashboard/addToDashboard.test.ts @@ -1,5 +1,5 @@ -import { DataQuery, MutableDataFrame } from '@grafana/data'; -import { defaultDashboard } from '@grafana/schema'; +import { MutableDataFrame } from '@grafana/data'; +import { DataQuery, defaultDashboard } from '@grafana/schema'; import { backendSrv } from 'app/core/services/backend_srv'; import * as api from 'app/features/dashboard/state/initDashboard'; import { ExplorePanelData } from 'app/types'; diff --git a/public/app/features/explore/AddToDashboard/addToDashboard.ts b/public/app/features/explore/AddToDashboard/addToDashboard.ts index 6946fa453f7..9720d5c2c76 100644 --- a/public/app/features/explore/AddToDashboard/addToDashboard.ts +++ b/public/app/features/explore/AddToDashboard/addToDashboard.ts @@ -1,4 +1,5 @@ -import { DataFrame, DataQuery, DataSourceRef } from '@grafana/data'; +import { DataFrame } from '@grafana/data'; +import { DataQuery, DataSourceRef } from '@grafana/schema'; import { backendSrv } from 'app/core/services/backend_srv'; import { getNewDashboardModelData, diff --git a/public/app/features/explore/AddToDashboard/index.test.tsx b/public/app/features/explore/AddToDashboard/index.test.tsx index f4b5fe989f0..a5bb2b2102b 100644 --- a/public/app/features/explore/AddToDashboard/index.test.tsx +++ b/public/app/features/explore/AddToDashboard/index.test.tsx @@ -3,9 +3,8 @@ import userEvent from '@testing-library/user-event'; import React, { ReactNode } from 'react'; import { Provider } from 'react-redux'; -import { DataQuery } from '@grafana/data'; import { locationService, setEchoSrv } from '@grafana/runtime'; -import { defaultDashboard } from '@grafana/schema'; +import { DataQuery, defaultDashboard } from '@grafana/schema'; import { backendSrv } from 'app/core/services/backend_srv'; import { contextSrv } from 'app/core/services/context_srv'; import { Echo } from 'app/core/services/echo/Echo'; diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 82bf6d498a0..0a0f2443d9e 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -8,7 +8,6 @@ import { Unsubscribable } from 'rxjs'; import { AbsoluteTimeRange, - DataQuery, GrafanaTheme2, LoadingState, QueryFixAction, @@ -19,6 +18,7 @@ import { } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { config, getDataSourceSrv, reportInteraction } from '@grafana/runtime'; +import { DataQuery } from '@grafana/schema'; import { CustomScrollbar, ErrorBoundaryAlert, diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index ce9c1266d5e..c5a620d4c04 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -16,7 +16,6 @@ import { LogsSortOrder, LinkModel, Field, - DataQuery, DataFrame, GrafanaTheme2, LoadingState, @@ -28,6 +27,7 @@ import { EventBus, } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; +import { DataQuery } from '@grafana/schema'; import { RadioButtonGroup, Button, diff --git a/public/app/features/explore/LogsNavigation.tsx b/public/app/features/explore/LogsNavigation.tsx index 97e53893396..ba3193c7594 100644 --- a/public/app/features/explore/LogsNavigation.tsx +++ b/public/app/features/explore/LogsNavigation.tsx @@ -2,8 +2,9 @@ import { css } from '@emotion/css'; import { isEqual } from 'lodash'; import React, { memo, useEffect, useRef, useState } from 'react'; -import { AbsoluteTimeRange, DataQuery, GrafanaTheme2, LogsSortOrder, TimeZone } from '@grafana/data'; +import { AbsoluteTimeRange, GrafanaTheme2, LogsSortOrder, TimeZone } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; +import { DataQuery } from '@grafana/schema'; import { Button, Icon, Spinner, useTheme2 } from '@grafana/ui'; import { TOP_BAR_LEVEL_HEIGHT } from 'app/core/components/AppChrome/types'; diff --git a/public/app/features/explore/QueryRows.test.tsx b/public/app/features/explore/QueryRows.test.tsx index 786bd98b09e..22983d9f0c9 100644 --- a/public/app/features/explore/QueryRows.test.tsx +++ b/public/app/features/explore/QueryRows.test.tsx @@ -2,8 +2,8 @@ import { fireEvent, render, screen } from '@testing-library/react'; import React from 'react'; import { Provider } from 'react-redux'; -import { DataQuery } from '@grafana/data'; import { setDataSourceSrv } from '@grafana/runtime'; +import { DataQuery } from '@grafana/schema'; import { configureStore } from 'app/store/configureStore'; import { ExploreId, ExploreState } from 'app/types'; diff --git a/public/app/features/explore/QueryRows.tsx b/public/app/features/explore/QueryRows.tsx index a1b7b0ce782..1707f5ef167 100644 --- a/public/app/features/explore/QueryRows.tsx +++ b/public/app/features/explore/QueryRows.tsx @@ -1,8 +1,9 @@ import { createSelector } from '@reduxjs/toolkit'; import React, { useCallback, useMemo } from 'react'; -import { CoreApp, DataQuery, DataSourceInstanceSettings } from '@grafana/data'; +import { CoreApp, DataSourceInstanceSettings } from '@grafana/data'; import { getDataSourceSrv, reportInteraction } from '@grafana/runtime'; +import { DataQuery } from '@grafana/schema'; import { getNextRefIdChar } from 'app/core/utils/query'; import { useDispatch, useSelector } from 'app/types'; import { ExploreId } from 'app/types/explore'; diff --git a/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx b/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx index 7a450f373a0..15b1f4ba4ff 100644 --- a/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryCard.test.tsx @@ -1,7 +1,8 @@ import { render, screen, fireEvent, getByText } from '@testing-library/react'; import React from 'react'; -import { DataSourceApi, DataQuery } from '@grafana/data'; +import { DataSourceApi } from '@grafana/data'; +import { DataQuery } from '@grafana/schema'; import appEvents from 'app/core/app_events'; import { mockDataSource } from 'app/features/alerting/unified/mocks'; import { DataSourceType } from 'app/features/alerting/unified/utils/datasource'; diff --git a/public/app/features/explore/RichHistory/RichHistoryCard.tsx b/public/app/features/explore/RichHistory/RichHistoryCard.tsx index 8a84806db18..ccf5e9eee6d 100644 --- a/public/app/features/explore/RichHistory/RichHistoryCard.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryCard.tsx @@ -2,8 +2,9 @@ import { css, cx } from '@emotion/css'; import React, { useState, useEffect } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { DataSourceApi, DataQuery, GrafanaTheme2 } from '@grafana/data'; +import { DataSourceApi, GrafanaTheme2 } from '@grafana/data'; import { config, getDataSourceSrv, reportInteraction } from '@grafana/runtime'; +import { DataQuery } from '@grafana/schema'; import { TextArea, Button, IconButton, useStyles2 } from '@grafana/ui'; import { notifyApp } from 'app/core/actions'; import appEvents from 'app/core/app_events'; diff --git a/public/app/features/explore/TraceView/TraceView.tsx b/public/app/features/explore/TraceView/TraceView.tsx index b0a33db5103..dfa0221260d 100644 --- a/public/app/features/explore/TraceView/TraceView.tsx +++ b/public/app/features/explore/TraceView/TraceView.tsx @@ -5,7 +5,6 @@ import React, { RefObject, useCallback, useMemo, useState } from 'react'; import { DataFrame, DataLink, - DataQuery, DataSourceApi, DataSourceJsonData, Field, @@ -16,6 +15,7 @@ import { SplitOpen, } from '@grafana/data'; import { getTemplateSrv } from '@grafana/runtime'; +import { DataQuery } from '@grafana/schema'; import { useStyles2 } from '@grafana/ui'; import { SpanBarOptionsData, diff --git a/public/app/features/explore/TraceView/createSpanLink.tsx b/public/app/features/explore/TraceView/createSpanLink.tsx index 29e963ab53a..b96c85ad190 100644 --- a/public/app/features/explore/TraceView/createSpanLink.tsx +++ b/public/app/features/explore/TraceView/createSpanLink.tsx @@ -4,7 +4,6 @@ import React from 'react'; import { DataFrame, DataLink, - DataQuery, DataSourceInstanceSettings, DataSourceJsonData, dateTime, @@ -17,6 +16,7 @@ import { TimeRange, } from '@grafana/data'; import { getTemplateSrv } from '@grafana/runtime'; +import { DataQuery } from '@grafana/schema'; import { Icon } from '@grafana/ui'; import { SpanLinkFunc, TraceSpan } from '@jaegertracing/jaeger-ui-components'; import { TraceToLogsOptions } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; diff --git a/public/app/features/explore/spec/helper/setup.tsx b/public/app/features/explore/spec/helper/setup.tsx index 1c163dcbeda..88a041a7bb1 100644 --- a/public/app/features/explore/spec/helper/setup.tsx +++ b/public/app/features/explore/spec/helper/setup.tsx @@ -6,15 +6,9 @@ import { Provider } from 'react-redux'; import { Route, Router } from 'react-router-dom'; import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; -import { - DataSourceApi, - DataSourceInstanceSettings, - DataSourceRef, - QueryEditorProps, - ScopedVars, - UrlQueryValue, -} from '@grafana/data'; +import { DataSourceApi, DataSourceInstanceSettings, QueryEditorProps, ScopedVars, UrlQueryValue } from '@grafana/data'; import { locationSearchToObject, locationService, setDataSourceSrv, setEchoSrv, config } from '@grafana/runtime'; +import { DataSourceRef } from '@grafana/schema'; import { GrafanaContext } from 'app/core/context/GrafanaContext'; import { GrafanaRoute } from 'app/core/navigation/GrafanaRoute'; import { Echo } from 'app/core/services/echo/Echo'; diff --git a/public/app/features/explore/state/datasource.test.ts b/public/app/features/explore/state/datasource.test.ts index 01a28e8dd95..606db7151f4 100644 --- a/public/app/features/explore/state/datasource.test.ts +++ b/public/app/features/explore/state/datasource.test.ts @@ -1,4 +1,5 @@ -import { DataQuery, DataSourceApi } from '@grafana/data'; +import { DataSourceApi } from '@grafana/data'; +import { DataQuery } from '@grafana/schema'; import { ExploreId, ExploreItemState } from 'app/types'; import { updateDatasourceInstanceAction, datasourceReducer } from './datasource'; diff --git a/public/app/features/explore/state/explorePane.ts b/public/app/features/explore/state/explorePane.ts index 20cff532e12..b3d7878864d 100644 --- a/public/app/features/explore/state/explorePane.ts +++ b/public/app/features/explore/state/explorePane.ts @@ -4,16 +4,15 @@ import { AnyAction } from 'redux'; import { EventBusExtended, - DataQuery, ExploreUrlState, TimeRange, HistoryItem, DataSourceApi, ExplorePanelsState, PreferredVisualisationType, - DataSourceRef, } from '@grafana/data'; import { getDataSourceSrv } from '@grafana/runtime'; +import { DataQuery, DataSourceRef } from '@grafana/schema'; import { DEFAULT_RANGE, getQueryKeys, diff --git a/public/app/features/explore/state/history.ts b/public/app/features/explore/state/history.ts index 84447f65b41..9697e2eb596 100644 --- a/public/app/features/explore/state/history.ts +++ b/public/app/features/explore/state/history.ts @@ -1,7 +1,8 @@ import { AnyAction, createAction } from '@reduxjs/toolkit'; -import { DataQuery, HistoryItem } from '@grafana/data'; +import { HistoryItem } from '@grafana/data'; import { config, logError } from '@grafana/runtime'; +import { DataQuery } from '@grafana/schema'; import { RICH_HISTORY_SETTING_KEYS } from 'app/core/history/richHistoryLocalStorageUtils'; import store from 'app/core/store'; import { diff --git a/public/app/features/explore/state/main.ts b/public/app/features/explore/state/main.ts index e052cd8fabf..750dce944c1 100644 --- a/public/app/features/explore/state/main.ts +++ b/public/app/features/explore/state/main.ts @@ -1,8 +1,9 @@ import { createAction } from '@reduxjs/toolkit'; import { AnyAction } from 'redux'; -import { DataQuery, ExploreUrlState, serializeStateToUrlParam, SplitOpenOptions, UrlQueryMap } from '@grafana/data'; +import { ExploreUrlState, serializeStateToUrlParam, SplitOpenOptions, UrlQueryMap } from '@grafana/data'; import { DataSourceSrv, locationService } from '@grafana/runtime'; +import { DataQuery } from '@grafana/schema'; import { GetExploreUrlArguments, stopQueryState } from 'app/core/utils/explore'; import { PanelModel } from 'app/features/dashboard/state'; import { ExploreId, ExploreItemState, ExploreState } from 'app/types/explore'; diff --git a/public/app/features/explore/state/query.test.ts b/public/app/features/explore/state/query.test.ts index bfb5223a52b..a75697284da 100644 --- a/public/app/features/explore/state/query.test.ts +++ b/public/app/features/explore/state/query.test.ts @@ -5,7 +5,6 @@ import { assertIsDefined } from 'test/helpers/asserts'; import { ArrayVector, - DataQuery, DataQueryResponse, DataSourceApi, DataSourceJsonData, @@ -15,6 +14,7 @@ import { RawTimeRange, SupplementaryQueryType, } from '@grafana/data'; +import { DataQuery } from '@grafana/schema'; import { ExploreId, ExploreItemState, StoreState, ThunkDispatch } from 'app/types'; import { reducerTester } from '../../../../test/core/redux/reducerTester'; diff --git a/public/app/features/explore/state/query.ts b/public/app/features/explore/state/query.ts index ea89c07ac4a..531ebeb5374 100644 --- a/public/app/features/explore/state/query.ts +++ b/public/app/features/explore/state/query.ts @@ -6,7 +6,6 @@ import { mergeMap, throttleTime } from 'rxjs/operators'; import { AbsoluteTimeRange, - DataQuery, DataQueryErrorType, DataQueryResponse, DataSourceApi, @@ -22,6 +21,7 @@ import { hasLogsVolumeSupport, } from '@grafana/data'; import { config, getDataSourceSrv, reportInteraction } from '@grafana/runtime'; +import { DataQuery } from '@grafana/schema'; import { buildQueryTransaction, ensureQueries, diff --git a/public/app/features/explore/state/utils.ts b/public/app/features/explore/state/utils.ts index 00bd72b69bc..868c82df53a 100644 --- a/public/app/features/explore/state/utils.ts +++ b/public/app/features/explore/state/utils.ts @@ -3,7 +3,6 @@ import { isEmpty, isObject, mapValues, omitBy } from 'lodash'; import { AbsoluteTimeRange, DataSourceApi, - DataSourceRef, EventBusExtended, ExploreUrlState, getDefaultTimeRange, @@ -11,6 +10,7 @@ import { LoadingState, PanelData, } from '@grafana/data'; +import { DataSourceRef } from '@grafana/schema'; import { ExplorePanelData } from 'app/types'; import { ExploreItemState } from 'app/types/explore'; diff --git a/public/app/features/explore/utils/decorators.ts b/public/app/features/explore/utils/decorators.ts index 4273fd994a0..f8354ed809c 100644 --- a/public/app/features/explore/utils/decorators.ts +++ b/public/app/features/explore/utils/decorators.ts @@ -9,9 +9,9 @@ import { getDisplayProcessor, PanelData, standardTransformers, - DataQuery, } from '@grafana/data'; import { config } from '@grafana/runtime'; +import { DataQuery } from '@grafana/schema'; import { dataFrameToLogsModel } from '../../../core/logsModel'; import { refreshIntervalToSortOrder } from '../../../core/utils/explore'; From dd597c3a1ecfeed8cb05606644752b13470d14b6 Mon Sep 17 00:00:00 2001 From: owensmallwood Date: Tue, 24 Jan 2023 18:23:39 -0600 Subject: [PATCH 005/172] PublicDashboards: Adds middleware for email sharing (#61950) adds Share field to PublicDashboard model --- pkg/services/publicdashboards/models/models.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/services/publicdashboards/models/models.go b/pkg/services/publicdashboards/models/models.go index 8274e6c8551..37d25bf0340 100644 --- a/pkg/services/publicdashboards/models/models.go +++ b/pkg/services/publicdashboards/models/models.go @@ -39,6 +39,7 @@ type PublicDashboard struct { AccessToken string `json:"accessToken" xorm:"access_token"` AnnotationsEnabled bool `json:"annotationsEnabled" xorm:"annotations_enabled"` TimeSelectionEnabled bool `json:"timeSelectionEnabled" xorm:"time_selection_enabled"` + Share string `json:"share"` CreatedBy int64 `json:"createdBy" xorm:"created_by"` UpdatedBy int64 `json:"updatedBy" xorm:"updated_by"` From 6bf1d06dbad392127c4dcbc32f16bb676d44aafc Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Tue, 24 Jan 2023 18:41:08 -0600 Subject: [PATCH 006/172] Canvas: Update scene and panel when entering panel view mode (#62043) --- public/app/plugins/panel/canvas/CanvasPanel.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/app/plugins/panel/canvas/CanvasPanel.tsx b/public/app/plugins/panel/canvas/CanvasPanel.tsx index f2b5341578d..a78f2b2d821 100644 --- a/public/app/plugins/panel/canvas/CanvasPanel.tsx +++ b/public/app/plugins/panel/canvas/CanvasPanel.tsx @@ -185,11 +185,10 @@ export class CanvasPanel extends Component { } // 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; const shouldShowAdvancedTypesSwitched = this.props.options.showAdvancedTypes !== nextProps.options.showAdvancedTypes; - if (shouldUpdateSceneAndPanel || inlineEditingSwitched || shouldShowAdvancedTypesSwitched) { + if (this.needsReload || inlineEditingSwitched || shouldShowAdvancedTypesSwitched) { if (inlineEditingSwitched) { // Replace scene div to prevent selecto instance leaks this.scene.revId++; From 421976e919b73122424544d5b20199311b39eb4d Mon Sep 17 00:00:00 2001 From: idafurjes <36131195+idafurjes@users.noreply.github.com> Date: Wed, 25 Jan 2023 09:14:32 +0100 Subject: [PATCH 007/172] Chore: Remove folders from models pkg (#61853) --- pkg/api/folder.go | 8 +- pkg/api/folder_test.go | 10 +- pkg/api/index.go | 8 +- pkg/middleware/auth.go | 8 +- pkg/models/folders.go | 60 ------------ pkg/services/dashboards/dashboard.go | 10 +- .../dashboards/dashboard_service_mock.go | 38 +++++--- pkg/services/dashboards/database/acl.go | 34 ++++--- .../database/database_folder_test.go | 91 ++++++++++--------- pkg/services/dashboards/models.go | 2 +- .../dashboards/service/dashboard_service.go | 5 +- pkg/services/dashboards/store_mock.go | 35 +++++-- pkg/services/folder/folderimpl/folder_test.go | 5 +- pkg/services/folder/folderimpl/sqlstore.go | 2 +- pkg/services/folder/model.go | 10 +- .../librarypanels/librarypanels_test.go | 34 +++---- pkg/services/ngalert/store/deltas_test.go | 14 +-- 17 files changed, 184 insertions(+), 190 deletions(-) delete mode 100644 pkg/models/folders.go diff --git a/pkg/api/folder.go b/pkg/api/folder.go index bfc6cad7e90..fcaf655848c 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -169,16 +169,16 @@ func (hs *HTTPServer) CreateFolder(c *models.ReqContext) response.Response { func (hs *HTTPServer) MoveFolder(c *models.ReqContext) response.Response { if hs.Features.IsEnabled(featuremgmt.FlagNestedFolders) { - cmd := models.MoveFolderCommand{} + cmd := folder.MoveFolderCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } var theFolder *folder.Folder var err error - if cmd.ParentUID != nil { + if cmd.NewParentUID != "" { moveCommand := folder.MoveFolderCommand{ UID: web.Params(c.Req)[":uid"], - NewParentUID: *cmd.ParentUID, + NewParentUID: cmd.NewParentUID, OrgID: c.OrgID, } theFolder, err = hs.folderService.Move(c.Req.Context(), &moveCommand) @@ -280,7 +280,7 @@ func (hs *HTTPServer) newToFolderDto(c *models.ReqContext, g guardian.DashboardG Id: folder.ID, Uid: folder.UID, Title: folder.Title, - Url: folder.Url, + Url: folder.URL, HasACL: folder.HasACL, CanSave: canSave, CanEdit: canEdit, diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index 620e8c53659..11a03ddefe5 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -34,8 +34,8 @@ func TestFoldersAPIEndpoint(t *testing.T) { folderService := &foldertest.FakeService{} t.Run("Given a correct request for creating a folder", func(t *testing.T) { - cmd := models.CreateFolderCommand{ - Uid: "uid", + cmd := folder.CreateFolderCommand{ + UID: "uid", Title: "Folder", } @@ -73,8 +73,8 @@ func TestFoldersAPIEndpoint(t *testing.T) { {Error: dashboards.ErrFolderFailedGenerateUniqueUid, ExpectedStatusCode: 500}, } - cmd := models.CreateFolderCommand{ - Uid: "uid", + cmd := folder.CreateFolderCommand{ + UID: "uid", Title: "Folder", } @@ -235,7 +235,7 @@ func callCreateFolder(sc *scenarioContext) { } func createFolderScenario(t *testing.T, desc string, url string, routePattern string, folderService folder.Service, - cmd models.CreateFolderCommand, fn scenarioFunc) { + cmd folder.CreateFolderCommand, fn scenarioFunc) { setUpRBACGuardian(t) t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { aclMockResp := []*dashboards.DashboardACLInfoDTO{} diff --git a/pkg/api/index.go b/pkg/api/index.go index dbf621190ef..7037121e86b 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -10,6 +10,7 @@ import ( ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/folder" pref "github.com/grafana/grafana/pkg/services/preference" "github.com/grafana/grafana/pkg/setting" ) @@ -21,11 +22,12 @@ const ( ) func (hs *HTTPServer) editorInAnyFolder(c *models.ReqContext) bool { - hasEditPermissionInFoldersQuery := models.HasEditPermissionInFoldersQuery{SignedInUser: c.SignedInUser} - if err := hs.DashboardService.HasEditPermissionInFolders(c.Req.Context(), &hasEditPermissionInFoldersQuery); err != nil { + hasEditPermissionInFoldersQuery := folder.HasEditPermissionInFoldersQuery{SignedInUser: c.SignedInUser} + hasEditPermissionInFoldersQueryResult, err := hs.DashboardService.HasEditPermissionInFolders(c.Req.Context(), &hasEditPermissionInFoldersQuery) + if err != nil { return false } - return hasEditPermissionInFoldersQuery.Result + return hasEditPermissionInFoldersQueryResult } func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewData, error) { diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go index 1ae4b33b80b..abb74face6c 100644 --- a/pkg/middleware/auth.go +++ b/pkg/middleware/auth.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/setting" @@ -214,12 +215,13 @@ func OrgAdminDashOrFolderAdminOrTeamAdmin(ss db.DB, ds dashboards.DashboardServi return } - hasAdminPermissionInDashOrFoldersQuery := models.HasAdminPermissionInDashboardsOrFoldersQuery{SignedInUser: c.SignedInUser} - if err := ds.HasAdminPermissionInDashboardsOrFolders(c.Req.Context(), &hasAdminPermissionInDashOrFoldersQuery); err != nil { + hasAdminPermissionInDashOrFoldersQuery := folder.HasAdminPermissionInDashboardsOrFoldersQuery{SignedInUser: c.SignedInUser} + hasAdminPermissionInDashOrFoldersQueryResult, err := ds.HasAdminPermissionInDashboardsOrFolders(c.Req.Context(), &hasAdminPermissionInDashOrFoldersQuery) + if err != nil { c.JsonApiErr(500, "Failed to check if user is a folder admin", err) } - if hasAdminPermissionInDashOrFoldersQuery.Result { + if hasAdminPermissionInDashOrFoldersQueryResult { return } diff --git a/pkg/models/folders.go b/pkg/models/folders.go deleted file mode 100644 index f0951ad8b94..00000000000 --- a/pkg/models/folders.go +++ /dev/null @@ -1,60 +0,0 @@ -package models - -import ( - "time" - - "github.com/grafana/grafana/pkg/services/user" -) - -type Folder struct { - Id int64 - Uid string - Title string - Url string - Version int - - Created time.Time - Updated time.Time - - UpdatedBy int64 - CreatedBy int64 - HasACL bool -} - -// NewFolder creates a new Folder -func NewFolder(title string) *Folder { - folder := &Folder{} - folder.Title = title - folder.Created = time.Now() - folder.Updated = time.Now() - return folder -} - -// -// COMMANDS -// - -type CreateFolderCommand struct { - Uid string `json:"uid"` - Title string `json:"title"` - - Result *Folder `json:"-"` -} - -type MoveFolderCommand struct { - ParentUID *string `json:"parentUid"` -} - -// -// QUERIES -// - -type HasEditPermissionInFoldersQuery struct { - SignedInUser *user.SignedInUser - Result bool -} - -type HasAdminPermissionInDashboardsOrFoldersQuery struct { - SignedInUser *user.SignedInUser - Result bool -} diff --git a/pkg/services/dashboards/dashboard.go b/pkg/services/dashboards/dashboard.go index f6fa8e2faf2..0caf638a379 100644 --- a/pkg/services/dashboards/dashboard.go +++ b/pkg/services/dashboards/dashboard.go @@ -21,8 +21,8 @@ type DashboardService interface { GetDashboards(ctx context.Context, query *GetDashboardsQuery) error GetDashboardTags(ctx context.Context, query *GetDashboardTagsQuery) error GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) error - HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *models.HasAdminPermissionInDashboardsOrFoldersQuery) error - HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error + HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *folder.HasAdminPermissionInDashboardsOrFoldersQuery) (bool, error) + HasEditPermissionInFolders(ctx context.Context, query *folder.HasEditPermissionInFoldersQuery) (bool, error) ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*Dashboard, error) MakeUserAdmin(ctx context.Context, orgID int64, userID, dashboardID int64, setViewAndEditPermissions bool) error SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*Dashboard, error) @@ -68,9 +68,9 @@ type Store interface { GetProvisionedDashboardData(ctx context.Context, name string) ([]*DashboardProvisioning, error) GetProvisionedDataByDashboardID(ctx context.Context, dashboardID int64) (*DashboardProvisioning, error) GetProvisionedDataByDashboardUID(ctx context.Context, orgID int64, dashboardUID string) (*DashboardProvisioning, error) - HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *models.HasAdminPermissionInDashboardsOrFoldersQuery) error - HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error - // SaveAlerts saves dashboard alertmodels. + HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *folder.HasAdminPermissionInDashboardsOrFoldersQuery) (bool, error) + HasEditPermissionInFolders(ctx context.Context, query *folder.HasEditPermissionInFoldersQuery) (bool, error) + // SaveAlerts saves dashboard alerts. SaveAlerts(ctx context.Context, dashID int64, alerts []*alertmodels.Alert) error SaveDashboard(ctx context.Context, cmd SaveDashboardCommand) (*Dashboard, error) SaveProvisionedDashboard(ctx context.Context, cmd SaveDashboardCommand, provisioning *DashboardProvisioning) (*Dashboard, error) diff --git a/pkg/services/dashboards/dashboard_service_mock.go b/pkg/services/dashboards/dashboard_service_mock.go index 65ead437663..7165629552a 100644 --- a/pkg/services/dashboards/dashboard_service_mock.go +++ b/pkg/services/dashboards/dashboard_service_mock.go @@ -5,8 +5,10 @@ package dashboards import ( context "context" - models "github.com/grafana/grafana/pkg/models" + folder "github.com/grafana/grafana/pkg/services/folder" mock "github.com/stretchr/testify/mock" + + models "github.com/grafana/grafana/pkg/models" ) // FakeDashboardService is an autogenerated mock type for the DashboardService type @@ -180,31 +182,45 @@ func (_m *FakeDashboardService) GetDashboards(ctx context.Context, query *GetDas } // HasAdminPermissionInDashboardsOrFolders provides a mock function with given fields: ctx, query -func (_m *FakeDashboardService) HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *models.HasAdminPermissionInDashboardsOrFoldersQuery) error { +func (_m *FakeDashboardService) HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *folder.HasAdminPermissionInDashboardsOrFoldersQuery) (bool, error) { ret := _m.Called(ctx, query) - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *models.HasAdminPermissionInDashboardsOrFoldersQuery) error); ok { + var r0 bool + if rf, ok := ret.Get(0).(func(context.Context, *folder.HasAdminPermissionInDashboardsOrFoldersQuery) bool); ok { r0 = rf(ctx, query) } else { - r0 = ret.Error(0) + r0 = ret.Get(0).(bool) } - return r0 + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *folder.HasAdminPermissionInDashboardsOrFoldersQuery) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } // HasEditPermissionInFolders provides a mock function with given fields: ctx, query -func (_m *FakeDashboardService) HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error { +func (_m *FakeDashboardService) HasEditPermissionInFolders(ctx context.Context, query *folder.HasEditPermissionInFoldersQuery) (bool, error) { ret := _m.Called(ctx, query) - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *models.HasEditPermissionInFoldersQuery) error); ok { + var r0 bool + if rf, ok := ret.Get(0).(func(context.Context, *folder.HasEditPermissionInFoldersQuery) bool); ok { r0 = rf(ctx, query) } else { - r0 = ret.Error(0) + r0 = ret.Get(0).(bool) } - return r0 + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *folder.HasEditPermissionInFoldersQuery) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } // ImportDashboard provides a mock function with given fields: ctx, dto diff --git a/pkg/services/dashboards/database/acl.go b/pkg/services/dashboards/database/acl.go index 93256cead83..6147eb22334 100644 --- a/pkg/services/dashboards/database/acl.go +++ b/pkg/services/dashboards/database/acl.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/org" ) @@ -97,13 +98,13 @@ func (d *DashboardStore) GetDashboardACLInfoList(ctx context.Context, query *das } // HasEditPermissionInFolders validates that an user have access to a certain folder -func (d *DashboardStore) HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error { - return d.store.WithDbSession(ctx, func(dbSession *db.Session) error { - if query.SignedInUser.HasRole(org.RoleEditor) { - query.Result = true - return nil - } - +func (d *DashboardStore) HasEditPermissionInFolders(ctx context.Context, query *folder.HasEditPermissionInFoldersQuery) (bool, error) { + var queryResult bool + if query.SignedInUser.HasRole(org.RoleEditor) { + queryResult = true + return queryResult, nil + } + err := d.store.WithDbSession(ctx, func(dbSession *db.Session) error { builder := db.NewSqlBuilder(d.cfg, d.store.GetDialect()) builder.Write("SELECT COUNT(dashboard.id) AS count FROM dashboard WHERE dashboard.org_id = ? AND dashboard.is_folder = ?", query.SignedInUser.OrgID, d.store.GetDialect().BooleanStr(true)) @@ -119,16 +120,21 @@ func (d *DashboardStore) HasEditPermissionInFolders(ctx context.Context, query * return err } - query.Result = len(resp) > 0 && resp[0].Count > 0 + queryResult = len(resp) > 0 && resp[0].Count > 0 return nil }) + if err != nil { + return queryResult, err + } + return queryResult, nil } -func (d *DashboardStore) HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *models.HasAdminPermissionInDashboardsOrFoldersQuery) error { - return d.store.WithDbSession(ctx, func(dbSession *db.Session) error { +func (d *DashboardStore) HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *folder.HasAdminPermissionInDashboardsOrFoldersQuery) (bool, error) { + var queryResult bool + err := d.store.WithDbSession(ctx, func(dbSession *db.Session) error { if query.SignedInUser.HasRole(org.RoleAdmin) { - query.Result = true + queryResult = true return nil } @@ -145,10 +151,14 @@ func (d *DashboardStore) HasAdminPermissionInDashboardsOrFolders(ctx context.Con return err } - query.Result = len(resp) > 0 && resp[0].Count > 0 + queryResult = len(resp) > 0 && resp[0].Count > 0 return nil }) + if err != nil { + return queryResult, err + } + return queryResult, nil } func (d *DashboardStore) DeleteACLByUser(ctx context.Context, userID int64) error { diff --git a/pkg/services/dashboards/database/database_folder_test.go b/pkg/services/dashboards/database/database_folder_test.go index e2ea275c414..59a42e8968c 100644 --- a/pkg/services/dashboards/database/database_folder_test.go +++ b/pkg/services/dashboards/database/database_folder_test.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -27,7 +28,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { } t.Run("Testing DB", func(t *testing.T) { var sqlStore *sqlstore.SQLStore - var folder, dashInRoot, childDash *dashboards.Dashboard + var flder, dashInRoot, childDash *dashboards.Dashboard var currentUser user.User var dashboardStore *DashboardStore @@ -38,10 +39,10 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { var err error dashboardStore, err = ProvideDashboardStore(sqlStore, &setting.Cfg{}, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) - folder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod", "webapp") + flder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod", "webapp") dashInRoot = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, false, "prod", "webapp") - childDash = insertTestDashboard(t, dashboardStore, "test dash 23", 1, folder.ID, false, "prod", "webapp") - insertTestDashboard(t, dashboardStore, "test dash 45", 1, folder.ID, false, "prod") + childDash = insertTestDashboard(t, dashboardStore, "test dash 23", 1, flder.ID, false, "prod", "webapp") + insertTestDashboard(t, dashboardStore, "test dash 45", 1, flder.ID, false, "prod") currentUser = createUser(t, sqlStore, "viewer", "Viewer", false) } @@ -53,20 +54,20 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { query := &models.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, - DashboardIds: []int64{folder.ID, dashInRoot.ID}, + DashboardIds: []int64{flder.ID, dashInRoot.ID}, } err := testSearchDashboards(dashboardStore, query) require.NoError(t, err) require.Equal(t, len(query.Result), 2) - require.Equal(t, query.Result[0].ID, folder.ID) + require.Equal(t, query.Result[0].ID, flder.ID) require.Equal(t, query.Result[1].ID, dashInRoot.ID) }) }) t.Run("and acl is set for dashboard folder", func(t *testing.T) { var otherUser int64 = 999 - err := updateDashboardACL(t, dashboardStore, folder.ID, dashboards.DashboardACL{ - DashboardID: folder.ID, + err := updateDashboardACL(t, dashboardStore, flder.ID, dashboards.DashboardACL{ + DashboardID: flder.ID, OrgID: 1, UserID: otherUser, Permission: models.PERMISSION_EDIT, @@ -76,7 +77,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { t.Run("should not return folder", func(t *testing.T) { query := &models.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, - OrgId: 1, DashboardIds: []int64{folder.ID, dashInRoot.ID}, + OrgId: 1, DashboardIds: []int64{flder.ID, dashInRoot.ID}, } err := testSearchDashboards(dashboardStore, query) require.NoError(t, err) @@ -86,8 +87,8 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { }) t.Run("when the user is given permission", func(t *testing.T) { - err := updateDashboardACL(t, dashboardStore, folder.ID, dashboards.DashboardACL{ - DashboardID: folder.ID, OrgID: 1, UserID: currentUser.ID, Permission: models.PERMISSION_EDIT, + err := updateDashboardACL(t, dashboardStore, flder.ID, dashboards.DashboardACL{ + DashboardID: flder.ID, OrgID: 1, UserID: currentUser.ID, Permission: models.PERMISSION_EDIT, }) require.NoError(t, err) @@ -95,12 +96,12 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { query := &models.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, - DashboardIds: []int64{folder.ID, dashInRoot.ID}, + DashboardIds: []int64{flder.ID, dashInRoot.ID}, } err := testSearchDashboards(dashboardStore, query) require.NoError(t, err) require.Equal(t, len(query.Result), 2) - require.Equal(t, query.Result[0].ID, folder.ID) + require.Equal(t, query.Result[0].ID, flder.ID) require.Equal(t, query.Result[1].ID, dashInRoot.ID) }) }) @@ -114,12 +115,12 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { OrgRole: org.RoleAdmin, }, OrgId: 1, - DashboardIds: []int64{folder.ID, dashInRoot.ID}, + DashboardIds: []int64{flder.ID, dashInRoot.ID}, } err := testSearchDashboards(dashboardStore, query) require.NoError(t, err) require.Equal(t, len(query.Result), 2) - require.Equal(t, query.Result[0].ID, folder.ID) + require.Equal(t, query.Result[0].ID, flder.ID) require.Equal(t, query.Result[1].ID, dashInRoot.ID) }) }) @@ -127,16 +128,16 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { t.Run("and acl is set for dashboard child and folder has all permissions removed", func(t *testing.T) { var otherUser int64 = 999 - err := updateDashboardACL(t, dashboardStore, folder.ID) + err := updateDashboardACL(t, dashboardStore, flder.ID) require.NoError(t, err) err = updateDashboardACL(t, dashboardStore, childDash.ID, dashboards.DashboardACL{ - DashboardID: folder.ID, OrgID: 1, UserID: otherUser, Permission: models.PERMISSION_EDIT, + DashboardID: flder.ID, OrgID: 1, UserID: otherUser, Permission: models.PERMISSION_EDIT, }) require.NoError(t, err) t.Run("should not return folder or child", func(t *testing.T) { query := &models.FindPersistedDashboardsQuery{ - SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, DashboardIds: []int64{folder.ID, childDash.ID, dashInRoot.ID}, + SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, DashboardIds: []int64{flder.ID, childDash.ID, dashInRoot.ID}, } err := testSearchDashboards(dashboardStore, query) require.NoError(t, err) @@ -151,7 +152,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { require.NoError(t, err) t.Run("should be able to search for child dashboard but not folder", func(t *testing.T) { - query := &models.FindPersistedDashboardsQuery{SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, DashboardIds: []int64{folder.ID, childDash.ID, dashInRoot.ID}} + query := &models.FindPersistedDashboardsQuery{SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, DashboardIds: []int64{flder.ID, childDash.ID, dashInRoot.ID}} err := testSearchDashboards(dashboardStore, query) require.NoError(t, err) require.Equal(t, len(query.Result), 2) @@ -169,12 +170,12 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { OrgRole: org.RoleAdmin, }, OrgId: 1, - DashboardIds: []int64{folder.ID, dashInRoot.ID, childDash.ID}, + DashboardIds: []int64{flder.ID, dashInRoot.ID, childDash.ID}, } err := testSearchDashboards(dashboardStore, query) require.NoError(t, err) require.Equal(t, len(query.Result), 3) - require.Equal(t, query.Result[0].ID, folder.ID) + require.Equal(t, query.Result[0].ID, flder.ID) require.Equal(t, query.Result[1].ID, childDash.ID) require.Equal(t, query.Result[2].ID, dashInRoot.ID) }) @@ -328,21 +329,21 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { }) t.Run("should have edit permission in folders", func(t *testing.T) { - query := &models.HasEditPermissionInFoldersQuery{ + query := &folder.HasEditPermissionInFoldersQuery{ SignedInUser: &user.SignedInUser{UserID: adminUser.ID, OrgID: 1, OrgRole: org.RoleAdmin}, } - err := dashboardStore.HasEditPermissionInFolders(context.Background(), query) + queryResult, err := dashboardStore.HasEditPermissionInFolders(context.Background(), query) require.NoError(t, err) - require.True(t, query.Result) + require.True(t, queryResult) }) t.Run("should have admin permission in folders", func(t *testing.T) { - query := &models.HasAdminPermissionInDashboardsOrFoldersQuery{ + query := &folder.HasAdminPermissionInDashboardsOrFoldersQuery{ SignedInUser: &user.SignedInUser{UserID: adminUser.ID, OrgID: 1, OrgRole: org.RoleAdmin}, } - err := dashboardStore.HasAdminPermissionInDashboardsOrFolders(context.Background(), query) + queryResult, err := dashboardStore.HasAdminPermissionInDashboardsOrFolders(context.Background(), query) require.NoError(t, err) - require.True(t, query.Result) + require.True(t, queryResult) }) }) @@ -376,21 +377,21 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { }) t.Run("should have edit permission in folders", func(t *testing.T) { - query := &models.HasEditPermissionInFoldersQuery{ + query := &folder.HasEditPermissionInFoldersQuery{ SignedInUser: &user.SignedInUser{UserID: editorUser.ID, OrgID: 1, OrgRole: org.RoleEditor}, } - err := dashboardStore.HasEditPermissionInFolders(context.Background(), query) + queryResult, err := dashboardStore.HasEditPermissionInFolders(context.Background(), query) go require.NoError(t, err) - require.True(t, query.Result) + require.True(t, queryResult) }) t.Run("should not have admin permission in folders", func(t *testing.T) { - query := &models.HasAdminPermissionInDashboardsOrFoldersQuery{ + query := &folder.HasAdminPermissionInDashboardsOrFoldersQuery{ SignedInUser: &user.SignedInUser{UserID: adminUser.ID, OrgID: 1, OrgRole: org.RoleEditor}, } - err := dashboardStore.HasAdminPermissionInDashboardsOrFolders(context.Background(), query) + queryResult, err := dashboardStore.HasAdminPermissionInDashboardsOrFolders(context.Background(), query) require.NoError(t, err) - require.False(t, query.Result) + require.False(t, queryResult) }) }) @@ -424,21 +425,21 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { t.Run("should not have edit permission in folders", func(t *testing.T) { setup3() - query := &models.HasEditPermissionInFoldersQuery{ + query := &folder.HasEditPermissionInFoldersQuery{ SignedInUser: &user.SignedInUser{UserID: viewerUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, } - err := dashboardStore.HasEditPermissionInFolders(context.Background(), query) + queryResult, err := dashboardStore.HasEditPermissionInFolders(context.Background(), query) go require.NoError(t, err) - require.False(t, query.Result) + require.False(t, queryResult) }) t.Run("should not have admin permission in folders", func(t *testing.T) { - query := &models.HasAdminPermissionInDashboardsOrFoldersQuery{ + query := &folder.HasAdminPermissionInDashboardsOrFoldersQuery{ SignedInUser: &user.SignedInUser{UserID: adminUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, } - err := dashboardStore.HasAdminPermissionInDashboardsOrFolders(context.Background(), query) + queryResult, err := dashboardStore.HasAdminPermissionInDashboardsOrFolders(context.Background(), query) require.NoError(t, err) - require.False(t, query.Result) + require.False(t, queryResult) }) t.Run("and admin permission is given for user with org role viewer in one dashboard folder", func(t *testing.T) { @@ -448,12 +449,12 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { require.NoError(t, err) t.Run("should have edit permission in folders", func(t *testing.T) { - query := &models.HasEditPermissionInFoldersQuery{ + query := &folder.HasEditPermissionInFoldersQuery{ SignedInUser: &user.SignedInUser{UserID: viewerUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, } - err := dashboardStore.HasEditPermissionInFolders(context.Background(), query) + queryResult, err := dashboardStore.HasEditPermissionInFolders(context.Background(), query) go require.NoError(t, err) - require.True(t, query.Result) + require.True(t, queryResult) }) }) @@ -464,12 +465,12 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { require.NoError(t, err) t.Run("should have edit permission in folders", func(t *testing.T) { - query := &models.HasEditPermissionInFoldersQuery{ + query := &folder.HasEditPermissionInFoldersQuery{ SignedInUser: &user.SignedInUser{UserID: viewerUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, } - err := dashboardStore.HasEditPermissionInFolders(context.Background(), query) + queryResult, err := dashboardStore.HasEditPermissionInFolders(context.Background(), query) go require.NoError(t, err) - require.True(t, query.Result) + require.True(t, queryResult) }) }) }) diff --git a/pkg/services/dashboards/models.go b/pkg/services/dashboards/models.go index 0b74c017793..5d67a74a926 100644 --- a/pkg/services/dashboards/models.go +++ b/pkg/services/dashboards/models.go @@ -336,7 +336,7 @@ func FromDashboard(dash *Dashboard) *folder.Folder { UID: dash.UID, Title: dash.Title, HasACL: dash.HasACL, - Url: GetFolderURL(dash.UID, dash.Slug), + URL: GetFolderURL(dash.UID, dash.Slug), Version: dash.Version, Created: dash.Created, CreatedBy: dash.CreatedBy, diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index ca29dd390c8..5c5c8d9612c 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" @@ -602,11 +603,11 @@ func (dr *DashboardServiceImpl) GetDashboardACLInfoList(ctx context.Context, que return dr.dashboardStore.GetDashboardACLInfoList(ctx, query) } -func (dr *DashboardServiceImpl) HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *models.HasAdminPermissionInDashboardsOrFoldersQuery) error { +func (dr *DashboardServiceImpl) HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *folder.HasAdminPermissionInDashboardsOrFoldersQuery) (bool, error) { return dr.dashboardStore.HasAdminPermissionInDashboardsOrFolders(ctx, query) } -func (dr *DashboardServiceImpl) HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error { +func (dr *DashboardServiceImpl) HasEditPermissionInFolders(ctx context.Context, query *folder.HasEditPermissionInFoldersQuery) (bool, error) { return dr.dashboardStore.HasEditPermissionInFolders(ctx, query) } diff --git a/pkg/services/dashboards/store_mock.go b/pkg/services/dashboards/store_mock.go index 361b376ece6..5abfe907689 100644 --- a/pkg/services/dashboards/store_mock.go +++ b/pkg/services/dashboards/store_mock.go @@ -6,6 +6,7 @@ import ( context "context" alertingmodels "github.com/grafana/grafana/pkg/services/alerting/models" + folder "github.com/grafana/grafana/pkg/services/folder" mock "github.com/stretchr/testify/mock" @@ -291,31 +292,45 @@ func (_m *FakeDashboardStore) GetProvisionedDataByDashboardUID(ctx context.Conte } // HasAdminPermissionInDashboardsOrFolders provides a mock function with given fields: ctx, query -func (_m *FakeDashboardStore) HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *models.HasAdminPermissionInDashboardsOrFoldersQuery) error { +func (_m *FakeDashboardStore) HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *folder.HasAdminPermissionInDashboardsOrFoldersQuery) (bool, error) { ret := _m.Called(ctx, query) - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *models.HasAdminPermissionInDashboardsOrFoldersQuery) error); ok { + var r0 bool + if rf, ok := ret.Get(0).(func(context.Context, *folder.HasAdminPermissionInDashboardsOrFoldersQuery) bool); ok { r0 = rf(ctx, query) } else { - r0 = ret.Error(0) + r0 = ret.Get(0).(bool) } - return r0 + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *folder.HasAdminPermissionInDashboardsOrFoldersQuery) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } // HasEditPermissionInFolders provides a mock function with given fields: ctx, query -func (_m *FakeDashboardStore) HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error { +func (_m *FakeDashboardStore) HasEditPermissionInFolders(ctx context.Context, query *folder.HasEditPermissionInFoldersQuery) (bool, error) { ret := _m.Called(ctx, query) - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *models.HasEditPermissionInFoldersQuery) error); ok { + var r0 bool + if rf, ok := ret.Get(0).(func(context.Context, *folder.HasEditPermissionInFoldersQuery) bool); ok { r0 = rf(ctx, query) } else { - r0 = ret.Error(0) + r0 = ret.Get(0).(bool) } - return r0 + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *folder.HasEditPermissionInFoldersQuery) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } // SaveAlerts provides a mock function with given fields: ctx, dashID, alerts diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index 4c27d2c5f87..b8497f26d0c 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -14,7 +14,6 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" @@ -142,8 +141,8 @@ func TestIntegrationFolderService(t *testing.T) { }) t.Run("When deleting folder by uid should return access denied error", func(t *testing.T) { - newFolder := models.NewFolder("Folder") - newFolder.Uid = folderUID + newFolder := folder.NewFolder("Folder", "") + newFolder.UID = folderUID folderStore.On("GetFolderByID", mock.Anything, orgID, folderId).Return(newFolder, nil) folderStore.On("GetFolderByUID", mock.Anything, orgID, folderUID).Return(newFolder, nil) diff --git a/pkg/services/folder/folderimpl/sqlstore.go b/pkg/services/folder/folderimpl/sqlstore.go index e3ca1f36bbb..40797519ee9 100644 --- a/pkg/services/folder/folderimpl/sqlstore.go +++ b/pkg/services/folder/folderimpl/sqlstore.go @@ -180,7 +180,7 @@ func (ss *sqlStore) Get(ctx context.Context, q folder.GetFolderQuery) (*folder.F } return nil }) - foldr.Url = dashboards.GetFolderURL(foldr.UID, slugify.Slugify(foldr.Title)) + foldr.URL = dashboards.GetFolderURL(foldr.UID, slugify.Slugify(foldr.Title)) return foldr, err } diff --git a/pkg/services/folder/model.go b/pkg/services/folder/model.go index 75e920ecccc..62ae6a77b0b 100644 --- a/pkg/services/folder/model.go +++ b/pkg/services/folder/model.go @@ -36,7 +36,7 @@ type Folder struct { // TODO: validate if this field is required/relevant to folders. // currently there is no such column Version int - Url string + URL string UpdatedBy int64 CreatedBy int64 HasACL bool @@ -146,3 +146,11 @@ type GetChildrenQuery struct { SignedInUser *user.SignedInUser `json:"-"` } + +type HasEditPermissionInFoldersQuery struct { + SignedInUser *user.SignedInUser +} + +type HasAdminPermissionInDashboardsOrFoldersQuery struct { + SignedInUser *user.SignedInUser +} diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index a01af5e1fcf..dc27e80e8bd 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -77,7 +77,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { Title: "Testing ConnectLibraryPanelsForDashboard", Data: simplejson.NewFromAny(dashJSON), } - dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id) + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) err := sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB) require.NoError(t, err) @@ -175,7 +175,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { Title: "Testing ConnectLibraryPanelsForDashboard", Data: simplejson.NewFromAny(dashJSON), } - dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id) + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) err = sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB) require.NoError(t, err) @@ -221,7 +221,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { Title: "Testing ConnectLibraryPanelsForDashboard", Data: simplejson.NewFromAny(dashJSON), } - dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id) + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) err := sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB) require.EqualError(t, err, errLibraryPanelHeaderUIDMissing.Error()) @@ -230,7 +230,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { scenarioWithLibraryPanel(t, "When an admin tries to store a dashboard with unused/removed library panels, it should disconnect unused/removed library panels", func(t *testing.T, sc scenarioContext) { unused, err := sc.elementService.CreateElement(sc.ctx, sc.user, libraryelements.CreateLibraryElementCommand{ - FolderID: sc.folder.Id, + FolderID: sc.folder.ID, Name: "Unused Libray Panel", Model: []byte(` { @@ -277,7 +277,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { Title: "Testing ConnectLibraryPanelsForDashboard", Data: simplejson.NewFromAny(dashJSON), } - dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.Id) + dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) err = sc.elementService.ConnectElementsToDashboard(sc.ctx, sc.user, []string{sc.initialResult.Result.UID}, dashInDB.ID) require.NoError(t, err) @@ -406,7 +406,7 @@ func TestImportLibraryPanelsForDashboard(t *testing.T) { _, err := sc.elementService.GetElement(sc.ctx, sc.user, existingUID) require.NoError(t, err) - err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, simplejson.New(), panels, sc.folder.Id) + err = sc.service.ImportLibraryPanelsForDashboard(sc.ctx, sc.user, simplejson.New(), panels, sc.folder.ID) require.NoError(t, err) element, err := sc.elementService.GetElement(sc.ctx, sc.user, existingUID) @@ -414,7 +414,7 @@ func TestImportLibraryPanelsForDashboard(t *testing.T) { var expected = getExpected(t, element, existingUID, existingName, sc.initialResult.Result.Model) expected.FolderID = sc.initialResult.Result.FolderID expected.Description = sc.initialResult.Result.Description - expected.Meta.FolderUID = sc.folder.Uid + expected.Meta.FolderUID = sc.folder.UID expected.Meta.FolderName = sc.folder.Title var result = toLibraryElement(t, element) if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" { @@ -601,7 +601,7 @@ type scenarioContext struct { service Service elementService libraryelements.Service user *user.SignedInUser - folder *models.Folder + folder *folder.Folder initialResult libraryPanelResult sqlStore db.DB } @@ -778,7 +778,7 @@ func scenarioWithLibraryPanel(t *testing.T, desc string, fn func(t *testing.T, s testScenario(t, desc, func(t *testing.T, sc scenarioContext) { command := libraryelements.CreateLibraryElementCommand{ - FolderID: sc.folder.Id, + FolderID: sc.folder.ID, Name: "Text - Library Panel", Model: []byte(` { @@ -877,15 +877,15 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo sqlStore: sqlStore, } - folder := createFolderWithACL(t, sc.sqlStore, "ScenarioFolder", sc.user, []folderACLItem{}) - sc.folder = &models.Folder{ - Id: folder.ID, - Uid: folder.UID, - Title: folder.Title, - Url: dashboards.GetFolderURL(folder.UID, slugify.Slugify(folder.Title)), + foldr := createFolderWithACL(t, sc.sqlStore, "ScenarioFolder", sc.user, []folderACLItem{}) + sc.folder = &folder.Folder{ + ID: foldr.ID, + UID: foldr.UID, + Title: foldr.Title, + URL: dashboards.GetFolderURL(foldr.UID, slugify.Slugify(foldr.Title)), Version: 0, - Created: folder.Created, - Updated: folder.Updated, + Created: foldr.Created, + Updated: foldr.Updated, UpdatedBy: 0, CreatedBy: 0, HasACL: false, diff --git a/pkg/services/ngalert/store/deltas_test.go b/pkg/services/ngalert/store/deltas_test.go index d3e63758e88..e9bc7b947ba 100644 --- a/pkg/services/ngalert/store/deltas_test.go +++ b/pkg/services/ngalert/store/deltas_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - grafana_models "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" "github.com/grafana/grafana/pkg/util" @@ -194,7 +194,7 @@ func TestCalculateChanges(t *testing.T) { groupKey := models.AlertRuleGroupKey{ OrgID: orgId, - NamespaceUID: namespace.Uid, + NamespaceUID: namespace.UID, RuleGroup: groupName, } @@ -442,12 +442,12 @@ func withUIDs(uids map[string]*models.AlertRule) func(rule *models.AlertRule) { } } -func randFolder() *grafana_models.Folder { - return &grafana_models.Folder{ - Id: rand.Int63(), - Uid: util.GenerateShortUID(), +func randFolder() *folder.Folder { + return &folder.Folder{ + ID: rand.Int63(), + UID: util.GenerateShortUID(), Title: "TEST-FOLDER-" + util.GenerateShortUID(), - Url: "", + URL: "", Version: 0, Created: time.Time{}, Updated: time.Time{}, From cebd71cc3629b2391702abf596f87239db9f5e04 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Wed, 25 Jan 2023 08:19:32 +0000 Subject: [PATCH 008/172] Docs: Update publish a plugin FAQ to cover more common questions (#62001) --- .../developers/plugins/publish-a-plugin.md | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/sources/developers/plugins/publish-a-plugin.md b/docs/sources/developers/plugins/publish-a-plugin.md index 3e3b51f3191..2896896df02 100644 --- a/docs/sources/developers/plugins/publish-a-plugin.md +++ b/docs/sources/developers/plugins/publish-a-plugin.md @@ -105,8 +105,28 @@ To speed up the time it takes to review your plugin: - No. We cannot guarantee specific publishing dates, as plugins are immediately published after a review based on our internal prioritization. +**Can I see metrics of my plugin installs, downloads or usage?** + +- No. We don't offer this information at the moment to plugin authors. + +**How can I update my plugin's catalog page?** + +- The plugin's catalog page content is extracted from the plugin README file. To update the plugin's catalog page, it is necessary to submit an updated plugin with the new content included in the README file. + +**Can I unlist my plugin from the Grafana Plugin's Catalog in case of a bug?** + +- In the event of a bug, unlisting the plugin from the Grafana Plugin's Catalog may be possible in exceptional cases, such as security concerns. However, we do not have control over the specific instances where the plugin is installed. + +**Can I distribute my plugin somewhere else than the Grafana Catalog?** + +- The official method for distributing Grafana plugins is through the Grafana Catalog. Alternative methods, such as installing private or development plugins on local Grafana instances, are available as per the guidelines provided in [this guide](https://grafana.com/docs/grafana/latest/administration/plugin-management/#install-plugin-on-local-grafana). + ## Publishing your plugin for the first time +**Do plugin signatures expire?** + +- Plugin signatures do not currently expire. + {{< figure src="/static/img/docs/plugins/plugins-submission-create2.png" class="docs-image--no-shadow" max-width="650px" >}} 1. [Sign in](https://grafana.com/auth/sign-in) to your Grafana Cloud account. From cd86758a354a3ada1797d14ce56f26a1407a410a Mon Sep 17 00:00:00 2001 From: Shirley <4163034+fridgepoet@users.noreply.github.com> Date: Wed, 25 Jan 2023 09:22:33 +0100 Subject: [PATCH 009/172] CloudWatch: Use grafana-aws-sdk v0.12.0 (#62007) --- go.mod | 5 +++-- go.sum | 8 ++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 85d53c9f914..56efdd983ea 100644 --- a/go.mod +++ b/go.mod @@ -61,7 +61,7 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/grafana/alerting v0.0.0-20230119191910-5ebb70a85264 github.com/grafana/cuetsy v0.1.5 - github.com/grafana/grafana-aws-sdk v0.11.0 + github.com/grafana/grafana-aws-sdk v0.12.0 github.com/grafana/grafana-azure-sdk-go v1.5.1 github.com/grafana/grafana-plugin-sdk-go v0.147.0 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 @@ -126,7 +126,7 @@ require ( gopkg.in/square/go-jose.v2 v2.5.1 gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 - xorm.io/builder v0.3.6 + xorm.io/builder v0.3.6 // indirect xorm.io/core v0.7.3 xorm.io/xorm v0.8.2 ) @@ -311,6 +311,7 @@ require ( github.com/google/gofuzz v1.2.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect github.com/gophercloud/gophercloud v0.24.0 // indirect + github.com/grafana/sqlds/v2 v2.3.10 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/golang-lru/v2 v2.0.1 // indirect github.com/hashicorp/memberlist v0.5.0 // indirect diff --git a/go.sum b/go.sum index 4cd7978beac..ce5ecd7b06f 100644 --- a/go.sum +++ b/go.sum @@ -1404,12 +1404,13 @@ github.com/grafana/dskit v0.0.0-20211011144203-3a88ec0b675f h1:FvvSVEbnGeM2bUivG 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= github.com/grafana/go-mssqldb v0.0.0-20210326084033-d0ce3c521036/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= -github.com/grafana/grafana-aws-sdk v0.11.0 h1:ncPD/UN0wNcKq3kEU90RdvrnK/6R4VW2Lo5dPcGk9t0= -github.com/grafana/grafana-aws-sdk v0.11.0/go.mod h1:5Iw3xY7iXJfNaYHrRHMXa/kaB2lWoyntg71PPLGvSs8= +github.com/grafana/grafana-aws-sdk v0.12.0 h1:eUjFdFZeZE+nyu/RMRz+qFxTBew69ToLBrbRhTbjkfM= +github.com/grafana/grafana-aws-sdk v0.12.0/go.mod h1:rCXLYoMpPqF90U7XqgVJ1HIAopFVF0bB3SXBVEJIm3I= github.com/grafana/grafana-azure-sdk-go v1.5.1 h1:d+wwqvWLYvPeGltxU50LNjvCykyJjoyd5exMYJW4nLM= github.com/grafana/grafana-azure-sdk-go v1.5.1/go.mod h1:OJJuBJ3MOoaq2mqD6xlPsArpL2R5j80TrDqPYr35Zak= github.com/grafana/grafana-google-sdk-go v0.0.0-20211104130251-b190293eaf58 h1:2ud7NNM7LrGPO4x0NFR8qLq68CqI4SmB7I2yRN2w9oE= github.com/grafana/grafana-google-sdk-go v0.0.0-20211104130251-b190293eaf58/go.mod h1:Vo2TKWfDVmNTELBUM+3lkrZvFtBws0qSZdXhQxRdJrE= +github.com/grafana/grafana-plugin-sdk-go v0.94.0/go.mod h1:3VXz4nCv6wH5SfgB3mlW39s+c+LetqSCjFj7xxPC5+M= github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk= github.com/grafana/grafana-plugin-sdk-go v0.147.0 h1:VavvJOa/Ubs+wzalzWIl+FQmdaD4vEK8KVYU0a8rf+E= github.com/grafana/grafana-plugin-sdk-go v0.147.0/go.mod h1:NMgO3t2gR5wyLx8bWZ9CTmpDk5Txp4wYFccFLHdYn3Q= @@ -1419,6 +1420,8 @@ github.com/grafana/prometheus-alertmanager v0.25.1-0.20230119183635-ec19b0a443b7 github.com/grafana/prometheus-alertmanager v0.25.1-0.20230119183635-ec19b0a443b7/go.mod h1:MnBfDPXJqXmmfPwQlCLvVUdqfnvrAw+hSPtDeaaFwj4= github.com/grafana/saml v0.4.13-0.20230123091136-3b6b1ec6c3cb h1:9PLj02xp4DeLTM2+ZyBMcN1sh0ir8GuF/1xXKyF+yws= github.com/grafana/saml v0.4.13-0.20230123091136-3b6b1ec6c3cb/go.mod h1:igEejV+fihTIlHXYP8zOec3V5A8y3lws5bQBFsTm4gA= +github.com/grafana/sqlds/v2 v2.3.10 h1:HWKhE0vR6LoEiE+Is8CSZOgaB//D1yqb2ntkass9Fd4= +github.com/grafana/sqlds/v2 v2.3.10/go.mod h1:c6ibxnxRVGxV/0YkEgvy7QpQH/lyifFyV7K/14xvdIs= github.com/grafana/thema v0.0.0-20230122235053-b4b6714dd1c9 h1:nAdsZkvPYNH6wDPkAi9JaDSIf5i2iVz4+Rqk4AOt6sE= github.com/grafana/thema v0.0.0-20230122235053-b4b6714dd1c9/go.mod h1:5j2nf4xmWhKr+1vyGouML8eJ8xERS5Jw/lhjs0eyz78= github.com/grafana/xorm v0.8.3-0.20220614223926-2fcda7565af6 h1:I9dh1MXGX0wGyxdV/Sl7+ugnki4Dfsy8lv2s5Yf887o= @@ -1966,6 +1969,7 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mjibson/esc v0.2.0/go.mod h1:9Hw9gxxfHulMF5OJKCyhYD7PzlSdhzXyaGEBRPH1OPs= From dbdd135298b1b20b69c8cc67d3510f0bf29651fc Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Wed, 25 Jan 2023 09:26:51 +0100 Subject: [PATCH 010/172] Alerting: Fix recording rules showing alert notification information. (#61988) * Fix recording rules form steps not showing alert information * Fix docs about creating cloud and recording rules * Update docs with suggested changes --- ...reate-mimir-loki-managed-recording-rule.md | 29 +++++++-------- .../components/rule-editor/DetailsStep.tsx | 8 ++--- .../rule-editor/ExpressionEditor.tsx | 36 ++++++++++++------- .../rule-editor/NotificationsStep.tsx | 10 ++++-- .../QueryAndExpressionsStep.tsx | 12 +++++-- 5 files changed, 56 insertions(+), 39 deletions(-) diff --git a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md index 6bbc25f90f6..4c9d6ee7e46 100644 --- a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md +++ b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md @@ -36,19 +36,17 @@ To create a Grafana Mimir or Loki managed recording rule 1. In the Grafana menu, click the **Alerting** (bell) icon to open the Alerting page listing existing alerts. 1. Click **New alert rule**. The new alerting rule page opens where the **Grafana managed alert** option is selected by default. -1. In Step 1, add the rule name. - - In **Rule name**, add a descriptive name. This name is displayed in the alert rule list. It is also the `alertname` label for every alert instance that is created from this rule. +1. In Step 1, add the rule name. The recording name must be a Prometheus metric name and contain no whitespace. + - In **Rule name**, add a descriptive name. 1. In Step 2, select **Mimir or Loki recording rule** option. - - Select your Loki or Prometheus data source, add the query to evaluate, and then select the alert condition. - - Enter a PromQL or LogQL expression. The rule fires if the evaluation result has at least one series with a value that is greater than 0. An alert is created for each series. -1. In Step 3, add the namespace, rule group, as well as additional metadata associated with the rule. + - Select your Loki or Prometheus data source. + - Enter a PromQL or LogQL query. +1. In Step 3, add the namespace and the group. - From the **Namespace** drop-down, select an existing rule namespace. Otherwise, click Add new and enter a name to create a new one. Namespaces can contain one or more rule groups and only have an organizational purpose. For more information, see [Grafana Mimir or Loki rule groups and namespaces]({{< relref "edit-mimir-loki-namespace-group/" >}}). - - From the **Group** drop-down, select an existing group within the selected namespace. Otherwise, click **Add new** and enter a name to create a new one. Newly created rules are appended to the end of the group. Rules within a group are run sequentially at a regular interval, with the same evaluation time. - - Add a description and summary to customize alert messages. Use the guidelines in [Annotations and labels for alerting]({{< relref "../fundamentals/annotation-label/" >}}). - - Add Runbook URL, panel, dashboard, and alert IDs. - - Add custom labels. -1. Click **Save** to save the rule or **Save and exit** to save the rule and go back to the Alerting page. -1. Next, create a notification for the rule. + - From the **Group** drop-down, select an existing group within the selected namespace. Otherwise, click **Add new** and enter a name to create a new one. +1. In Step 4, add the custom labels. + - Add custom labels selecting existing key-value pairs from the drop down, or add new labels by entering the new key or value. +1. Click **Save** to save the recording rule or **Save and exit** to save the recording rule and go back to the Alerting page. 1. In the Grafana menu, click the **Alerting** (bell) icon to open the Alerting page listing existing alerts. 1. Click **New alert rule**. @@ -57,13 +55,12 @@ To create a Grafana Mimir or Loki managed recording rule 1. In Step 2, add the type, and storage location. - From the **Rule type** drop-down, select **Mimir / Loki managed alert**. - From the **Select data source** drop-down, select an external Prometheus, an external Loki, or a Grafana Cloud data source. - - From the **Namespace** drop-down, select an existing rule namespace. Otherwise, click **Add new** and enter a name to create a new one. Namespaces can contain one or more rule groups and only have an organizational purpose. - - From the **Group** drop-down, select an existing group within the selected namespace. Otherwise, click **Add new** and enter a name to create a new one. Newly created rules are appended to the end of the group. Rules within a group are run sequentially at a regular interval, with the same evaluation time. - {{< figure src="/static/img/docs/alerting/unified/rule-edit-mimir-alert-type-8-0.png" max-width="550px" caption="Alert details" >}} -1. In Step 3, add the query to evaluate. - Enter a PromQL or LogQL expression. The rule fires if the evaluation result has at least one series with a value that is greater than 0. An alert is created for each series. - {{< figure src="/static/img/docs/alerting/unified/rule-edit-mimir-query-8-0.png" max-width="550px" caption="Alert details" >}} +1. In Step 3, add evaluation behavior. + - Enter a valid **For** duration. The expression has to be true for this long for the alert to be fired. 1. In Step 4, add additional metadata associated with the rule. + - From the **Namespace** drop-down, select an existing rule namespace. Otherwise, click Add new and enter a name to create a new one. Namespaces can contain one or more rule groups and only have an organizational purpose. For more information, see [Grafana Mimir or Loki rule groups and namespaces]({{< relref "edit-mimir-loki-namespace-group/" >}}). + - From the **Group** drop-down, select an existing group within the selected namespace. Otherwise, click **Add new** and enter a name to create a new one. Newly created rules are appended to the end of the group. Rules within a group are run sequentially at a regular interval, with the same evaluation time. - Add a description and summary to customize alert messages. Use the guidelines in [Annotations and labels for alerting]({{< relref "../fundamentals/annotation-label/" >}}). - Add Runbook URL, panel, dashboard, and alert IDs. 1. In Step 5, add custom labels. diff --git a/public/app/features/alerting/unified/components/rule-editor/DetailsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/DetailsStep.tsx index 4c6bcf2adae..993c1060fad 100644 --- a/public/app/features/alerting/unified/components/rule-editor/DetailsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/DetailsStep.tsx @@ -17,13 +17,11 @@ export function DetailsStep() { return ( {(ruleFormType === RuleFormType.cloudRecording || ruleFormType === RuleFormType.cloudAlerting) && diff --git a/public/app/features/alerting/unified/components/rule-editor/ExpressionEditor.tsx b/public/app/features/alerting/unified/components/rule-editor/ExpressionEditor.tsx index 4c08764ce30..a803455386d 100644 --- a/public/app/features/alerting/unified/components/rule-editor/ExpressionEditor.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/ExpressionEditor.tsx @@ -16,9 +16,15 @@ export interface ExpressionEditorProps { value?: string; onChange: (value: string) => void; dataSourceName: string; // will be a prometheus or loki datasource + showPreviewAlertsButton: boolean; } -export const ExpressionEditor: FC = ({ value, onChange, dataSourceName }) => { +export const ExpressionEditor: FC = ({ + value, + onChange, + dataSourceName, + showPreviewAlertsButton = true, +}) => { const styles = useStyles2(getStyles); const { mapToValue, mapToQuery } = useQueryMappers(dataSourceName); @@ -77,17 +83,23 @@ export const ExpressionEditor: FC = ({ value, onChange, d datasource={dataSource} /> -
- - {previewLoaded && !previewHasAlerts && ( - - There are no firing alerts for your query. - - )} - {previewHasAlerts && } -
+ {showPreviewAlertsButton && ( +
+ + {previewLoaded && !previewHasAlerts && ( + + There are no firing alerts for your query. + + )} + {previewHasAlerts && } +
+ )} ); }; diff --git a/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx index d985d37012a..26432889163 100644 --- a/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx @@ -23,12 +23,16 @@ export const NotificationsStep = () => { return (
- {!hasLabelsDefined && ( + {!hasLabelsDefined && type !== RuleFormType.cloudRecording && ( Root route – default for all alerts diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx index ce06d292ea3..dc7ea4c1377 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx @@ -20,17 +20,17 @@ import { refIdExists } from '../util'; import { AlertType } from './AlertType'; import { - duplicateQuery, addNewDataQuery, addNewExpression, + duplicateQuery, queriesAndExpressionsReducer, removeExpression, rewireExpressions, setDataQueries, updateExpression, updateExpressionRefId, - updateExpressionType, updateExpressionTimeRange, + updateExpressionType, } from './reducer'; interface Props { @@ -161,7 +161,13 @@ export const QueryAndExpressionsStep: FC = ({ editingExistingRule }) => { { - return ; + return ( + + ); }} control={control} rules={{ From 87023c85cb767e275bda9f054d1863c5bab03caa Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 25 Jan 2023 09:26:30 +0000 Subject: [PATCH 011/172] Changelog: Updated changelog for 9.2.10 (#62066) * Changelog: Updated changelog for 9.2.10 * Update CHANGELOG.md * Update CHANGELOG.md Co-authored-by: Horst Gutmann --- CHANGELOG.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c30477ca0a3..b0d0ab7c437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ -# 9.3.2 (unreleased) +# 9.3.2 (2023-12-13) ### Features and enhancements @@ -288,6 +288,23 @@ The following functions and classes related to logs are deprecated in the `grafa - **Toolkit:** Deprecate `component:create` command. [#56086](https://github.com/grafana/grafana/pull/56086), [@academo](https://github.com/academo) - **Toolkit:** Remove changelog command. [#56073](https://github.com/grafana/grafana/pull/56073), [@gitstart](https://github.com/gitstart) + + +# 9.2.10 (2023-01-24) + +### Features and enhancements + +- **TextPanel:** Refactor to functional component (#60885). [#61940](https://github.com/grafana/grafana/pull/61940), [@ryantxu](https://github.com/ryantxu) +- **[v9.2.x] Chore:** Upgrade Go to 1.19.4. [#60826](https://github.com/grafana/grafana/pull/60826), [@sakjur](https://github.com/sakjur) + +### Bug fixes + +- **Live:** Fix `Subscription to the channel already exists` live streaming error. [#61420](https://github.com/grafana/grafana/pull/61420), [@grafanabot](https://github.com/grafanabot) +- **Live:** Fix `Subscription to the channel already exists` live streaming error. [#61419](https://github.com/grafana/grafana/pull/61419), [@grafanabot](https://github.com/grafanabot) +- **Live:** Fix `Subscription to the channel already exists` live streaming error. [#61406](https://github.com/grafana/grafana/pull/61406), [@ArturWierzbicki](https://github.com/ArturWierzbicki) + + + # 9.2.7 (2022-11-29) From 8c3e4487bac32c438c2a2723bb380022886183ba Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 25 Jan 2023 09:30:59 +0000 Subject: [PATCH 012/172] Changelog: Updated changelog for 9.3.4 (#62067) --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0d0ab7c437..5059cff21cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,30 @@ + + +# 9.3.4 (2023-01-25) + +### Features and enhancements + +- **Prometheus:** Add default editor configuration. [#61510](https://github.com/grafana/grafana/pull/61510), [@itsmylife](https://github.com/itsmylife) +- **TextPanel:** Refactor to functional component (#60885). [#61937](https://github.com/grafana/grafana/pull/61937), [@ryantxu](https://github.com/ryantxu) + +### Bug fixes + +- **Alerting:** Fix webhook to use correct key for decrypting token. [#61717](https://github.com/grafana/grafana/pull/61717), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **Alerting:** Set error annotation on EvaluationError regardless of underlying error type. [#61506](https://github.com/grafana/grafana/pull/61506), [@alexweav](https://github.com/alexweav) +- **Datasources:** Fix Proxy by UID Failing for UIDs with a Hyphen. [#61723](https://github.com/grafana/grafana/pull/61723), [@csmarchbanks](https://github.com/csmarchbanks) +- **Elasticsearch:** Fix creating of span link with no tags. [#61753](https://github.com/grafana/grafana/pull/61753), [@ivanahuckova](https://github.com/ivanahuckova) +- **Elasticsearch:** Fix failing requests when using SigV4. [#61923](https://github.com/grafana/grafana/pull/61923), [@svennergr](https://github.com/svennergr) +- **Elasticsearch:** Fix toggle-settings are not shown correctly. [#61751](https://github.com/grafana/grafana/pull/61751), [@svennergr](https://github.com/svennergr) +- **Explore:** Be sure time range key bindings are mounted after clear. [#61892](https://github.com/grafana/grafana/pull/61892), [@gelicia](https://github.com/gelicia) +- **Explore:** Unsync time ranges when a pane is closed. [#61369](https://github.com/grafana/grafana/pull/61369), [@Elfo404](https://github.com/Elfo404) +- **Logs:** Lines with long words do not break properly. [#61707](https://github.com/grafana/grafana/pull/61707), [@svennergr](https://github.com/svennergr) +- **Loki:** Fix misaligned derived fields settings. [#61475](https://github.com/grafana/grafana/pull/61475), [@svennergr](https://github.com/svennergr) +- **Query Builder:** Fix max width of input component to prevent overflows. [#61798](https://github.com/grafana/grafana/pull/61798), [@matyax](https://github.com/matyax) +- **Search:** Auto focus input elements. [#61443](https://github.com/grafana/grafana/pull/61443), [@ryantxu](https://github.com/ryantxu) +- **Search:** Fix empty folder message showing when by starred dashboards. [#61610](https://github.com/grafana/grafana/pull/61610), [@eledobleefe](https://github.com/eledobleefe) +- **Table Panel:** Fix image of image cell overflowing table cell and cells ignoring text alignment setting when a data link is added. [#59392](https://github.com/grafana/grafana/pull/59392), [@oscarkilhed](https://github.com/oscarkilhed) + + # 9.3.2 (2023-12-13) From b54b80f473ce010a3377d1907b449f382db0bb8f Mon Sep 17 00:00:00 2001 From: idafurjes <36131195+idafurjes@users.noreply.github.com> Date: Wed, 25 Jan 2023 10:36:26 +0100 Subject: [PATCH 013/172] Chore: Remove Result from dashboard models (#61997) * Chore: Remove Result from dashboard models * Fix lint tests * Fix dashboard service tests * Fix API tests * Remove commented out code * Chore: Merge main - cleanup --- pkg/api/annotations.go | 20 +-- pkg/api/annotations_test.go | 44 +++--- pkg/api/common_test.go | 7 +- pkg/api/dashboard.go | 26 ++-- pkg/api/dashboard_permission_test.go | 9 +- pkg/api/dashboard_snapshot_test.go | 40 +++--- pkg/api/dashboard_test.go | 126 +++++++----------- pkg/api/folder_test.go | 15 +-- pkg/api/playlist_play.go | 5 +- pkg/api/preferences.go | 16 +-- pkg/api/preferences_test.go | 18 +-- pkg/api/stars.go | 4 +- .../ossaccesscontrol/permissions_services.go | 15 ++- pkg/services/alerting/eval_context.go | 5 +- .../comments/commentmodel/permissions.go | 10 +- pkg/services/dashboards/dashboard.go | 22 +-- .../dashboards/dashboard_service_mock.go | 95 +++++++++---- pkg/services/dashboards/database/acl.go | 14 +- pkg/services/dashboards/database/acl_test.go | 126 +++++++++--------- pkg/services/dashboards/database/database.go | 110 +++++++++------ .../database/database_provisioning_test.go | 10 +- .../dashboards/database/database_test.go | 72 +++++----- pkg/services/dashboards/models.go | 26 +--- .../dashboards/service/dashboard_service.go | 15 +-- .../service/dashboard_service_test.go | 12 +- pkg/services/dashboards/store_mock.go | 95 +++++++++---- pkg/services/folder/folderimpl/folder.go | 4 +- pkg/services/folder/folderimpl/folder_test.go | 8 +- .../guardian/accesscontrol_guardian.go | 15 ++- .../guardian/accesscontrol_guardian_test.go | 11 +- pkg/services/guardian/guardian.go | 12 +- pkg/services/guardian/guardian_test.go | 56 ++++---- pkg/services/guardian/guardian_util_test.go | 21 +-- .../librarypanels/librarypanels_test.go | 5 +- pkg/services/live/features/dashboard.go | 10 +- pkg/services/navtree/navtreeimpl/navtree.go | 4 +- .../ngalert/state/historian/dashboard.go | 6 +- .../ngalert/state/historian/dashboard_test.go | 9 +- .../service/dashboard_updater.go | 5 +- .../plugindashboards/service/service.go | 7 +- .../plugindashboards/service/service_test.go | 8 +- .../alerting/rules_provisioner.go | 6 +- .../provisioning/dashboards/file_reader.go | 6 +- .../dashboards/file_reader_test.go | 4 +- pkg/services/provisioning/utils/utils.go | 2 +- pkg/services/screenshot/screenshot.go | 9 +- pkg/services/screenshot/screenshot_test.go | 8 +- pkg/services/team/teamimpl/store_test.go | 18 +-- pkg/services/user/userimpl/store_test.go | 22 +-- 49 files changed, 625 insertions(+), 588 deletions(-) diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index d48132b769b..cddf8d1529e 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -51,7 +51,7 @@ func (hs *HTTPServer) GetAnnotations(c *models.ReqContext) response.Response { // When dashboard UID present in the request, we ignore dashboard ID if query.DashboardUid != "" { dq := dashboards.GetDashboardQuery{UID: query.DashboardUid, OrgID: c.OrgID} - err := hs.DashboardService.GetDashboard(c.Req.Context(), &dq) + dqResult, err := hs.DashboardService.GetDashboard(c.Req.Context(), &dq) if err != nil { if hs.Features.IsEnabled(featuremgmt.FlagDashboardsFromStorage) { // OK... the storage UIDs do not (yet?) exist in the DashboardService @@ -59,7 +59,7 @@ func (hs *HTTPServer) GetAnnotations(c *models.ReqContext) response.Response { return response.Error(http.StatusBadRequest, "Invalid dashboard UID in annotation request", err) } } else { - query.DashboardId = dq.Result.ID + query.DashboardId = dqResult.ID } } @@ -80,10 +80,10 @@ func (hs *HTTPServer) GetAnnotations(c *models.ReqContext) response.Response { item.DashboardUID = val } else { query := dashboards.GetDashboardQuery{ID: item.DashboardId, OrgID: c.OrgID} - err := hs.DashboardService.GetDashboard(c.Req.Context(), &query) - if err == nil && query.Result != nil { - item.DashboardUID = &query.Result.UID - dashboardCache[item.DashboardId] = &query.Result.UID + queryResult, err := hs.DashboardService.GetDashboard(c.Req.Context(), &query) + if err == nil && queryResult != nil { + item.DashboardUID = &queryResult.UID + dashboardCache[item.DashboardId] = &queryResult.UID } } } @@ -123,9 +123,9 @@ func (hs *HTTPServer) PostAnnotation(c *models.ReqContext) response.Response { // overwrite dashboardId when dashboardUID is not empty if cmd.DashboardUID != "" { query := dashboards.GetDashboardQuery{OrgID: c.OrgID, UID: cmd.DashboardUID} - err := hs.DashboardService.GetDashboard(c.Req.Context(), &query) + queryResult, err := hs.DashboardService.GetDashboard(c.Req.Context(), &query) if err == nil { - cmd.DashboardId = query.Result.ID + cmd.DashboardId = queryResult.ID } } @@ -380,9 +380,9 @@ func (hs *HTTPServer) MassDeleteAnnotations(c *models.ReqContext) response.Respo if cmd.DashboardUID != "" { query := dashboards.GetDashboardQuery{OrgID: c.OrgID, UID: cmd.DashboardUID} - err := hs.DashboardService.GetDashboard(c.Req.Context(), &query) + queryResult, err := hs.DashboardService.GetDashboard(c.Req.Context(), &query) if err == nil { - cmd.DashboardId = query.Result.ID + cmd.DashboardId = queryResult.ID } } diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go index 74219f30f0b..b8c190c62be 100644 --- a/pkg/api/annotations_test.go +++ b/pkg/api/annotations_test.go @@ -227,13 +227,8 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { t.Run("Should be able to do anything", func(t *testing.T) { dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardQuery) - q.Result = &dashboards.Dashboard{ - ID: q.ID, - UID: q.UID, - } - }).Return(nil) + result := &dashboards.Dashboard{} + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(result, nil) postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, cmd, store, dashSvc, func(sc *scenarioContext) { setUpACL() sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() @@ -244,6 +239,7 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { setUpACL() sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 200, sc.resp.Code) + dashSvc.AssertCalled(t, "GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")) }) @@ -267,13 +263,17 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { }) dashSvc = dashboards.NewFakeDashboardService(t) + result = &dashboards.Dashboard{ + ID: 1, + UID: deleteWithDashboardUIDCmd.DashboardUID, + } dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { q := args.Get(1).(*dashboards.GetDashboardQuery) - q.Result = &dashboards.Dashboard{ - ID: 1, + result = &dashboards.Dashboard{ + ID: q.ID, UID: deleteWithDashboardUIDCmd.DashboardUID, } - }).Return(nil) + }).Return(result, nil) deleteAnnotationsScenario(t, "When calling POST with dashboardUID on", "/api/annotations/mass-delete", "/api/annotations/mass-delete", role, deleteWithDashboardUIDCmd, mockStore, dashSvc, func(sc *scenarioContext) { setUpACL() @@ -287,13 +287,15 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { } func postAnnotationScenario(t *testing.T, desc string, url string, routePattern string, role org.RoleType, - cmd dtos.PostAnnotationsCmd, store db.DB, dashSvc dashboards.DashboardService, fn scenarioFunc) { + cmd dtos.PostAnnotationsCmd, store db.DB, dashSvc *dashboards.FakeDashboardService, fn scenarioFunc) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { hs := setupSimpleHTTPServer(nil) hs.SQLStore = store hs.DashboardService = dashSvc sc := setupScenarioContext(t, url) + sc.dashboardService = dashSvc + sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { c.Req.Body = mockRequestBody(cmd) c.Req.Header.Add("Content-Type", "application/json") @@ -301,12 +303,10 @@ func postAnnotationScenario(t *testing.T, desc string, url string, routePattern sc.context.UserID = testUserID sc.context.OrgID = testOrgID sc.context.OrgRole = role - return hs.PostAnnotation(c) }) sc.m.Post(routePattern, sc.defaultHandler) - fn(sc) }) } @@ -680,20 +680,22 @@ func setUpACL() { store := dbtest.NewFakeDB() teamSvc := &teamtest.FakeService{} dashSvc := &dashboards.FakeDashboardService{} + qResult := []*dashboards.DashboardACLInfoDTO{ + {Role: &viewerRole, Permission: models.PERMISSION_VIEW}, + {Role: &editorRole, Permission: models.PERMISSION_EDIT}, + } dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardACLInfoListQuery) - q.Result = []*dashboards.DashboardACLInfoDTO{ - {Role: &viewerRole, Permission: models.PERMISSION_VIEW}, - {Role: &editorRole, Permission: models.PERMISSION_EDIT}, - } - }).Return(nil) + // q := args.Get(1).(*dashboards.GetDashboardACLInfoListQuery) + + }).Return(qResult, nil) + var result *dashboards.Dashboard dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { q := args.Get(1).(*dashboards.GetDashboardQuery) - q.Result = &dashboards.Dashboard{ + result = &dashboards.Dashboard{ ID: q.ID, UID: q.UID, } - }).Return(nil) + }).Return(result, nil) guardian.InitLegacyGuardian(store, dashSvc, teamSvc) } diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index bb96b785526..5f9748e673e 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -188,6 +188,7 @@ type scenarioContext struct { authInfoService *logintest.AuthInfoServiceFake dashboardVersionService dashver.Service userService user.Service + dashboardService dashboards.DashboardService } func (sc *scenarioContext) exec() { @@ -538,10 +539,8 @@ func setUp(confs ...setUpConf) *HTTPServer { } teamSvc := &teamtest.FakeService{} dashSvc := &dashboards.FakeDashboardService{} - dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardACLInfoListQuery) - q.Result = aclMockResp - }).Return(nil) + qResult := aclMockResp + dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult, nil) guardian.InitLegacyGuardian(store, dashSvc, teamSvc) return hs } diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index a2e051d23ef..95b1e98960c 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -190,15 +190,16 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response { // lookup folder title if dash.FolderID > 0 { query := dashboards.GetDashboardQuery{ID: dash.FolderID, OrgID: c.OrgID} - if err := hs.DashboardService.GetDashboard(c.Req.Context(), &query); err != nil { + queryResult, err := hs.DashboardService.GetDashboard(c.Req.Context(), &query) + if err != nil { if errors.Is(err, dashboards.ErrFolderNotFound) { return response.Error(404, "Folder not found", err) } return response.Error(500, "Dashboard folder could not be read", err) } - meta.FolderUid = query.Result.UID - meta.FolderTitle = query.Result.Title - meta.FolderUrl = query.Result.GetURL() + meta.FolderUid = queryResult.UID + meta.FolderTitle = queryResult.Title + meta.FolderUrl = queryResult.GetURL() } provisioningData, err := hs.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardID(c.Req.Context(), dash.ID) @@ -281,11 +282,12 @@ func (hs *HTTPServer) getDashboardHelper(ctx context.Context, orgID int64, id in query = dashboards.GetDashboardQuery{ID: id, OrgID: orgID} } - if err := hs.DashboardService.GetDashboard(ctx, &query); err != nil { + queryResult, err := hs.DashboardService.GetDashboard(ctx, &query) + if err != nil { return nil, response.Error(404, "Dashboard not found", err) } - return query.Result, nil + return queryResult, nil } // DeleteDashboardByUID swagger:route DELETE /dashboards/uid/{uid} dashboards deleteDashboardByUID @@ -531,9 +533,9 @@ func (hs *HTTPServer) GetHomeDashboard(c *models.ReqContext) response.Response { if preference.HomeDashboardID != 0 { slugQuery := dashboards.GetDashboardRefByIDQuery{ID: preference.HomeDashboardID} - err := hs.DashboardService.GetDashboardUIDByID(c.Req.Context(), &slugQuery) + slugQueryResult, err := hs.DashboardService.GetDashboardUIDByID(c.Req.Context(), &slugQuery) if err == nil { - url := dashboards.GetDashboardURL(slugQuery.Result.UID, slugQuery.Result.Slug) + url := dashboards.GetDashboardURL(slugQueryResult.UID, slugQueryResult.Slug) dashRedirect := dtos.DashboardRedirect{RedirectUri: url} return response.JSON(http.StatusOK, &dashRedirect) } @@ -1016,13 +1018,13 @@ func (hs *HTTPServer) RestoreDashboardVersion(c *models.ReqContext) response.Res // 500: internalServerError func (hs *HTTPServer) GetDashboardTags(c *models.ReqContext) { query := dashboards.GetDashboardTagsQuery{OrgID: c.OrgID} - err := hs.DashboardService.GetDashboardTags(c.Req.Context(), &query) + queryResult, err := hs.DashboardService.GetDashboardTags(c.Req.Context(), &query) if err != nil { c.JsonApiErr(500, "Failed to get tags from database", err) return } - c.JSON(http.StatusOK, query.Result) + c.JSON(http.StatusOK, queryResult) } // GetDashboardUIDs converts internal ids to UIDs @@ -1037,11 +1039,11 @@ func (hs *HTTPServer) GetDashboardUIDs(c *models.ReqContext) { continue } q.ID = id - err = hs.DashboardService.GetDashboardUIDByID(c.Req.Context(), q) + qResult, err := hs.DashboardService.GetDashboardUIDByID(c.Req.Context(), q) if err != nil { continue } - uids = append(uids, q.Result.UID) + uids = append(uids, qResult.UID) } c.JSON(http.StatusOK, uids) } diff --git a/pkg/api/dashboard_permission_test.go b/pkg/api/dashboard_permission_test.go index 369b7858a6d..1b07b5937bf 100644 --- a/pkg/api/dashboard_permission_test.go +++ b/pkg/api/dashboard_permission_test.go @@ -27,13 +27,8 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { t.Run("Dashboard permissions test", func(t *testing.T) { settings := setting.NewCfg() dashboardStore := &dashboards.FakeDashboardStore{} - dashboardStore.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardQuery) - q.Result = &dashboards.Dashboard{ - ID: q.ID, - UID: q.UID, - } - }).Return(nil, nil) + qResult := &dashboards.Dashboard{} + dashboardStore.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(qResult, nil) defer dashboardStore.AssertExpectations(t) features := featuremgmt.WithFeatures() diff --git a/pkg/api/dashboard_snapshot_test.go b/pkg/api/dashboard_snapshot_test.go index 551fa25ffbd..f6afee1cf02 100644 --- a/pkg/api/dashboard_snapshot_test.go +++ b/pkg/api/dashboard_snapshot_test.go @@ -72,14 +72,15 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { teamSvc := &teamtest.FakeService{} dashSvc := dashboards.NewFakeDashboardService(t) + var qResult *dashboards.Dashboard dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { q := args.Get(1).(*dashboards.GetDashboardQuery) - q.Result = &dashboards.Dashboard{ + qResult = &dashboards.Dashboard{ ID: q.ID, UID: q.UID, } - }).Return(nil).Maybe() - dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(nil).Maybe() + }).Return(qResult, nil).Maybe() + dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(nil, nil).Maybe() hs.DashboardService = dashSvc guardian.InitLegacyGuardian(sc.sqlStore, dashSvc, teamSvc) @@ -118,13 +119,11 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { t.Run("When user is editor and dashboard has default ACL", func(t *testing.T) { teamSvc := &teamtest.FakeService{} dashSvc := &dashboards.FakeDashboardService{} - dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardACLInfoListQuery) - q.Result = []*dashboards.DashboardACLInfoDTO{ - {Role: &viewerRole, Permission: models.PERMISSION_VIEW}, - {Role: &editorRole, Permission: models.PERMISSION_EDIT}, - } - }).Return(nil) + qResult := []*dashboards.DashboardACLInfoDTO{ + {Role: &viewerRole, Permission: models.PERMISSION_VIEW}, + {Role: &editorRole, Permission: models.PERMISSION_EDIT}, + } + dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult, nil) loggedInUserScenarioWithRole(t, "Should be able to delete a snapshot when calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", org.RoleEditor, func(sc *scenarioContext) { @@ -134,20 +133,13 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { externalRequest = req }) dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardQuery) - q.Result = &dashboards.Dashboard{ - ID: q.ID, - OrgID: q.OrgID, - } - }).Return(nil).Maybe() - dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardACLInfoListQuery) - q.Result = []*dashboards.DashboardACLInfoDTO{ - {Role: &viewerRole, Permission: models.PERMISSION_VIEW}, - {Role: &editorRole, Permission: models.PERMISSION_EDIT}, - } - }).Return(nil) + qResult := &dashboards.Dashboard{} + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(qResult, nil).Maybe() + qResultACL := []*dashboards.DashboardACLInfoDTO{ + {Role: &viewerRole, Permission: models.PERMISSION_VIEW}, + {Role: &editorRole, Permission: models.PERMISSION_EDIT}, + } + dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResultACL, nil) guardian.InitLegacyGuardian(sc.sqlStore, dashSvc, teamSvc) hs := &HTTPServer{dashboardsnapshotsService: setUpSnapshotTest(t, 0, ts.URL), DashboardService: dashSvc} sc.handlerFunc = hs.DeleteDashboardSnapshot diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 5daa4816767..f141d773a64 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -137,10 +137,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { fakeDashboardVersionService.ExpectedDashboardVersion = &dashver.DashboardVersionDTO{} teamService := &teamtest.FakeService{} dashboardService := dashboards.NewFakeDashboardService(t) - dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardQuery) - q.Result = fakeDash - }).Return(nil) + dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(fakeDash, nil) mockSQLStore := dbtest.NewFakeDB() hs := &HTTPServer{ @@ -158,13 +155,11 @@ func TestDashboardAPIEndpoint(t *testing.T) { setUp := func() { viewerRole := org.RoleViewer editorRole := org.RoleEditor - dashboardService.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardACLInfoListQuery) - q.Result = []*dashboards.DashboardACLInfoDTO{ - {Role: &viewerRole, Permission: models.PERMISSION_VIEW}, - {Role: &editorRole, Permission: models.PERMISSION_EDIT}, - } - }).Return(nil) + qResult := []*dashboards.DashboardACLInfoDTO{ + {Role: &viewerRole, Permission: models.PERMISSION_VIEW}, + {Role: &editorRole, Permission: models.PERMISSION_EDIT}, + } + dashboardService.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult, nil) guardian.InitLegacyGuardian(mockSQLStore, dashboardService, teamService) } @@ -245,20 +240,16 @@ func TestDashboardAPIEndpoint(t *testing.T) { fakeDashboardVersionService.ExpectedDashboardVersion = &dashver.DashboardVersionDTO{} teamService := &teamtest.FakeService{} dashboardService := dashboards.NewFakeDashboardService(t) - dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardQuery) - q.Result = fakeDash - }).Return(nil) - dashboardService.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardACLInfoListQuery) - q.Result = []*dashboards.DashboardACLInfoDTO{ - { - DashboardID: 1, - Permission: models.PERMISSION_EDIT, - UserID: 200, - }, - } - }).Return(nil) + + dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(fakeDash, nil) + qResult := []*dashboards.DashboardACLInfoDTO{ + { + DashboardID: 1, + Permission: models.PERMISSION_EDIT, + UserID: 200, + }, + } + dashboardService.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult, nil) mockSQLStore := dbtest.NewFakeDB() cfg := setting.NewCfg() @@ -381,12 +372,10 @@ func TestDashboardAPIEndpoint(t *testing.T) { setting.ViewersCanEdit = false dashboardService := dashboards.NewFakeDashboardService(t) - dashboardService.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardACLInfoListQuery) - q.Result = []*dashboards.DashboardACLInfoDTO{ - {OrgID: 1, DashboardID: 2, UserID: 1, Permission: models.PERMISSION_EDIT}, - } - }).Return(nil) + qResult := []*dashboards.DashboardACLInfoDTO{ + {OrgID: 1, DashboardID: 2, UserID: 1, Permission: models.PERMISSION_EDIT}, + } + dashboardService.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult, nil) guardian.InitLegacyGuardian(mockSQLStore, dashboardService, teamService) } @@ -404,10 +393,8 @@ func TestDashboardAPIEndpoint(t *testing.T) { loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { setUpInner() dashboardService := dashboards.NewFakeDashboardService(t) - dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardQuery) - q.Result = dashboards.NewDashboard("test") - }).Return(nil) + qResult := dashboards.NewDashboard("test") + dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(qResult, nil) dashboardService.On("DeleteDashboard", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")).Return(nil) hs.callDeleteDashboardByUID(t, sc, dashboardService) @@ -443,12 +430,10 @@ func TestDashboardAPIEndpoint(t *testing.T) { setting.ViewersCanEdit = true dashboardService := dashboards.NewFakeDashboardService(t) - dashboardService.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardACLInfoListQuery) - q.Result = []*dashboards.DashboardACLInfoDTO{ - {OrgID: 1, DashboardID: 2, UserID: 1, Permission: models.PERMISSION_VIEW}, - } - }).Return(nil) + qResult := []*dashboards.DashboardACLInfoDTO{ + {OrgID: 1, DashboardID: 2, UserID: 1, Permission: models.PERMISSION_VIEW}, + } + dashboardService.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult, nil) guardian.InitLegacyGuardian(mockSQLStore, dashboardService, teamService) } @@ -483,12 +468,10 @@ func TestDashboardAPIEndpoint(t *testing.T) { setting.ViewersCanEdit = true dashboardService := dashboards.NewFakeDashboardService(t) - dashboardService.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardACLInfoListQuery) - q.Result = []*dashboards.DashboardACLInfoDTO{ - {OrgID: 1, DashboardID: 2, UserID: 1, Permission: models.PERMISSION_ADMIN}, - } - }).Return(nil) + qResult := []*dashboards.DashboardACLInfoDTO{ + {OrgID: 1, DashboardID: 2, UserID: 1, Permission: models.PERMISSION_ADMIN}, + } + dashboardService.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult, nil) guardian.InitLegacyGuardian(mockSQLStore, dashboardService, teamService) } @@ -506,10 +489,8 @@ func TestDashboardAPIEndpoint(t *testing.T) { setUpInner() sc.sqlStore = mockSQLStore dashboardService := dashboards.NewFakeDashboardService(t) - dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardQuery) - q.Result = dashboards.NewDashboard("test") - }).Return(nil) + qResult := dashboards.NewDashboard("test") + dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(qResult, nil) dashboardService.On("DeleteDashboard", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")).Return(nil) hs.callDeleteDashboardByUID(t, sc, dashboardService) @@ -536,12 +517,10 @@ func TestDashboardAPIEndpoint(t *testing.T) { setUpInner := func() { dashboardService := dashboards.NewFakeDashboardService(t) - dashboardService.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardACLInfoListQuery) - q.Result = []*dashboards.DashboardACLInfoDTO{ - {OrgID: 1, DashboardID: 2, UserID: 1, Permission: models.PERMISSION_VIEW}, - } - }).Return(nil) + qResult := []*dashboards.DashboardACLInfoDTO{ + {OrgID: 1, DashboardID: 2, UserID: 1, Permission: models.PERMISSION_VIEW}, + } + dashboardService.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult, nil) guardian.InitLegacyGuardian(mockSQLStore, dashboardService, teamService) } @@ -808,14 +787,9 @@ func TestDashboardAPIEndpoint(t *testing.T) { setUp := func() { teamSvc := &teamtest.FakeService{} dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(nil) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardQuery) - q.Result = &dashboards.Dashboard{ - OrgID: q.OrgID, - ID: q.ID, - } - }).Return(nil) + dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(nil, nil) + qResult := &dashboards.Dashboard{} + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(qResult, nil) guardian.InitLegacyGuardian(sqlmock, dashSvc, teamSvc) } @@ -860,10 +834,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { fakeDash.HasACL = false dashboardService := dashboards.NewFakeDashboardService(t) - dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardQuery) - q.Result = fakeDash - }).Return(nil) + dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(fakeDash, nil) dashboardService.On("SaveDashboard", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), mock.AnythingOfType("bool")).Run(func(args mock.Arguments) { cmd := args.Get(1).(*dashboards.SaveDashboardDTO) cmd.Dashboard = &dashboards.Dashboard{ @@ -897,10 +868,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { fakeDash.HasACL = false dashboardService := dashboards.NewFakeDashboardService(t) - dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardQuery) - q.Result = fakeDash - }).Return(nil) + dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(fakeDash, nil) dashboardService.On("SaveDashboard", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), mock.AnythingOfType("bool")).Run(func(args mock.Arguments) { cmd := args.Get(1).(*dashboards.SaveDashboardDTO) cmd.Dashboard = &dashboards.Dashboard{ @@ -937,14 +905,10 @@ func TestDashboardAPIEndpoint(t *testing.T) { dataValue, err := simplejson.NewJson([]byte(`{"id": 1, "editable": true, "style": "dark"}`)) require.NoError(t, err) - dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardQuery) - q.Result = &dashboards.Dashboard{ID: 1, Data: dataValue} - }).Return(nil) - dashboardService.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardACLInfoListQuery) - q.Result = []*dashboards.DashboardACLInfoDTO{{OrgID: testOrgID, DashboardID: 1, UserID: testUserID, Permission: models.PERMISSION_EDIT}} - }).Return(nil) + qResult := &dashboards.Dashboard{ID: 1, Data: dataValue} + dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(qResult, nil) + qResult2 := []*dashboards.DashboardACLInfoDTO{{OrgID: testOrgID, DashboardID: 1, UserID: testUserID, Permission: models.PERMISSION_EDIT}} + dashboardService.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult2, nil) guardian.InitLegacyGuardian(mockSQLStore, dashboardService, teamService) loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", org.RoleEditor, func(sc *scenarioContext) { diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index 11a03ddefe5..3a8f201560f 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -241,17 +241,10 @@ func createFolderScenario(t *testing.T, desc string, url string, routePattern st aclMockResp := []*dashboards.DashboardACLInfoDTO{} teamSvc := &teamtest.FakeService{} dashSvc := &dashboards.FakeDashboardService{} - dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardACLInfoListQuery) - q.Result = aclMockResp - }).Return(nil) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardQuery) - q.Result = &dashboards.Dashboard{ - ID: q.ID, - UID: q.UID, - } - }).Return(nil) + qResult1 := aclMockResp + dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult1, nil) + qResult := &dashboards.Dashboard{} + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(qResult, nil) store := dbtest.NewFakeDB() guardian.InitLegacyGuardian(store, dashSvc, teamSvc) hs := HTTPServer{ diff --git a/pkg/api/playlist_play.go b/pkg/api/playlist_play.go index 62274f85aec..d024d2c4168 100644 --- a/pkg/api/playlist_play.go +++ b/pkg/api/playlist_play.go @@ -19,11 +19,12 @@ func (hs *HTTPServer) populateDashboardsByID(ctx context.Context, dashboardByIDs if len(dashboardByIDs) > 0 { dashboardQuery := dashboards.GetDashboardsQuery{DashboardIDs: dashboardByIDs} - if err := hs.DashboardService.GetDashboards(ctx, &dashboardQuery); err != nil { + dashboardQueryResult, err := hs.DashboardService.GetDashboards(ctx, &dashboardQuery) + if err != nil { return result, err } - for _, item := range dashboardQuery.Result { + for _, item := range dashboardQueryResult { result = append(result, dtos.PlaylistDashboard{ Id: item.ID, Slug: item.Slug, diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go index a005b9cdbc9..7f9761723d5 100644 --- a/pkg/api/preferences.go +++ b/pkg/api/preferences.go @@ -35,11 +35,11 @@ func (hs *HTTPServer) SetHomeDashboard(c *models.ReqContext) response.Response { if query.UID == "" { dashboardID = 0 // clear the value } else { - err := hs.DashboardService.GetDashboard(c.Req.Context(), &query) + queryResult, err := hs.DashboardService.GetDashboard(c.Req.Context(), &query) if err != nil { return response.Error(404, "Dashboard not found", err) } - dashboardID = query.Result.ID + dashboardID = queryResult.ID } } @@ -77,9 +77,9 @@ func (hs *HTTPServer) getPreferencesFor(ctx context.Context, orgID, userID, team // when homedashboardID is 0, that means it is the default home dashboard, no UID would be returned in the response if preference.HomeDashboardID != 0 { query := dashboards.GetDashboardQuery{ID: preference.HomeDashboardID, OrgID: orgID} - err = hs.DashboardService.GetDashboard(ctx, &query) + queryResult, err := hs.DashboardService.GetDashboard(ctx, &query) if err == nil { - dashboardUID = query.Result.UID + dashboardUID = queryResult.UID } } @@ -136,11 +136,11 @@ func (hs *HTTPServer) updatePreferencesFor(ctx context.Context, orgID, userID, t // clear the value dashboardID = 0 } else { - err := hs.DashboardService.GetDashboard(ctx, &query) + queryResult, err := hs.DashboardService.GetDashboard(ctx, &query) if err != nil { return response.Error(404, "Dashboard not found", err) } - dashboardID = query.Result.ID + dashboardID = queryResult.ID } } dtoCmd.HomeDashboardID = dashboardID @@ -196,11 +196,11 @@ func (hs *HTTPServer) patchPreferencesFor(ctx context.Context, orgID, userID, te defaultDash := int64(0) dashboardID = &defaultDash } else { - err := hs.DashboardService.GetDashboard(ctx, &query) + queryResult, err := hs.DashboardService.GetDashboard(ctx, &query) if err != nil { return response.Error(404, "Dashboard not found", err) } - dashboardID = &query.Result.ID + dashboardID = &queryResult.ID } } dtoCmd.HomeDashboardID = dashboardID diff --git a/pkg/api/preferences_test.go b/pkg/api/preferences_test.go index 6b9abd2090e..871e0f63bad 100644 --- a/pkg/api/preferences_test.go +++ b/pkg/api/preferences_test.go @@ -39,10 +39,8 @@ func TestAPIEndpoint_GetCurrentOrgPreferences_LegacyAccessControl(t *testing.T) cfg := setting.NewCfg() cfg.RBACEnabled = false dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardQuery) - q.Result = &dashboards.Dashboard{UID: "home", ID: 1} - }).Return(nil) + qResult := &dashboards.Dashboard{UID: "home", ID: 1} + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(qResult, nil) prefService := preftest.NewPreferenceServiceFake() prefService.ExpectedPreference = &pref.Preference{HomeDashboardID: 1, Theme: "dark"} @@ -80,10 +78,8 @@ func TestAPIEndpoint_GetCurrentOrgPreferences_AccessControl(t *testing.T) { prefService.ExpectedPreference = &pref.Preference{HomeDashboardID: 1, Theme: "dark"} dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardQuery) - q.Result = &dashboards.Dashboard{UID: "home", ID: 1} - }).Return(nil) + qResult := &dashboards.Dashboard{UID: "home", ID: 1} + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(qResult, nil) server := SetupAPITestServer(t, func(hs *HTTPServer) { hs.Cfg = setting.NewCfg() @@ -196,10 +192,8 @@ func TestAPIEndpoint_PatchUserPreferences(t *testing.T) { cfg.RBACEnabled = false dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardQuery) - q.Result = &dashboards.Dashboard{UID: "home", ID: 1} - }).Return(nil) + qResult := &dashboards.Dashboard{UID: "home", ID: 1} + dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(qResult, nil) server := SetupAPITestServer(t, func(hs *HTTPServer) { hs.Cfg = cfg diff --git a/pkg/api/stars.go b/pkg/api/stars.go index 47bd698b969..128d10e4e62 100644 --- a/pkg/api/stars.go +++ b/pkg/api/stars.go @@ -27,11 +27,11 @@ func (hs *HTTPServer) GetStars(c *models.ReqContext) response.Response { ID: dashboardId, OrgID: c.OrgID, } - err := hs.DashboardService.GetDashboard(c.Req.Context(), query) + queryResult, err := hs.DashboardService.GetDashboard(c.Req.Context(), query) // Grafana admin users may have starred dashboards in multiple orgs. This will avoid returning errors when the dashboard is in another org if err == nil { - uids = append(uids, query.Result.UID) + uids = append(uids, queryResult.UID) } } return response.JSON(200, uids) diff --git a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go index a4ede7777ab..2397e710fa2 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go @@ -120,10 +120,11 @@ func ProvideDashboardPermissions( ) (*DashboardPermissionsService, error) { getDashboard := func(ctx context.Context, orgID int64, resourceID string) (*dashboards.Dashboard, error) { query := &dashboards.GetDashboardQuery{UID: resourceID, OrgID: orgID} - if _, err := dashboardStore.GetDashboard(ctx, query); err != nil { + queryResult, err := dashboardStore.GetDashboard(ctx, query) + if err != nil { return nil, err } - return query.Result, nil + return queryResult, nil } options := resourcepermissions.Options{ @@ -148,10 +149,11 @@ func ProvideDashboardPermissions( } if dashboard.FolderID > 0 { query := &dashboards.GetDashboardQuery{ID: dashboard.FolderID, OrgID: orgID} - if _, err := dashboardStore.GetDashboard(ctx, query); err != nil { + queryResult, err := dashboardStore.GetDashboard(ctx, query) + if err != nil { return nil, err } - return []string{dashboards.ScopeFoldersProvider.GetResourceScopeUID(query.Result.UID)}, nil + return []string{dashboards.ScopeFoldersProvider.GetResourceScopeUID(queryResult.UID)}, nil } return []string{}, nil }, @@ -202,11 +204,12 @@ func ProvideFolderPermissions( ResourceAttribute: "uid", ResourceValidator: func(ctx context.Context, orgID int64, resourceID string) error { query := &dashboards.GetDashboardQuery{UID: resourceID, OrgID: orgID} - if _, err := dashboardStore.GetDashboard(ctx, query); err != nil { + queryResult, err := dashboardStore.GetDashboard(ctx, query) + if err != nil { return err } - if !query.Result.IsFolder { + if !queryResult.IsFolder { return errors.New("not found") } diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index d5375cae94d..ab31688aa9e 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -123,11 +123,12 @@ func (c *EvalContext) GetDashboardUID() (*dashboards.DashboardRef, error) { } uidQuery := &dashboards.GetDashboardRefByIDQuery{ID: c.Rule.DashboardID} - if err := c.dashboardService.GetDashboardUIDByID(c.Ctx, uidQuery); err != nil { + uidQueryResult, err := c.dashboardService.GetDashboardUIDByID(c.Ctx, uidQuery) + if err != nil { return nil, err } - c.dashboardRef = uidQuery.Result + c.dashboardRef = uidQueryResult return c.dashboardRef, nil } diff --git a/pkg/services/comments/commentmodel/permissions.go b/pkg/services/comments/commentmodel/permissions.go index c11f54179e4..19803966939 100644 --- a/pkg/services/comments/commentmodel/permissions.go +++ b/pkg/services/comments/commentmodel/permissions.go @@ -30,18 +30,20 @@ func NewPermissionChecker(sqlStore db.DB, features featuremgmt.FeatureToggles, func (c *PermissionChecker) getDashboardByUid(ctx context.Context, orgID int64, uid string) (*dashboards.Dashboard, error) { query := dashboards.GetDashboardQuery{UID: uid, OrgID: orgID} - if err := c.dashboardService.GetDashboard(ctx, &query); err != nil { + queryResult, err := c.dashboardService.GetDashboard(ctx, &query) + if err != nil { return nil, err } - return query.Result, nil + return queryResult, nil } func (c *PermissionChecker) getDashboardById(ctx context.Context, orgID int64, id int64) (*dashboards.Dashboard, error) { query := dashboards.GetDashboardQuery{ID: id, OrgID: orgID} - if err := c.dashboardService.GetDashboard(ctx, &query); err != nil { + queryResult, err := c.dashboardService.GetDashboard(ctx, &query) + if err != nil { return nil, err } - return query.Result, nil + return queryResult, nil } func (c *PermissionChecker) CheckReadPermissions(ctx context.Context, orgId int64, signedInUser *user.SignedInUser, objectType string, objectID string) (bool, error) { diff --git a/pkg/services/dashboards/dashboard.go b/pkg/services/dashboards/dashboard.go index 0caf638a379..a52b98244e5 100644 --- a/pkg/services/dashboards/dashboard.go +++ b/pkg/services/dashboards/dashboard.go @@ -16,11 +16,11 @@ type DashboardService interface { BuildSaveDashboardCommand(ctx context.Context, dto *SaveDashboardDTO, shouldValidateAlerts bool, validateProvisionedDashboard bool) (*SaveDashboardCommand, error) DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error FindDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) - GetDashboard(ctx context.Context, query *GetDashboardQuery) error - GetDashboardACLInfoList(ctx context.Context, query *GetDashboardACLInfoListQuery) error - GetDashboards(ctx context.Context, query *GetDashboardsQuery) error - GetDashboardTags(ctx context.Context, query *GetDashboardTagsQuery) error - GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) error + GetDashboard(ctx context.Context, query *GetDashboardQuery) (*Dashboard, error) + GetDashboardACLInfoList(ctx context.Context, query *GetDashboardACLInfoListQuery) ([]*DashboardACLInfoDTO, error) + GetDashboards(ctx context.Context, query *GetDashboardsQuery) ([]*Dashboard, error) + GetDashboardTags(ctx context.Context, query *GetDashboardTagsQuery) ([]*DashboardTagCloudItem, error) + GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) (*DashboardRef, error) HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *folder.HasAdminPermissionInDashboardsOrFoldersQuery) (bool, error) HasEditPermissionInFolders(ctx context.Context, query *folder.HasEditPermissionInFoldersQuery) (bool, error) ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*Dashboard, error) @@ -34,7 +34,7 @@ type DashboardService interface { // PluginService is a service for operating on plugin dashboards. type PluginService interface { - GetDashboardsByPluginID(ctx context.Context, query *GetDashboardsByPluginIDQuery) error + GetDashboardsByPluginID(ctx context.Context, query *GetDashboardsByPluginIDQuery) ([]*Dashboard, error) } // DashboardProvisioningService is a service for operating on provisioned dashboards. @@ -59,12 +59,12 @@ type Store interface { DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *DeleteOrphanedProvisionedDashboardsCommand) error FindDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) GetDashboard(ctx context.Context, query *GetDashboardQuery) (*Dashboard, error) - GetDashboardACLInfoList(ctx context.Context, query *GetDashboardACLInfoListQuery) error - GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) error - GetDashboards(ctx context.Context, query *GetDashboardsQuery) error + GetDashboardACLInfoList(ctx context.Context, query *GetDashboardACLInfoListQuery) ([]*DashboardACLInfoDTO, error) + GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) (*DashboardRef, error) + GetDashboards(ctx context.Context, query *GetDashboardsQuery) ([]*Dashboard, error) // GetDashboardsByPluginID retrieves dashboards identified by plugin. - GetDashboardsByPluginID(ctx context.Context, query *GetDashboardsByPluginIDQuery) error - GetDashboardTags(ctx context.Context, query *GetDashboardTagsQuery) error + GetDashboardsByPluginID(ctx context.Context, query *GetDashboardsByPluginIDQuery) ([]*Dashboard, error) + GetDashboardTags(ctx context.Context, query *GetDashboardTagsQuery) ([]*DashboardTagCloudItem, error) GetProvisionedDashboardData(ctx context.Context, name string) ([]*DashboardProvisioning, error) GetProvisionedDataByDashboardID(ctx context.Context, dashboardID int64) (*DashboardProvisioning, error) GetProvisionedDataByDashboardUID(ctx context.Context, orgID int64, dashboardUID string) (*DashboardProvisioning, error) diff --git a/pkg/services/dashboards/dashboard_service_mock.go b/pkg/services/dashboards/dashboard_service_mock.go index 7165629552a..e0d3622ded3 100644 --- a/pkg/services/dashboards/dashboard_service_mock.go +++ b/pkg/services/dashboards/dashboard_service_mock.go @@ -112,73 +112,118 @@ func (_m *FakeDashboardService) FindDashboards(ctx context.Context, query *model } // GetDashboard provides a mock function with given fields: ctx, query -func (_m *FakeDashboardService) GetDashboard(ctx context.Context, query *GetDashboardQuery) error { +func (_m *FakeDashboardService) GetDashboard(ctx context.Context, query *GetDashboardQuery) (*Dashboard, error) { ret := _m.Called(ctx, query) - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardQuery) error); ok { + var r0 *Dashboard + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardQuery) *Dashboard); ok { r0 = rf(ctx, query) } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).(*Dashboard) + } } - return r0 + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *GetDashboardQuery) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } // GetDashboardACLInfoList provides a mock function with given fields: ctx, query -func (_m *FakeDashboardService) GetDashboardACLInfoList(ctx context.Context, query *GetDashboardACLInfoListQuery) error { +func (_m *FakeDashboardService) GetDashboardACLInfoList(ctx context.Context, query *GetDashboardACLInfoListQuery) ([]*DashboardACLInfoDTO, error) { ret := _m.Called(ctx, query) - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardACLInfoListQuery) error); ok { + var r0 []*DashboardACLInfoDTO + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardACLInfoListQuery) []*DashboardACLInfoDTO); ok { r0 = rf(ctx, query) } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*DashboardACLInfoDTO) + } } - return r0 + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *GetDashboardACLInfoListQuery) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } // GetDashboardTags provides a mock function with given fields: ctx, query -func (_m *FakeDashboardService) GetDashboardTags(ctx context.Context, query *GetDashboardTagsQuery) error { +func (_m *FakeDashboardService) GetDashboardTags(ctx context.Context, query *GetDashboardTagsQuery) ([]*DashboardTagCloudItem, error) { ret := _m.Called(ctx, query) - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardTagsQuery) error); ok { + var r0 []*DashboardTagCloudItem + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardTagsQuery) []*DashboardTagCloudItem); ok { r0 = rf(ctx, query) } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*DashboardTagCloudItem) + } } - return r0 + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *GetDashboardTagsQuery) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } // GetDashboardUIDByID provides a mock function with given fields: ctx, query -func (_m *FakeDashboardService) GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) error { +func (_m *FakeDashboardService) GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) (*DashboardRef, error) { ret := _m.Called(ctx, query) - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardRefByIDQuery) error); ok { + var r0 *DashboardRef + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardRefByIDQuery) *DashboardRef); ok { r0 = rf(ctx, query) } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).(*DashboardRef) + } } - return r0 + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *GetDashboardRefByIDQuery) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } // GetDashboards provides a mock function with given fields: ctx, query -func (_m *FakeDashboardService) GetDashboards(ctx context.Context, query *GetDashboardsQuery) error { +func (_m *FakeDashboardService) GetDashboards(ctx context.Context, query *GetDashboardsQuery) ([]*Dashboard, error) { ret := _m.Called(ctx, query) - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardsQuery) error); ok { + var r0 []*Dashboard + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardsQuery) []*Dashboard); ok { r0 = rf(ctx, query) } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*Dashboard) + } } - return r0 + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *GetDashboardsQuery) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } // HasAdminPermissionInDashboardsOrFolders provides a mock function with given fields: ctx, query diff --git a/pkg/services/dashboards/database/acl.go b/pkg/services/dashboards/database/acl.go index 6147eb22334..3e4185cea75 100644 --- a/pkg/services/dashboards/database/acl.go +++ b/pkg/services/dashboards/database/acl.go @@ -15,9 +15,9 @@ import ( // 1) Permissions for the dashboard // 2) permissions for its parent folder // 3) if no specific permissions have been set for the dashboard or its parent folder then get the default permissions -func (d *DashboardStore) GetDashboardACLInfoList(ctx context.Context, query *dashboards.GetDashboardACLInfoListQuery) error { +func (d *DashboardStore) GetDashboardACLInfoList(ctx context.Context, query *dashboards.GetDashboardACLInfoListQuery) ([]*dashboards.DashboardACLInfoDTO, error) { + queryResult := make([]*dashboards.DashboardACLInfoDTO, 0) outerErr := d.store.WithDbSession(ctx, func(dbSession *db.Session) error { - query.Result = make([]*dashboards.DashboardACLInfoDTO, 0) falseStr := d.store.GetDialect().BooleanStr(false) if query.DashboardID == 0 { @@ -41,7 +41,7 @@ func (d *DashboardStore) GetDashboardACLInfoList(ctx context.Context, query *das falseStr + ` AS inherited FROM dashboard_acl as da WHERE da.dashboard_id = -1` - return dbSession.SQL(sql).Find(&query.Result) + return dbSession.SQL(sql).Find(&queryResult) } rawSQL := ` @@ -83,18 +83,18 @@ func (d *DashboardStore) GetDashboardACLInfoList(ctx context.Context, query *das ORDER BY da.id ASC ` - return dbSession.SQL(rawSQL, query.OrgID, query.DashboardID).Find(&query.Result) + return dbSession.SQL(rawSQL, query.OrgID, query.DashboardID).Find(&queryResult) }) if outerErr != nil { - return outerErr + return nil, outerErr } - for _, p := range query.Result { + for _, p := range queryResult { p.PermissionName = p.Permission.String() } - return nil + return queryResult, nil } // HasEditPermissionInFolders validates that an user have access to a certain folder diff --git a/pkg/services/dashboards/database/acl_test.go b/pkg/services/dashboards/database/acl_test.go index d632170b0cf..dc505a5944e 100644 --- a/pkg/services/dashboards/database/acl_test.go +++ b/pkg/services/dashboards/database/acl_test.go @@ -54,34 +54,34 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { setup(t) query := dashboards.GetDashboardACLInfoListQuery{DashboardID: savedFolder.ID, OrgID: 1} - err := dashboardStore.GetDashboardACLInfoList(context.Background(), &query) + queryResult, err := dashboardStore.GetDashboardACLInfoList(context.Background(), &query) require.Nil(t, err) - require.Equal(t, 2, len(query.Result)) + require.Equal(t, 2, len(queryResult)) defaultPermissionsId := int64(-1) - require.Equal(t, defaultPermissionsId, query.Result[0].DashboardID) - require.Equal(t, org.RoleViewer, *query.Result[0].Role) - require.False(t, query.Result[0].Inherited) - require.Equal(t, defaultPermissionsId, query.Result[1].DashboardID) - require.Equal(t, org.RoleEditor, *query.Result[1].Role) - require.False(t, query.Result[1].Inherited) + require.Equal(t, defaultPermissionsId, queryResult[0].DashboardID) + require.Equal(t, org.RoleViewer, *queryResult[0].Role) + require.False(t, queryResult[0].Inherited) + require.Equal(t, defaultPermissionsId, queryResult[1].DashboardID) + require.Equal(t, org.RoleEditor, *queryResult[1].Role) + require.False(t, queryResult[1].Inherited) }) t.Run("Dashboard acl should include acl for parent folder", func(t *testing.T) { setup(t) query := dashboards.GetDashboardACLInfoListQuery{DashboardID: childDash.ID, OrgID: 1} - err := dashboardStore.GetDashboardACLInfoList(context.Background(), &query) + queryResult, err := dashboardStore.GetDashboardACLInfoList(context.Background(), &query) require.Nil(t, err) - require.Equal(t, 2, len(query.Result)) + require.Equal(t, 2, len(queryResult)) defaultPermissionsId := int64(-1) - require.Equal(t, defaultPermissionsId, query.Result[0].DashboardID) - require.Equal(t, org.RoleViewer, *query.Result[0].Role) - require.True(t, query.Result[0].Inherited) - require.Equal(t, defaultPermissionsId, query.Result[1].DashboardID) - require.Equal(t, org.RoleEditor, *query.Result[1].Role) - require.True(t, query.Result[1].Inherited) + require.Equal(t, defaultPermissionsId, queryResult[0].DashboardID) + require.Equal(t, org.RoleViewer, *queryResult[0].Role) + require.True(t, queryResult[0].Inherited) + require.Equal(t, defaultPermissionsId, queryResult[1].DashboardID) + require.Equal(t, org.RoleEditor, *queryResult[1].Role) + require.True(t, queryResult[1].Inherited) }) t.Run("Folder with removed default permissions returns no acl items", func(t *testing.T) { @@ -90,10 +90,10 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { require.Nil(t, err) query := dashboards.GetDashboardACLInfoListQuery{DashboardID: childDash.ID, OrgID: 1} - err = dashboardStore.GetDashboardACLInfoList(context.Background(), &query) + queryResult, err := dashboardStore.GetDashboardACLInfoList(context.Background(), &query) require.Nil(t, err) - require.Equal(t, 0, len(query.Result)) + require.Equal(t, 0, len(queryResult)) }) t.Run("Given a dashboard folder and a user", func(t *testing.T) { @@ -110,11 +110,11 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { t.Run("When reading dashboard acl should include acl for parent folder", func(t *testing.T) { query := dashboards.GetDashboardACLInfoListQuery{DashboardID: childDash.ID, OrgID: 1} - err := dashboardStore.GetDashboardACLInfoList(context.Background(), &query) + queryResult, err := dashboardStore.GetDashboardACLInfoList(context.Background(), &query) require.Nil(t, err) - require.Equal(t, 1, len(query.Result)) - require.Equal(t, savedFolder.ID, query.Result[0].DashboardID) + require.Equal(t, 1, len(queryResult)) + require.Equal(t, savedFolder.ID, queryResult[0].DashboardID) }) t.Run("Given child dashboard permission", func(t *testing.T) { @@ -129,14 +129,14 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { t.Run("When reading dashboard acl should include acl for parent folder and child", func(t *testing.T) { query := dashboards.GetDashboardACLInfoListQuery{OrgID: 1, DashboardID: childDash.ID} - err := dashboardStore.GetDashboardACLInfoList(context.Background(), &query) + queryResult, err := dashboardStore.GetDashboardACLInfoList(context.Background(), &query) require.Nil(t, err) - require.Equal(t, 2, len(query.Result)) - require.Equal(t, savedFolder.ID, query.Result[0].DashboardID) - require.True(t, query.Result[0].Inherited) - require.Equal(t, childDash.ID, query.Result[1].DashboardID) - require.False(t, query.Result[1].Inherited) + require.Equal(t, 2, len(queryResult)) + require.Equal(t, savedFolder.ID, queryResult[0].DashboardID) + require.True(t, queryResult[0].Inherited) + require.Equal(t, childDash.ID, queryResult[1].DashboardID) + require.False(t, queryResult[1].Inherited) }) }) }) @@ -153,19 +153,19 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { query := dashboards.GetDashboardACLInfoListQuery{OrgID: 1, DashboardID: childDash.ID} - err = dashboardStore.GetDashboardACLInfoList(context.Background(), &query) + queryResult, err := dashboardStore.GetDashboardACLInfoList(context.Background(), &query) require.Nil(t, err) defaultPermissionsId := int64(-1) - require.Equal(t, 3, len(query.Result)) - require.Equal(t, defaultPermissionsId, query.Result[0].DashboardID) - require.Equal(t, org.RoleViewer, *query.Result[0].Role) - require.True(t, query.Result[0].Inherited) - require.Equal(t, defaultPermissionsId, query.Result[1].DashboardID) - require.Equal(t, org.RoleEditor, *query.Result[1].Role) - require.True(t, query.Result[1].Inherited) - require.Equal(t, childDash.ID, query.Result[2].DashboardID) - require.False(t, query.Result[2].Inherited) + require.Equal(t, 3, len(queryResult)) + require.Equal(t, defaultPermissionsId, queryResult[0].DashboardID) + require.Equal(t, org.RoleViewer, *queryResult[0].Role) + require.True(t, queryResult[0].Inherited) + require.Equal(t, defaultPermissionsId, queryResult[1].DashboardID) + require.Equal(t, org.RoleEditor, *queryResult[1].Role) + require.True(t, queryResult[1].Inherited) + require.Equal(t, childDash.ID, queryResult[2].DashboardID) + require.False(t, queryResult[2].Inherited) }) t.Run("Add and delete dashboard permission", func(t *testing.T) { @@ -179,23 +179,23 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { require.Nil(t, err) q1 := &dashboards.GetDashboardACLInfoListQuery{DashboardID: savedFolder.ID, OrgID: 1} - err = dashboardStore.GetDashboardACLInfoList(context.Background(), q1) + q1Result, err := dashboardStore.GetDashboardACLInfoList(context.Background(), q1) require.Nil(t, err) - require.Equal(t, savedFolder.ID, q1.Result[0].DashboardID) - require.Equal(t, models.PERMISSION_EDIT, q1.Result[0].Permission) - require.Equal(t, "Edit", q1.Result[0].PermissionName) - require.Equal(t, currentUser.ID, q1.Result[0].UserID) - require.Equal(t, currentUser.Login, q1.Result[0].UserLogin) - require.Equal(t, currentUser.Email, q1.Result[0].UserEmail) + require.Equal(t, savedFolder.ID, q1Result[0].DashboardID) + require.Equal(t, models.PERMISSION_EDIT, q1Result[0].Permission) + require.Equal(t, "Edit", q1Result[0].PermissionName) + require.Equal(t, currentUser.ID, q1Result[0].UserID) + require.Equal(t, currentUser.Login, q1Result[0].UserLogin) + require.Equal(t, currentUser.Email, q1Result[0].UserEmail) err = updateDashboardACL(t, dashboardStore, savedFolder.ID) require.Nil(t, err) q3 := &dashboards.GetDashboardACLInfoListQuery{DashboardID: savedFolder.ID, OrgID: 1} - err = dashboardStore.GetDashboardACLInfoList(context.Background(), q3) + q3Result, err := dashboardStore.GetDashboardACLInfoList(context.Background(), q3) require.Nil(t, err) - require.Equal(t, 0, len(q3.Result)) + require.Equal(t, 0, len(q3Result)) }) t.Run("Should be able to add a user permission for a team", func(t *testing.T) { @@ -213,11 +213,11 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { require.Nil(t, err) q1 := &dashboards.GetDashboardACLInfoListQuery{DashboardID: savedFolder.ID, OrgID: 1} - err = dashboardStore.GetDashboardACLInfoList(context.Background(), q1) + q1Result, err := dashboardStore.GetDashboardACLInfoList(context.Background(), q1) require.Nil(t, err) - require.Equal(t, savedFolder.ID, q1.Result[0].DashboardID) - require.Equal(t, models.PERMISSION_EDIT, q1.Result[0].Permission) - require.Equal(t, team1.ID, q1.Result[0].TeamID) + require.Equal(t, savedFolder.ID, q1Result[0].DashboardID) + require.Equal(t, models.PERMISSION_EDIT, q1Result[0].Permission) + require.Equal(t, team1.ID, q1Result[0].TeamID) }) t.Run("Should be able to update an existing permission for a team", func(t *testing.T) { @@ -234,12 +234,12 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { require.Nil(t, err) q3 := &dashboards.GetDashboardACLInfoListQuery{DashboardID: savedFolder.ID, OrgID: 1} - err = dashboardStore.GetDashboardACLInfoList(context.Background(), q3) + q3Result, err := dashboardStore.GetDashboardACLInfoList(context.Background(), q3) require.Nil(t, err) - require.Equal(t, 1, len(q3.Result)) - require.Equal(t, savedFolder.ID, q3.Result[0].DashboardID) - require.Equal(t, models.PERMISSION_ADMIN, q3.Result[0].Permission) - require.Equal(t, team1.ID, q3.Result[0].TeamID) + require.Equal(t, 1, len(q3Result)) + require.Equal(t, savedFolder.ID, q3Result[0].DashboardID) + require.Equal(t, models.PERMISSION_ADMIN, q3Result[0].Permission) + require.Equal(t, team1.ID, q3Result[0].TeamID) }) }) @@ -250,17 +250,17 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { query := dashboards.GetDashboardACLInfoListQuery{DashboardID: rootFolderId, OrgID: 1} - err := dashboardStore.GetDashboardACLInfoList(context.Background(), &query) + queryResult, err := dashboardStore.GetDashboardACLInfoList(context.Background(), &query) require.Nil(t, err) - require.Equal(t, 2, len(query.Result)) + require.Equal(t, 2, len(queryResult)) defaultPermissionsId := int64(-1) - require.Equal(t, defaultPermissionsId, query.Result[0].DashboardID) - require.Equal(t, org.RoleViewer, *query.Result[0].Role) - require.False(t, query.Result[0].Inherited) - require.Equal(t, defaultPermissionsId, query.Result[1].DashboardID) - require.Equal(t, org.RoleEditor, *query.Result[1].Role) - require.False(t, query.Result[1].Inherited) + require.Equal(t, defaultPermissionsId, queryResult[0].DashboardID) + require.Equal(t, org.RoleViewer, *queryResult[0].Role) + require.False(t, queryResult[0].Inherited) + require.Equal(t, defaultPermissionsId, queryResult[1].DashboardID) + require.Equal(t, org.RoleEditor, *queryResult[1].Role) + require.False(t, queryResult[1].Inherited) }) t.Run("Delete acl by user", func(t *testing.T) { diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index ec5a46893fe..5190219f2ae 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -206,26 +206,37 @@ func (d *DashboardStore) GetProvisionedDashboardData(ctx context.Context, name s } func (d *DashboardStore) SaveProvisionedDashboard(ctx context.Context, cmd dashboards.SaveDashboardCommand, provisioning *dashboards.DashboardProvisioning) (*dashboards.Dashboard, error) { - err := d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error { - if err := saveDashboard(sess, &cmd, d.emitEntityEvent()); err != nil { + var result *dashboards.Dashboard + var err error + err = d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error { + result, err = saveDashboard(sess, &cmd, d.emitEntityEvent()) + if err != nil { return err } if provisioning.Updated == 0 { - provisioning.Updated = cmd.Result.Updated.Unix() + provisioning.Updated = result.Updated.Unix() } - return saveProvisionedData(sess, provisioning, cmd.Result) + return saveProvisionedData(sess, provisioning, result) }) - - return cmd.Result, err + return result, err } func (d *DashboardStore) SaveDashboard(ctx context.Context, cmd dashboards.SaveDashboardCommand) (*dashboards.Dashboard, error) { - err := d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error { - return saveDashboard(sess, &cmd, d.emitEntityEvent()) + var result *dashboards.Dashboard + var err error + err = d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error { + result, err = saveDashboard(sess, &cmd, d.emitEntityEvent()) + if err != nil { + return err + } + return nil }) - return cmd.Result, err + if err != nil { + return nil, err + } + return result, err } func (d *DashboardStore) UpdateDashboardACL(ctx context.Context, dashboardID int64, items []*dashboards.DashboardACL) error { @@ -476,7 +487,7 @@ func getExistingDashboardByTitleAndFolder(sess *db.Session, dash *dashboards.Das return isParentFolderChanged, nil } -func saveDashboard(sess *db.Session, cmd *dashboards.SaveDashboardCommand, emitEntityEvent bool) error { +func saveDashboard(sess *db.Session, cmd *dashboards.SaveDashboardCommand, emitEntityEvent bool) (*dashboards.Dashboard, error) { dash := cmd.GetDashboardModel() userId := cmd.UserID @@ -489,10 +500,10 @@ func saveDashboard(sess *db.Session, cmd *dashboards.SaveDashboardCommand, emitE var existing dashboards.Dashboard dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.ID, dash.OrgID).Get(&existing) if err != nil { - return err + return nil, err } if !dashWithIdExists { - return dashboards.ErrDashboardNotFound + return nil, dashboards.ErrDashboardNotFound } // check for is someone else has written in between @@ -500,20 +511,20 @@ func saveDashboard(sess *db.Session, cmd *dashboards.SaveDashboardCommand, emitE if cmd.Overwrite { dash.SetVersion(existing.Version) } else { - return dashboards.ErrDashboardVersionMismatch + return nil, dashboards.ErrDashboardVersionMismatch } } // do not allow plugin dashboard updates without overwrite flag if existing.PluginID != "" && !cmd.Overwrite { - return dashboards.UpdatePluginDashboardError{PluginId: existing.PluginID} + return nil, dashboards.UpdatePluginDashboardError{PluginId: existing.PluginID} } } if dash.UID == "" { uid, err := generateNewDashboardUid(sess, dash.OrgID) if err != nil { - return err + return nil, err } dash.SetUID(uid) } @@ -545,11 +556,11 @@ func saveDashboard(sess *db.Session, cmd *dashboards.SaveDashboardCommand, emitE } if err != nil { - return err + return nil, err } if affectedRows == 0 { - return dashboards.ErrDashboardNotFound + return nil, dashboards.ErrDashboardNotFound } dashVersion := &dashver.DashboardVersion{ @@ -565,14 +576,14 @@ func saveDashboard(sess *db.Session, cmd *dashboards.SaveDashboardCommand, emitE // insert version entry if affectedRows, err = sess.Insert(dashVersion); err != nil { - return err + return nil, err } else if affectedRows == 0 { - return dashboards.ErrDashboardNotFound + return nil, dashboards.ErrDashboardNotFound } // delete existing tags if _, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.ID); err != nil { - return err + return nil, err } // insert new tags @@ -580,20 +591,18 @@ func saveDashboard(sess *db.Session, cmd *dashboards.SaveDashboardCommand, emitE if len(tags) > 0 { for _, tag := range tags { if _, err := sess.Insert(DashboardTag{DashboardId: dash.ID, Term: tag}); err != nil { - return err + return nil, err } } } - cmd.Result = dash - if emitEntityEvent { _, err := sess.Insert(createEntityEvent(dash, store.EntityEventTypeUpdate)) if err != nil { - return err + return dash, err } } - return nil + return dash, nil } func generateNewDashboardUid(sess *db.Session, orgId int64) (string, error) { @@ -750,15 +759,18 @@ func (d *DashboardStore) deleteAlertByIdInternal(alertId int64, reason string, s return nil } -func (d *DashboardStore) GetDashboardsByPluginID(ctx context.Context, query *dashboards.GetDashboardsByPluginIDQuery) error { - return d.store.WithDbSession(ctx, func(dbSession *db.Session) error { - var dashboards = make([]*dashboards.Dashboard, 0) +func (d *DashboardStore) GetDashboardsByPluginID(ctx context.Context, query *dashboards.GetDashboardsByPluginIDQuery) ([]*dashboards.Dashboard, error) { + var dashboards = make([]*dashboards.Dashboard, 0) + err := d.store.WithDbSession(ctx, func(dbSession *db.Session) error { whereExpr := "org_id=? AND plugin_id=? AND is_folder=" + d.store.GetDialect().BooleanStr(false) err := dbSession.Where(whereExpr, query.OrgID, query.PluginID).Find(&dashboards) - query.Result = dashboards return err }) + if err != nil { + return nil, err + } + return dashboards, nil } func (d *DashboardStore) DeleteDashboard(ctx context.Context, cmd *dashboards.DeleteDashboardCommand) error { @@ -924,6 +936,7 @@ func (d *DashboardStore) deleteAlertDefinition(dashboardId int64, sess *db.Sessi } func (d *DashboardStore) GetDashboard(ctx context.Context, query *dashboards.GetDashboardQuery) (*dashboards.Dashboard, error) { + var queryResult *dashboards.Dashboard err := d.store.WithDbSession(ctx, func(sess *db.Session) error { if query.ID == 0 && len(query.Slug) == 0 && len(query.UID) == 0 { return dashboards.ErrDashboardIdentifierNotSet @@ -940,35 +953,37 @@ func (d *DashboardStore) GetDashboard(ctx context.Context, query *dashboards.Get dashboard.SetID(dashboard.ID) dashboard.SetUID(dashboard.UID) - query.Result = &dashboard + queryResult = &dashboard return nil }) - return query.Result, err + return queryResult, err } -func (d *DashboardStore) GetDashboardUIDByID(ctx context.Context, query *dashboards.GetDashboardRefByIDQuery) error { - return d.store.WithDbSession(ctx, func(sess *db.Session) error { +func (d *DashboardStore) GetDashboardUIDByID(ctx context.Context, query *dashboards.GetDashboardRefByIDQuery) (*dashboards.DashboardRef, error) { + us := &dashboards.DashboardRef{} + err := d.store.WithDbSession(ctx, func(sess *db.Session) error { var rawSQL = `SELECT uid, slug from dashboard WHERE Id=?` - us := &dashboards.DashboardRef{} exists, err := sess.SQL(rawSQL, query.ID).Get(us) if err != nil { return err } else if !exists { return dashboards.ErrDashboardNotFound } - query.Result = us return nil }) + if err != nil { + return nil, err + } + return us, nil } -func (d *DashboardStore) GetDashboards(ctx context.Context, query *dashboards.GetDashboardsQuery) error { - return d.store.WithDbSession(ctx, func(sess *db.Session) error { +func (d *DashboardStore) GetDashboards(ctx context.Context, query *dashboards.GetDashboardsQuery) ([]*dashboards.Dashboard, error) { + var dashboards = make([]*dashboards.Dashboard, 0) + err := d.store.WithDbSession(ctx, func(sess *db.Session) error { if len(query.DashboardIDs) == 0 && len(query.DashboardUIDs) == 0 { return star.ErrCommandValidationFailed } - - var dashboards = make([]*dashboards.Dashboard, 0) var session *xorm.Session if len(query.DashboardIDs) > 0 { session = sess.In("id", query.DashboardIDs) @@ -980,9 +995,12 @@ func (d *DashboardStore) GetDashboards(ctx context.Context, query *dashboards.Ge } err := session.Find(&dashboards) - query.Result = dashboards return err }) + if err != nil { + return nil, err + } + return dashboards, nil } func (d *DashboardStore) FindDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) { @@ -1067,8 +1085,9 @@ func (d *DashboardStore) FindDashboards(ctx context.Context, query *models.FindP return res, nil } -func (d *DashboardStore) GetDashboardTags(ctx context.Context, query *dashboards.GetDashboardTagsQuery) error { - return d.store.WithDbSession(ctx, func(dbSession *db.Session) error { +func (d *DashboardStore) GetDashboardTags(ctx context.Context, query *dashboards.GetDashboardTagsQuery) ([]*dashboards.DashboardTagCloudItem, error) { + queryResult := make([]*dashboards.DashboardTagCloudItem, 0) + err := d.store.WithDbSession(ctx, func(dbSession *db.Session) error { sql := `SELECT COUNT(*) as count, term @@ -1078,11 +1097,14 @@ func (d *DashboardStore) GetDashboardTags(ctx context.Context, query *dashboards GROUP BY term ORDER BY term` - query.Result = make([]*dashboards.DashboardTagCloudItem, 0) sess := dbSession.SQL(sql, query.OrgID) - err := sess.Find(&query.Result) + err := sess.Find(&queryResult) return err }) + if err != nil { + return nil, err + } + return queryResult, nil } // CountDashboardsInFolder returns a count of all dashboards associated with the diff --git a/pkg/services/dashboards/database/database_provisioning_test.go b/pkg/services/dashboards/database/database_provisioning_test.go index fecc9cc74b9..d4cf0e47f74 100644 --- a/pkg/services/dashboards/database/database_provisioning_test.go +++ b/pkg/services/dashboards/database/database_provisioning_test.go @@ -81,19 +81,19 @@ func TestIntegrationDashboardProvisioningTest(t *testing.T) { require.Nil(t, err) query := &dashboards.GetDashboardsQuery{DashboardIDs: []int64{anotherDash.ID}} - err = dashboardStore.GetDashboards(context.Background(), query) + queryResult, err := dashboardStore.GetDashboards(context.Background(), query) require.Nil(t, err) - require.NotNil(t, query.Result) + require.NotNil(t, queryResult) deleteCmd := &dashboards.DeleteOrphanedProvisionedDashboardsCommand{ReaderNames: []string{"default"}} require.Nil(t, dashboardStore.DeleteOrphanedProvisionedDashboards(context.Background(), deleteCmd)) query = &dashboards.GetDashboardsQuery{DashboardIDs: []int64{dash.ID, anotherDash.ID}} - err = dashboardStore.GetDashboards(context.Background(), query) + queryResult, err = dashboardStore.GetDashboards(context.Background(), query) require.Nil(t, err) - require.Equal(t, 1, len(query.Result)) - require.Equal(t, dashId, query.Result[0].ID) + require.Equal(t, 1, len(queryResult)) + require.Equal(t, dashId, queryResult[0].ID) }) t.Run("Can query for provisioned dashboards", func(t *testing.T) { diff --git a/pkg/services/dashboards/database/database_test.go b/pkg/services/dashboards/database/database_test.go index 8ca8b5b6010..18b03364180 100644 --- a/pkg/services/dashboards/database/database_test.go +++ b/pkg/services/dashboards/database/database_test.go @@ -80,14 +80,14 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { OrgID: 1, } - _, err := dashboardStore.GetDashboard(context.Background(), &query) + queryResult, err := dashboardStore.GetDashboard(context.Background(), &query) require.NoError(t, err) - require.Equal(t, query.Result.Title, "test dash 23") - require.Equal(t, query.Result.Slug, "test-dash-23") - require.Equal(t, query.Result.ID, savedDash.ID) - require.Equal(t, query.Result.UID, savedDash.UID) - require.False(t, query.Result.IsFolder) + require.Equal(t, queryResult.Title, "test dash 23") + require.Equal(t, queryResult.Slug, "test-dash-23") + require.Equal(t, queryResult.ID, savedDash.ID) + require.Equal(t, queryResult.UID, savedDash.UID) + require.False(t, queryResult.IsFolder) }) t.Run("Should be able to get dashboard by slug", func(t *testing.T) { @@ -97,14 +97,14 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { OrgID: 1, } - _, err := dashboardStore.GetDashboard(context.Background(), &query) + queryResult, err := dashboardStore.GetDashboard(context.Background(), &query) require.NoError(t, err) - require.Equal(t, query.Result.Title, "test dash 23") - require.Equal(t, query.Result.Slug, "test-dash-23") - require.Equal(t, query.Result.ID, savedDash.ID) - require.Equal(t, query.Result.UID, savedDash.UID) - require.False(t, query.Result.IsFolder) + require.Equal(t, queryResult.Title, "test dash 23") + require.Equal(t, queryResult.Slug, "test-dash-23") + require.Equal(t, queryResult.ID, savedDash.ID) + require.Equal(t, queryResult.UID, savedDash.UID) + require.False(t, queryResult.IsFolder) }) t.Run("Should be able to get dashboard by uid", func(t *testing.T) { @@ -114,22 +114,22 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { OrgID: 1, } - _, err := dashboardStore.GetDashboard(context.Background(), &query) + queryResult, err := dashboardStore.GetDashboard(context.Background(), &query) require.NoError(t, err) - require.Equal(t, query.Result.Title, "test dash 23") - require.Equal(t, query.Result.Slug, "test-dash-23") - require.Equal(t, query.Result.ID, savedDash.ID) - require.Equal(t, query.Result.UID, savedDash.UID) - require.False(t, query.Result.IsFolder) + require.Equal(t, queryResult.Title, "test dash 23") + require.Equal(t, queryResult.Slug, "test-dash-23") + require.Equal(t, queryResult.ID, savedDash.ID) + require.Equal(t, queryResult.UID, savedDash.UID) + require.False(t, queryResult.IsFolder) }) t.Run("Should be able to get a dashboard UID by ID", func(t *testing.T) { setup() query := dashboards.GetDashboardRefByIDQuery{ID: savedDash.ID} - err := dashboardStore.GetDashboardUIDByID(context.Background(), &query) + queryResult, err := dashboardStore.GetDashboardUIDByID(context.Background(), &query) require.NoError(t, err) - require.Equal(t, query.Result.UID, savedDash.UID) + require.Equal(t, queryResult.UID, savedDash.UID) }) t.Run("Shouldn't be able to get a dashboard with just an OrgID", func(t *testing.T) { @@ -145,14 +145,14 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { t.Run("Should be able to get dashboards by IDs & UIDs", func(t *testing.T) { setup() query := dashboards.GetDashboardsQuery{DashboardIDs: []int64{savedDash.ID, savedDash2.ID}} - err := dashboardStore.GetDashboards(context.Background(), &query) + queryResult, err := dashboardStore.GetDashboards(context.Background(), &query) require.NoError(t, err) - assert.Equal(t, len(query.Result), 2) + assert.Equal(t, len(queryResult), 2) query = dashboards.GetDashboardsQuery{DashboardUIDs: []string{savedDash.UID, savedDash2.UID}} - err = dashboardStore.GetDashboards(context.Background(), &query) + queryResult, err = dashboardStore.GetDashboards(context.Background(), &query) require.NoError(t, err) - assert.Equal(t, len(query.Result), 2) + assert.Equal(t, len(queryResult), 2) }) t.Run("Should be able to delete dashboard", func(t *testing.T) { @@ -220,13 +220,13 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { OrgID: 1, } - _, err = dashboardStore.GetDashboard(context.Background(), &query) + queryResult, err := dashboardStore.GetDashboard(context.Background(), &query) require.NoError(t, err) - require.Equal(t, query.Result.FolderID, int64(0)) - require.Equal(t, query.Result.CreatedBy, savedDash.CreatedBy) - require.WithinDuration(t, query.Result.Created, savedDash.Created, 3*time.Second) - require.Equal(t, query.Result.UpdatedBy, int64(100)) - require.False(t, query.Result.Updated.IsZero()) + require.Equal(t, queryResult.FolderID, int64(0)) + require.Equal(t, queryResult.CreatedBy, savedDash.CreatedBy) + require.WithinDuration(t, queryResult.Created, savedDash.Created, 3*time.Second) + require.Equal(t, queryResult.UpdatedBy, int64(100)) + require.False(t, queryResult.Updated.IsZero()) }) t.Run("Should be able to delete empty folder", func(t *testing.T) { @@ -308,9 +308,9 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { query := dashboards.GetDashboardsQuery{ DashboardIDs: []int64{savedFolder.ID, savedDash.ID}, } - err = dashboardStore.GetDashboards(context.Background(), &query) + queryResult, err := dashboardStore.GetDashboards(context.Background(), &query) require.NoError(t, err) - require.Equal(t, len(query.Result), 0) + require.Equal(t, len(queryResult), 0) pubdashConfig, err = publicDashboardStore.FindByAccessToken(context.Background(), "an-access-token") require.Nil(t, err) @@ -382,10 +382,10 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { setup() query := dashboards.GetDashboardTagsQuery{OrgID: 1} - err := dashboardStore.GetDashboardTags(context.Background(), &query) + queryResult, err := dashboardStore.GetDashboardTags(context.Background(), &query) require.NoError(t, err) - require.Equal(t, len(query.Result), 2) + require.Equal(t, len(queryResult), 2) }) t.Run("Should be able to find dashboard folder", func(t *testing.T) { @@ -603,9 +603,9 @@ func TestIntegrationDashboardDataAccessGivenPluginWithImportedDashboards(t *test OrgID: 1, } - err = dashboardStore.GetDashboardsByPluginID(context.Background(), &query) + queryResult, err := dashboardStore.GetDashboardsByPluginID(context.Background(), &query) require.NoError(t, err) - require.Equal(t, len(query.Result), 2) + require.Equal(t, len(queryResult), 2) } func TestIntegrationDashboard_SortingOptions(t *testing.T) { diff --git a/pkg/services/dashboards/models.go b/pkg/services/dashboards/models.go index 5d67a74a926..8bc4c78dc05 100644 --- a/pkg/services/dashboards/models.go +++ b/pkg/services/dashboards/models.go @@ -198,8 +198,6 @@ type SaveDashboardCommand struct { IsFolder bool `json:"isFolder"` UpdatedAt time.Time - - Result *Dashboard `json:"-"` } type ValidateDashboardCommand struct { @@ -209,7 +207,6 @@ type ValidateDashboardCommand struct { type TrimDashboardCommand struct { Dashboard *simplejson.Json `json:"dashboard" binding:"Required"` Meta *simplejson.Json `json:"meta"` - Result *Dashboard `json:"-"` } type DashboardProvisioning struct { @@ -240,8 +237,6 @@ type GetDashboardQuery struct { ID int64 // optional if slug is set UID string // optional if slug is set OrgID int64 - - Result *Dashboard } type DashboardTagCloudItem struct { @@ -250,33 +245,18 @@ type DashboardTagCloudItem struct { } type GetDashboardTagsQuery struct { - OrgID int64 - Result []*DashboardTagCloudItem + OrgID int64 } type GetDashboardsQuery struct { DashboardIDs []int64 DashboardUIDs []string OrgID int64 - Result []*Dashboard } type GetDashboardsByPluginIDQuery struct { OrgID int64 PluginID string - Result []*Dashboard -} - -type GetDashboardSlugByIdQuery struct { - ID int64 - Result string -} - -type GetDashboardsBySlugQuery struct { - OrgID int64 - Slug string - - Result []*Dashboard } type DashboardRef struct { @@ -285,8 +265,7 @@ type DashboardRef struct { } type GetDashboardRefByIDQuery struct { - ID int64 - Result *DashboardRef + ID int64 } type SaveDashboardDTO struct { @@ -418,5 +397,4 @@ func (dto *DashboardACLInfoDTO) IsDuplicateOf(other *DashboardACLInfoDTO) bool { type GetDashboardACLInfoListQuery struct { DashboardID int64 OrgID int64 - Result []*DashboardACLInfoDTO } diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 5c5c8d9612c..01823757044 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -483,7 +483,7 @@ func (dr *DashboardServiceImpl) UnprovisionDashboard(ctx context.Context, dashbo return dr.dashboardStore.UnprovisionDashboard(ctx, dashboardId) } -func (dr *DashboardServiceImpl) GetDashboardsByPluginID(ctx context.Context, query *dashboards.GetDashboardsByPluginIDQuery) error { +func (dr *DashboardServiceImpl) GetDashboardsByPluginID(ctx context.Context, query *dashboards.GetDashboardsByPluginIDQuery) ([]*dashboards.Dashboard, error) { return dr.dashboardStore.GetDashboardsByPluginID(ctx, query) } @@ -522,16 +522,15 @@ func (dr *DashboardServiceImpl) setDefaultPermissions(ctx context.Context, dto * return nil } -func (dr *DashboardServiceImpl) GetDashboard(ctx context.Context, query *dashboards.GetDashboardQuery) error { - _, err := dr.dashboardStore.GetDashboard(ctx, query) - return err +func (dr *DashboardServiceImpl) GetDashboard(ctx context.Context, query *dashboards.GetDashboardQuery) (*dashboards.Dashboard, error) { + return dr.dashboardStore.GetDashboard(ctx, query) } -func (dr *DashboardServiceImpl) GetDashboardUIDByID(ctx context.Context, query *dashboards.GetDashboardRefByIDQuery) error { +func (dr *DashboardServiceImpl) GetDashboardUIDByID(ctx context.Context, query *dashboards.GetDashboardRefByIDQuery) (*dashboards.DashboardRef, error) { return dr.dashboardStore.GetDashboardUIDByID(ctx, query) } -func (dr *DashboardServiceImpl) GetDashboards(ctx context.Context, query *dashboards.GetDashboardsQuery) error { +func (dr *DashboardServiceImpl) GetDashboards(ctx context.Context, query *dashboards.GetDashboardsQuery) ([]*dashboards.Dashboard, error) { return dr.dashboardStore.GetDashboards(ctx, query) } @@ -599,7 +598,7 @@ func makeQueryResult(query *models.FindPersistedDashboardsQuery, res []dashboard } } -func (dr *DashboardServiceImpl) GetDashboardACLInfoList(ctx context.Context, query *dashboards.GetDashboardACLInfoListQuery) error { +func (dr *DashboardServiceImpl) GetDashboardACLInfoList(ctx context.Context, query *dashboards.GetDashboardACLInfoListQuery) ([]*dashboards.DashboardACLInfoDTO, error) { return dr.dashboardStore.GetDashboardACLInfoList(ctx, query) } @@ -611,7 +610,7 @@ func (dr *DashboardServiceImpl) HasEditPermissionInFolders(ctx context.Context, return dr.dashboardStore.HasEditPermissionInFolders(ctx, query) } -func (dr *DashboardServiceImpl) GetDashboardTags(ctx context.Context, query *dashboards.GetDashboardTagsQuery) error { +func (dr *DashboardServiceImpl) GetDashboardTags(ctx context.Context, query *dashboards.GetDashboardTagsQuery) ([]*dashboards.DashboardTagCloudItem, error) { return dr.dashboardStore.GetDashboardTags(ctx, query) } diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index 3617e62debe..2f3b80a0be4 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -260,22 +260,22 @@ func TestDashboardService(t *testing.T) { t.Run("When org user is deleted", func(t *testing.T) { fakeStore := dashboards.FakeDashboardStore{} - fakeStore.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(nil) + fakeStore.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(nil, nil) t.Run("Should remove dependent permissions for deleted org user", func(t *testing.T) { - permQuery := &dashboards.GetDashboardACLInfoListQuery{DashboardID: 1, OrgID: 1, Result: nil} + permQuery := &dashboards.GetDashboardACLInfoListQuery{DashboardID: 1, OrgID: 1} - err := fakeStore.GetDashboardACLInfoList(context.Background(), permQuery) + permQueryResult, err := fakeStore.GetDashboardACLInfoList(context.Background(), permQuery) require.NoError(t, err) - require.Equal(t, len(permQuery.Result), 0) + require.Equal(t, len(permQueryResult), 0) }) t.Run("Should not remove dashboard permissions for same user in another org", func(t *testing.T) { fakeStore := dashboards.FakeDashboardStore{} - fakeStore.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(nil) + fakeStore.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(nil, nil) permQuery := &dashboards.GetDashboardACLInfoListQuery{DashboardID: 2, OrgID: 3} - err := fakeStore.GetDashboardACLInfoList(context.Background(), permQuery) + _, err := fakeStore.GetDashboardACLInfoList(context.Background(), permQuery) require.NoError(t, err) }) }) diff --git a/pkg/services/dashboards/store_mock.go b/pkg/services/dashboards/store_mock.go index 5abfe907689..d9dce524db4 100644 --- a/pkg/services/dashboards/store_mock.go +++ b/pkg/services/dashboards/store_mock.go @@ -153,73 +153,118 @@ func (_m *FakeDashboardStore) GetDashboard(ctx context.Context, query *GetDashbo } // GetDashboardACLInfoList provides a mock function with given fields: ctx, query -func (_m *FakeDashboardStore) GetDashboardACLInfoList(ctx context.Context, query *GetDashboardACLInfoListQuery) error { +func (_m *FakeDashboardStore) GetDashboardACLInfoList(ctx context.Context, query *GetDashboardACLInfoListQuery) ([]*DashboardACLInfoDTO, error) { ret := _m.Called(ctx, query) - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardACLInfoListQuery) error); ok { + var r0 []*DashboardACLInfoDTO + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardACLInfoListQuery) []*DashboardACLInfoDTO); ok { r0 = rf(ctx, query) } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*DashboardACLInfoDTO) + } } - return r0 + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *GetDashboardACLInfoListQuery) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } // GetDashboardTags provides a mock function with given fields: ctx, query -func (_m *FakeDashboardStore) GetDashboardTags(ctx context.Context, query *GetDashboardTagsQuery) error { +func (_m *FakeDashboardStore) GetDashboardTags(ctx context.Context, query *GetDashboardTagsQuery) ([]*DashboardTagCloudItem, error) { ret := _m.Called(ctx, query) - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardTagsQuery) error); ok { + var r0 []*DashboardTagCloudItem + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardTagsQuery) []*DashboardTagCloudItem); ok { r0 = rf(ctx, query) } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*DashboardTagCloudItem) + } } - return r0 + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *GetDashboardTagsQuery) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } // GetDashboardUIDByID provides a mock function with given fields: ctx, query -func (_m *FakeDashboardStore) GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) error { +func (_m *FakeDashboardStore) GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) (*DashboardRef, error) { ret := _m.Called(ctx, query) - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardRefByIDQuery) error); ok { + var r0 *DashboardRef + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardRefByIDQuery) *DashboardRef); ok { r0 = rf(ctx, query) } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).(*DashboardRef) + } } - return r0 + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *GetDashboardRefByIDQuery) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } // GetDashboards provides a mock function with given fields: ctx, query -func (_m *FakeDashboardStore) GetDashboards(ctx context.Context, query *GetDashboardsQuery) error { +func (_m *FakeDashboardStore) GetDashboards(ctx context.Context, query *GetDashboardsQuery) ([]*Dashboard, error) { ret := _m.Called(ctx, query) - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardsQuery) error); ok { + var r0 []*Dashboard + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardsQuery) []*Dashboard); ok { r0 = rf(ctx, query) } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*Dashboard) + } } - return r0 + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *GetDashboardsQuery) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } // GetDashboardsByPluginID provides a mock function with given fields: ctx, query -func (_m *FakeDashboardStore) GetDashboardsByPluginID(ctx context.Context, query *GetDashboardsByPluginIDQuery) error { +func (_m *FakeDashboardStore) GetDashboardsByPluginID(ctx context.Context, query *GetDashboardsByPluginIDQuery) ([]*Dashboard, error) { ret := _m.Called(ctx, query) - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardsByPluginIDQuery) error); ok { + var r0 []*Dashboard + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardsByPluginIDQuery) []*Dashboard); ok { r0 = rf(ctx, query) } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*Dashboard) + } } - return r0 + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *GetDashboardsByPluginIDQuery) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } // GetProvisionedDashboardData provides a mock function with given fields: ctx, name diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index f84e8f7565e..65bb786db1a 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -408,12 +408,12 @@ func (s *Service) legacyUpdate(ctx context.Context, cmd *folder.UpdateFolderComm logger := s.log.FromContext(ctx) query := dashboards.GetDashboardQuery{OrgID: cmd.OrgID, UID: cmd.UID} - _, err := s.dashboardStore.GetDashboard(ctx, &query) + queryResult, err := s.dashboardStore.GetDashboard(ctx, &query) if err != nil { return nil, toFolderError(err) } - dashFolder := query.Result + dashFolder := queryResult currentTitle := dashFolder.Title if !dashFolder.IsFolder { diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index b8497f26d0c..948178d3ace 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -126,11 +126,9 @@ func TestIntegrationFolderService(t *testing.T) { title := "Folder-TEST" t.Run("When updating folder should return access denied error", func(t *testing.T) { - dashStore.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { - folder := args.Get(1).(*dashboards.GetDashboardQuery) - folder.Result = dashboards.NewDashboard("dashboard-test") - folder.Result.IsFolder = true - }).Return(&dashboards.Dashboard{}, nil) + folderResult := dashboards.NewDashboard("dashboard-test") + folderResult.IsFolder = true + dashStore.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(folderResult, nil) _, err := service.Update(context.Background(), &folder.UpdateFolderCommand{ UID: folderUID, OrgID: orgID, diff --git a/pkg/services/guardian/accesscontrol_guardian.go b/pkg/services/guardian/accesscontrol_guardian.go index 1ed6bdc35b9..b2d7bad4100 100644 --- a/pkg/services/guardian/accesscontrol_guardian.go +++ b/pkg/services/guardian/accesscontrol_guardian.go @@ -37,13 +37,14 @@ func NewAccessControlDashboardGuardian( OrgID: user.OrgID, } - if err := dashboardService.GetDashboard(ctx, q); err != nil { + qResult, err := dashboardService.GetDashboard(ctx, q) + if err != nil { if errors.Is(err, dashboards.ErrDashboardNotFound) { return nil, ErrGuardianDashboardNotFound.Errorf("failed to get dashboard by UID: %w", err) } return nil, ErrGuardianGetDashboardFailure.Errorf("failed to get dashboard by UID: %w", err) } - dashboard = q.Result + dashboard = qResult } return &AccessControlDashboardGuardian{ @@ -74,13 +75,14 @@ func NewAccessControlDashboardGuardianByUID( OrgID: user.OrgID, } - if err := dashboardService.GetDashboard(ctx, q); err != nil { + qResult, err := dashboardService.GetDashboard(ctx, q) + if err != nil { if errors.Is(err, dashboards.ErrDashboardNotFound) { return nil, ErrGuardianDashboardNotFound.Errorf("failed to get dashboard by UID: %w", err) } return nil, ErrGuardianGetDashboardFailure.Errorf("failed to get dashboard by UID: %w", err) } - dashboard = q.Result + dashboard = qResult } return &AccessControlDashboardGuardian{ @@ -337,8 +339,9 @@ func (a *AccessControlDashboardGuardian) loadParentFolder(folderID int64) (*dash return &dashboards.Dashboard{UID: accesscontrol.GeneralFolderUID}, nil } folderQuery := &dashboards.GetDashboardQuery{ID: folderID, OrgID: a.user.OrgID} - if err := a.dashboardService.GetDashboard(a.ctx, folderQuery); err != nil { + folderQueryResult, err := a.dashboardService.GetDashboard(a.ctx, folderQuery) + if err != nil { return nil, err } - return folderQuery.Result, nil + return folderQueryResult, nil } diff --git a/pkg/services/guardian/accesscontrol_guardian_test.go b/pkg/services/guardian/accesscontrol_guardian_test.go index 319ce4d26b7..748164cbe04 100644 --- a/pkg/services/guardian/accesscontrol_guardian_test.go +++ b/pkg/services/guardian/accesscontrol_guardian_test.go @@ -616,30 +616,31 @@ func setupAccessControlGuardianTest(t *testing.T, uid string, permissions []acce require.NoError(t, err) if dashboardSvc == nil { fakeDashboardService := dashboards.NewFakeDashboardService(t) + qResult := &dashboards.Dashboard{} fakeDashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { q := args.Get(1).(*dashboards.GetDashboardQuery) - q.Result = &dashboards.Dashboard{ + qResult = &dashboards.Dashboard{ ID: q.ID, UID: q.UID, OrgID: q.OrgID, } - }).Return(nil) + }).Return(qResult, nil) dashboardSvc = fakeDashboardService } g, err := NewAccessControlDashboardGuardian(context.Background(), dash.ID, &user.SignedInUser{OrgID: 1}, store, ac, folderPermissions, dashboardPermissions, dashboardSvc) require.NoError(t, err) + g.dashboard = dash return g, dash } func testDashSvc(t *testing.T) dashboards.DashboardService { dashSvc := dashboards.NewFakeDashboardService(t) + var d *dashboards.Dashboard dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardQuery) d := dashboards.NewDashboard("mocked") d.ID = 1 d.UID = "1" - q.Result = d - }).Return(nil) + }).Return(d, nil) return dashSvc } diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index f33c124f654..e64a1f7a763 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -81,7 +81,7 @@ func newDashboardGuardian(ctx context.Context, dashId int64, orgId int64, user * OrgID: orgId, } - if err := dashSvc.GetDashboard(ctx, q); err != nil { + if _, err := dashSvc.GetDashboard(ctx, q); err != nil { if errors.Is(err, dashboards.ErrDashboardNotFound) { return nil, ErrGuardianDashboardNotFound.Errorf("failed to get dashboard by UID: %w", err) } @@ -110,13 +110,14 @@ func newDashboardGuardianByUID(ctx context.Context, dashUID string, orgId int64, OrgID: orgId, } - if err := dashSvc.GetDashboard(ctx, q); err != nil { + qResult, err := dashSvc.GetDashboard(ctx, q) + if err != nil { if errors.Is(err, dashboards.ErrDashboardNotFound) { return nil, ErrGuardianDashboardNotFound.Errorf("failed to get dashboard by UID: %w", err) } return nil, ErrGuardianGetDashboardFailure.Errorf("failed to get dashboard by UID: %w", err) } - dashID = q.Result.ID + dashID = qResult.ID } return &dashboardGuardianImpl{ @@ -306,10 +307,11 @@ func (g *dashboardGuardianImpl) GetACL() ([]*dashboards.DashboardACLInfoDTO, err } query := dashboards.GetDashboardACLInfoListQuery{DashboardID: g.dashId, OrgID: g.orgId} - if err := g.dashboardService.GetDashboardACLInfoList(g.ctx, &query); err != nil { + queryResult, err := g.dashboardService.GetDashboardACLInfoList(g.ctx, &query) + if err != nil { return nil, err } - g.acl = query.Result + g.acl = queryResult return g.acl, nil } diff --git a/pkg/services/guardian/guardian_test.go b/pkg/services/guardian/guardian_test.go index d10611094c1..7da22aa0c6f 100644 --- a/pkg/services/guardian/guardian_test.go +++ b/pkg/services/guardian/guardian_test.go @@ -690,22 +690,21 @@ func TestGuardianGetHiddenACL(t *testing.T) { t.Run("Get hidden ACL tests", func(t *testing.T) { store := dbtest.NewFakeDB() dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardACLInfoListQuery) - q.Result = []*dashboards.DashboardACLInfoDTO{ - {Inherited: false, UserID: 1, UserLogin: "user1", Permission: models.PERMISSION_EDIT}, - {Inherited: false, UserID: 2, UserLogin: "user2", Permission: models.PERMISSION_ADMIN}, - {Inherited: true, UserID: 3, UserLogin: "user3", Permission: models.PERMISSION_VIEW}, - } - }).Return(nil) + qResult := []*dashboards.DashboardACLInfoDTO{ + {Inherited: false, UserID: 1, UserLogin: "user1", Permission: models.PERMISSION_EDIT}, + {Inherited: false, UserID: 2, UserLogin: "user2", Permission: models.PERMISSION_ADMIN}, + {Inherited: true, UserID: 3, UserLogin: "user3", Permission: models.PERMISSION_VIEW}, + } + dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult, nil) + var qResultDash *dashboards.Dashboard dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { q := args.Get(1).(*dashboards.GetDashboardQuery) - q.Result = &dashboards.Dashboard{ + qResultDash = &dashboards.Dashboard{ ID: q.ID, UID: q.UID, OrgID: q.OrgID, } - }).Return(nil) + }).Return(qResultDash, nil) cfg := setting.NewCfg() cfg.HiddenUsers = map[string]struct{}{"user2": {}} @@ -734,13 +733,9 @@ func TestGuardianGetHiddenACL(t *testing.T) { IsGrafanaAdmin: true, } dashSvc := dashboards.NewFakeDashboardService(t) + qResult := &dashboards.Dashboard{} dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardQuery) - q.Result = &dashboards.Dashboard{ - ID: q.ID, - UID: q.UID, - } - }).Return(nil) + }).Return(qResult, nil) g, err := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store, dashSvc, &teamtest.FakeService{}) require.NoError(t, err) @@ -756,27 +751,26 @@ func TestGuardianGetACLWithoutDuplicates(t *testing.T) { t.Run("Get hidden ACL tests", func(t *testing.T) { store := dbtest.NewFakeDB() dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardACLInfoListQuery) - q.Result = []*dashboards.DashboardACLInfoDTO{ - {Inherited: true, UserID: 3, UserLogin: "user3", Permission: models.PERMISSION_EDIT}, - {Inherited: false, UserID: 3, UserLogin: "user3", Permission: models.PERMISSION_VIEW}, - {Inherited: false, UserID: 2, UserLogin: "user2", Permission: models.PERMISSION_ADMIN}, - {Inherited: true, UserID: 4, UserLogin: "user4", Permission: models.PERMISSION_ADMIN}, - {Inherited: false, UserID: 4, UserLogin: "user4", Permission: models.PERMISSION_ADMIN}, - {Inherited: false, UserID: 5, UserLogin: "user5", Permission: models.PERMISSION_EDIT}, - {Inherited: true, UserID: 6, UserLogin: "user6", Permission: models.PERMISSION_VIEW}, - {Inherited: false, UserID: 6, UserLogin: "user6", Permission: models.PERMISSION_EDIT}, - } - }).Return(nil) + qResult := []*dashboards.DashboardACLInfoDTO{ + {Inherited: true, UserID: 3, UserLogin: "user3", Permission: models.PERMISSION_EDIT}, + {Inherited: false, UserID: 3, UserLogin: "user3", Permission: models.PERMISSION_VIEW}, + {Inherited: false, UserID: 2, UserLogin: "user2", Permission: models.PERMISSION_ADMIN}, + {Inherited: true, UserID: 4, UserLogin: "user4", Permission: models.PERMISSION_ADMIN}, + {Inherited: false, UserID: 4, UserLogin: "user4", Permission: models.PERMISSION_ADMIN}, + {Inherited: false, UserID: 5, UserLogin: "user5", Permission: models.PERMISSION_EDIT}, + {Inherited: true, UserID: 6, UserLogin: "user6", Permission: models.PERMISSION_VIEW}, + {Inherited: false, UserID: 6, UserLogin: "user6", Permission: models.PERMISSION_EDIT}, + } + dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult, nil) + qResultDash := &dashboards.Dashboard{} dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { q := args.Get(1).(*dashboards.GetDashboardQuery) - q.Result = &dashboards.Dashboard{ + qResultDash = &dashboards.Dashboard{ ID: q.ID, UID: q.UID, OrgID: q.OrgID, } - }).Return(nil) + }).Return(qResultDash, nil) t.Run("Should get acl without duplicates", func(t *testing.T) { user := &user.SignedInUser{ diff --git a/pkg/services/guardian/guardian_util_test.go b/pkg/services/guardian/guardian_util_test.go index 982ce3b9024..cba02cca47c 100644 --- a/pkg/services/guardian/guardian_util_test.go +++ b/pkg/services/guardian/guardian_util_test.go @@ -47,13 +47,14 @@ func orgRoleScenario(desc string, t *testing.T, role org.RoleType, fn scenarioFu store := dbtest.NewFakeDB() fakeDashboardService := dashboards.NewFakeDashboardService(t) + var qResult *dashboards.Dashboard fakeDashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { q := args.Get(1).(*dashboards.GetDashboardQuery) - q.Result = &dashboards.Dashboard{ + qResult = &dashboards.Dashboard{ ID: q.ID, UID: q.UID, } - }).Return(nil) + }).Return(qResult, nil) guard, err := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store, fakeDashboardService, &teamtest.FakeService{}) require.NoError(t, err) @@ -78,13 +79,14 @@ func apiKeyScenario(desc string, t *testing.T, role org.RoleType, fn scenarioFun } store := dbtest.NewFakeDB() dashSvc := dashboards.NewFakeDashboardService(t) + var qResult *dashboards.Dashboard dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { q := args.Get(1).(*dashboards.GetDashboardQuery) - q.Result = &dashboards.Dashboard{ + qResult = &dashboards.Dashboard{ ID: q.ID, UID: q.UID, } - }).Return(nil) + }).Return(qResult, nil) guard, err := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store, dashSvc, &teamtest.FakeService{}) require.NoError(t, err) @@ -114,18 +116,17 @@ func permissionScenario(desc string, dashboardID int64, sc *scenarioContext, teamSvc := &teamtest.FakeService{ExpectedTeamsByUser: teams} dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardACLInfoListQuery) - q.Result = permissions - }).Return(nil) + qResult := permissions + dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult, nil) + qResultDash := &dashboards.Dashboard{} dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { q := args.Get(1).(*dashboards.GetDashboardQuery) - q.Result = &dashboards.Dashboard{ + qResultDash = &dashboards.Dashboard{ ID: q.ID, UID: q.UID, OrgID: q.OrgID, } - }).Return(nil) + }).Return(qResultDash, nil) sc.permissionScenario = desc g, err := newDashboardGuardian(context.Background(), dashboardID, sc.givenUser.OrgID, sc.givenUser, store, dashSvc, teamSvc) diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index dc27e80e8bd..9a3efd9c2db 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -766,13 +766,14 @@ func scenarioWithLibraryPanel(t *testing.T, desc string, fn func(t *testing.T, s store := dbtest.NewFakeDB() dashSvc := dashboards.NewFakeDashboardService(t) + var result *dashboards.Dashboard dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { q := args.Get(1).(*dashboards.GetDashboardQuery) - q.Result = &dashboards.Dashboard{ + result = &dashboards.Dashboard{ ID: q.ID, UID: q.UID, } - }).Return(nil) + }).Return(result, nil) guardian.InitLegacyGuardian(store, dashSvc, &teamtest.FakeService{}) t.Helper() diff --git a/pkg/services/live/features/dashboard.go b/pkg/services/live/features/dashboard.go index 9614229b53e..d98b4ddf73d 100644 --- a/pkg/services/live/features/dashboard.go +++ b/pkg/services/live/features/dashboard.go @@ -67,12 +67,13 @@ func (h *DashboardHandler) OnSubscribe(ctx context.Context, user *user.SignedInU // make sure can view this dashboard if len(parts) == 2 && parts[0] == "uid" { query := dashboards.GetDashboardQuery{UID: parts[1], OrgID: user.OrgID} - if err := h.DashboardService.GetDashboard(ctx, &query); err != nil { + queryResult, err := h.DashboardService.GetDashboard(ctx, &query) + if err != nil { logger.Error("Error getting dashboard", "query", query, "error", err) return model.SubscribeReply{}, backend.SubscribeStreamStatusNotFound, nil } - dash := query.Result + dash := queryResult guard, err := guardian.NewByDashboard(ctx, dash, user.OrgID, user) if err != nil { return model.SubscribeReply{}, backend.SubscribeStreamStatusPermissionDenied, err @@ -117,12 +118,13 @@ func (h *DashboardHandler) OnPublish(ctx context.Context, user *user.SignedInUse return model.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("ignore???") } query := dashboards.GetDashboardQuery{UID: parts[1], OrgID: user.OrgID} - if err := h.DashboardService.GetDashboard(ctx, &query); err != nil { + queryResult, err := h.DashboardService.GetDashboard(ctx, &query) + if err != nil { logger.Error("Unknown dashboard", "query", query) return model.PublishReply{}, backend.PublishStreamStatusNotFound, nil } - guard, err := guardian.NewByDashboard(ctx, query.Result, user.OrgID, user) + guard, err := guardian.NewByDashboard(ctx, queryResult, user.OrgID, user) if err != nil { logger.Error("Failed to create guardian", "err", err) return model.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("internal error") diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index a5a1215e7f7..e7b48fe4a04 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -335,9 +335,9 @@ func (s *ServiceImpl) buildStarredItemsNavLinks(c *models.ReqContext) ([]*navtre ID: dashboardId, OrgID: c.OrgID, } - err := s.dashboardService.GetDashboard(c.Req.Context(), query) + queryResult, err := s.dashboardService.GetDashboard(c.Req.Context(), query) if err == nil { - starredDashboards = append(starredDashboards, query.Result) + starredDashboards = append(starredDashboards, queryResult) } } diff --git a/pkg/services/ngalert/state/historian/dashboard.go b/pkg/services/ngalert/state/historian/dashboard.go index ec311e6ff05..74b097298e2 100644 --- a/pkg/services/ngalert/state/historian/dashboard.go +++ b/pkg/services/ngalert/state/historian/dashboard.go @@ -54,16 +54,16 @@ func (r *dashboardResolver) getID(ctx context.Context, orgID int64, uid string) UID: uid, OrgID: orgID, } - err := r.dashboards.GetDashboard(ctx, query) + queryResult, err := r.dashboards.GetDashboard(ctx, query) // We also cache lookups where we don't find anything. if err != nil && errors.Is(err, dashboards.ErrDashboardNotFound) { result = err } else if err != nil { return 0, err - } else if query.Result == nil { + } else if queryResult == nil { result = dashboards.ErrDashboardNotFound } else { - result = query.Result.ID + result = queryResult.ID } // By setting the cache inside the singleflighted routine, we avoid any accidental re-queries that could get initiated after the query completes. diff --git a/pkg/services/ngalert/state/historian/dashboard_test.go b/pkg/services/ngalert/state/historian/dashboard_test.go index f8e7dba20a4..f455ef0f70d 100644 --- a/pkg/services/ngalert/state/historian/dashboard_test.go +++ b/pkg/services/ngalert/state/historian/dashboard_test.go @@ -14,9 +14,8 @@ func TestDashboardResolver(t *testing.T) { t.Run("fetches dashboards from dashboard service", func(t *testing.T) { dbs := &dashboards.FakeDashboardService{} exp := int64(14) - dbs.On("GetDashboard", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { - args.Get(1).(*dashboards.GetDashboardQuery).Result = &dashboards.Dashboard{ID: exp} - }).Return(nil) + result := &dashboards.Dashboard{ID: exp} + dbs.On("GetDashboard", mock.Anything, mock.Anything).Return(result, nil) sut := createDashboardResolverSut(dbs) id, err := sut.getID(context.Background(), 1, "dashboard-uid") @@ -27,9 +26,7 @@ func TestDashboardResolver(t *testing.T) { t.Run("fetches dashboardNotFound if underlying dashboard does not exist", func(t *testing.T) { dbs := &dashboards.FakeDashboardService{} - dbs.On("GetDashboard", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { - args.Get(1).(*dashboards.GetDashboardQuery).Result = nil - }).Return(dashboards.ErrDashboardNotFound) + dbs.On("GetDashboard", mock.Anything, mock.Anything).Return(nil, dashboards.ErrDashboardNotFound) sut := createDashboardResolverSut(dbs) _, err := sut.getID(context.Background(), 1, "not-exist") diff --git a/pkg/services/plugindashboards/service/dashboard_updater.go b/pkg/services/plugindashboards/service/dashboard_updater.go index 11a76ee07fb..03cd21907f4 100644 --- a/pkg/services/plugindashboards/service/dashboard_updater.go +++ b/pkg/services/plugindashboards/service/dashboard_updater.go @@ -143,11 +143,12 @@ func (du *DashboardUpdater) handlePluginStateChanged(ctx context.Context, event du.syncPluginDashboards(ctx, p, event.OrgId) } else { query := dashboards.GetDashboardsByPluginIDQuery{PluginID: event.PluginId, OrgID: event.OrgId} - if err := du.dashboardPluginService.GetDashboardsByPluginID(ctx, &query); err != nil { + queryResult, err := du.dashboardPluginService.GetDashboardsByPluginID(ctx, &query) + if err != nil { return err } - for _, dash := range query.Result { + for _, dash := range queryResult { du.logger.Info("Deleting plugin dashboard", "pluginId", event.PluginId, "dashboard", dash.Slug) if err := du.dashboardService.DeleteDashboard(ctx, dash.ID, dash.OrgID); err != nil { return err diff --git a/pkg/services/plugindashboards/service/service.go b/pkg/services/plugindashboards/service/service.go index 53e8b8e7288..48dd814bd20 100644 --- a/pkg/services/plugindashboards/service/service.go +++ b/pkg/services/plugindashboards/service/service.go @@ -42,7 +42,8 @@ func (s Service) ListPluginDashboards(ctx context.Context, req *plugindashboards // load current dashboards query := dashboards.GetDashboardsByPluginIDQuery{OrgID: req.OrgID, PluginID: req.PluginID} - if err := s.dashboardPluginService.GetDashboardsByPluginID(ctx, &query); err != nil { + queryResult, err := s.dashboardPluginService.GetDashboardsByPluginID(ctx, &query) + if err != nil { return nil, err } @@ -67,7 +68,7 @@ func (s Service) ListPluginDashboards(ctx context.Context, req *plugindashboards res.Revision = dashboard.Data.Get("revision").MustInt64(1) // find existing dashboard - for _, existingDash := range query.Result { + for _, existingDash := range queryResult { if existingDash.Slug == dashboard.Slug { res.UID = existingDash.UID res.DashboardId = existingDash.ID @@ -84,7 +85,7 @@ func (s Service) ListPluginDashboards(ctx context.Context, req *plugindashboards } // find deleted dashboards - for _, dash := range query.Result { + for _, dash := range queryResult { if _, exists := existingMatches[dash.ID]; !exists { result = append(result, &plugindashboards.PluginDashboard{ UID: dash.UID, diff --git a/pkg/services/plugindashboards/service/service_test.go b/pkg/services/plugindashboards/service/service_test.go index 9765006fb6e..2349602ebdc 100644 --- a/pkg/services/plugindashboards/service/service_test.go +++ b/pkg/services/plugindashboards/service/service_test.go @@ -207,11 +207,11 @@ type dashboardPluginServiceMock struct { args []*dashmodels.GetDashboardsByPluginIDQuery } -func (d *dashboardPluginServiceMock) GetDashboardsByPluginID(ctx context.Context, query *dashmodels.GetDashboardsByPluginIDQuery) error { - query.Result = []*dashmodels.Dashboard{} +func (d *dashboardPluginServiceMock) GetDashboardsByPluginID(ctx context.Context, query *dashmodels.GetDashboardsByPluginIDQuery) ([]*dashmodels.Dashboard, error) { + queryResult := []*dashmodels.Dashboard{} if dashboards, exists := d.pluginDashboards[query.PluginID]; exists { - query.Result = dashboards + queryResult = dashboards } if d.args == nil { @@ -220,5 +220,5 @@ func (d *dashboardPluginServiceMock) GetDashboardsByPluginID(ctx context.Context d.args = append(d.args, query) - return nil + return queryResult, nil } diff --git a/pkg/services/provisioning/alerting/rules_provisioner.go b/pkg/services/provisioning/alerting/rules_provisioner.go index 5d816b325be..55750ff0421 100644 --- a/pkg/services/provisioning/alerting/rules_provisioner.go +++ b/pkg/services/provisioning/alerting/rules_provisioner.go @@ -102,7 +102,7 @@ func (prov *defaultAlertRuleProvisioner) getOrCreateFolderUID( Slug: slugify.Slugify(folderName), OrgID: orgID, } - err := prov.dashboardService.GetDashboard(ctx, cmd) + cmdResult, err := prov.dashboardService.GetDashboard(ctx, cmd) if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) { return "", err } @@ -123,9 +123,9 @@ func (prov *defaultAlertRuleProvisioner) getOrCreateFolderUID( return dbDash.UID, nil } - if !cmd.Result.IsFolder { + if !cmdResult.IsFolder { return "", fmt.Errorf("got invalid response. expected folder, found dashboard") } - return cmd.Result.UID, nil + return cmdResult.UID, nil } diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 8448a431620..b2900b696d3 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -300,7 +300,7 @@ func (fr *FileReader) getOrCreateFolderID(ctx context.Context, cfg *config, serv } cmd := &dashboards.GetDashboardQuery{Slug: slugify.Slugify(folderName), OrgID: cfg.OrgID} - err := fr.dashboardStore.GetDashboard(ctx, cmd) + result, err := fr.dashboardStore.GetDashboard(ctx, cmd) if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) { return 0, err @@ -326,11 +326,11 @@ func (fr *FileReader) getOrCreateFolderID(ctx context.Context, cfg *config, serv return dbDash.ID, nil } - if !cmd.Result.IsFolder { + if !result.IsFolder { return 0, fmt.Errorf("got invalid response. expected folder, found dashboard") } - return cmd.Result.ID, nil + return result.ID, nil } func resolveSymlink(fileinfo os.FileInfo, path string) (os.FileInfo, error) { diff --git a/pkg/services/provisioning/dashboards/file_reader_test.go b/pkg/services/provisioning/dashboards/file_reader_test.go index d7a4649c888..df6fc66ade9 100644 --- a/pkg/services/provisioning/dashboards/file_reader_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_test.go @@ -512,6 +512,6 @@ func (ffi FakeFileInfo) Sys() interface{} { type fakeDashboardStore struct{} -func (fds *fakeDashboardStore) GetDashboard(_ context.Context, _ *dashboards.GetDashboardQuery) error { - return dashboards.ErrDashboardNotFound +func (fds *fakeDashboardStore) GetDashboard(_ context.Context, _ *dashboards.GetDashboardQuery) (*dashboards.Dashboard, error) { + return nil, dashboards.ErrDashboardNotFound } diff --git a/pkg/services/provisioning/utils/utils.go b/pkg/services/provisioning/utils/utils.go index 40153769f6f..05e649aa586 100644 --- a/pkg/services/provisioning/utils/utils.go +++ b/pkg/services/provisioning/utils/utils.go @@ -10,7 +10,7 @@ import ( ) type DashboardStore interface { - GetDashboard(context.Context, *dashboards.GetDashboardQuery) error + GetDashboard(context.Context, *dashboards.GetDashboardQuery) (*dashboards.Dashboard, error) } func CheckOrgExists(ctx context.Context, orgService org.Service, orgID int64) error { diff --git a/pkg/services/screenshot/screenshot.go b/pkg/services/screenshot/screenshot.go index eb5bf01ddfa..0e88b9c62f3 100644 --- a/pkg/services/screenshot/screenshot.go +++ b/pkg/services/screenshot/screenshot.go @@ -86,7 +86,8 @@ func (s *HeadlessScreenshotService) Take(ctx context.Context, opts ScreenshotOpt defer func() { s.duration.Observe(time.Since(start).Seconds()) }() q := dashboards.GetDashboardQuery{UID: opts.DashboardUID} - if err := s.ds.GetDashboard(ctx, &q); err != nil { + qResult, err := s.ds.GetDashboard(ctx, &q) + if err != nil { s.instrumentError(err) return nil, err } @@ -94,9 +95,9 @@ func (s *HeadlessScreenshotService) Take(ctx context.Context, opts ScreenshotOpt opts = opts.SetDefaults() u := url.URL{} - u.Path = path.Join("d-solo", q.Result.UID, q.Result.Slug) + u.Path = path.Join("d-solo", qResult.UID, qResult.Slug) p := u.Query() - p.Add("orgId", strconv.FormatInt(q.Result.OrgID, 10)) + p.Add("orgId", strconv.FormatInt(qResult.OrgID, 10)) p.Add("panelId", strconv.FormatInt(opts.PanelID, 10)) p.Add("from", opts.From) p.Add("to", opts.To) @@ -104,7 +105,7 @@ func (s *HeadlessScreenshotService) Take(ctx context.Context, opts ScreenshotOpt renderOpts := rendering.Opts{ AuthOpts: rendering.AuthOpts{ - OrgID: q.Result.OrgID, + OrgID: qResult.OrgID, OrgRole: org.RoleAdmin, }, ErrorOpts: rendering.ErrorOpts{ diff --git a/pkg/services/screenshot/screenshot_test.go b/pkg/services/screenshot/screenshot_test.go index 42557f6956e..24794ea8527 100644 --- a/pkg/services/screenshot/screenshot_test.go +++ b/pkg/services/screenshot/screenshot_test.go @@ -26,7 +26,7 @@ func TestHeadlessScreenshotService(t *testing.T) { s := NewHeadlessScreenshotService(&d, r, prometheus.NewRegistry()) // a non-existent dashboard should return error - d.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(dashboards.ErrDashboardNotFound).Once() + d.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(nil, dashboards.ErrDashboardNotFound).Once() ctx := context.Background() opts := ScreenshotOptions{} screenshot, err := s.Take(ctx, opts) @@ -34,10 +34,8 @@ func TestHeadlessScreenshotService(t *testing.T) { assert.Nil(t, screenshot) // should take a screenshot - d.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardQuery) - q.Result = &dashboards.Dashboard{ID: 1, UID: "foo", Slug: "bar", OrgID: 2} - }).Return(nil) + qResult := &dashboards.Dashboard{ID: 1, UID: "foo", Slug: "bar", OrgID: 2} + d.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(qResult, nil) renderOpts := rendering.Opts{ AuthOpts: rendering.AuthOpts{ diff --git a/pkg/services/team/teamimpl/store_test.go b/pkg/services/team/teamimpl/store_test.go index 98c3bae7d78..a759671115b 100644 --- a/pkg/services/team/teamimpl/store_test.go +++ b/pkg/services/team/teamimpl/store_test.go @@ -312,10 +312,10 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { require.Equal(t, err, team.ErrTeamNotFound) permQuery := &dashboards.GetDashboardACLInfoListQuery{DashboardID: 1, OrgID: testOrgID} - err = getDashboardACLInfoList(sqlStore, permQuery) + permQueryResult, err := getDashboardACLInfoList(sqlStore, permQuery) require.NoError(t, err) - require.Equal(t, len(permQuery.Result), 0) + require.Equal(t, len(permQueryResult), 0) }) t.Run("Should be able to return if user is admin of teams or not", func(t *testing.T) { @@ -654,9 +654,9 @@ func updateDashboardACL(t *testing.T, sqlStore *sqlstore.SQLStore, dashboardID i // This function was copied from pkg/services/dashboards/database to circumvent // import cycles. When this org-related code is refactored into a service the // tests can the real GetDashboardACLInfoList functions -func getDashboardACLInfoList(s *sqlstore.SQLStore, query *dashboards.GetDashboardACLInfoListQuery) error { +func getDashboardACLInfoList(s *sqlstore.SQLStore, query *dashboards.GetDashboardACLInfoListQuery) ([]*dashboards.DashboardACLInfoDTO, error) { + queryResult := make([]*dashboards.DashboardACLInfoDTO, 0) outerErr := s.WithDbSession(context.Background(), func(dbSession *db.Session) error { - query.Result = make([]*dashboards.DashboardACLInfoDTO, 0) falseStr := s.GetDialect().BooleanStr(false) if query.DashboardID == 0 { @@ -680,7 +680,7 @@ func getDashboardACLInfoList(s *sqlstore.SQLStore, query *dashboards.GetDashboar falseStr + ` AS inherited FROM dashboard_acl as da WHERE da.dashboard_id = -1` - return dbSession.SQL(sql).Find(&query.Result) + return dbSession.SQL(sql).Find(&queryResult) } rawSQL := ` @@ -722,16 +722,16 @@ func getDashboardACLInfoList(s *sqlstore.SQLStore, query *dashboards.GetDashboar ORDER BY da.id ASC ` - return dbSession.SQL(rawSQL, query.OrgID, query.DashboardID).Find(&query.Result) + return dbSession.SQL(rawSQL, query.OrgID, query.DashboardID).Find(&queryResult) }) if outerErr != nil { - return outerErr + return nil, outerErr } - for _, p := range query.Result { + for _, p := range queryResult { p.PermissionName = p.Permission.String() } - return nil + return queryResult, nil } diff --git a/pkg/services/user/userimpl/store_test.go b/pkg/services/user/userimpl/store_test.go index 97a3bb58b4e..57561580190 100644 --- a/pkg/services/user/userimpl/store_test.go +++ b/pkg/services/user/userimpl/store_test.go @@ -432,10 +432,10 @@ func TestIntegrationUserDataAccess(t *testing.T) { require.Nil(t, err) permQuery := &dashboards.GetDashboardACLInfoListQuery{DashboardID: 1, OrgID: users[0].OrgID} - err = userStore.getDashboardACLInfoList(permQuery) + permQueryResult, err := userStore.getDashboardACLInfoList(permQuery) require.Nil(t, err) - require.Len(t, permQuery.Result, 0) + require.Len(t, permQueryResult, 0) // A user is an org member and has been assigned permissions // Re-init DB @@ -488,10 +488,10 @@ func TestIntegrationUserDataAccess(t *testing.T) { require.Nil(t, err) permQuery = &dashboards.GetDashboardACLInfoListQuery{DashboardID: 1, OrgID: users[0].OrgID} - err = userStore.getDashboardACLInfoList(permQuery) + permQueryResult, err = userStore.getDashboardACLInfoList(permQuery) require.Nil(t, err) - require.Len(t, permQuery.Result, 0) + require.Len(t, permQueryResult, 0) }) t.Run("Testing DB - return list of users that the SignedInUser has permission to read", func(t *testing.T) { @@ -855,9 +855,9 @@ func updateDashboardACL(t *testing.T, sqlStore db.DB, dashboardID int64, items . // This function was copied from pkg/services/dashboards/database to circumvent // import cycles. When this org-related code is refactored into a service the // tests can the real GetDashboardACLInfoList functions -func (ss *sqlStore) getDashboardACLInfoList(query *dashboards.GetDashboardACLInfoListQuery) error { +func (ss *sqlStore) getDashboardACLInfoList(query *dashboards.GetDashboardACLInfoListQuery) ([]*dashboards.DashboardACLInfoDTO, error) { + queryResult := make([]*dashboards.DashboardACLInfoDTO, 0) outerErr := ss.db.WithDbSession(context.Background(), func(dbSession *db.Session) error { - query.Result = make([]*dashboards.DashboardACLInfoDTO, 0) falseStr := ss.dialect.BooleanStr(false) if query.DashboardID == 0 { @@ -881,7 +881,7 @@ func (ss *sqlStore) getDashboardACLInfoList(query *dashboards.GetDashboardACLInf falseStr + ` AS inherited FROM dashboard_acl as da WHERE da.dashboard_id = -1` - return dbSession.SQL(sql).Find(&query.Result) + return dbSession.SQL(sql).Find(&queryResult) } rawSQL := ` @@ -923,18 +923,18 @@ func (ss *sqlStore) getDashboardACLInfoList(query *dashboards.GetDashboardACLInf ORDER BY da.id ASC ` - return dbSession.SQL(rawSQL, query.OrgID, query.DashboardID).Find(&query.Result) + return dbSession.SQL(rawSQL, query.OrgID, query.DashboardID).Find(&queryResult) }) if outerErr != nil { - return outerErr + return nil, outerErr } - for _, p := range query.Result { + for _, p := range queryResult { p.PermissionName = p.Permission.String() } - return nil + return queryResult, nil } func createOrgAndUserSvc(t *testing.T, store db.DB, cfg *setting.Cfg) (org.Service, user.Service) { From 504fec46d2ab54073cef9728507052f8a79c21f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 25 Jan 2023 10:38:33 +0100 Subject: [PATCH 014/172] CommandPalette: Design tweaks and design fixes (#61971) * Command palette design tweaks start * Fix some item styling issues * Updated placeholder * Style fixes * Fix gradient border pos issue, and heading padding * Fix header top margin * Restore padding * Update * Change to md modal --- .../commandPalette/CommandPalette.tsx | 68 ++++++++----------- .../features/commandPalette/ResultItem.tsx | 29 ++++---- public/locales/en-US/grafana.json | 2 +- public/locales/pseudo-LOCALE/grafana.json | 2 +- 4 files changed, 46 insertions(+), 55 deletions(-) diff --git a/public/app/features/commandPalette/CommandPalette.tsx b/public/app/features/commandPalette/CommandPalette.tsx index 7ee00813631..3c3af74f770 100644 --- a/public/app/features/commandPalette/CommandPalette.tsx +++ b/public/app/features/commandPalette/CommandPalette.tsx @@ -57,10 +57,12 @@ export const CommandPalette = () => {
- +
+ +
@@ -92,22 +94,18 @@ const RenderResults = ({ dashboardResults }: RenderResultsProps) => { return ( { - // These items are rendered in a container, in a virtual list, so we cannot - // use :first/last-child selectors, so we must mimic them in JS - const isFirstItem = items[0] === item; - const isLastItem = items[items.length - 1] === item; + const isFirst = items[0] === item; const renderedItem = typeof item === 'string' ? ( -
-
{item}
-
+
{item}
) : ( ); - return isLastItem ?
{renderedItem}
: renderedItem; + return renderedItem; }} /> ); @@ -129,47 +127,39 @@ const getSearchStyles = (theme: GrafanaTheme2) => ({ }, }), animator: css({ - maxWidth: theme.breakpoints.values.sm, // supposed to be 600... + maxWidth: theme.breakpoints.values.md, width: '100%', - background: theme.colors.background.canvas, + background: theme.colors.background.primary, color: theme.colors.text.primary, - borderRadius: theme.shape.borderRadius(4), + borderRadius: theme.shape.borderRadius(2), + border: `1px solid ${theme.colors.border.weak}`, overflow: 'hidden', boxShadow: theme.shadows.z3, }), search: css({ - padding: theme.spacing(2, 3), + padding: theme.spacing(1.5, 2), fontSize: theme.typography.fontSize, width: '100%', boxSizing: 'border-box', outline: 'none', border: 'none', - background: theme.colors.background.canvas, - color: theme.colors.text.primary, - borderBottom: `1px solid ${theme.colors.border.medium}`, + background: theme.components.input.background, + color: theme.components.input.text, + borderBottom: `1px solid ${theme.colors.border.weak}`, }), - - // Virtual list measures margin incorrectly, so we need to split padding before/after border - // over and inner and outer element - sectionHeader: css({ - paddingTop: theme.spacing(2), - fontSize: theme.typography.h6.fontSize, - fontWeight: theme.typography.body.fontWeight, - color: theme.colors.text.secondary, - }), - sectionHeaderInner: css({ - padding: theme.spacing(1, 2), - borderTop: `1px solid ${theme.colors.border.medium}`, - }), - - // We don't need the header above the first section - sectionHeaderInnerFirst: css({ - borderTop: 'none', - paddingTop: 0, - }), - - // Last item gets extra padding so it's not clipped by the rounded corners on the container - lastItem: css({ + resultsContainer: css({ paddingBottom: theme.spacing(1), }), + sectionHeader: css({ + padding: theme.spacing(1.5, 2, 1, 2), + fontSize: theme.typography.bodySmall.fontSize, + fontWeight: theme.typography.fontWeightMedium, + color: theme.colors.text.primary, + borderTop: `1px solid ${theme.colors.border.weak}`, + marginTop: theme.spacing(1), + }), + sectionHeaderFirst: css({ + borderTop: 'none', + marginTop: 0, + }), }); diff --git a/public/app/features/commandPalette/ResultItem.tsx b/public/app/features/commandPalette/ResultItem.tsx index fc4858fa00e..464276f10e2 100644 --- a/public/app/features/commandPalette/ResultItem.tsx +++ b/public/app/features/commandPalette/ResultItem.tsx @@ -1,9 +1,9 @@ -import { css } from '@emotion/css'; +import { css, cx } from '@emotion/css'; import { ActionId, ActionImpl } from 'kbar'; import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { useTheme2 } from '@grafana/ui'; +import { useStyles2 } from '@grafana/ui'; export const ResultItem = React.forwardRef( ( @@ -31,8 +31,7 @@ export const ResultItem = React.forwardRef( return action.ancestors.slice(index + 1); }, [action.ancestors, currentRootActionId]); - const theme = useTheme2(); - const styles = getResultItemStyles(theme, active); + const styles = useStyles2(getResultItemStyles); let name = action.name; @@ -42,7 +41,7 @@ export const ResultItem = React.forwardRef( } return ( -
+
{action.icon}
@@ -63,31 +62,33 @@ export const ResultItem = React.forwardRef( ); } ); + ResultItem.displayName = 'ResultItem'; -const getResultItemStyles = (theme: GrafanaTheme2, isActive: boolean) => { - const textColor = isActive ? theme.colors.text.maxContrast : theme.colors.text.primary; - const rowBackgroundColor = isActive ? theme.colors.background.primary : 'transparent'; - const shortcutBackgroundColor = isActive ? theme.colors.background.secondary : theme.colors.background.primary; +const getResultItemStyles = (theme: GrafanaTheme2) => { return { row: css({ - color: textColor, padding: theme.spacing(1, 2), - background: rowBackgroundColor, display: 'flex', alightItems: 'center', justifyContent: 'space-between', cursor: 'pointer', position: 'relative', + borderRadius: theme.shape.borderRadius(2), + margin: theme.spacing(0, 1), + }), + activeRow: css({ + color: theme.colors.text.maxContrast, + background: theme.colors.emphasize(theme.colors.background.primary, 0.03), '&:before': { - display: isActive ? 'block' : 'none', + display: 'block', content: '" "', position: 'absolute', left: 0, top: 0, bottom: 0, width: theme.spacing(0.5), - borderRadius: theme.shape.borderRadius(1), + borderRadius: theme.shape.borderRadius(2), backgroundImage: theme.colors.gradients.brandVertical, }, }), @@ -103,7 +104,7 @@ const getResultItemStyles = (theme: GrafanaTheme2, isActive: boolean) => { }), shortcut: css({ padding: theme.spacing(0, 1), - background: shortcutBackgroundColor, + background: theme.colors.background.secondary, borderRadius: theme.shape.borderRadius(), fontSize: theme.typography.fontSize, }), diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index c2bb24742a0..1f9efec846a 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -13,7 +13,7 @@ "search": "Search" }, "search-box": { - "placeholder": "Search Grafana" + "placeholder": "Search or jump to..." }, "section": { "actions": "Actions", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index b480c15e316..266e11b36c8 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -13,7 +13,7 @@ "search": "Ŝęäřčĥ" }, "search-box": { - "placeholder": "Ŝęäřčĥ Ğřäƒäʼnä" + "placeholder": "Ŝęäřčĥ őř ĵūmp ŧő..." }, "section": { "actions": "Åčŧįőʼnş", From e05fc9212ad6b65f499f2b282f739e1b9cef51ec Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 25 Jan 2023 09:44:12 +0000 Subject: [PATCH 015/172] Changelog: Updated changelog for 8.5.20 (#62073) --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5059cff21cd..cf51c8a8de5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1997,6 +1997,16 @@ In the Loki data source, for consistency and performance reasons, we changed how The dependency to [grafana/aws-sdk](https://github.com/grafana/grafana-aws-sdk-react) is moved from [grafana/ui](https://github.com/grafana/grafana/blob/main/packages/grafana-ui/package.json) to the plugin. This means that any plugin that use SIGV4 auth need to pass a SIGV4 editor component as a prop to the `DataSourceHttpSettings` component. Issue [#43559](https://github.com/grafana/grafana/issues/43559) + + +# 8.5.20 (2023-01-25) + +### Features and enhancements + +- **Chore:** Upgrade Go to 1.19.4 [v8.5.x]. [#60824](https://github.com/grafana/grafana/pull/60824), [@sakjur](https://github.com/sakjur) + + + # 8.5.15 (2022-11-08) From b2b48398d05ffb52b6b369b41e52041dd51a538e Mon Sep 17 00:00:00 2001 From: Dimitris Sotirakis Date: Wed, 25 Jan 2023 11:57:18 +0200 Subject: [PATCH 016/172] Chore: update latest.json to 9.3.4 (#62080) --- latest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/latest.json b/latest.json index f617f9d7f46..f131e29f9ad 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "stable": "9.3.1", + "stable": "9.3.4", "testing": "9.3.0" } From bbf7c0a147ed006ab37b08796458912b80c236dc Mon Sep 17 00:00:00 2001 From: gotjosh Date: Wed, 25 Jan 2023 10:06:36 +0000 Subject: [PATCH 017/172] Update alerting to the latest main (#62003) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 56efdd983ea..7595bdc6861 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,7 @@ require ( github.com/google/uuid v1.3.0 github.com/google/wire v0.5.0 github.com/gorilla/websocket v1.5.0 - github.com/grafana/alerting v0.0.0-20230119191910-5ebb70a85264 + github.com/grafana/alerting v0.0.0-20230124145916-c6a7791d037e github.com/grafana/cuetsy v0.1.5 github.com/grafana/grafana-aws-sdk v0.12.0 github.com/grafana/grafana-azure-sdk-go v1.5.1 diff --git a/go.sum b/go.sum index ce5ecd7b06f..7c6fb1c4d0f 100644 --- a/go.sum +++ b/go.sum @@ -1394,8 +1394,8 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/alerting v0.0.0-20230119191910-5ebb70a85264 h1:ApQsGfr05Yma0dpd1+ib11cJQcpmet+E/Ke/aQkdT1Y= -github.com/grafana/alerting v0.0.0-20230119191910-5ebb70a85264/go.mod h1:NoSLbfmUwE+omWFReFrLtbtOItmvTbuQERJ6XFYp9ME= +github.com/grafana/alerting v0.0.0-20230124145916-c6a7791d037e h1:YCxvmaPXHGiaQdWy6qeQExivxWzpwUyM0sh73vudWhw= +github.com/grafana/alerting v0.0.0-20230124145916-c6a7791d037e/go.mod h1:NoSLbfmUwE+omWFReFrLtbtOItmvTbuQERJ6XFYp9ME= github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw= github.com/grafana/codejen v0.0.3/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s= github.com/grafana/cuetsy v0.1.5 h1:mnFwAXdbqCsyL8r7kkdUMJ4kOAR26cxIPmrZj7JzTeY= From eb1fed792905a08108fc2e8a4f8dc529448c7c57 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Wed, 25 Jan 2023 11:45:52 +0100 Subject: [PATCH 018/172] Plugins: Add plugin resource tests (#62014) * remove plugin context from response * remove integration test indicator --- pkg/api/plugin_resource_test.go | 131 ++++++++++++++++++++ pkg/tsdb/testdatasource/resource_handler.go | 5 - 2 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 pkg/api/plugin_resource_test.go diff --git a/pkg/api/plugin_resource_test.go b/pkg/api/plugin_resource_test.go new file mode 100644 index 00000000000..a7cf4e9bdfe --- /dev/null +++ b/pkg/api/plugin_resource_test.go @@ -0,0 +1,131 @@ +package api + +import ( + "context" + "encoding/json" + "errors" + "io" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana-azure-sdk-go/azsettings" + "github.com/grafana/grafana-plugin-sdk-go/backend" + + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/localcache" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin" + "github.com/grafana/grafana/pkg/plugins/backendplugin/provider" + "github.com/grafana/grafana/pkg/plugins/config" + pluginClient "github.com/grafana/grafana/pkg/plugins/manager/client" + "github.com/grafana/grafana/pkg/plugins/manager/fakes" + "github.com/grafana/grafana/pkg/plugins/manager/loader" + "github.com/grafana/grafana/pkg/plugins/manager/registry" + "github.com/grafana/grafana/pkg/plugins/manager/signature" + "github.com/grafana/grafana/pkg/plugins/manager/store" + "github.com/grafana/grafana/pkg/plugins/plugincontext" + "github.com/grafana/grafana/pkg/services/accesscontrol" + datasources "github.com/grafana/grafana/pkg/services/datasources/fakes" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest" + pluginSettings "github.com/grafana/grafana/pkg/services/pluginsettings/service" + "github.com/grafana/grafana/pkg/services/pluginsintegration" + "github.com/grafana/grafana/pkg/services/quota/quotatest" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tsdb/cloudwatch" + "github.com/grafana/grafana/pkg/tsdb/testdatasource" + "github.com/grafana/grafana/pkg/web/webtest" +) + +func TestCallResource(t *testing.T) { + staticRootPath, err := filepath.Abs("../../public/") + require.NoError(t, err) + + cfg := setting.NewCfg() + cfg.StaticRootPath = staticRootPath + cfg.IsFeatureToggleEnabled = func(_ string) bool { + return false + } + cfg.Azure = &azsettings.AzureSettings{} + + coreRegistry := coreplugin.ProvideCoreRegistry(nil, &cloudwatch.CloudWatchService{}, nil, nil, nil, nil, + nil, nil, nil, nil, testdatasource.ProvideService(cfg, featuremgmt.WithFeatures()), nil, nil, nil, nil, nil, nil) + pCfg := config.ProvideConfig(setting.ProvideProvider(cfg), cfg) + reg := registry.ProvideService() + l := loader.ProvideService(pCfg, fakes.NewFakeLicensingService(), signature.NewUnsignedAuthorizer(pCfg), + reg, provider.ProvideService(coreRegistry), fakes.NewFakeRoleRegistry()) + ps, err := store.ProvideService(cfg, pCfg, reg, l) + require.NoError(t, err) + + pcp := plugincontext.ProvideService(localcache.ProvideService(), ps, &datasources.FakeCacheService{}, &datasources.FakeDataSourceService{}, pluginSettings.ProvideService(db.InitTestDB(t), nil)) + + srv := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.Cfg = cfg + hs.PluginContextProvider = pcp + hs.QuotaService = quotatest.New(false, nil) + hs.pluginStore = ps + hs.pluginClient = pluginClient.ProvideService(reg, pCfg) + }) + + t.Run("Test successful response is received for valid request", func(t *testing.T) { + req := srv.NewPostRequest("/api/plugins/testdata/resources/test", strings.NewReader("{ \"test\": true }")) + webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{ + 1: accesscontrol.GroupScopesByAction([]accesscontrol.Permission{ + {Action: plugins.ActionAppAccess, Scope: plugins.ScopeProvider.GetResourceAllScope()}, + }), + }}) + resp, err := srv.SendJSON(req) + require.NoError(t, err) + + b, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + var body = make(map[string]interface{}) + err = json.Unmarshal(b, &body) + require.NoError(t, err) + + require.Equal(t, "Hello world from test datasource!", body["message"]) + require.NoError(t, resp.Body.Close()) + require.Equal(t, 200, resp.StatusCode) + }) + + pc, err := pluginClient.NewDecorator(&fakes.FakePluginClient{ + CallResourceHandlerFunc: backend.CallResourceHandlerFunc(func(ctx context.Context, + req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { + return errors.New("something went wrong") + }), + }, pluginsintegration.CreateMiddlewares(cfg, &oauthtokentest.Service{})...) + require.NoError(t, err) + + srv = SetupAPITestServer(t, func(hs *HTTPServer) { + hs.Cfg = cfg + hs.PluginContextProvider = pcp + hs.QuotaService = quotatest.New(false, nil) + hs.pluginStore = ps + hs.pluginClient = pc + }) + + t.Run("Test error is properly propagated to API response", func(t *testing.T) { + req := srv.NewGetRequest("/api/plugins/testdata/resources/scenarios") + webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{ + 1: accesscontrol.GroupScopesByAction([]accesscontrol.Permission{ + {Action: plugins.ActionAppAccess, Scope: plugins.ScopeProvider.GetResourceAllScope()}, + }), + }}) + resp, err := srv.SendJSON(req) + require.NoError(t, err) + + body := new(strings.Builder) + _, err = io.Copy(body, resp.Body) + require.NoError(t, err) + + expectedBody := `{ "error": "something went wrong", "message": "Failed to call resource", "traceID": "" }` + require.JSONEq(t, expectedBody, body.String()) + require.NoError(t, resp.Body.Close()) + require.Equal(t, 500, resp.StatusCode) + }) +} diff --git a/pkg/tsdb/testdatasource/resource_handler.go b/pkg/tsdb/testdatasource/resource_handler.go index 06615f22919..4d6eba4bd53 100644 --- a/pkg/tsdb/testdatasource/resource_handler.go +++ b/pkg/tsdb/testdatasource/resource_handler.go @@ -10,8 +10,6 @@ import ( "time" "github.com/grafana/grafana/pkg/infra/log" - - "github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter" ) func (s *Service) registerRoutes() *http.ServeMux { @@ -130,8 +128,6 @@ func createJSONHandler(logger log.Logger) http.Handler { } } - config := httpadapter.PluginConfigFromContext(req.Context()) - data := map[string]interface{}{ "message": "Hello world from test datasource!", "request": map[string]interface{}{ @@ -139,7 +135,6 @@ func createJSONHandler(logger log.Logger) http.Handler { "url": req.URL, "headers": req.Header, "body": reqData, - "config": config, }, } bytes, err := json.Marshal(&data) From a6f0b69d6f89f25e92ec71f9916576509ea2563f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 25 Jan 2023 11:34:35 +0000 Subject: [PATCH 019/172] Update dependency eslint-plugin-jest to v27.2.1 (#61523) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 99c2f4ef579..4c9e41870cc 100644 --- a/package.json +++ b/package.json @@ -182,7 +182,7 @@ "eslint": "8.31.0", "eslint-config-prettier": "8.6.0", "eslint-plugin-import": "^2.26.0", - "eslint-plugin-jest": "27.1.3", + "eslint-plugin-jest": "27.2.1", "eslint-plugin-jsdoc": "39.6.2", "eslint-plugin-jsx-a11y": "6.7.1", "eslint-plugin-lodash": "7.4.0", diff --git a/yarn.lock b/yarn.lock index 7750c3aa7f2..ec9eccf3b51 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19199,9 +19199,9 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-jest@npm:27.1.3": - version: 27.1.3 - resolution: "eslint-plugin-jest@npm:27.1.3" +"eslint-plugin-jest@npm:27.2.1": + version: 27.2.1 + resolution: "eslint-plugin-jest@npm:27.2.1" dependencies: "@typescript-eslint/utils": ^5.10.0 peerDependencies: @@ -19212,7 +19212,7 @@ __metadata: optional: true jest: optional: true - checksum: 427f39ad4bb50b4e50a1f6aba04962ee3686e25b716d3e4dff47a304c2a352a35b032fec7350b84dc6362838525d93a70f7ae0f961b182c79bf602e90ebb1a55 + checksum: 579a4d26304cc6748b2e6dff6c965ea7a21b618d8b051eb02727d25cf5c7767f6db8ef5237531635ff77e242b983b973e7cb8c820a4d20d5bda73358c452a8ab languageName: node linkType: hard @@ -21754,7 +21754,7 @@ __metadata: eslint: 8.31.0 eslint-config-prettier: 8.6.0 eslint-plugin-import: ^2.26.0 - eslint-plugin-jest: 27.1.3 + eslint-plugin-jest: 27.2.1 eslint-plugin-jsdoc: 39.6.2 eslint-plugin-jsx-a11y: 6.7.1 eslint-plugin-lodash: 7.4.0 From 5f6616ff3eb2fb18c84af388b152394a67068309 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Wed, 25 Jan 2023 03:40:53 -0800 Subject: [PATCH 020/172] Dashboard schema: Review and mature graphTooltip property (#62082) * Review and mature graphTooltip property of Dashboard kind * Review --- kinds/dashboard/dashboard_kind.cue | 7 ++++--- .../src/raw/dashboard/x/dashboard_types.gen.ts | 3 +++ pkg/kindsys/report.json | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/kinds/dashboard/dashboard_kind.cue b/kinds/dashboard/dashboard_kind.cue index 74a478b786d..3af116c52e6 100644 --- a/kinds/dashboard/dashboard_kind.cue +++ b/kinds/dashboard/dashboard_kind.cue @@ -33,8 +33,9 @@ lineage: seqs: [ // Timezone of dashboard, timezone?: *"browser" | "utc" | "" @grafanamaturity(NeedsExpertReview) // Whether a dashboard is editable or not. - editable: bool | *true - graphTooltip: #DashboardCursorSync @grafanamaturity(NeedsExpertReview) + editable: bool | *true + // Configuration of dashboard cursor sync behavior. + graphTooltip: #DashboardCursorSync // Time range for dashboard, e.g. last 6 hours, last 7 days, etc time?: { from: string | *"now-6h" @@ -291,7 +292,7 @@ lineage: seqs: [ // 0 for no shared crosshair or tooltip (default). // 1 for shared crosshair. // 2 for shared crosshair AND shared tooltip. - #DashboardCursorSync: *0 | 1 | 2 @cuetsy(kind="enum",memberNames="Off|Crosshair|Tooltip") @grafanamaturity(NeedsExpertReview) + #DashboardCursorSync: *0 | 1 | 2 @cuetsy(kind="enum",memberNames="Off|Crosshair|Tooltip") // Schema for panel targets is specified by datasource // plugins. We use a placeholder definition, which the Go diff --git a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts index fad06ea56cf..4bf93c84df6 100644 --- a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts +++ b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts @@ -651,6 +651,9 @@ export interface Dashboard { */ fiscalYearStartMonth?: number; gnetId?: string; + /** + * Configuration of dashboard cursor sync behavior. + */ graphTooltip: DashboardCursorSync; /** * Unique numeric identifier for the dashboard. diff --git a/pkg/kindsys/report.json b/pkg/kindsys/report.json index 67d72700ab8..3b3aaddec33 100644 --- a/pkg/kindsys/report.json +++ b/pkg/kindsys/report.json @@ -283,7 +283,7 @@ 0, 0 ], - "grafanaMaturityCount": 144, + "grafanaMaturityCount": 142, "lineageIsGroup": false, "links": { "docs": "https://grafana.com/docs/grafana/next/developers/kinds/core/dashboard/schema-reference", From 143ee0c49f0d56bdca0849df10acfd03471faf05 Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Wed, 25 Jan 2023 13:39:54 +0100 Subject: [PATCH 021/172] Auth: Add skip_org_role_sync to GitLab OAuth (#62055) * Auth: Add skip_org_role_sync to GitLab OAuth - add: tests - docs added * Update pkg/login/social/gitlab_oauth.go Co-authored-by: Karl Persson * fix: for import Co-authored-by: Karl Persson --- conf/defaults.ini | 1 + conf/sample.ini | 1 + .../setup-grafana/configure-grafana/_index.md | 15 ++++++++++ .../configure-authentication/gitlab/index.md | 13 +++++++++ packages/grafana-data/src/types/config.ts | 1 + pkg/api/frontendsettings.go | 1 + pkg/login/social/gitlab_oauth.go | 28 +++++++++++++------ pkg/login/social/gitlab_oauth_test.go | 15 ++++++++++ pkg/login/social/social.go | 7 +++-- pkg/setting/setting.go | 11 ++++++++ public/app/features/admin/UserAdminPage.tsx | 5 +++- 11 files changed, 85 insertions(+), 13 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 2a5884efc9e..56fe0791c0d 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -546,6 +546,7 @@ allowed_groups = role_attribute_path = role_attribute_strict = false allow_assign_grafana_admin = false +skip_org_role_sync = false #################################### Google Auth ######################### [auth.google] diff --git a/conf/sample.ini b/conf/sample.ini index f509496244d..799151eb864 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -550,6 +550,7 @@ ;role_attribute_path = ;role_attribute_strict = false ;allow_assign_grafana_admin = false +;skip_org_role_sync = false #################################### Google Auth ########################## [auth.google] diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index dc936faa815..47f502a7da5 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -904,6 +904,21 @@ The following table shows the OAuth provider's setting with the default value an | Google | false | true | User organization roles are set with `defaultRole` and the org role can be changed for Google synced users. | | Google | true | true | User organization roles are set with `defaultRole` for Google. For other providers, the synchronization will be skipped, and the org role can be changed, along with other OAuth provider users' org roles. | +### [auth.gitlab] skip_org_role_sync + +When a user logs in the first time, Grafana sets the organization role based on the value specified in `AutoAssignOrgRole`. If you want to manage organization roles, set the `skip_org_role_sync` option to `true`. GitLab syncs organization roles and sets Grafana Admins. +This also impacts `allow_assign_grafana_admin` setting, by not syncing the grafana admin role from GitLab. + +> **Note:** There is a separate setting called `oauth_skip_org_role_update_sync` which has a different scope. While `skip_org_role_sync` only applies to the specific OAuth provider, `oauth_skip_org_role_update_sync` is a generic setting that affects all configured OAuth providers. + +The following table shows the OAuth provider's setting with the default value and the skip org role sync setting. +| OAuth Provider | `oauth_skip_org_role_sync_update` | `skip_org_role_sync` | Behavior | +| --- | --- | --- | --- | +| GitLab | false | false | User organization roles are set with `defaultRole` and cannot be changed | +| Github | true | false | User organization roles are set with `defaultRole` for GitLab, and Grafana Admins are set. For other providers, the synchronization is skipped, and the org role can be changed, along with other OAuth provider users' org roles. | +| GitLab | false | true | User organization roles are set with `defaultRole`, and the organization role can be changed for GitLab synced users. | +| GitLab | true | true | User organization roles are set with `defaultRole` for GitLab. For other providers, the synchronization is skipped, and the org role can be changed, along with other OAuth provider users' org roles. | + ### api_key_max_seconds_to_live Limit of API key seconds to live before expiration. Default is -1 (unlimited). diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md index 333e190083f..2fe7302ab1d 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md @@ -223,3 +223,16 @@ the correct teams. Your GitLab groups can be referenced in the same way as `allowed_groups`, like `example` or `foo/bar`. [Learn more about Team Sync]({{< relref "../../configure-team-sync/" >}}) + +## Skip organization role sync + +To prevent the sync of organization roles from GitLab, set `skip_org_role_sync` to `true`. This is useful if you want to manage the organization roles for your users from within Grafana. +This also impacts the `allow_assign_grafana_admin` setting by not syncing the Grafana admin role from GitLab. + +```ini +[auth.gitlab] +# .. +# prevents the sync of org roles from Github +skip_org_role_sync = true +`` +``` diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index a9668155e67..5ef7e3943d6 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -226,6 +226,7 @@ export interface AuthSettings { LDAPSkipOrgRoleSync?: boolean; JWTAuthSkipOrgRoleSync?: boolean; GrafanaComSkipOrgRoleSync?: boolean; + GitLabSkipOrgRoleSync?: boolean; AzureADSkipOrgRoleSync?: boolean; GoogleSkipOrgRoleSync?: boolean; DisableSyncLock?: boolean; diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 725b488acc9..9ceb984e53b 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -151,6 +151,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i "GoogleSkipOrgRoleSync": hs.Cfg.GoogleSkipOrgRoleSync, "JWTAuthSkipOrgRoleSync": hs.Cfg.JWTAuthSkipOrgRoleSync, "GrafanaComSkipOrgRoleSync": hs.Cfg.GrafanaComSkipOrgRoleSync, + "GitLabSkipOrgRoleSync": hs.Cfg.GitLabSkipOrgRoleSync, "AzureADSkipOrgRoleSync": hs.Cfg.AzureADSkipOrgRoleSync, "DisableSyncLock": hs.Cfg.DisableSyncLock, }, diff --git a/pkg/login/social/gitlab_oauth.go b/pkg/login/social/gitlab_oauth.go index c7cd9d51d13..5f0f48ee960 100644 --- a/pkg/login/social/gitlab_oauth.go +++ b/pkg/login/social/gitlab_oauth.go @@ -7,12 +7,15 @@ import ( "regexp" "golang.org/x/oauth2" + + "github.com/grafana/grafana/pkg/models/roletype" ) type SocialGitlab struct { *SocialBase - allowedGroups []string - apiUrl string + allowedGroups []string + apiUrl string + skipOrgRoleSync bool } func (s *SocialGitlab) IsGroupMember(groups []string) bool { @@ -107,14 +110,21 @@ func (s *SocialGitlab) UserInfo(client *http.Client, _ *oauth2.Token) (*BasicUse groups := s.GetGroups(client) - role, grafanaAdmin := s.extractRoleAndAdmin(response.Body, groups, true) - if s.roleAttributeStrict && !role.IsValid() { - return nil, &InvalidBasicRoleError{idP: "Gitlab", assignedRole: string(role)} - } - + var role roletype.RoleType var isGrafanaAdmin *bool = nil - if s.allowAssignGrafanaAdmin { - isGrafanaAdmin = &grafanaAdmin + if !s.skipOrgRoleSync { + var grafanaAdmin bool + role, grafanaAdmin = s.extractRoleAndAdmin(response.Body, groups, true) + if s.roleAttributeStrict && !role.IsValid() { + return nil, &InvalidBasicRoleError{idP: "Gitlab", assignedRole: string(role)} + } + + if s.allowAssignGrafanaAdmin { + isGrafanaAdmin = &grafanaAdmin + } + } + if s.allowAssignGrafanaAdmin && s.skipOrgRoleSync { + s.log.Debug("allowAssignGrafanaAdmin and skipOrgRoleSync are both set, Grafana Admin role will not be synced, consider setting one or the other") } userInfo := &BasicUserInfo{ diff --git a/pkg/login/social/gitlab_oauth_test.go b/pkg/login/social/gitlab_oauth_test.go index e95b4ed326e..188b23e7d44 100644 --- a/pkg/login/social/gitlab_oauth_test.go +++ b/pkg/login/social/gitlab_oauth_test.go @@ -27,16 +27,19 @@ const ( ) func TestSocialGitlab_UserInfo(t *testing.T) { + var nilPointer *bool provider := SocialGitlab{ SocialBase: &SocialBase{ log: newLogger("gitlab_oauth_test", "debug"), }, + skipOrgRoleSync: false, } type conf struct { AllowAssignGrafanaAdmin bool RoleAttributeStrict bool AutoAssignOrgRole org.RoleType + SkipOrgRoleSync bool } tests := []struct { @@ -83,6 +86,17 @@ func TestSocialGitlab_UserInfo(t *testing.T) { ExpectedRole: "Editor", ExpectedGrafanaAdmin: falseBoolPtr(), }, + { + Name: "Should not sync role, return empty role and nil pointer for GrafanaAdmin for skip org role sync set to true", + Cfg: conf{SkipOrgRoleSync: true}, + UserRespBody: editorUserRespBody, + GroupsRespBody: "[" + strings.Join([]string{viewerGroup, editorGroup}, ",") + "]", + RoleAttributePath: gitlabAttrPath, + ExpectedLogin: "gitlab-editor", + ExpectedEmail: "gitlab-editor@example.org", + ExpectedRole: "", + ExpectedGrafanaAdmin: nilPointer, + }, { // Case that's going to change with Grafana 10 Name: "No fallback to default org role (will change in Grafana 10)", Cfg: conf{AutoAssignOrgRole: org.RoleViewer}, @@ -126,6 +140,7 @@ func TestSocialGitlab_UserInfo(t *testing.T) { provider.allowAssignGrafanaAdmin = test.Cfg.AllowAssignGrafanaAdmin provider.autoAssignOrgRole = string(test.Cfg.AutoAssignOrgRole) provider.roleAttributeStrict = test.Cfg.RoleAttributeStrict + provider.skipOrgRoleSync = test.Cfg.SkipOrgRoleSync t.Run(test.Name, func(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/login/social/social.go b/pkg/login/social/social.go index bc3f68eaea1..5139360bab7 100644 --- a/pkg/login/social/social.go +++ b/pkg/login/social/social.go @@ -152,9 +152,10 @@ func ProvideService(cfg *setting.Cfg, features *featuremgmt.FeatureManager) *Soc // GitLab. if name == "gitlab" { ss.socialMap["gitlab"] = &SocialGitlab{ - SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features), - apiUrl: info.ApiUrl, - allowedGroups: util.SplitString(sec.Key("allowed_groups").String()), + SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features), + apiUrl: info.ApiUrl, + allowedGroups: util.SplitString(sec.Key("allowed_groups").String()), + skipOrgRoleSync: cfg.GitLabSkipOrgRoleSync, } } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 8fac999f44a..e351a070ce5 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -430,6 +430,9 @@ type Cfg struct { // Google GoogleSkipOrgRoleSync bool + // Gitlab + GitLabSkipOrgRoleSync bool + // LDAP LDAPEnabled bool LDAPSkipOrgRoleSync bool @@ -1377,6 +1380,11 @@ func readAuthGoogleSettings(iniFile *ini.File, cfg *Cfg) { cfg.GoogleSkipOrgRoleSync = sec.Key("skip_org_role_sync").MustBool(false) } +func readAuthGitlabSettings(iniFile *ini.File, cfg *Cfg) { + sec := iniFile.Section("auth.gitlab") + cfg.GitLabSkipOrgRoleSync = sec.Key("skip_org_role_sync").MustBool(false) +} + func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { auth := iniFile.Section("auth") @@ -1434,6 +1442,9 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { // Google Auth readAuthGoogleSettings(iniFile, cfg) + // GitLab Auth + readAuthGitlabSettings(iniFile, cfg) + // anonymous access AnonymousEnabled = iniFile.Section("auth.anonymous").Key("enabled").MustBool(false) cfg.AnonymousEnabled = AnonymousEnabled diff --git a/public/app/features/admin/UserAdminPage.tsx b/public/app/features/admin/UserAdminPage.tsx index 73b38f7c180..302e2ad96ca 100644 --- a/public/app/features/admin/UserAdminPage.tsx +++ b/public/app/features/admin/UserAdminPage.tsx @@ -39,7 +39,7 @@ interface OwnProps extends GrafanaRouteComponentProps<{ id: string }> { error?: UserAdminError; } -const SyncedOAuthLabels: string[] = ['GitHub', 'GitLab', 'OAuth']; +const SyncedOAuthLabels: string[] = ['GitHub', 'OAuth']; export class UserAdminPage extends PureComponent { async componentDidMount() { @@ -113,6 +113,7 @@ export class UserAdminPage extends PureComponent { user?.isExternal && user?.authLabels?.some((r) => SyncedOAuthLabels.includes(r)); const isSAMLUser = user?.isExternal && user?.authLabels?.includes('SAML'); const isGoogleUser = user?.isExternal && user?.authLabels?.includes('Google'); + const isGitLabUser = user?.isExternal && user?.authLabels?.includes('GitLab'); const isAuthProxyUser = user?.isExternal && user?.authLabels?.includes('Auth Proxy'); const isAzureADUser = user?.isExternal && user?.authLabels?.includes('AzureAD'); const isGrafanaComUser = user?.isExternal && user?.authLabels?.includes('grafana.com'); @@ -122,6 +123,7 @@ export class UserAdminPage extends PureComponent { !( isAuthProxyUser || isGoogleUser || + isGitLabUser || isOAuthUserWithSkippableSync || isSAMLUser || isLDAPUser || @@ -136,6 +138,7 @@ export class UserAdminPage extends PureComponent { // both OAuthSkipOrgRoleUpdateSync and specific provider settings needs to be false for a user to be synced (!config.auth.OAuthSkipOrgRoleUpdateSync && !config.auth.GrafanaComSkipOrgRoleSync && isGrafanaComUser) || (!config.auth.OAuthSkipOrgRoleUpdateSync && !config.auth.AzureADSkipOrgRoleSync && isAzureADUser) || + (!config.auth.OAuthSkipOrgRoleUpdateSync && !config.auth.GitLabSkipOrgRoleSync && isGitLabUser) || (!config.auth.OAuthSkipOrgRoleUpdateSync && !config.auth.GoogleSkipOrgRoleSync && isGoogleUser)); const pageNav: NavModelItem = { From c3cc236b561402afe7738f9de8312cdf92be2f3e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 25 Jan 2023 12:55:00 +0000 Subject: [PATCH 022/172] Update dependency eslint to v8.32.0 (#61894) * Update dependency eslint to v8.32.0 * update sdk Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Ashley Harrison --- .yarn/sdks/eslint/package.json | 2 +- package.json | 2 +- yarn.lock | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.yarn/sdks/eslint/package.json b/.yarn/sdks/eslint/package.json index 379e652a6e4..c0e941b7605 100644 --- a/.yarn/sdks/eslint/package.json +++ b/.yarn/sdks/eslint/package.json @@ -1,6 +1,6 @@ { "name": "eslint", - "version": "8.30.0-sdk", + "version": "8.32.0-sdk", "main": "./lib/api.js", "type": "commonjs" } diff --git a/package.json b/package.json index 4c9e41870cc..cb962ccccab 100644 --- a/package.json +++ b/package.json @@ -179,7 +179,7 @@ "esbuild": "0.16.17", "esbuild-loader": "2.21.0", "esbuild-plugin-browserslist": "^0.6.0", - "eslint": "8.31.0", + "eslint": "8.32.0", "eslint-config-prettier": "8.6.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jest": "27.2.1", diff --git a/yarn.lock b/yarn.lock index ec9eccf3b51..83a78188c02 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19561,9 +19561,9 @@ __metadata: languageName: node linkType: hard -"eslint@npm:8.31.0": - version: 8.31.0 - resolution: "eslint@npm:8.31.0" +"eslint@npm:8.32.0": + version: 8.32.0 + resolution: "eslint@npm:8.32.0" dependencies: "@eslint/eslintrc": ^1.4.1 "@humanwhocodes/config-array": ^0.11.8 @@ -19606,7 +19606,7 @@ __metadata: text-table: ^0.2.0 bin: eslint: bin/eslint.js - checksum: 5e5688bb864edc6b12d165849994812eefa67fb3fc44bb26f53659b63edcd8bcc68389d27cc6cc9e5b79ee22f24b6f311fa3ed047bddcafdec7d84c1b5561e4f + checksum: 23c8fb3c57291eecd9c1448faf603226a8f885022a2cd96e303459bf72e39b7f54987c6fb948f0f9eecaf7085600e6eb0663482a35ea83da12e9f9141a22b91e languageName: node linkType: hard @@ -21751,7 +21751,7 @@ __metadata: esbuild: 0.16.17 esbuild-loader: 2.21.0 esbuild-plugin-browserslist: ^0.6.0 - eslint: 8.31.0 + eslint: 8.32.0 eslint-config-prettier: 8.6.0 eslint-plugin-import: ^2.26.0 eslint-plugin-jest: 27.2.1 From bd4e3f0d169093d8b4d7dfddcaac40f9e3e35d86 Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Wed, 25 Jan 2023 13:23:18 +0000 Subject: [PATCH 023/172] API: Correctly use new grafana_com.api_url setting in /api/gnet proxy (#60893) During the review of the initial PR adding this (#59506) I removed a new global variable from the setting package, but forgot to update the reference to the new setting, so the API URL wasn't actually being used. This PR updates the proxy endpoint to use the API URL correctly. Aside: I'm not a huge fan of how the error is being ignored when parsing the URL, but I think that should be addressed in a separate PR if anyone has a suggestion for how we should handle it. (Should we check that the URL is valid when parsing config?) --- pkg/api/grafana_com_proxy.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/api/grafana_com_proxy.go b/pkg/api/grafana_com_proxy.go index db974883d49..ca9cf6a71bb 100644 --- a/pkg/api/grafana_com_proxy.go +++ b/pkg/api/grafana_com_proxy.go @@ -9,7 +9,6 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/proxyutil" "github.com/grafana/grafana/pkg/web" @@ -24,8 +23,8 @@ var grafanaComProxyTransport = &http.Transport{ TLSHandshakeTimeout: 10 * time.Second, } -func ReverseProxyGnetReq(logger log.Logger, proxyPath string, version string) *httputil.ReverseProxy { - url, _ := url.Parse(setting.GrafanaComUrl) +func ReverseProxyGnetReq(logger log.Logger, proxyPath string, version string, grafanaComUrl string) *httputil.ReverseProxy { + url, _ := url.Parse(grafanaComUrl) director := func(req *http.Request) { req.URL.Scheme = url.Scheme @@ -48,7 +47,7 @@ func ReverseProxyGnetReq(logger log.Logger, proxyPath string, version string) *h func (hs *HTTPServer) ProxyGnetRequest(c *models.ReqContext) { proxyPath := web.Params(c.Req)["*"] - proxy := ReverseProxyGnetReq(c.Logger, proxyPath, hs.Cfg.BuildVersion) + proxy := ReverseProxyGnetReq(c.Logger, proxyPath, hs.Cfg.BuildVersion, hs.Cfg.GrafanaComAPIURL) proxy.Transport = grafanaComProxyTransport proxy.ServeHTTP(c.Resp, c.Req) } From 85bf098fdbfd2bdf16b87bec9eaa1634781e3621 Mon Sep 17 00:00:00 2001 From: Alexa V <239999+axelavargas@users.noreply.github.com> Date: Wed, 25 Jan 2023 13:28:13 +0000 Subject: [PATCH 024/172] Dashboard schema: Review and mature fiscalYearStartMonth property (#62105) --- .../developers/kinds/core/dashboard/schema-reference.md | 2 +- kinds/dashboard/dashboard_kind.cue | 4 ++-- .../grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts | 3 ++- pkg/kinds/dashboard/dashboard_types_gen.go | 2 +- pkg/kindsys/report.json | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/sources/developers/kinds/core/dashboard/schema-reference.md b/docs/sources/developers/kinds/core/dashboard/schema-reference.md index 695d39fcc96..772f595b369 100644 --- a/docs/sources/developers/kinds/core/dashboard/schema-reference.md +++ b/docs/sources/developers/kinds/core/dashboard/schema-reference.md @@ -22,7 +22,7 @@ title: Dashboard kind | `style` | string | **Yes** | Theme of dashboard. Possible values are: `dark`, `light`. Default: `dark`. | | `annotations` | [object](#annotations) | No | TODO docs | | `description` | string | No | Description of dashboard. | -| `fiscalYearStartMonth` | integer | No | TODO docs | +| `fiscalYearStartMonth` | integer | No | The month that the fiscal year starts on. 0 = January, 11 = December Default: `0`. | | `gnetId` | string | No | | | `id` | integer | No | Unique numeric identifier for the dashboard.
TODO must isolate or remove identifiers local to a Grafana instance...? | | `links` | [DashboardLink](#dashboardlink)[] | No | TODO docs | diff --git a/kinds/dashboard/dashboard_kind.cue b/kinds/dashboard/dashboard_kind.cue index 3af116c52e6..e27e1364109 100644 --- a/kinds/dashboard/dashboard_kind.cue +++ b/kinds/dashboard/dashboard_kind.cue @@ -56,8 +56,8 @@ lineage: seqs: [ // TODO docs time_options: [...string] | *["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] } @grafanamaturity(NeedsExpertReview) - // TODO docs - fiscalYearStartMonth?: uint8 & <13 @grafanamaturity(NeedsExpertReview) + // The month that the fiscal year starts on. 0 = January, 11 = December + fiscalYearStartMonth?: uint8 & <12 | *0 // TODO docs liveNow?: bool @grafanamaturity(NeedsExpertReview) // TODO docs diff --git a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts index 4bf93c84df6..d09b8ab7c0e 100644 --- a/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts +++ b/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts @@ -647,7 +647,7 @@ export interface Dashboard { */ editable: boolean; /** - * TODO docs + * The month that the fiscal year starts on. 0 = January, 11 = December */ fiscalYearStartMonth?: number; gnetId?: string; @@ -800,6 +800,7 @@ export interface Dashboard { export const defaultDashboard: Partial = { editable: true, + fiscalYearStartMonth: 0, graphTooltip: DashboardCursorSync.Off, links: [], panels: [], diff --git a/pkg/kinds/dashboard/dashboard_types_gen.go b/pkg/kinds/dashboard/dashboard_types_gen.go index c14456ce35a..2e65c507032 100644 --- a/pkg/kinds/dashboard/dashboard_types_gen.go +++ b/pkg/kinds/dashboard/dashboard_types_gen.go @@ -684,7 +684,7 @@ type Dashboard struct { // Whether a dashboard is editable or not. Editable bool `json:"editable"` - // TODO docs + // The month that the fiscal year starts on. 0 = January, 11 = December FiscalYearStartMonth *int `json:"fiscalYearStartMonth,omitempty"` GnetId *string `json:"gnetId,omitempty"` diff --git a/pkg/kindsys/report.json b/pkg/kindsys/report.json index 3b3aaddec33..7ed9ccdc6b4 100644 --- a/pkg/kindsys/report.json +++ b/pkg/kindsys/report.json @@ -283,7 +283,7 @@ 0, 0 ], - "grafanaMaturityCount": 142, + "grafanaMaturityCount": 141, "lineageIsGroup": false, "links": { "docs": "https://grafana.com/docs/grafana/next/developers/kinds/core/dashboard/schema-reference", From 0d7e303809412f1c7339aa6d54d5130089b9556a Mon Sep 17 00:00:00 2001 From: Timur Olzhabayev Date: Wed, 25 Jan 2023 15:07:59 +0100 Subject: [PATCH 025/172] Chore: Keeping the list of plugin executable complete (#60840) * Keeping the list of executable complete * Update pkg/plugins/storage/fs.go Co-authored-by: Will Browne Co-authored-by: Will Browne --- pkg/plugins/storage/fs.go | 2 +- pkg/plugins/storage/fs_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/plugins/storage/fs.go b/pkg/plugins/storage/fs.go index 302305a1196..e1bf0b99163 100644 --- a/pkg/plugins/storage/fs.go +++ b/pkg/plugins/storage/fs.go @@ -220,7 +220,7 @@ func isSymlinkRelativeTo(basePath string, symlinkDestPath string, symlinkOrigPat func extractFile(file *zip.File, filePath string) (err error) { fileMode := file.Mode() // This is entry point for backend plugins so we want to make them executable - if strings.HasSuffix(filePath, "_linux_amd64") || strings.HasSuffix(filePath, "_darwin_amd64") { + if strings.HasSuffix(filePath, "_linux_amd64") || strings.HasSuffix(filePath, "_linux_arm") || strings.HasSuffix(filePath, "_linux_arm64") || strings.HasSuffix(filePath, "_darwin_amd64") || strings.HasSuffix(filePath, "_darwin_arm64") || strings.HasSuffix(filePath, "_windows_amd64.exe") { fileMode = os.FileMode(0755) } diff --git a/pkg/plugins/storage/fs_test.go b/pkg/plugins/storage/fs_test.go index 044bb0ede12..e55e69842e2 100644 --- a/pkg/plugins/storage/fs_test.go +++ b/pkg/plugins/storage/fs_test.go @@ -157,7 +157,7 @@ func TestExtractFiles(t *testing.T) { // File in zip has permission 644 fileInfo, err = os.Stat(pluginsDir + "/grafana-simple-json-datasource/simple-plugin_windows_amd64.exe") require.NoError(t, err) - require.Equal(t, "-rw-r--r--", fileInfo.Mode().String()) + require.Equal(t, "-rwxr-xr-x", fileInfo.Mode().String()) // File in zip has permission 755 fileInfo, err = os.Stat(pluginsDir + "/grafana-simple-json-datasource/non-plugin-binary") From 13de1afcbe58897091c8e7a9c707655032f8b453 Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Wed, 25 Jan 2023 09:09:27 -0500 Subject: [PATCH 026/172] API: Change how Cache-Control and related headers are set (#62021) - change Cache-Control from no-cache to no-store - do not set (and remove if set) older Pragma/Expires --- pkg/middleware/middleware.go | 6 +++--- pkg/middleware/middleware_test.go | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 940fd5e36c1..9bb6c6cb835 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -69,9 +69,9 @@ func addSecurityHeaders(w web.ResponseWriter, cfg *setting.Cfg) { } func addNoCacheHeaders(w web.ResponseWriter) { - w.Header().Set("Cache-Control", "no-cache") - w.Header().Set("Pragma", "no-cache") - w.Header().Set("Expires", "-1") + w.Header().Set("Cache-Control", "no-store") + w.Header().Del("Pragma") + w.Header().Del("Expires") } func addXFrameOptionsDenyHeader(w web.ResponseWriter) { diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index cd522c12136..8ee2ad64a19 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -128,7 +128,7 @@ func TestMiddleWareContentSecurityPolicyHeaders(t *testing.T) { } func TestMiddlewareContext(t *testing.T) { - const noCache = "no-cache" + const noStore = "no-store" configureJWTAuthHeader := func(cfg *setting.Cfg) { cfg.JWTAuthEnabled = true @@ -147,9 +147,9 @@ func TestMiddlewareContext(t *testing.T) { middlewareScenario(t, "middleware should add Cache-Control header for requests to API", func(t *testing.T, sc *scenarioContext) { sc.fakeReq("GET", "/api/search").exec() - assert.Equal(t, noCache, sc.resp.Header().Get("Cache-Control")) - assert.Equal(t, noCache, sc.resp.Header().Get("Pragma")) - assert.Equal(t, "-1", sc.resp.Header().Get("Expires")) + assert.Equal(t, noStore, sc.resp.Header().Get("Cache-Control")) + assert.Empty(t, sc.resp.Header().Get("Pragma")) + assert.Empty(t, sc.resp.Header().Get("Expires")) }) middlewareScenario(t, "middleware should not add Cache-Control header for requests to datasource proxy API", func( @@ -175,9 +175,9 @@ func TestMiddlewareContext(t *testing.T) { } sc.fakeReq("GET", "/").exec() require.Equal(t, 200, sc.resp.Code) - assert.Equal(t, noCache, sc.resp.Header().Get("Cache-Control")) - assert.Equal(t, noCache, sc.resp.Header().Get("Pragma")) - assert.Equal(t, "-1", sc.resp.Header().Get("Expires")) + assert.Equal(t, noStore, sc.resp.Header().Get("Cache-Control")) + assert.Empty(t, sc.resp.Header().Get("Pragma")) + assert.Empty(t, sc.resp.Header().Get("Expires")) }) middlewareScenario(t, "middleware should add X-Frame-Options header with deny for request when not allowing embedding", func( From 529e6c379fb2ef3dd81eee0b6e3be1277db26ce3 Mon Sep 17 00:00:00 2001 From: idafurjes <36131195+idafurjes@users.noreply.github.com> Date: Wed, 25 Jan 2023 15:09:44 +0100 Subject: [PATCH 027/172] Chore: Remove Result field from dashboard snapshot mode (#62089) Chore: Remove Result field from dashboard snapshot mode; --- pkg/api/dashboard_snapshot.go | 61 ++++++++------- pkg/api/dashboard_snapshot_test.go | 39 +++++----- .../dashboardsnapshots/database/database.go | 47 +++++++---- .../database/database_test.go | 78 +++++++++---------- pkg/services/dashboardsnapshots/models.go | 34 ++++---- pkg/services/dashboardsnapshots/service.go | 6 +- .../dashboardsnapshots/service/service.go | 26 +++---- .../service/service_test.go | 8 +- .../dashboardsnapshots/service_mock.go | 69 +++++++++++----- pkg/services/dashboardsnapshots/store.go | 6 +- pkg/services/export/entity_store.go | 18 ++--- pkg/services/export/export_snapshots.go | 10 +-- 12 files changed, 220 insertions(+), 182 deletions(-) diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index ff7d8da6124..aa8033a2d5c 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -114,9 +114,9 @@ func (hs *HTTPServer) CreateDashboardSnapshot(c *models.ReqContext) response.Res } var snapshotUrl string - cmd.ExternalUrl = "" - cmd.OrgId = c.OrgID - cmd.UserId = c.UserID + cmd.ExternalURL = "" + cmd.OrgID = c.OrgID + cmd.UserID = c.UserID originalDashboardURL, err := createOriginalDashboardURL(hs.Cfg.AppURL, &cmd) if err != nil { return response.Error(http.StatusInternalServerError, "Invalid app URL", err) @@ -137,8 +137,8 @@ func (hs *HTTPServer) CreateDashboardSnapshot(c *models.ReqContext) response.Res snapshotUrl = response.Url cmd.Key = response.Key cmd.DeleteKey = response.DeleteKey - cmd.ExternalUrl = response.Url - cmd.ExternalDeleteUrl = response.DeleteUrl + cmd.ExternalURL = response.Url + cmd.ExternalDeleteURL = response.DeleteUrl cmd.Dashboard = simplejson.New() metrics.MApiDashboardSnapshotExternal.Inc() @@ -168,7 +168,8 @@ func (hs *HTTPServer) CreateDashboardSnapshot(c *models.ReqContext) response.Res metrics.MApiDashboardSnapshotCreate.Inc() } - if err := hs.dashboardsnapshotsService.CreateDashboardSnapshot(c.Req.Context(), &cmd); err != nil { + result, err := hs.dashboardsnapshotsService.CreateDashboardSnapshot(c.Req.Context(), &cmd) + if err != nil { c.JsonApiErr(http.StatusInternalServerError, "Failed to create snapshot", err) return nil } @@ -178,7 +179,7 @@ func (hs *HTTPServer) CreateDashboardSnapshot(c *models.ReqContext) response.Res "deleteKey": cmd.DeleteKey, "url": snapshotUrl, "deleteUrl": setting.ToAbsUrl("api/snapshots-delete/" + cmd.DeleteKey), - "id": cmd.Result.Id, + "id": result.ID, }) return nil } @@ -201,12 +202,12 @@ func (hs *HTTPServer) GetDashboardSnapshot(c *models.ReqContext) response.Respon query := &dashboardsnapshots.GetDashboardSnapshotQuery{Key: key} - err := hs.dashboardsnapshotsService.GetDashboardSnapshot(c.Req.Context(), query) + queryResult, err := hs.dashboardsnapshotsService.GetDashboardSnapshot(c.Req.Context(), query) if err != nil { return response.Err(err) } - snapshot := query.Result + snapshot := queryResult // expired snapshots should also be removed from db if snapshot.Expires.Before(time.Now()) { @@ -279,19 +280,19 @@ func (hs *HTTPServer) DeleteDashboardSnapshotByDeleteKey(c *models.ReqContext) r } query := &dashboardsnapshots.GetDashboardSnapshotQuery{DeleteKey: key} - err := hs.dashboardsnapshotsService.GetDashboardSnapshot(c.Req.Context(), query) + queryResult, err := hs.dashboardsnapshotsService.GetDashboardSnapshot(c.Req.Context(), query) if err != nil { return response.Err(err) } - if query.Result.External { - err := deleteExternalDashboardSnapshot(query.Result.ExternalDeleteUrl) + if queryResult.External { + err := deleteExternalDashboardSnapshot(queryResult.ExternalDeleteURL) if err != nil { return response.Error(500, "Failed to delete external dashboard", err) } } - cmd := &dashboardsnapshots.DeleteDashboardSnapshotCommand{DeleteKey: query.Result.DeleteKey} + cmd := &dashboardsnapshots.DeleteDashboardSnapshotCommand{DeleteKey: queryResult.DeleteKey} if err := hs.dashboardsnapshotsService.DeleteDashboardSnapshot(c.Req.Context(), cmd); err != nil { return response.Error(500, "Failed to delete dashboard snapshot", err) @@ -299,7 +300,7 @@ func (hs *HTTPServer) DeleteDashboardSnapshotByDeleteKey(c *models.ReqContext) r return response.JSON(http.StatusOK, util.DynMap{ "message": "Snapshot deleted. It might take an hour before it's cleared from any CDN caches.", - "id": query.Result.Id, + "id": queryResult.ID, }) } @@ -320,16 +321,16 @@ func (hs *HTTPServer) DeleteDashboardSnapshot(c *models.ReqContext) response.Res query := &dashboardsnapshots.GetDashboardSnapshotQuery{Key: key} - err := hs.dashboardsnapshotsService.GetDashboardSnapshot(c.Req.Context(), query) + queryResult, err := hs.dashboardsnapshotsService.GetDashboardSnapshot(c.Req.Context(), query) if err != nil { return response.Err(err) } - if query.Result == nil { + if queryResult == nil { return response.Error(http.StatusNotFound, "Failed to get dashboard snapshot", nil) } - if query.Result.External { - err := deleteExternalDashboardSnapshot(query.Result.ExternalDeleteUrl) + if queryResult.External { + err := deleteExternalDashboardSnapshot(queryResult.ExternalDeleteURL) if err != nil { return response.Error(http.StatusInternalServerError, "Failed to delete external dashboard", err) } @@ -339,7 +340,7 @@ func (hs *HTTPServer) DeleteDashboardSnapshot(c *models.ReqContext) response.Res // which before RBAC would result in a dashboard which has no ACL. A dashboard without an ACL would fallback // to the user’s org role, which for editors and admins would essentially always be allowed here. With RBAC, // all permissions must be explicit, so the lack of a rule for dashboard 0 means the guardian will reject. - dashboardID := query.Result.Dashboard.Get("id").MustInt64() + dashboardID := queryResult.Dashboard.Get("id").MustInt64() if dashboardID != 0 { guardian, err := guardian.New(c.Req.Context(), dashboardID, c.OrgID, c.SignedInUser) @@ -353,12 +354,12 @@ func (hs *HTTPServer) DeleteDashboardSnapshot(c *models.ReqContext) response.Res return response.Error(http.StatusInternalServerError, "Error while checking permissions for snapshot", err) } - if !canEdit && query.Result.UserId != c.SignedInUser.UserID && !errors.Is(err, dashboards.ErrDashboardNotFound) { + if !canEdit && queryResult.UserID != c.SignedInUser.UserID && !errors.Is(err, dashboards.ErrDashboardNotFound) { return response.Error(http.StatusForbidden, "Access denied to this snapshot", nil) } } - cmd := &dashboardsnapshots.DeleteDashboardSnapshotCommand{DeleteKey: query.Result.DeleteKey} + cmd := &dashboardsnapshots.DeleteDashboardSnapshotCommand{DeleteKey: queryResult.DeleteKey} if err := hs.dashboardsnapshotsService.DeleteDashboardSnapshot(c.Req.Context(), cmd); err != nil { return response.Error(http.StatusInternalServerError, "Failed to delete dashboard snapshot", err) @@ -366,7 +367,7 @@ func (hs *HTTPServer) DeleteDashboardSnapshot(c *models.ReqContext) response.Res return response.JSON(http.StatusOK, util.DynMap{ "message": "Snapshot deleted. It might take an hour before it's cleared from any CDN caches.", - "id": query.Result.Id, + "id": queryResult.ID, }) } @@ -388,25 +389,25 @@ func (hs *HTTPServer) SearchDashboardSnapshots(c *models.ReqContext) response.Re searchQuery := dashboardsnapshots.GetDashboardSnapshotsQuery{ Name: query, Limit: limit, - OrgId: c.OrgID, + OrgID: c.OrgID, SignedInUser: c.SignedInUser, } - err := hs.dashboardsnapshotsService.SearchDashboardSnapshots(c.Req.Context(), &searchQuery) + searchQueryResult, err := hs.dashboardsnapshotsService.SearchDashboardSnapshots(c.Req.Context(), &searchQuery) if err != nil { return response.Error(500, "Search failed", err) } - dtos := make([]*dashboardsnapshots.DashboardSnapshotDTO, len(searchQuery.Result)) - for i, snapshot := range searchQuery.Result { + dtos := make([]*dashboardsnapshots.DashboardSnapshotDTO, len(searchQueryResult)) + for i, snapshot := range searchQueryResult { dtos[i] = &dashboardsnapshots.DashboardSnapshotDTO{ - Id: snapshot.Id, + ID: snapshot.ID, Name: snapshot.Name, Key: snapshot.Key, - OrgId: snapshot.OrgId, - UserId: snapshot.UserId, + OrgID: snapshot.OrgID, + UserID: snapshot.UserID, External: snapshot.External, - ExternalUrl: snapshot.ExternalUrl, + ExternalURL: snapshot.ExternalURL, Expires: snapshot.Expires, Created: snapshot.Created, Updated: snapshot.Updated, diff --git a/pkg/api/dashboard_snapshot_test.go b/pkg/api/dashboard_snapshot_test.go index f6afee1cf02..a80eec50d8b 100644 --- a/pkg/api/dashboard_snapshot_test.go +++ b/pkg/api/dashboard_snapshot_test.go @@ -41,25 +41,22 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { dashSnapSvc := dashboardsnapshots.NewMockService(t) dashSnapSvc.On("DeleteDashboardSnapshot", mock.Anything, mock.AnythingOfType("*dashboardsnapshots.DeleteDashboardSnapshotCommand")).Return(nil).Maybe() - dashSnapSvc.On("GetDashboardSnapshot", mock.Anything, mock.AnythingOfType("*dashboardsnapshots.GetDashboardSnapshotQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboardsnapshots.GetDashboardSnapshotQuery) - res := &dashboardsnapshots.DashboardSnapshot{ - Id: 1, - Key: "12345", - DeleteKey: "54321", - Dashboard: jsonModel, - Expires: time.Now().Add(time.Duration(1000) * time.Second), - UserId: 999999, - } - if userId != 0 { - res.UserId = userId - } - if deleteUrl != "" { - res.External = true - res.ExternalDeleteUrl = deleteUrl - } - q.Result = res - }).Return(nil) + res := &dashboardsnapshots.DashboardSnapshot{ + ID: 1, + Key: "12345", + DeleteKey: "54321", + Dashboard: jsonModel, + Expires: time.Now().Add(time.Duration(1000) * time.Second), + UserID: 999999, + } + if userId != 0 { + res.UserID = userId + } + if deleteUrl != "" { + res.External = true + res.ExternalDeleteURL = deleteUrl + } + dashSnapSvc.On("GetDashboardSnapshot", mock.Anything, mock.AnythingOfType("*dashboardsnapshots.GetDashboardSnapshotQuery")).Return(res, nil) dashSnapSvc.On("DeleteDashboardSnapshot", mock.Anything, mock.AnythingOfType("*dashboardsnapshots.DeleteDashboardSnapshotCommand")).Return(nil).Maybe() return dashSnapSvc } @@ -256,7 +253,7 @@ func TestGetDashboardSnapshotNotFound(t *testing.T) { dashSnapSvc. On("GetDashboardSnapshot", mock.Anything, mock.AnythingOfType("*dashboardsnapshots.GetDashboardSnapshotQuery")). Run(func(args mock.Arguments) {}). - Return(dashboardsnapshots.ErrBaseNotFound.Errorf("")) + Return(nil, dashboardsnapshots.ErrBaseNotFound.Errorf("")) return dashSnapSvc } @@ -305,7 +302,7 @@ func TestGetDashboardSnapshotFailure(t *testing.T) { dashSnapSvc. On("GetDashboardSnapshot", mock.Anything, mock.AnythingOfType("*dashboardsnapshots.GetDashboardSnapshotQuery")). Run(func(args mock.Arguments) {}). - Return(errors.New("something went wrong")) + Return(nil, errors.New("something went wrong")) return dashSnapSvc } diff --git a/pkg/services/dashboardsnapshots/database/database.go b/pkg/services/dashboardsnapshots/database/database.go index 443f1ea9077..a303b3810a1 100644 --- a/pkg/services/dashboardsnapshots/database/database.go +++ b/pkg/services/dashboardsnapshots/database/database.go @@ -45,8 +45,9 @@ func (d *DashboardSnapshotStore) DeleteExpiredSnapshots(ctx context.Context, cmd }) } -func (d *DashboardSnapshotStore) CreateDashboardSnapshot(ctx context.Context, cmd *dashboardsnapshots.CreateDashboardSnapshotCommand) error { - return d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error { +func (d *DashboardSnapshotStore) CreateDashboardSnapshot(ctx context.Context, cmd *dashboardsnapshots.CreateDashboardSnapshotCommand) (*dashboardsnapshots.DashboardSnapshot, error) { + var result *dashboardsnapshots.DashboardSnapshot + err := d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error { var expires = time.Now().Add(time.Hour * 24 * 365 * 50) if cmd.Expires > 0 { expires = time.Now().Add(time.Second * time.Duration(cmd.Expires)) @@ -56,11 +57,11 @@ func (d *DashboardSnapshotStore) CreateDashboardSnapshot(ctx context.Context, cm Name: cmd.Name, Key: cmd.Key, DeleteKey: cmd.DeleteKey, - OrgId: cmd.OrgId, - UserId: cmd.UserId, + OrgID: cmd.OrgID, + UserID: cmd.UserID, External: cmd.External, - ExternalUrl: cmd.ExternalUrl, - ExternalDeleteUrl: cmd.ExternalDeleteUrl, + ExternalURL: cmd.ExternalURL, + ExternalDeleteURL: cmd.ExternalDeleteURL, Dashboard: simplejson.New(), DashboardEncrypted: cmd.DashboardEncrypted, Expires: expires, @@ -68,10 +69,14 @@ func (d *DashboardSnapshotStore) CreateDashboardSnapshot(ctx context.Context, cm Updated: time.Now(), } _, err := sess.Insert(snapshot) - cmd.Result = snapshot + result = snapshot return err }) + if err != nil { + return nil, err + } + return result, nil } func (d *DashboardSnapshotStore) DeleteDashboardSnapshot(ctx context.Context, cmd *dashboardsnapshots.DeleteDashboardSnapshotCommand) error { @@ -82,8 +87,9 @@ func (d *DashboardSnapshotStore) DeleteDashboardSnapshot(ctx context.Context, cm }) } -func (d *DashboardSnapshotStore) GetDashboardSnapshot(ctx context.Context, query *dashboardsnapshots.GetDashboardSnapshotQuery) error { - return d.store.WithDbSession(ctx, func(sess *db.Session) error { +func (d *DashboardSnapshotStore) GetDashboardSnapshot(ctx context.Context, query *dashboardsnapshots.GetDashboardSnapshotQuery) (*dashboardsnapshots.DashboardSnapshot, error) { + var queryResult *dashboardsnapshots.DashboardSnapshot + err := d.store.WithDbSession(ctx, func(sess *db.Session) error { snapshot := dashboardsnapshots.DashboardSnapshot{Key: query.Key, DeleteKey: query.DeleteKey} has, err := sess.Get(&snapshot) @@ -93,15 +99,20 @@ func (d *DashboardSnapshotStore) GetDashboardSnapshot(ctx context.Context, query return dashboardsnapshots.ErrBaseNotFound.Errorf("dashboard snapshot not found") } - query.Result = &snapshot + queryResult = &snapshot return nil }) + if err != nil { + return nil, err + } + return queryResult, nil } // SearchDashboardSnapshots returns a list of all snapshots for admins // for other roles, it returns snapshots created by the user -func (d *DashboardSnapshotStore) SearchDashboardSnapshots(ctx context.Context, query *dashboardsnapshots.GetDashboardSnapshotsQuery) error { - return d.store.WithDbSession(ctx, func(sess *db.Session) error { +func (d *DashboardSnapshotStore) SearchDashboardSnapshots(ctx context.Context, query *dashboardsnapshots.GetDashboardSnapshotsQuery) (dashboardsnapshots.DashboardSnapshotsList, error) { + var queryResult dashboardsnapshots.DashboardSnapshotsList + err := d.store.WithDbSession(ctx, func(sess *db.Session) error { var snapshots = make(dashboardsnapshots.DashboardSnapshotsList, 0) if query.Limit > 0 { sess.Limit(query.Limit) @@ -115,16 +126,20 @@ func (d *DashboardSnapshotStore) SearchDashboardSnapshots(ctx context.Context, q // admins can see all snapshots, everyone else can only see their own snapshots switch { case query.SignedInUser.OrgRole == org.RoleAdmin: - sess.Where("org_id = ?", query.OrgId) + sess.Where("org_id = ?", query.OrgID) case !query.SignedInUser.IsAnonymous: - sess.Where("org_id = ? AND user_id = ?", query.OrgId, query.SignedInUser.UserID) + sess.Where("org_id = ? AND user_id = ?", query.OrgID, query.SignedInUser.UserID) default: - query.Result = snapshots + queryResult = snapshots return nil } err := sess.Find(&snapshots) - query.Result = snapshots + queryResult = snapshots return err }) + if err != nil { + return dashboardsnapshots.DashboardSnapshotsList{}, err + } + return queryResult, nil } diff --git a/pkg/services/dashboardsnapshots/database/database_test.go b/pkg/services/dashboardsnapshots/database/database_test.go index 5d8d052175a..6df8b42bcca 100644 --- a/pkg/services/dashboardsnapshots/database/database_test.go +++ b/pkg/services/dashboardsnapshots/database/database_test.go @@ -43,23 +43,23 @@ func TestIntegrationDashboardSnapshotDBAccess(t *testing.T) { cmd := dashboardsnapshots.CreateDashboardSnapshotCommand{ Key: "hej", DashboardEncrypted: encryptedDashboard, - UserId: 1000, - OrgId: 1, + UserID: 1000, + OrgID: 1, } - err = dashStore.CreateDashboardSnapshot(context.Background(), &cmd) + result, err := dashStore.CreateDashboardSnapshot(context.Background(), &cmd) require.NoError(t, err) t.Run("Should be able to get snapshot by key", func(t *testing.T) { query := dashboardsnapshots.GetDashboardSnapshotQuery{Key: "hej"} - err := dashStore.GetDashboardSnapshot(context.Background(), &query) + queryResult, err := dashStore.GetDashboardSnapshot(context.Background(), &query) require.NoError(t, err) - assert.NotNil(t, query.Result) + assert.NotNil(t, queryResult) decryptedDashboard, err := secretsService.Decrypt( context.Background(), - query.Result.DashboardEncrypted, + queryResult.DashboardEncrypted, ) require.NoError(t, err) @@ -71,43 +71,43 @@ func TestIntegrationDashboardSnapshotDBAccess(t *testing.T) { t.Run("And the user has the admin role", func(t *testing.T) { query := dashboardsnapshots.GetDashboardSnapshotsQuery{ - OrgId: 1, + OrgID: 1, SignedInUser: &user.SignedInUser{OrgRole: org.RoleAdmin}, } - err := dashStore.SearchDashboardSnapshots(context.Background(), &query) + queryResult, err := dashStore.SearchDashboardSnapshots(context.Background(), &query) require.NoError(t, err) t.Run("Should return all the snapshots", func(t *testing.T) { - assert.NotNil(t, query.Result) - assert.Len(t, query.Result, 1) + assert.NotNil(t, queryResult) + assert.Len(t, queryResult, 1) }) }) t.Run("And the user has the editor role and has created a snapshot", func(t *testing.T) { query := dashboardsnapshots.GetDashboardSnapshotsQuery{ - OrgId: 1, + OrgID: 1, SignedInUser: &user.SignedInUser{OrgRole: org.RoleEditor, UserID: 1000}, } - err := dashStore.SearchDashboardSnapshots(context.Background(), &query) + queryResult, err := dashStore.SearchDashboardSnapshots(context.Background(), &query) require.NoError(t, err) t.Run("Should return all the snapshots", func(t *testing.T) { - require.NotNil(t, query.Result) - assert.Len(t, query.Result, 1) + require.NotNil(t, queryResult) + assert.Len(t, queryResult, 1) }) }) t.Run("And the user has the editor role and has not created any snapshot", func(t *testing.T) { query := dashboardsnapshots.GetDashboardSnapshotsQuery{ - OrgId: 1, + OrgID: 1, SignedInUser: &user.SignedInUser{OrgRole: org.RoleEditor, UserID: 2}, } - err := dashStore.SearchDashboardSnapshots(context.Background(), &query) + queryResult, err := dashStore.SearchDashboardSnapshots(context.Background(), &query) require.NoError(t, err) t.Run("Should not return any snapshots", func(t *testing.T) { - require.NotNil(t, query.Result) - assert.Empty(t, query.Result) + require.NotNil(t, queryResult) + assert.Empty(t, queryResult) }) }) @@ -118,29 +118,29 @@ func TestIntegrationDashboardSnapshotDBAccess(t *testing.T) { Dashboard: simplejson.NewFromAny(map[string]interface{}{ "hello": "mupp", }), - UserId: 0, - OrgId: 1, + UserID: 0, + OrgID: 1, } - err := dashStore.CreateDashboardSnapshot(context.Background(), &cmd) + _, err := dashStore.CreateDashboardSnapshot(context.Background(), &cmd) require.NoError(t, err) t.Run("Should not return any snapshots", func(t *testing.T) { query := dashboardsnapshots.GetDashboardSnapshotsQuery{ - OrgId: 1, + OrgID: 1, SignedInUser: &user.SignedInUser{OrgRole: org.RoleEditor, IsAnonymous: true, UserID: 0}, } - err := dashStore.SearchDashboardSnapshots(context.Background(), &query) + queryResult, err := dashStore.SearchDashboardSnapshots(context.Background(), &query) require.NoError(t, err) - require.NotNil(t, query.Result) - assert.Empty(t, query.Result) + require.NotNil(t, queryResult) + assert.Empty(t, queryResult) }) }) t.Run("Should have encrypted dashboard data", func(t *testing.T) { decryptedDashboard, err := secretsService.Decrypt( context.Background(), - cmd.Result.DashboardEncrypted, + result.DashboardEncrypted, ) require.NoError(t, err) @@ -167,27 +167,27 @@ func TestIntegrationDeleteExpiredSnapshots(t *testing.T) { require.NoError(t, err) query := dashboardsnapshots.GetDashboardSnapshotsQuery{ - OrgId: 1, + OrgID: 1, SignedInUser: &user.SignedInUser{OrgRole: org.RoleAdmin}, } - err = dashStore.SearchDashboardSnapshots(context.Background(), &query) + queryResult, err := dashStore.SearchDashboardSnapshots(context.Background(), &query) require.NoError(t, err) - assert.Len(t, query.Result, 1) - assert.Equal(t, nonExpiredSnapshot.Key, query.Result[0].Key) + assert.Len(t, queryResult, 1) + assert.Equal(t, nonExpiredSnapshot.Key, queryResult[0].Key) err = dashStore.DeleteExpiredSnapshots(context.Background(), &dashboardsnapshots.DeleteExpiredSnapshotsCommand{}) require.NoError(t, err) query = dashboardsnapshots.GetDashboardSnapshotsQuery{ - OrgId: 1, + OrgID: 1, SignedInUser: &user.SignedInUser{OrgRole: org.RoleAdmin}, } - err = dashStore.SearchDashboardSnapshots(context.Background(), &query) + queryResult, err = dashStore.SearchDashboardSnapshots(context.Background(), &query) require.NoError(t, err) - require.Len(t, query.Result, 1) - require.Equal(t, nonExpiredSnapshot.Key, query.Result[0].Key) + require.Len(t, queryResult, 1) + require.Equal(t, nonExpiredSnapshot.Key, queryResult[0].Key) }) } @@ -198,22 +198,22 @@ func createTestSnapshot(t *testing.T, dashStore *DashboardSnapshotStore, key str Dashboard: simplejson.NewFromAny(map[string]interface{}{ "hello": "mupp", }), - UserId: 1000, - OrgId: 1, + UserID: 1000, + OrgID: 1, Expires: expires, } - err := dashStore.CreateDashboardSnapshot(context.Background(), &cmd) + result, err := dashStore.CreateDashboardSnapshot(context.Background(), &cmd) require.NoError(t, err) // Set expiry date manually - to be able to create expired snapshots if expires < 0 { expireDate := time.Now().Add(time.Second * time.Duration(expires)) err = dashStore.store.WithDbSession(context.Background(), func(sess *db.Session) error { - _, err := sess.Exec("UPDATE dashboard_snapshot SET expires = ? WHERE id = ?", expireDate, cmd.Result.Id) + _, err := sess.Exec("UPDATE dashboard_snapshot SET expires = ? WHERE id = ?", expireDate, result.ID) return err }) require.NoError(t, err) } - return cmd.Result + return result } diff --git a/pkg/services/dashboardsnapshots/models.go b/pkg/services/dashboardsnapshots/models.go index b527c6dcfc0..e145113b73c 100644 --- a/pkg/services/dashboardsnapshots/models.go +++ b/pkg/services/dashboardsnapshots/models.go @@ -9,15 +9,15 @@ import ( // DashboardSnapshot model type DashboardSnapshot struct { - Id int64 + ID int64 `xorm:"pk autoincr 'id'"` Name string Key string DeleteKey string - OrgId int64 - UserId int64 + OrgID int64 `xorm:"org_id"` + UserID int64 `xorm:"user_id"` External bool - ExternalUrl string - ExternalDeleteUrl string + ExternalURL string `xorm:"external_url"` + ExternalDeleteURL string `xorm:"external_delete_url"` Expires time.Time Created time.Time @@ -29,13 +29,13 @@ type DashboardSnapshot struct { // DashboardSnapshotDTO without dashboard map type DashboardSnapshotDTO struct { - Id int64 `json:"id"` + ID int64 `json:"id" xorm:"id"` Name string `json:"name"` Key string `json:"key"` - OrgId int64 `json:"orgId"` - UserId int64 `json:"userId"` + OrgID int64 `json:"orgId" xorm:"org_id"` + UserID int64 `json:"userId" xorm:"user_id"` External bool `json:"external"` - ExternalUrl string `json:"externalUrl"` + ExternalURL string `json:"externalUrl" xorm:"external_url"` Expires time.Time `json:"expires"` Created time.Time `json:"created"` @@ -63,8 +63,8 @@ type CreateDashboardSnapshotCommand struct { // required:false // default: false External bool `json:"external"` - ExternalUrl string `json:"-"` - ExternalDeleteUrl string `json:"-"` + ExternalURL string `json:"-"` + ExternalDeleteURL string `json:"-"` // Define the unique key. Required if `external` is `true`. // required:false @@ -73,12 +73,10 @@ type CreateDashboardSnapshotCommand struct { // required:false DeleteKey string `json:"deleteKey"` - OrgId int64 `json:"-"` - UserId int64 `json:"-"` + OrgID int64 `json:"-"` + UserID int64 `json:"-"` DashboardEncrypted []byte `json:"-"` - - Result *DashboardSnapshot } type DeleteDashboardSnapshotCommand struct { @@ -92,8 +90,6 @@ type DeleteExpiredSnapshotsCommand struct { type GetDashboardSnapshotQuery struct { Key string DeleteKey string - - Result *DashboardSnapshot } type DashboardSnapshotsList []*DashboardSnapshotDTO @@ -101,8 +97,6 @@ type DashboardSnapshotsList []*DashboardSnapshotDTO type GetDashboardSnapshotsQuery struct { Name string Limit int - OrgId int64 + OrgID int64 SignedInUser *user.SignedInUser - - Result DashboardSnapshotsList } diff --git a/pkg/services/dashboardsnapshots/service.go b/pkg/services/dashboardsnapshots/service.go index dd63c6476d9..ae229d27fed 100644 --- a/pkg/services/dashboardsnapshots/service.go +++ b/pkg/services/dashboardsnapshots/service.go @@ -6,9 +6,9 @@ import ( //go:generate mockery --name Service --structname MockService --inpackage --filename service_mock.go type Service interface { - CreateDashboardSnapshot(context.Context, *CreateDashboardSnapshotCommand) error + CreateDashboardSnapshot(context.Context, *CreateDashboardSnapshotCommand) (*DashboardSnapshot, error) DeleteDashboardSnapshot(context.Context, *DeleteDashboardSnapshotCommand) error DeleteExpiredSnapshots(context.Context, *DeleteExpiredSnapshotsCommand) error - GetDashboardSnapshot(context.Context, *GetDashboardSnapshotQuery) error - SearchDashboardSnapshots(context.Context, *GetDashboardSnapshotsQuery) error + GetDashboardSnapshot(context.Context, *GetDashboardSnapshotQuery) (*DashboardSnapshot, error) + SearchDashboardSnapshots(context.Context, *GetDashboardSnapshotsQuery) (DashboardSnapshotsList, error) } diff --git a/pkg/services/dashboardsnapshots/service/service.go b/pkg/services/dashboardsnapshots/service/service.go index 4a8b26cae87..247f0e7aec9 100644 --- a/pkg/services/dashboardsnapshots/service/service.go +++ b/pkg/services/dashboardsnapshots/service/service.go @@ -25,15 +25,15 @@ func ProvideService(store dashboardsnapshots.Store, secretsService secrets.Servi return s } -func (s *ServiceImpl) CreateDashboardSnapshot(ctx context.Context, cmd *dashboardsnapshots.CreateDashboardSnapshotCommand) error { +func (s *ServiceImpl) CreateDashboardSnapshot(ctx context.Context, cmd *dashboardsnapshots.CreateDashboardSnapshotCommand) (*dashboardsnapshots.DashboardSnapshot, error) { marshalledData, err := cmd.Dashboard.Encode() if err != nil { - return err + return nil, err } encryptedDashboard, err := s.secretsService.Encrypt(ctx, marshalledData, secrets.WithoutScope()) if err != nil { - return err + return nil, err } cmd.DashboardEncrypted = encryptedDashboard @@ -41,34 +41,34 @@ func (s *ServiceImpl) CreateDashboardSnapshot(ctx context.Context, cmd *dashboar return s.store.CreateDashboardSnapshot(ctx, cmd) } -func (s *ServiceImpl) GetDashboardSnapshot(ctx context.Context, query *dashboardsnapshots.GetDashboardSnapshotQuery) error { - err := s.store.GetDashboardSnapshot(ctx, query) +func (s *ServiceImpl) GetDashboardSnapshot(ctx context.Context, query *dashboardsnapshots.GetDashboardSnapshotQuery) (*dashboardsnapshots.DashboardSnapshot, error) { + queryResult, err := s.store.GetDashboardSnapshot(ctx, query) if err != nil { - return err + return nil, err } - if query.Result.DashboardEncrypted != nil { - decryptedDashboard, err := s.secretsService.Decrypt(ctx, query.Result.DashboardEncrypted) + if queryResult.DashboardEncrypted != nil { + decryptedDashboard, err := s.secretsService.Decrypt(ctx, queryResult.DashboardEncrypted) if err != nil { - return err + return nil, err } dashboard, err := simplejson.NewJson(decryptedDashboard) if err != nil { - return err + return nil, err } - query.Result.Dashboard = dashboard + queryResult.Dashboard = dashboard } - return err + return queryResult, err } func (s *ServiceImpl) DeleteDashboardSnapshot(ctx context.Context, cmd *dashboardsnapshots.DeleteDashboardSnapshotCommand) error { return s.store.DeleteDashboardSnapshot(ctx, cmd) } -func (s *ServiceImpl) SearchDashboardSnapshots(ctx context.Context, query *dashboardsnapshots.GetDashboardSnapshotsQuery) error { +func (s *ServiceImpl) SearchDashboardSnapshots(ctx context.Context, query *dashboardsnapshots.GetDashboardSnapshotsQuery) (dashboardsnapshots.DashboardSnapshotsList, error) { return s.store.SearchDashboardSnapshots(ctx, query) } diff --git a/pkg/services/dashboardsnapshots/service/service_test.go b/pkg/services/dashboardsnapshots/service/service_test.go index 88a72ee6273..27ef9761518 100644 --- a/pkg/services/dashboardsnapshots/service/service_test.go +++ b/pkg/services/dashboardsnapshots/service/service_test.go @@ -42,10 +42,10 @@ func TestDashboardSnapshotsService(t *testing.T) { Dashboard: dashboard, } - err = s.CreateDashboardSnapshot(ctx, &cmd) + result, err := s.CreateDashboardSnapshot(ctx, &cmd) require.NoError(t, err) - decrypted, err := s.secretsService.Decrypt(ctx, cmd.Result.DashboardEncrypted) + decrypted, err := s.secretsService.Decrypt(ctx, result.DashboardEncrypted) require.NoError(t, err) require.Equal(t, rawDashboard, decrypted) @@ -59,10 +59,10 @@ func TestDashboardSnapshotsService(t *testing.T) { DeleteKey: dashboardKey, } - err := s.GetDashboardSnapshot(ctx, &query) + queryResult, err := s.GetDashboardSnapshot(ctx, &query) require.NoError(t, err) - decrypted, err := query.Result.Dashboard.Encode() + decrypted, err := queryResult.Dashboard.Encode() require.NoError(t, err) require.Equal(t, rawDashboard, decrypted) diff --git a/pkg/services/dashboardsnapshots/service_mock.go b/pkg/services/dashboardsnapshots/service_mock.go index efca62367bf..95ae9708c92 100644 --- a/pkg/services/dashboardsnapshots/service_mock.go +++ b/pkg/services/dashboardsnapshots/service_mock.go @@ -1,10 +1,9 @@ -// Code generated by mockery v2.12.2. DO NOT EDIT. +// Code generated by mockery v2.16.0. DO NOT EDIT. package dashboardsnapshots import ( context "context" - testing "testing" mock "github.com/stretchr/testify/mock" ) @@ -15,17 +14,26 @@ type MockService struct { } // CreateDashboardSnapshot provides a mock function with given fields: _a0, _a1 -func (_m *MockService) CreateDashboardSnapshot(_a0 context.Context, _a1 *CreateDashboardSnapshotCommand) error { +func (_m *MockService) CreateDashboardSnapshot(_a0 context.Context, _a1 *CreateDashboardSnapshotCommand) (*DashboardSnapshot, error) { ret := _m.Called(_a0, _a1) - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *CreateDashboardSnapshotCommand) error); ok { + var r0 *DashboardSnapshot + if rf, ok := ret.Get(0).(func(context.Context, *CreateDashboardSnapshotCommand) *DashboardSnapshot); ok { r0 = rf(_a0, _a1) } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).(*DashboardSnapshot) + } } - return r0 + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *CreateDashboardSnapshotCommand) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } // DeleteDashboardSnapshot provides a mock function with given fields: _a0, _a1 @@ -57,35 +65,58 @@ func (_m *MockService) DeleteExpiredSnapshots(_a0 context.Context, _a1 *DeleteEx } // GetDashboardSnapshot provides a mock function with given fields: _a0, _a1 -func (_m *MockService) GetDashboardSnapshot(_a0 context.Context, _a1 *GetDashboardSnapshotQuery) error { +func (_m *MockService) GetDashboardSnapshot(_a0 context.Context, _a1 *GetDashboardSnapshotQuery) (*DashboardSnapshot, error) { ret := _m.Called(_a0, _a1) - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardSnapshotQuery) error); ok { + var r0 *DashboardSnapshot + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardSnapshotQuery) *DashboardSnapshot); ok { r0 = rf(_a0, _a1) } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).(*DashboardSnapshot) + } } - return r0 + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *GetDashboardSnapshotQuery) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } // SearchDashboardSnapshots provides a mock function with given fields: _a0, _a1 -func (_m *MockService) SearchDashboardSnapshots(_a0 context.Context, _a1 *GetDashboardSnapshotsQuery) error { +func (_m *MockService) SearchDashboardSnapshots(_a0 context.Context, _a1 *GetDashboardSnapshotsQuery) (DashboardSnapshotsList, error) { ret := _m.Called(_a0, _a1) - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardSnapshotsQuery) error); ok { + var r0 DashboardSnapshotsList + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardSnapshotsQuery) DashboardSnapshotsList); ok { r0 = rf(_a0, _a1) } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).(DashboardSnapshotsList) + } } - return r0 + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *GetDashboardSnapshotsQuery) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -// NewMockService creates a new instance of MockService. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. -func NewMockService(t testing.TB) *MockService { +type mockConstructorTestingTNewMockService interface { + mock.TestingT + Cleanup(func()) +} + +// NewMockService creates a new instance of MockService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewMockService(t mockConstructorTestingTNewMockService) *MockService { mock := &MockService{} mock.Mock.Test(t) diff --git a/pkg/services/dashboardsnapshots/store.go b/pkg/services/dashboardsnapshots/store.go index 392c21f795d..b11c0189aef 100644 --- a/pkg/services/dashboardsnapshots/store.go +++ b/pkg/services/dashboardsnapshots/store.go @@ -5,9 +5,9 @@ import ( ) type Store interface { - CreateDashboardSnapshot(context.Context, *CreateDashboardSnapshotCommand) error + CreateDashboardSnapshot(context.Context, *CreateDashboardSnapshotCommand) (*DashboardSnapshot, error) DeleteDashboardSnapshot(context.Context, *DeleteDashboardSnapshotCommand) error DeleteExpiredSnapshots(context.Context, *DeleteExpiredSnapshotsCommand) error - GetDashboardSnapshot(context.Context, *GetDashboardSnapshotQuery) error - SearchDashboardSnapshots(context.Context, *GetDashboardSnapshotsQuery) error + GetDashboardSnapshot(context.Context, *GetDashboardSnapshotQuery) (*DashboardSnapshot, error) + SearchDashboardSnapshots(context.Context, *GetDashboardSnapshotsQuery) (DashboardSnapshotsList, error) } diff --git a/pkg/services/export/entity_store.go b/pkg/services/export/entity_store.go index c6f3af1a4d3..57bd79eae94 100644 --- a/pkg/services/export/entity_store.go +++ b/pkg/services/export/entity_store.go @@ -256,34 +256,34 @@ func (e *entityStoreJob) start(ctx context.Context) { rowUser.OrgID = orgId rowUser.UserID = 1 cmd := &dashboardsnapshots.GetDashboardSnapshotsQuery{ - OrgId: orgId, + OrgID: orgId, Limit: 500000, SignedInUser: rowUser, } - err := e.dashboardsnapshots.SearchDashboardSnapshots(ctx, cmd) + result, err := e.dashboardsnapshots.SearchDashboardSnapshots(ctx, cmd) if err != nil { e.status.Status = "error: " + err.Error() return } - for _, dto := range cmd.Result { + for _, dto := range result { m := snapshot.Model{ Name: dto.Name, - ExternalURL: dto.ExternalUrl, + ExternalURL: dto.ExternalURL, Expires: dto.Expires.UnixMilli(), } - rowUser.OrgID = dto.OrgId - rowUser.UserID = dto.UserId + rowUser.OrgID = dto.OrgID + rowUser.UserID = dto.UserID snapcmd := &dashboardsnapshots.GetDashboardSnapshotQuery{ Key: dto.Key, } - err = e.dashboardsnapshots.GetDashboardSnapshot(ctx, snapcmd) + snapcmdResult, err := e.dashboardsnapshots.GetDashboardSnapshot(ctx, snapcmd) if err == nil { - res := snapcmd.Result + res := snapcmdResult m.DeleteKey = res.DeleteKey - m.ExternalURL = res.ExternalUrl + m.ExternalURL = res.ExternalURL snap := res.Dashboard m.DashboardUID = snap.Get("uid").MustString("") diff --git a/pkg/services/export/export_snapshots.go b/pkg/services/export/export_snapshots.go index 24195a23693..715e93d087d 100644 --- a/pkg/services/export/export_snapshots.go +++ b/pkg/services/export/export_snapshots.go @@ -10,7 +10,7 @@ import ( func exportSnapshots(helper *commitHelper, job *gitExportJob) error { cmd := &dashboardsnapshots.GetDashboardSnapshotsQuery{ - OrgId: helper.orgID, + OrgID: helper.orgID, Limit: 500000, SignedInUser: nil, } @@ -18,12 +18,12 @@ func exportSnapshots(helper *commitHelper, job *gitExportJob) error { return fmt.Errorf("snapshots requires an admin user") } - err := job.dashboardsnapshotsService.SearchDashboardSnapshots(helper.ctx, cmd) + result, err := job.dashboardsnapshotsService.SearchDashboardSnapshots(helper.ctx, cmd) if err != nil { return err } - if len(cmd.Result) < 1 { + if len(result) < 1 { return nil // nothing } @@ -32,9 +32,9 @@ func exportSnapshots(helper *commitHelper, job *gitExportJob) error { comment: "Export snapshots", } - for _, snapshot := range cmd.Result { + for _, snapshot := range result { gitcmd.body = append(gitcmd.body, commitBody{ - fpath: filepath.Join(helper.orgDir, "snapshot", fmt.Sprintf("%d-snapshot.json", snapshot.Id)), + fpath: filepath.Join(helper.orgDir, "snapshot", fmt.Sprintf("%d-snapshot.json", snapshot.ID)), body: prettyJSON(snapshot), }) } From 6bd11e0ebfa388723dd9ae7e83f5fdc6dac2f237 Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Wed, 25 Jan 2023 15:16:08 +0100 Subject: [PATCH 028/172] Auth: Add skip_org_role_sync setting for github (#61673) * add: skip_org_role_sync setting for github * fix: frontend * rearranged tests * refactor: assignGrafanaAdmin skip also * Add: tests for allowGrafanaAdmin - both for the case when both settings are set and the setting for only allowGrafanaAdmin * Apply suggestions from code review Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> * Update docs/sources/setup-grafana/configure-grafana/_index.md Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> * Update pkg/login/social/github_oauth.go Co-authored-by: Ieva * added vairable inside scope * Update docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md * Update docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> Co-authored-by: Ieva --- .../setup-grafana/configure-grafana/_index.md | 15 ++++++++ .../configure-authentication/github/index.md | 13 +++++++ packages/grafana-data/src/types/config.ts | 1 + pkg/api/frontendsettings.go | 1 + pkg/login/social/github_oauth.go | 26 ++++++++++---- pkg/login/social/github_oauth_test.go | 36 +++++++++++++++++++ pkg/login/social/social.go | 1 + pkg/setting/setting.go | 12 +++++++ public/app/features/admin/UserAdminPage.tsx | 5 ++- 9 files changed, 103 insertions(+), 7 deletions(-) diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 47f502a7da5..8c9c85661d0 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -904,6 +904,21 @@ The following table shows the OAuth provider's setting with the default value an | Google | false | true | User organization roles are set with `defaultRole` and the org role can be changed for Google synced users. | | Google | true | true | User organization roles are set with `defaultRole` for Google. For other providers, the synchronization will be skipped, and the org role can be changed, along with other OAuth provider users' org roles. | +### [auth.github] skip_org_role_sync + +When a user logs in the first time, Grafana sets the organization role based on the value specified in `AutoAssignOrgRole`. If you want to manage organization roles, set the `skip_org_role_sync` option to `true`. GitHub syncs organization roles and sets Grafana Admins. +This also impacts `allow_assign_grafana_admin` setting, by not syncing the grafana admin role from GitHub. + +> **Note:** There is a separate setting called `oauth_skip_org_role_update_sync` which has a different scope. While `skip_org_role_sync` only applies to the specific OAuth provider, `oauth_skip_org_role_update_sync` is a generic setting that affects all configured OAuth providers. + +The following table shows the OAuth provider's setting with the default value and the skip org role sync setting. +| OAuth Provider | `oauth_skip_org_role_sync_update` | `skip_org_role_sync` | Behavior | +| --- | --- | --- | --- | +| GitHub | false | false | User organization roles are set with `defaultRole` and cannot be changed | +| Github | true | false | User organization roles are set with `defaultRole` for GitHub, and Grafana Admins are set. For other providers, the synchronization is skipped, and the org role can be changed, along with other OAuth provider users' org roles. | +| GitHub | false | true | User organization roles are set with `defaultRole`, and the organization role can be changed for GitHub synced users. | +| GitHub | true | true | User organization roles are set with `defaultRole` for Google. For other providers, the synchronization is skipped, and the org role can be changed, along with other OAuth provider users' org roles. | + ### [auth.gitlab] skip_org_role_sync When a user logs in the first time, Grafana sets the organization role based on the value specified in `AutoAssignOrgRole`. If you want to manage organization roles, set the `skip_org_role_sync` option to `true`. GitLab syncs organization roles and sets Grafana Admins. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md index 9146e224623..81703a7c0fe 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md @@ -203,3 +203,16 @@ Your GitHub teams can be referenced in two ways: Example: `@grafana/developers` [Learn more about Team Sync]({{< relref "../../configure-team-sync/" >}}) + +## Skip organization role sync + +To prevent the sync of organization roles from GitHub, set `skip_org_role_sync` to `true`. This is useful if you want to manage the organization roles for your users from within Grafana. +This also impacts the `allow_assign_grafana_admin` setting by not syncing the Grafana admin role from GitHub. + +```ini +[auth.github] +# .. +# prevents the sync of org roles from Github +skip_org_role_sync = true +`` +``` diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index 5ef7e3943d6..dfbde50d60a 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -226,6 +226,7 @@ export interface AuthSettings { LDAPSkipOrgRoleSync?: boolean; JWTAuthSkipOrgRoleSync?: boolean; GrafanaComSkipOrgRoleSync?: boolean; + GithubSkipOrgRoleSync?: boolean; GitLabSkipOrgRoleSync?: boolean; AzureADSkipOrgRoleSync?: boolean; GoogleSkipOrgRoleSync?: boolean; diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 9ceb984e53b..089b8f21a67 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -148,6 +148,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i "OAuthSkipOrgRoleUpdateSync": hs.Cfg.OAuthSkipOrgRoleUpdateSync, "SAMLSkipOrgRoleSync": hs.Cfg.SectionWithEnvOverrides("auth.saml").Key("skip_org_role_sync").MustBool(false), "LDAPSkipOrgRoleSync": hs.Cfg.LDAPSkipOrgRoleSync, + "GithubSkipOrgRoleSync": hs.Cfg.GithubSkipOrgRoleSync, "GoogleSkipOrgRoleSync": hs.Cfg.GoogleSkipOrgRoleSync, "JWTAuthSkipOrgRoleSync": hs.Cfg.JWTAuthSkipOrgRoleSync, "GrafanaComSkipOrgRoleSync": hs.Cfg.GrafanaComSkipOrgRoleSync, diff --git a/pkg/login/social/github_oauth.go b/pkg/login/social/github_oauth.go index ca56f2888a7..f170d6d9194 100644 --- a/pkg/login/social/github_oauth.go +++ b/pkg/login/social/github_oauth.go @@ -8,6 +8,8 @@ import ( "regexp" "golang.org/x/oauth2" + + "github.com/grafana/grafana/pkg/models/roletype" ) type SocialGithub struct { @@ -15,6 +17,7 @@ type SocialGithub struct { allowedOrganizations []string apiUrl string teamIds []int + skipOrgRoleSync bool } type GithubTeam struct { @@ -201,14 +204,25 @@ func (s *SocialGithub) UserInfo(client *http.Client, token *oauth2.Token) (*Basi teams := convertToGroupList(teamMemberships) - role, grafanaAdmin := s.extractRoleAndAdmin(response.Body, teams, true) - if s.roleAttributeStrict && !role.IsValid() { - return nil, &InvalidBasicRoleError{idP: "Github", assignedRole: string(role)} + var role roletype.RoleType + var isGrafanaAdmin *bool = nil + + if !s.skipOrgRoleSync { + var grafanaAdmin bool + role, grafanaAdmin = s.extractRoleAndAdmin(response.Body, teams, true) + + if s.roleAttributeStrict && !role.IsValid() { + return nil, &InvalidBasicRoleError{idP: "Github", assignedRole: string(role)} + } + + if s.allowAssignGrafanaAdmin { + isGrafanaAdmin = &grafanaAdmin + } } - var isGrafanaAdmin *bool = nil - if s.allowAssignGrafanaAdmin { - isGrafanaAdmin = &grafanaAdmin + // we skip allowing assignment of GrafanaAdmin if skipOrgRoleSync is present + if s.allowAssignGrafanaAdmin && s.skipOrgRoleSync { + s.log.Debug("allowAssignGrafanaAdmin and skipOrgRoleSync are both set, Grafana Admin role will not be synced, consider setting one or the other") } userInfo := &BasicUserInfo{ diff --git a/pkg/login/social/github_oauth_test.go b/pkg/login/social/github_oauth_test.go index cdb400b15ea..9d92e21cd35 100644 --- a/pkg/login/social/github_oauth_test.go +++ b/pkg/login/social/github_oauth_test.go @@ -112,11 +112,14 @@ const testGHUserJSON = `{ }` func TestSocialGitHub_UserInfo(t *testing.T) { + var boolPointer *bool tests := []struct { name string userRawJSON string userTeamsRawJSON string settingAutoAssignOrgRole string + settingAllowGrafanaAdmin bool + settingSkipOrgRoleSync bool roleAttributePath string autoAssignOrgRole string want *BasicUserInfo @@ -167,6 +170,38 @@ func TestSocialGitHub_UserInfo(t *testing.T) { Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"}, }, }, + { + name: "Should be empty role if setting skipOrgRoleSync is set to true", + roleAttributePath: "contains(groups[*], '@github/justice-league') && 'Editor' || 'Viewer'", + settingSkipOrgRoleSync: true, + userRawJSON: testGHUserJSON, + userTeamsRawJSON: testGHUserTeamsJSON, + want: &BasicUserInfo{ + Id: "1", + Name: "monalisa octocat", + Email: "octocat@github.com", + Login: "octocat", + Role: "", + Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"}, + }, + }, + { + name: "Should return nil pointer if allowGrafanaAdmin and skipOrgRoleSync setting is set to true", + roleAttributePath: "contains(groups[*], '@github/justice-league') && 'Editor' || 'Viewer'", + settingSkipOrgRoleSync: true, + settingAllowGrafanaAdmin: true, + userRawJSON: testGHUserJSON, + userTeamsRawJSON: testGHUserTeamsJSON, + want: &BasicUserInfo{ + Id: "1", + Name: "monalisa octocat", + Email: "octocat@github.com", + Login: "octocat", + Role: "", + Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"}, + IsGrafanaAdmin: boolPointer, + }, + }, { // Case that's going to change with Grafana 10 name: "No fallback to default org role (will change in Grafana 10)", roleAttributePath: "", @@ -208,6 +243,7 @@ func TestSocialGitHub_UserInfo(t *testing.T) { allowedOrganizations: []string{}, apiUrl: server.URL + "/user", teamIds: []int{}, + skipOrgRoleSync: tt.settingSkipOrgRoleSync, } token := &oauth2.Token{ diff --git a/pkg/login/social/social.go b/pkg/login/social/social.go index 5139360bab7..5cc6d9f0969 100644 --- a/pkg/login/social/social.go +++ b/pkg/login/social/social.go @@ -146,6 +146,7 @@ func ProvideService(cfg *setting.Cfg, features *featuremgmt.FeatureManager) *Soc apiUrl: info.ApiUrl, teamIds: sec.Key("team_ids").Ints(","), allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()), + skipOrgRoleSync: cfg.GithubSkipOrgRoleSync, } } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index e351a070ce5..80830297702 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -467,6 +467,9 @@ type Cfg struct { // then Live uses AppURL as the only allowed origin. LiveAllowedOrigins []string + // Github OAuth + GithubSkipOrgRoleSync bool + // Grafana.com URL, used for OAuth redirect. GrafanaComURL string // Grafana.com API URL. Can be set separately to GrafanaComURL @@ -1375,6 +1378,11 @@ func readAuthGrafanaComSettings(iniFile *ini.File, cfg *Cfg) { cfg.GrafanaComSkipOrgRoleSync = sec.Key("skip_org_role_sync").MustBool(false) } +func readAuthGithubSettings(iniFile *ini.File, cfg *Cfg) { + sec := iniFile.Section("auth.github") + cfg.GithubSkipOrgRoleSync = sec.Key("skip_org_role_sync").MustBool(false) +} + func readAuthGoogleSettings(iniFile *ini.File, cfg *Cfg) { sec := iniFile.Section("auth.google") cfg.GoogleSkipOrgRoleSync = sec.Key("skip_org_role_sync").MustBool(false) @@ -1501,7 +1509,11 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { cfg.AuthProxyHeadersEncoded = authProxy.Key("headers_encoded").MustBool(false) + // GrafanaCom readAuthGrafanaComSettings(iniFile, cfg) + + // Github + readAuthGithubSettings(iniFile, cfg) return nil } diff --git a/public/app/features/admin/UserAdminPage.tsx b/public/app/features/admin/UserAdminPage.tsx index 302e2ad96ca..dc84ebc0f84 100644 --- a/public/app/features/admin/UserAdminPage.tsx +++ b/public/app/features/admin/UserAdminPage.tsx @@ -39,7 +39,7 @@ interface OwnProps extends GrafanaRouteComponentProps<{ id: string }> { error?: UserAdminError; } -const SyncedOAuthLabels: string[] = ['GitHub', 'OAuth']; +const SyncedOAuthLabels: string[] = ['OAuth']; export class UserAdminPage extends PureComponent { async componentDidMount() { @@ -113,6 +113,7 @@ export class UserAdminPage extends PureComponent { user?.isExternal && user?.authLabels?.some((r) => SyncedOAuthLabels.includes(r)); const isSAMLUser = user?.isExternal && user?.authLabels?.includes('SAML'); const isGoogleUser = user?.isExternal && user?.authLabels?.includes('Google'); + const isGithubUser = user?.isExternal && user?.authLabels?.includes('GitHub'); const isGitLabUser = user?.isExternal && user?.authLabels?.includes('GitLab'); const isAuthProxyUser = user?.isExternal && user?.authLabels?.includes('Auth Proxy'); const isAzureADUser = user?.isExternal && user?.authLabels?.includes('AzureAD'); @@ -127,6 +128,7 @@ export class UserAdminPage extends PureComponent { isOAuthUserWithSkippableSync || isSAMLUser || isLDAPUser || + isGithubUser || isAzureADUser || isJWTUser || isGrafanaComUser @@ -137,6 +139,7 @@ export class UserAdminPage extends PureComponent { (!config.auth.JWTAuthSkipOrgRoleSync && isJWTUser) || // both OAuthSkipOrgRoleUpdateSync and specific provider settings needs to be false for a user to be synced (!config.auth.OAuthSkipOrgRoleUpdateSync && !config.auth.GrafanaComSkipOrgRoleSync && isGrafanaComUser) || + (!config.auth.OAuthSkipOrgRoleUpdateSync && !config.auth.GithubSkipOrgRoleSync && isGithubUser) || (!config.auth.OAuthSkipOrgRoleUpdateSync && !config.auth.AzureADSkipOrgRoleSync && isAzureADUser) || (!config.auth.OAuthSkipOrgRoleUpdateSync && !config.auth.GitLabSkipOrgRoleSync && isGitLabUser) || (!config.auth.OAuthSkipOrgRoleUpdateSync && !config.auth.GoogleSkipOrgRoleSync && isGoogleUser)); From e46aa2e4e610c76280ec65fd824d51a91be80feb Mon Sep 17 00:00:00 2001 From: juanicabanas Date: Wed, 25 Jan 2023 11:30:42 -0300 Subject: [PATCH 029/172] PublicDashboards: Footer alignment fix for Firefox browser (#62108) footer x axis aligment fixed --- .../PublicDashboardsFooter.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/public/app/features/dashboard/components/PublicDashboardFooter/PublicDashboardsFooter.tsx b/public/app/features/dashboard/components/PublicDashboardFooter/PublicDashboardsFooter.tsx index 69b004a959f..4b65096307d 100644 --- a/public/app/features/dashboard/components/PublicDashboardFooter/PublicDashboardsFooter.tsx +++ b/public/app/features/dashboard/components/PublicDashboardFooter/PublicDashboardsFooter.tsx @@ -17,11 +17,9 @@ export const PublicDashboardFooter = function () { return conf.hide ? null : ( ); }; @@ -41,10 +39,13 @@ const getStyles = (theme: GrafanaTheme2) => ({ display: flex; justify-content: end; height: 30px; - padding: ${theme.spacing(0, 1, 0, 1)}; + padding: ${theme.spacing(0, 2, 0, 1)}; `, - logoText: css` - margin-right: ${theme.spacing(1)}; + link: css` + display: flex; + gap: 4px; + justify-content: end; + align-items: center; `, logoImg: css` height: 100%; From 7c85db5bfaa742c1026bf6d776210ca84e11d89e Mon Sep 17 00:00:00 2001 From: Isabella Siu Date: Wed, 25 Jan 2023 09:39:42 -0500 Subject: [PATCH 030/172] CloudWatch Logs: Set default logs query and disable button when empty (#61956) --- .../components/QueryHeader.test.tsx | 95 ++++++++++++++++++- .../cloudwatch/components/QueryHeader.tsx | 12 ++- .../datasource/cloudwatch/defaultQueries.ts | 3 +- 3 files changed, 103 insertions(+), 7 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryHeader.test.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryHeader.test.tsx index 2e782cc07a1..9cc6fef9582 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryHeader.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryHeader.test.tsx @@ -6,6 +6,7 @@ import { config } from '@grafana/runtime'; import { setupMockedDataSource } from '../__mocks__/CloudWatchDataSource'; import { validLogsQuery, validMetricSearchBuilderQuery } from '../__mocks__/queries'; +import { DEFAULT_LOGS_QUERY_STRING } from '../defaultQueries'; import QueryHeader from './QueryHeader'; @@ -16,11 +17,10 @@ const ds = setupMockedDataSource({ ds.datasource.resources.getRegions = jest.fn().mockResolvedValue([]); describe('QueryHeader', () => { - afterEach(() => { - config.featureToggles.cloudWatchCrossAccountQuerying = originalFeatureToggleValue; - }); - describe('when changing region', () => { + afterEach(() => { + config.featureToggles.cloudWatchCrossAccountQuerying = originalFeatureToggleValue; + }); const { datasource } = setupMockedDataSource(); datasource.resources.getRegions = jest.fn().mockResolvedValue([ { value: 'us-east-2', label: 'us-east-2' }, @@ -117,4 +117,91 @@ describe('QueryHeader', () => { expect(datasource.resources.isMonitoringAccount).not.toHaveBeenCalledWith(); }); }); + + describe('when changing query mode', () => { + const { datasource } = setupMockedDataSource(); + it('should set default log query when switching to log mode', async () => { + const onChange = jest.fn(); + datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(false); + render( + + ); + expect(await screen.findByText('CloudWatch Metrics')).toBeInTheDocument(); + await selectEvent.select(await screen.findByLabelText('Query mode'), 'CloudWatch Logs', { + container: document.body, + }); + expect(onChange).toHaveBeenCalledWith({ + ...validMetricSearchBuilderQuery, + logGroupNames: undefined, + logGroups: [], + queryMode: 'Logs', + sqlExpression: '', + expression: DEFAULT_LOGS_QUERY_STRING, + }); + }); + + it('should set expression to empty when switching to metrics mode', async () => { + const onChange = jest.fn(); + datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(false); + render( + + ); + expect(await screen.findByText('CloudWatch Logs')).toBeInTheDocument(); + await selectEvent.select(await screen.findByLabelText('Query mode'), 'CloudWatch Metrics', { + container: document.body, + }); + expect(onChange).toHaveBeenCalledWith({ + ...validMetricSearchBuilderQuery, + logGroupNames: undefined, + logGroups: [], + sqlExpression: '', + expression: '', + }); + }); + }); + describe('log expression', () => { + const { datasource } = setupMockedDataSource(); + it('should disable run query button when empty', async () => { + const onChange = jest.fn(); + datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(false); + render( + + ); + expect(await screen.findByText('Run queries')).toBeInTheDocument(); + expect(screen.getByText('Run queries').closest('button')).toBeDisabled(); + }); + it('should enable run query button when set', async () => { + const onChange = jest.fn(); + datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(false); + render( + + ); + expect(await screen.findByText('Run queries')).toBeInTheDocument(); + expect(screen.getByText('Run queries').closest('button')).not.toBeDisabled(); + }); + }); }); diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx index 76f05ee0468..e5f1e5eb4e6 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx @@ -6,7 +6,8 @@ import { config } from '@grafana/runtime'; import { Badge, Button } from '@grafana/ui'; import { CloudWatchDatasource } from '../datasource'; -import { isCloudWatchMetricsQuery } from '../guards'; +import { DEFAULT_LOGS_QUERY_STRING } from '../defaultQueries'; +import { isCloudWatchLogsQuery, isCloudWatchMetricsQuery } from '../guards'; import { useIsMonitoringAccount, useRegions } from '../hooks'; import { CloudWatchJsonData, CloudWatchQuery, CloudWatchQueryMode, MetricQueryType } from '../types'; @@ -34,12 +35,19 @@ const QueryHeader: React.FC = ({ const { queryMode, region } = query; const isMonitoringAccount = useIsMonitoringAccount(datasource.resources, query.region); const [regions, regionIsLoading] = useRegions(datasource); + const emptyLogsExpression = isCloudWatchLogsQuery(query) ? !query.expression : false; const onQueryModeChange = ({ value }: SelectableValue) => { if (value && value !== queryMode) { + // reset expression to a default string when the query mode changes + let expression = ''; + if (value === 'Logs') { + expression = DEFAULT_LOGS_QUERY_STRING; + } onChange({ ...datasource.getDefaultQuery(CoreApp.Unknown), ...query, + expression, queryMode: value, }); } @@ -100,7 +108,7 @@ const QueryHeader: React.FC = ({ size="sm" onClick={onRunQuery} icon={data?.state === LoadingState.Loading ? 'fa fa-spinner' : undefined} - disabled={data?.state === LoadingState.Loading} + disabled={data?.state === LoadingState.Loading || emptyLogsExpression} > Run queries diff --git a/public/app/plugins/datasource/cloudwatch/defaultQueries.ts b/public/app/plugins/datasource/cloudwatch/defaultQueries.ts index 5eff9bbd8cb..e921f9d75cc 100644 --- a/public/app/plugins/datasource/cloudwatch/defaultQueries.ts +++ b/public/app/plugins/datasource/cloudwatch/defaultQueries.ts @@ -16,13 +16,14 @@ export const DEFAULT_METRICS_QUERY: Omit = { matchExact: true, }; +export const DEFAULT_LOGS_QUERY_STRING = 'fields @timestamp, @message |\n sort @timestamp desc |\n limit 20'; + export const getDefaultLogsQuery = ( defaultLogGroups?: LogGroup[], legacyDefaultLogGroups?: string[] ): Omit => ({ id: '', region: 'default', - expression: '', // in case legacy default log groups have been defined in the ConfigEditor, they will be migrated in the LogGroupsField component or the next time the ConfigEditor is opened. // the migration requires async backend calls, so we don't want to do it here as it would block the UI. logGroupNames: legacyDefaultLogGroups, From b0b2b722906071fb34ac391d0f9d25e20102feab Mon Sep 17 00:00:00 2001 From: ying-jeanne <74549700+ying-jeanne@users.noreply.github.com> Date: Wed, 25 Jan 2023 22:58:54 +0800 Subject: [PATCH 031/172] [API Split] Move star api inside of packages (#61987) move star api inside of packages --- pkg/api/api.go | 12 ++-- pkg/api/http_server.go | 4 ++ pkg/cmd/grafana-cli/runner/wire.go | 2 + pkg/server/wire.go | 2 + .../stars.go => services/star/api/api.go} | 60 +++++++++++++++---- 5 files changed, 62 insertions(+), 18 deletions(-) rename pkg/{api/stars.go => services/star/api/api.go} (72%) diff --git a/pkg/api/api.go b/pkg/api/api.go index 89b0749a59b..c38ed35c7b1 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -233,14 +233,16 @@ 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.Get("/stars", routing.Wrap(hs.starApi.GetStars)) // Deprecated: use /stars/dashboard/uid/:uid API instead. - userRoute.Post("/stars/dashboard/:id", routing.Wrap(hs.StarDashboard)) + // nolint:staticcheck + userRoute.Post("/stars/dashboard/:id", routing.Wrap(hs.starApi.StarDashboard)) // Deprecated: use /stars/dashboard/uid/:uid API instead. - userRoute.Delete("/stars/dashboard/:id", routing.Wrap(hs.UnstarDashboard)) + // nolint:staticcheck + userRoute.Delete("/stars/dashboard/:id", routing.Wrap(hs.starApi.UnstarDashboard)) - userRoute.Post("/stars/dashboard/uid/:uid", routing.Wrap(hs.StarDashboardByUID)) - userRoute.Delete("/stars/dashboard/uid/:uid", routing.Wrap(hs.UnstarDashboardByUID)) + userRoute.Post("/stars/dashboard/uid/:uid", routing.Wrap(hs.starApi.StarDashboardByUID)) + userRoute.Delete("/stars/dashboard/uid/:uid", routing.Wrap(hs.starApi.UnstarDashboardByUID)) userRoute.Put("/password", routing.Wrap(hs.ChangeUserPassword)) userRoute.Get("/quotas", routing.Wrap(hs.GetUserQuotas)) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 2a90a6f04a2..c3500501a20 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -89,6 +89,7 @@ import ( "github.com/grafana/grafana/pkg/services/shorturls" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/star" + starApi "github.com/grafana/grafana/pkg/services/star/api" "github.com/grafana/grafana/pkg/services/stats" "github.com/grafana/grafana/pkg/services/store" "github.com/grafana/grafana/pkg/services/store/entity/httpentitystore" @@ -213,6 +214,7 @@ type HTTPServer struct { oauthTokenService oauthtoken.OAuthTokenService statsService stats.Service authnService authn.Service + starApi *starApi.API } type ServerOptions struct { @@ -257,6 +259,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi queryLibraryHTTPService querylibrary.HTTPService, queryLibraryService querylibrary.Service, oauthTokenService oauthtoken.OAuthTokenService, statsService stats.Service, authnService authn.Service, k8saccess k8saccess.K8SAccess, // required so that the router is registered + starApi *starApi.API, ) (*HTTPServer, error) { web.Env = cfg.Env m := web.New() @@ -363,6 +366,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi oauthTokenService: oauthTokenService, statsService: statsService, authnService: authnService, + starApi: starApi, } if hs.Listener != nil { hs.log.Debug("Using provided listener") diff --git a/pkg/cmd/grafana-cli/runner/wire.go b/pkg/cmd/grafana-cli/runner/wire.go index 21959dda2cf..42e6dc49d37 100644 --- a/pkg/cmd/grafana-cli/runner/wire.go +++ b/pkg/cmd/grafana-cli/runner/wire.go @@ -102,6 +102,7 @@ import ( "github.com/grafana/grafana/pkg/services/shorturls" "github.com/grafana/grafana/pkg/services/shorturls/shorturlimpl" "github.com/grafana/grafana/pkg/services/sqlstore" + starApi "github.com/grafana/grafana/pkg/services/star/api" "github.com/grafana/grafana/pkg/services/star/starimpl" "github.com/grafana/grafana/pkg/services/store" entitystoredummy "github.com/grafana/grafana/pkg/services/store/entity/dummy" @@ -289,6 +290,7 @@ var wireSet = wire.NewSet( publicdashboardsStore.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*publicdashboardsStore.PublicDashboardStoreImpl)), publicdashboardsApi.ProvideApi, + starApi.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, teamimpl.ProvideService, diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 54595f3f44a..875035f75b6 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -116,6 +116,7 @@ import ( "github.com/grafana/grafana/pkg/services/shorturls" "github.com/grafana/grafana/pkg/services/shorturls/shorturlimpl" "github.com/grafana/grafana/pkg/services/sqlstore" + starApi "github.com/grafana/grafana/pkg/services/star/api" "github.com/grafana/grafana/pkg/services/star/starimpl" "github.com/grafana/grafana/pkg/services/stats/statsimpl" "github.com/grafana/grafana/pkg/services/store" @@ -327,6 +328,7 @@ var wireBasicSet = wire.NewSet( publicdashboardsStore.ProvideStore, wire.Bind(new(publicdashboards.Store), new(*publicdashboardsStore.PublicDashboardStoreImpl)), publicdashboardsApi.ProvideApi, + starApi.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, statsimpl.ProvideService, diff --git a/pkg/api/stars.go b/pkg/services/star/api/api.go similarity index 72% rename from pkg/api/stars.go rename to pkg/services/star/api/api.go index 128d10e4e62..1867e952116 100644 --- a/pkg/api/stars.go +++ b/pkg/services/star/api/api.go @@ -1,6 +1,7 @@ package api import ( + "context" "net/http" "strconv" @@ -11,12 +12,45 @@ import ( "github.com/grafana/grafana/pkg/web" ) -func (hs *HTTPServer) GetStars(c *models.ReqContext) response.Response { +type API struct { + starService star.Service + dashboardService dashboards.DashboardService +} + +func ProvideApi( + starService star.Service, + dashboardService dashboards.DashboardService, +) *API { + api := &API{ + starService: starService, + dashboardService: dashboardService, + } + return api +} + +func (api *API) getDashboardHelper(ctx context.Context, orgID int64, id int64, uid string) (*dashboards.Dashboard, response.Response) { + var query dashboards.GetDashboardQuery + + if len(uid) > 0 { + query = dashboards.GetDashboardQuery{UID: uid, ID: id, OrgID: orgID} + } else { + query = dashboards.GetDashboardQuery{ID: id, OrgID: orgID} + } + + result, err := api.dashboardService.GetDashboard(ctx, &query) + if err != nil { + return nil, response.Error(404, "Dashboard not found", err) + } + + return result, nil +} + +func (api *API) GetStars(c *models.ReqContext) response.Response { query := star.GetUserStarsQuery{ UserID: c.SignedInUser.UserID, } - iuserstars, err := hs.starService.GetByUser(c.Req.Context(), &query) + iuserstars, err := api.starService.GetByUser(c.Req.Context(), &query) if err != nil { return response.Error(500, "Failed to get user stars", err) } @@ -27,7 +61,7 @@ func (hs *HTTPServer) GetStars(c *models.ReqContext) response.Response { ID: dashboardId, OrgID: c.OrgID, } - queryResult, err := hs.DashboardService.GetDashboard(c.Req.Context(), query) + queryResult, err := api.dashboardService.GetDashboard(c.Req.Context(), query) // Grafana admin users may have starred dashboards in multiple orgs. This will avoid returning errors when the dashboard is in another org if err == nil { @@ -51,7 +85,7 @@ func (hs *HTTPServer) GetStars(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) StarDashboard(c *models.ReqContext) response.Response { +func (api *API) StarDashboard(c *models.ReqContext) response.Response { id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "Invalid dashboard ID", nil) @@ -62,7 +96,7 @@ func (hs *HTTPServer) StarDashboard(c *models.ReqContext) response.Response { return response.Error(400, "Missing dashboard id", nil) } - if err := hs.starService.Add(c.Req.Context(), &cmd); err != nil { + if err := api.starService.Add(c.Req.Context(), &cmd); err != nil { return response.Error(http.StatusInternalServerError, "Failed to star dashboard", err) } @@ -81,12 +115,12 @@ func (hs *HTTPServer) StarDashboard(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) StarDashboardByUID(c *models.ReqContext) response.Response { +func (api *API) StarDashboardByUID(c *models.ReqContext) response.Response { uid := web.Params(c.Req)[":uid"] if uid == "" { return response.Error(http.StatusBadRequest, "Invalid dashboard UID", nil) } - dash, rsp := hs.getDashboardHelper(c.Req.Context(), c.OrgID, 0, uid) + dash, rsp := api.getDashboardHelper(c.Req.Context(), c.OrgID, 0, uid) if rsp != nil { return rsp @@ -94,7 +128,7 @@ func (hs *HTTPServer) StarDashboardByUID(c *models.ReqContext) response.Response cmd := star.StarDashboardCommand{UserID: c.UserID, DashboardID: dash.ID} - if err := hs.starService.Add(c.Req.Context(), &cmd); err != nil { + if err := api.starService.Add(c.Req.Context(), &cmd); err != nil { return response.Error(http.StatusInternalServerError, "Failed to star dashboard", err) } @@ -117,7 +151,7 @@ func (hs *HTTPServer) StarDashboardByUID(c *models.ReqContext) response.Response // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) UnstarDashboard(c *models.ReqContext) response.Response { +func (api *API) UnstarDashboard(c *models.ReqContext) response.Response { id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Error(http.StatusBadRequest, "Invalid dashboard ID", nil) @@ -128,7 +162,7 @@ func (hs *HTTPServer) UnstarDashboard(c *models.ReqContext) response.Response { return response.Error(400, "Missing dashboard id", nil) } - if err := hs.starService.Delete(c.Req.Context(), &cmd); err != nil { + if err := api.starService.Delete(c.Req.Context(), &cmd); err != nil { return response.Error(http.StatusInternalServerError, "Failed to unstar dashboard", err) } @@ -147,19 +181,19 @@ func (hs *HTTPServer) UnstarDashboard(c *models.ReqContext) response.Response { // 401: unauthorisedError // 403: forbiddenError // 500: internalServerError -func (hs *HTTPServer) UnstarDashboardByUID(c *models.ReqContext) response.Response { +func (api *API) UnstarDashboardByUID(c *models.ReqContext) response.Response { uid := web.Params(c.Req)[":uid"] if uid == "" { return response.Error(http.StatusBadRequest, "Invalid dashboard UID", nil) } - dash, rsp := hs.getDashboardHelper(c.Req.Context(), c.OrgID, 0, uid) + dash, rsp := api.getDashboardHelper(c.Req.Context(), c.OrgID, 0, uid) if rsp != nil { return rsp } cmd := star.UnstarDashboardCommand{UserID: c.UserID, DashboardID: dash.ID} - if err := hs.starService.Delete(c.Req.Context(), &cmd); err != nil { + if err := api.starService.Delete(c.Req.Context(), &cmd); err != nil { return response.Error(http.StatusInternalServerError, "Failed to unstar dashboard", err) } From 7c27c866f6f58915779ddf7db7386ff91ef2ad88 Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Wed, 25 Jan 2023 10:04:08 -0500 Subject: [PATCH 032/172] chore: update folder model (json tags) to match previous model (#62117) chore: update folder model to match previous model --- pkg/api/folder.go | 9 +++------ pkg/services/folder/model.go | 4 ++-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/pkg/api/folder.go b/pkg/api/folder.go index fcaf655848c..1a9aa95afcf 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -176,12 +176,9 @@ func (hs *HTTPServer) MoveFolder(c *models.ReqContext) response.Response { var theFolder *folder.Folder var err error if cmd.NewParentUID != "" { - moveCommand := folder.MoveFolderCommand{ - UID: web.Params(c.Req)[":uid"], - NewParentUID: cmd.NewParentUID, - OrgID: c.OrgID, - } - theFolder, err = hs.folderService.Move(c.Req.Context(), &moveCommand) + cmd.OrgID = c.OrgID + cmd.UID = web.Params(c.Req)[":uid"] + theFolder, err = hs.folderService.Move(c.Req.Context(), &cmd) if err != nil { return response.Error(http.StatusInternalServerError, "update folder uid failed", err) } diff --git a/pkg/services/folder/model.go b/pkg/services/folder/model.go index 62ae6a77b0b..50394685b9b 100644 --- a/pkg/services/folder/model.go +++ b/pkg/services/folder/model.go @@ -95,8 +95,8 @@ type UpdateFolderCommand struct { // MoveFolderCommand captures the information required by the folder service // to move a folder. type MoveFolderCommand struct { - UID string `json:"uid"` - NewParentUID string `json:"newParentUid"` + UID string `json:"-"` + NewParentUID string `json:"parentUid"` OrgID int64 `json:"-"` SignedInUser *user.SignedInUser `json:"-"` From 0c8a2bbfd542e61427b5d9b538c9a63b325df9f8 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Wed, 25 Jan 2023 15:31:16 +0000 Subject: [PATCH 033/172] copy .github folder into golang build container since we rely on codeowners (#62122) * copy .github folder into golang build container since we rely on codeowners * remove .github for .dockerignore --- .dockerignore | 1 - Dockerfile | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.dockerignore b/.dockerignore index 7ed8d01615e..1df915d2461 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,7 +2,6 @@ .dockerignore .git .gitignore -.github .vscode bin data* diff --git a/Dockerfile b/Dockerfile index e9bbd124e01..e4ce997e7b8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,6 +53,7 @@ COPY public/api-spec.json public/api-spec.json COPY pkg pkg COPY scripts scripts COPY conf conf +COPY .github .github RUN make build-go From 0a1f31814a1b9b9749db8fd55aa7348252fb4915 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Wed, 25 Jan 2023 09:39:17 -0600 Subject: [PATCH 034/172] Plugins: add UI for more supported datasources with secure socks proxy feature toggle (#61962) --- .../SecureSocksProxySettings.tsx | 45 +++++++++++++++++++ packages/grafana-ui/src/components/index.ts | 1 + .../configuration/ConfigEditor.tsx | 6 ++- .../graphite/configuration/ConfigEditor.tsx | 13 +++++- .../influxdb/components/ConfigEditor.tsx | 16 ++++++- .../jaeger/components/ConfigEditor.tsx | 6 ++- .../loki/configuration/ConfigEditor.tsx | 28 ++---------- public/app/plugins/datasource/loki/types.ts | 1 - .../opentsdb/components/ConfigEditor.tsx | 6 ++- .../prometheus/configuration/ConfigEditor.tsx | 29 ++---------- .../plugins/datasource/prometheus/types.ts | 1 - .../tempo/configuration/ConfigEditor.tsx | 6 ++- .../datasource/zipkin/ConfigEditor.tsx | 6 ++- 13 files changed, 106 insertions(+), 58 deletions(-) create mode 100644 packages/grafana-ui/src/components/DataSourceSettings/SecureSocksProxySettings.tsx diff --git a/packages/grafana-ui/src/components/DataSourceSettings/SecureSocksProxySettings.tsx b/packages/grafana-ui/src/components/DataSourceSettings/SecureSocksProxySettings.tsx new file mode 100644 index 00000000000..79e244a99b3 --- /dev/null +++ b/packages/grafana-ui/src/components/DataSourceSettings/SecureSocksProxySettings.tsx @@ -0,0 +1,45 @@ +import React from 'react'; + +import { DataSourceJsonData, DataSourcePluginOptionsEditorProps } from '@grafana/data'; + +import { InlineSwitch } from '../../components/Switch/Switch'; +import { InlineField } from '../Forms/InlineField'; + +export interface Props + extends Pick, 'options' | 'onOptionsChange'> {} + +export interface SecureSocksProxyConfig extends DataSourceJsonData { + enableSecureSocksProxy?: boolean; +} + +export function SecureSocksProxySettings({ + options, + onOptionsChange, +}: Props): JSX.Element { + return ( + <> +

Secure Socks Proxy

+
+
+
+ + + onOptionsChange({ + ...options, + jsonData: { ...options.jsonData, enableSecureSocksProxy: event!.currentTarget.checked }, + }) + } + /> + +
+
+
+ + ); +} diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 154e11ad566..37afc91a265 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -185,6 +185,7 @@ export { export { ErrorWithStack } from './ErrorBoundary/ErrorWithStack'; export { DataSourceHttpSettings } from './DataSourceSettings/DataSourceHttpSettings'; export { AlertingSettings } from './DataSourceSettings/AlertingSettings'; +export { SecureSocksProxySettings } from './DataSourceSettings/SecureSocksProxySettings'; export { TLSAuthSettings } from './DataSourceSettings/TLSAuthSettings'; export { CertificationKey } from './DataSourceSettings/CertificationKey'; export { Spinner } from './Spinner/Spinner'; diff --git a/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.tsx b/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.tsx index eb288ebc49d..93b20ad676d 100644 --- a/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.tsx +++ b/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useRef } from 'react'; import { SIGV4ConnectionConfig } from '@grafana/aws-sdk'; import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; -import { Alert, DataSourceHttpSettings } from '@grafana/ui'; +import { Alert, DataSourceHttpSettings, SecureSocksProxySettings } from '@grafana/ui'; import { config } from 'app/core/config'; import { ElasticsearchOptions } from '../types'; @@ -57,6 +57,10 @@ export const ConfigEditor = (props: Props) => { renderSigV4Editor={} /> + {config.featureToggles.secureSocksDatasourceProxy && ( + + )} + { dataSourceConfig={options} onChange={onOptionsChange} /> + {config.featureToggles.secureSocksDatasourceProxy && ( + + )}

Graphite details

diff --git a/public/app/plugins/datasource/influxdb/components/ConfigEditor.tsx b/public/app/plugins/datasource/influxdb/components/ConfigEditor.tsx index 7918fc0546b..331f5fed3c2 100644 --- a/public/app/plugins/datasource/influxdb/components/ConfigEditor.tsx +++ b/public/app/plugins/datasource/influxdb/components/ConfigEditor.tsx @@ -11,7 +11,17 @@ import { onUpdateDatasourceSecureJsonDataOption, updateDatasourcePluginJsonDataOption, } from '@grafana/data'; -import { Alert, DataSourceHttpSettings, InfoBox, InlineField, InlineFormLabel, LegacyForms, Select } from '@grafana/ui'; +import { + Alert, + DataSourceHttpSettings, + InfoBox, + InlineField, + InlineFormLabel, + LegacyForms, + Select, + SecureSocksProxySettings, +} from '@grafana/ui'; +import { config } from 'app/core/config'; const { Input, SecretFormField } = LegacyForms; import { BROWSER_MODE_DISABLED_MESSAGE } from '../constants'; @@ -316,6 +326,10 @@ export class ConfigEditor extends PureComponent { onChange={onOptionsChange} /> + {config.featureToggles.secureSocksDatasourceProxy && ( + + )} +

InfluxDB Details

diff --git a/public/app/plugins/datasource/jaeger/components/ConfigEditor.tsx b/public/app/plugins/datasource/jaeger/components/ConfigEditor.tsx index 2ad3f9865f7..6b20122169d 100644 --- a/public/app/plugins/datasource/jaeger/components/ConfigEditor.tsx +++ b/public/app/plugins/datasource/jaeger/components/ConfigEditor.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { DataSourceHttpSettings } from '@grafana/ui'; +import { DataSourceHttpSettings, SecureSocksProxySettings } from '@grafana/ui'; import { SpanBarSettings } from '@jaegertracing/jaeger-ui-components'; import { NodeGraphSettings } from 'app/core/components/NodeGraphSettings'; import { TraceToLogsSettings } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; @@ -20,6 +20,10 @@ export const ConfigEditor = ({ options, onOptionsChange }: Props) => { onChange={onOptionsChange} /> + {config.featureToggles.secureSocksDatasourceProxy && ( + + )} +
diff --git a/public/app/plugins/datasource/loki/configuration/ConfigEditor.tsx b/public/app/plugins/datasource/loki/configuration/ConfigEditor.tsx index 64403d5328d..73706113841 100644 --- a/public/app/plugins/datasource/loki/configuration/ConfigEditor.tsx +++ b/public/app/plugins/datasource/loki/configuration/ConfigEditor.tsx @@ -1,12 +1,8 @@ import React from 'react'; -import { - DataSourcePluginOptionsEditorProps, - DataSourceSettings, - onUpdateDatasourceJsonDataOptionChecked, -} from '@grafana/data'; +import { DataSourcePluginOptionsEditorProps, DataSourceSettings } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { AlertingSettings, DataSourceHttpSettings, InlineField, InlineSwitch } from '@grafana/ui'; +import { AlertingSettings, DataSourceHttpSettings, SecureSocksProxySettings } from '@grafana/ui'; import { LokiOptions } from '../types'; @@ -32,7 +28,6 @@ const setDerivedFields = makeJsonUpdater('derivedFields'); export const ConfigEditor = (props: Props) => { const { options, onOptionsChange } = props; - const socksProxy = config.featureToggles.secureSocksDatasourceProxy; return ( <> @@ -43,23 +38,8 @@ export const ConfigEditor = (props: Props) => { onChange={onOptionsChange} /> - {socksProxy && ( - <> -

Secure Socks Proxy

-
-
- - - -
- + {config.featureToggles.secureSocksDatasourceProxy && ( + )} options={options} onOptionsChange={onOptionsChange} /> diff --git a/public/app/plugins/datasource/loki/types.ts b/public/app/plugins/datasource/loki/types.ts index 574d3e1a52f..ec900a654ab 100644 --- a/public/app/plugins/datasource/loki/types.ts +++ b/public/app/plugins/datasource/loki/types.ts @@ -56,7 +56,6 @@ export interface LokiOptions extends DataSourceJsonData { derivedFields?: DerivedFieldConfig[]; alertmanager?: string; keepCookies?: string[]; - enableSecureSocksProxy?: boolean; } export interface LokiStats { diff --git a/public/app/plugins/datasource/opentsdb/components/ConfigEditor.tsx b/public/app/plugins/datasource/opentsdb/components/ConfigEditor.tsx index fc0cf8395cf..061a37acbf9 100644 --- a/public/app/plugins/datasource/opentsdb/components/ConfigEditor.tsx +++ b/public/app/plugins/datasource/opentsdb/components/ConfigEditor.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; -import { DataSourceHttpSettings } from '@grafana/ui'; +import { config } from '@grafana/runtime'; +import { DataSourceHttpSettings, SecureSocksProxySettings } from '@grafana/ui'; import { OpenTsdbOptions } from '../types'; @@ -17,6 +18,9 @@ export const ConfigEditor = (props: DataSourcePluginOptionsEditorProps + {config.featureToggles.secureSocksDatasourceProxy && ( + + )} ); diff --git a/public/app/plugins/datasource/prometheus/configuration/ConfigEditor.tsx b/public/app/plugins/datasource/prometheus/configuration/ConfigEditor.tsx index f1ca5d5806a..65aa4a669a1 100644 --- a/public/app/plugins/datasource/prometheus/configuration/ConfigEditor.tsx +++ b/public/app/plugins/datasource/prometheus/configuration/ConfigEditor.tsx @@ -1,12 +1,8 @@ import React, { useRef } from 'react'; import { SIGV4ConnectionConfig } from '@grafana/aws-sdk'; -import { - DataSourcePluginOptionsEditorProps, - DataSourceSettings, - onUpdateDatasourceJsonDataOptionChecked, -} from '@grafana/data'; -import { AlertingSettings, DataSourceHttpSettings, Alert, InlineField, InlineSwitch } from '@grafana/ui'; +import { DataSourcePluginOptionsEditorProps, DataSourceSettings } from '@grafana/data'; +import { AlertingSettings, DataSourceHttpSettings, Alert, SecureSocksProxySettings } from '@grafana/ui'; import { config } from 'app/core/config'; import { PromOptions } from '../types'; @@ -29,8 +25,6 @@ export const ConfigEditor = (props: Props) => { azureSettingsUI: AzureAuthSettings, }; - const socksProxy = config.featureToggles.secureSocksDatasourceProxy; - return ( <> {options.access === 'direct' && ( @@ -49,23 +43,8 @@ export const ConfigEditor = (props: Props) => { renderSigV4Editor={} /> - {socksProxy && ( - <> -

Secure Socks Proxy

-
-
- - - -
- + {config.featureToggles.secureSocksDatasourceProxy && ( + )} options={options} onOptionsChange={onOptionsChange} /> diff --git a/public/app/plugins/datasource/prometheus/types.ts b/public/app/plugins/datasource/prometheus/types.ts index 3fbede5a34c..62aec75433b 100644 --- a/public/app/plugins/datasource/prometheus/types.ts +++ b/public/app/plugins/datasource/prometheus/types.ts @@ -34,7 +34,6 @@ export interface PromOptions extends DataSourceJsonData { exemplarTraceIdDestinations?: ExemplarTraceIdDestination[]; prometheusType?: PromApplication; prometheusVersion?: string; - enableSecureSocksProxy?: boolean; defaultEditor?: QueryEditorMode; } diff --git a/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx b/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx index 32f633a5414..b883cc8e611 100644 --- a/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx +++ b/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { DataSourceHttpSettings } from '@grafana/ui'; +import { DataSourceHttpSettings, SecureSocksProxySettings } from '@grafana/ui'; import { SpanBarSettings } from '@jaegertracing/jaeger-ui-components'; import { NodeGraphSettings } from 'app/core/components/NodeGraphSettings'; import { TraceToLogsSettings } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; @@ -25,6 +25,10 @@ export const ConfigEditor = ({ options, onOptionsChange }: Props) => { onChange={onOptionsChange} /> + {config.featureToggles.secureSocksDatasourceProxy && ( + + )} +
diff --git a/public/app/plugins/datasource/zipkin/ConfigEditor.tsx b/public/app/plugins/datasource/zipkin/ConfigEditor.tsx index b6fc6fe8255..f3621bfef59 100644 --- a/public/app/plugins/datasource/zipkin/ConfigEditor.tsx +++ b/public/app/plugins/datasource/zipkin/ConfigEditor.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { DataSourceHttpSettings } from '@grafana/ui'; +import { DataSourceHttpSettings, SecureSocksProxySettings } from '@grafana/ui'; import { SpanBarSettings } from '@jaegertracing/jaeger-ui-components'; import { NodeGraphSettings } from 'app/core/components/NodeGraphSettings'; import { TraceToLogsSettings } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; @@ -20,6 +20,10 @@ export const ConfigEditor = ({ options, onOptionsChange }: Props) => { onChange={onOptionsChange} /> + {config.featureToggles.secureSocksDatasourceProxy && ( + + )} +
From e2899dd6bd4fa09023e068da84678b0c6cc1604b Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Wed, 25 Jan 2023 11:17:09 -0500 Subject: [PATCH 035/172] Docs: Update expression documentation to mention no data (#61934) Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> --- .../query-transform-data/expression-queries/index.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/sources/panels-visualizations/query-transform-data/expression-queries/index.md b/docs/sources/panels-visualizations/query-transform-data/expression-queries/index.md index 6e5af762599..e2724f6818c 100644 --- a/docs/sources/panels-visualizations/query-transform-data/expression-queries/index.md +++ b/docs/sources/panels-visualizations/query-transform-data/expression-queries/index.md @@ -219,3 +219,8 @@ For more information about expressions, refer to [About expressions]({{< relref 1. Write the expression. 1. Click **Apply**. + +## Special cases + +When any queried data source returns no series or numbers, the expression engine returns `NoData`. For example, if a request contains two data source queries that are merged by an expression, if `NoData` is returned by at least one of the data source queries, then the returned result for the entire query is `NoData`. +For more information about how [Grafana Alerting]({{< relref "../../../alerting/" >}}) processes `NoData` results, refer to [No data and error handling]({{< relref "../../../alerting/alerting-rules/create-grafana-managed-rule/#no-data-and-error-handling" >}}). From 4dafdcbdc4cd134250c622dd696489fcfc03958b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 25 Jan 2023 16:27:55 +0000 Subject: [PATCH 036/172] Update dependency fork-ts-checker-webpack-plugin to v7.3.0 (#62129) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index cb962ccccab..a9079fa8025 100644 --- a/package.json +++ b/package.json @@ -190,7 +190,7 @@ "eslint-plugin-react-hooks": "4.6.0", "eslint-webpack-plugin": "3.2.0", "expose-loader": "4.0.0", - "fork-ts-checker-webpack-plugin": "7.2.13", + "fork-ts-checker-webpack-plugin": "7.3.0", "fs-extra": "10.1.0", "glob": "8.0.3", "html-loader": "4.2.0", diff --git a/yarn.lock b/yarn.lock index 83a78188c02..4aac1aa2a42 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20613,9 +20613,9 @@ __metadata: languageName: node linkType: hard -"fork-ts-checker-webpack-plugin@npm:7.2.13": - version: 7.2.13 - resolution: "fork-ts-checker-webpack-plugin@npm:7.2.13" +"fork-ts-checker-webpack-plugin@npm:7.3.0": + version: 7.3.0 + resolution: "fork-ts-checker-webpack-plugin@npm:7.3.0" dependencies: "@babel/code-frame": ^7.16.7 chalk: ^4.1.2 @@ -20636,7 +20636,7 @@ __metadata: peerDependenciesMeta: vue-template-compiler: optional: true - checksum: 3d4694c6fee4b8b2f213d0d10a3f40da770ca0ed3aa2a3dc8d1e701ad1ecaed3a1507f77a1b0cea6ef80539b04d8e5f5f02560e688d310bcb9e8c81f684d2950 + checksum: 49c2af801e264349a3fdf0afe4ad33065960c43bd7e56c8351a5e0d32c8c54146cc89d6a0b70b1e0f810de96787bd0c7fd275cc8727a9aea1a077c53de99659a languageName: node linkType: hard @@ -21766,7 +21766,7 @@ __metadata: fast-deep-equal: ^3.1.3 fast-json-patch: 3.1.1 file-saver: 2.0.5 - fork-ts-checker-webpack-plugin: 7.2.13 + fork-ts-checker-webpack-plugin: 7.3.0 framework-utils: ^1.1.0 fs-extra: 10.1.0 glob: 8.0.3 From 08806924d87c0050797fed415f7db451f8d28ee6 Mon Sep 17 00:00:00 2001 From: Polina Boneva <13227501+polibb@users.noreply.github.com> Date: Wed, 25 Jan 2023 18:29:53 +0200 Subject: [PATCH 037/172] [Chore] Add unit tests to PanelChrome component (#61695) * PanelChrome: test loadingState and status * add some tests * fix error-related tests * clean up * fix tests * pass aria-label from PanelChrome to ToolbarButton * add prop comments and clean up --- .../PanelChrome/PanelChrome.test.tsx | 79 ++++++++++++++++--- .../components/PanelChrome/PanelChrome.tsx | 31 +++++--- .../components/PanelChrome/PanelStatus.tsx | 4 +- 3 files changed, 93 insertions(+), 21 deletions(-) diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.test.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.test.tsx index 2d2169e4812..68eaeebc3f9 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.test.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.test.tsx @@ -1,6 +1,8 @@ import { screen, render } from '@testing-library/react'; import React from 'react'; +import { LoadingState } from '@grafana/data'; + import { PanelChrome, PanelChromeProps } from './PanelChrome'; const setup = (propOverrides?: Partial) => { @@ -35,18 +37,50 @@ it('renders an empty panel with padding', () => { expect(screen.getByText("Panel's Content").parentElement).not.toHaveStyle({ padding: '0px' }); }); -it('renders panel with a header if prop title', () => { +// Check for backwards compatibility +it('renders panel header if prop title', () => { setup({ title: 'Test Panel Header' }); expect(screen.getByTestId('header-container')).toBeInTheDocument(); }); -it('renders panel with a header with title in place if prop title', () => { +// Check for backwards compatibility +it('renders panel with title in place if prop title', () => { setup({ title: 'Test Panel Header' }); expect(screen.getByText('Test Panel Header')).toBeInTheDocument(); }); +// Check for backwards compatibility +it('renders panel with a header if prop leftItems', () => { + setup({ + leftItems: [
This should be a self-contained node
], + }); + + expect(screen.getByTestId('header-container')).toBeInTheDocument(); +}); + +// todo implement when hoverHeader is implemented +it.skip('renders panel without header if no title, no leftItems, and hoverHeader is undefined', () => { + setup(); + + expect(screen.getByTestId('header-container')).toBeInTheDocument(); +}); + +// todo implement when hoverHeader is implemented +it.skip('renders panel with a fixed header if prop hoverHeader is false', () => { + setup({ hoverHeader: false }); + + expect(screen.getByTestId('header-container')).toBeInTheDocument(); +}); + +// todo implement when hoverHeader is implemented +it.skip('renders panel with a hovering header if prop hoverHeader is true', () => { + setup({ title: 'Test Panel Header', hoverHeader: true }); + + expect(screen.queryByTestId('header-container')).not.toBeInTheDocument(); +}); + it('renders panel with a header if prop titleItems', () => { setup({ titleItems: [
This should be a self-contained node
], @@ -63,11 +97,6 @@ it('renders panel with a header with icons in place if prop titleItems', () => { expect(screen.getByTestId('title-items-container')).toBeInTheDocument(); }); -it.skip('renders panel with a fixed header if prop hoverHeader is false', () => { - // setup({ title: 'Test Panel Header', hoverHeader: false }); - // expect(screen.getByTestId('header-container')).toBeInTheDocument(); -}); - it('renders panel with a header if prop menu', () => { setup({ menu:
Menu
}); @@ -81,8 +110,38 @@ it('renders panel with a show-on-hover menu icon if prop menu', () => { expect(screen.getByTestId('panel-menu-button')).not.toBeVisible(); }); -it.skip('renders states in the panel header if any given', () => {}); +it('renders error status in the panel header if any given', () => { + setup({ statusMessage: 'Error test' }); -it.skip('renders leftItems in the panel header if any given when no states prop is given', () => {}); + expect(screen.getByLabelText('Panel status')).toBeInTheDocument(); +}); -it.skip('renders states in the panel header if both leftItems and states are given', () => {}); +it('does not render error status in the panel header if loadingState is error, but no statusMessage', () => { + setup({ loadingState: LoadingState.Error, statusMessage: '' }); + + expect(screen.queryByTestId('panel-status')).not.toBeInTheDocument(); +}); + +it('renders loading indicator in the panel header if loadingState is loading', () => { + setup({ loadingState: LoadingState.Loading }); + + expect(screen.getByLabelText('Panel loading bar')).toBeInTheDocument(); +}); + +it('renders loading indicator in the panel header if loadingState is loading regardless of not having a header', () => { + setup({ loadingState: LoadingState.Loading, hoverHeader: true }); + + expect(screen.getByLabelText('Panel loading bar')).toBeInTheDocument(); +}); + +it('renders loading indicator in the panel header if loadingState is loading regardless of having a header', () => { + setup({ loadingState: LoadingState.Loading, hoverHeader: false }); + + expect(screen.getByLabelText('Panel loading bar')).toBeInTheDocument(); +}); + +it('renders streaming indicator in the panel header if loadingState is streaming', () => { + setup({ loadingState: LoadingState.Streaming }); + + expect(screen.getByTestId('panel-streaming')).toBeInTheDocument(); +}); diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx index 9f91c0cc315..5ea484bbf2b 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx @@ -26,10 +26,13 @@ export interface PanelChromeProps { description?: string | (() => string); titleItems?: ReactNode[]; menu?: ReactElement | (() => ReactElement); - /** dragClass, hoverHeader not yet implemented */ dragClass?: string; dragClassCancel?: string; hoverHeader?: boolean; + /** + * Use only to indicate loading or streaming data in the panel. + * Any other values of loadingState are ignored. + */ loadingState?: LoadingState; /** * Used to display status message (used for panel errors currently) @@ -39,11 +42,13 @@ export interface PanelChromeProps { * Handle opening error details view (like inspect / error tab) */ statusMessageOnClick?: (e: React.SyntheticEvent) => void; - /** @deprecated in favor of props - * status for errors and loadingState for loading and streaming + /** + * @deprecated in favor of props + * statusMessage for error messages + * and loadingState for loading and streaming data * which will serve the same purpose - * of showing/interacting with the panel's data state - * */ + * of showing/interacting with the panel's state + */ leftItems?: ReactNode[]; } @@ -64,14 +69,13 @@ export function PanelChrome({ description = '', titleItems = [], menu, - // dragClass, + dragClass, + dragClassCancel, hoverHeader = false, loadingState, statusMessage, statusMessageOnClick, leftItems, - dragClass, - dragClassCancel, }: PanelChromeProps) { const theme = useTheme2(); const styles = useStyles2(getStyles); @@ -108,7 +112,9 @@ export function PanelChrome({ return (
- {loadingState === LoadingState.Loading ? : null} + {loadingState === LoadingState.Loading ? ( + + ) : null}
@@ -127,7 +133,7 @@ export function PanelChrome({ )} {loadingState === LoadingState.Streaming && ( -
+
@@ -156,6 +162,7 @@ export function PanelChrome({ className={cx(styles.errorContainer, dragClassCancel)} message={statusMessage} onClick={statusMessageOnClick} + ariaLabel="Panel status" /> )}
@@ -229,6 +236,7 @@ const getStyles = (theme: GrafanaTheme2) => { }, }), loadingBarContainer: css({ + label: 'panel-loading-bar-container', position: 'absolute', top: 0, width: '100%', @@ -246,6 +254,7 @@ const getStyles = (theme: GrafanaTheme2) => { padding: theme.spacing(0, 0, 0, 1), }), streaming: css({ + label: 'panel-streaming', marginRight: 0, color: theme.colors.success.text, @@ -254,6 +263,7 @@ const getStyles = (theme: GrafanaTheme2) => { }, }), title: css({ + label: 'panel-title', marginBottom: 0, // override default h6 margin-bottom textOverflow: 'ellipsis', overflow: 'hidden', @@ -270,6 +280,7 @@ const getStyles = (theme: GrafanaTheme2) => { alignItems: 'center', }), menuItem: css({ + label: 'panel-menu', visibility: 'hidden', border: 'none', }), diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelStatus.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelStatus.tsx index 3a61ca6ec7b..15bbc4a3a08 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelStatus.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelStatus.tsx @@ -10,9 +10,10 @@ export interface Props { className?: string; message?: string; onClick?: (e: React.SyntheticEvent) => void; + ariaLabel?: string; } -export function PanelStatus({ className, message, onClick }: Props) { +export function PanelStatus({ className, message, onClick, ariaLabel = 'status' }: Props) { const styles = useStyles2(getStyles); return ( @@ -22,6 +23,7 @@ export function PanelStatus({ className, message, onClick }: Props) { variant={'destructive'} icon="exclamation-triangle" tooltip={message || ''} + aria-label={ariaLabel} /> ); } From 069dc2d357b34aeb9f68d13a4e2aadfcd5ab68b0 Mon Sep 17 00:00:00 2001 From: Taewoo K Date: Wed, 25 Jan 2023 11:32:35 -0500 Subject: [PATCH 038/172] add export customHeadersSettings component (#62131) --- packages/grafana-ui/src/components/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 37afc91a265..368c4beb04f 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -184,6 +184,7 @@ export { } from './ErrorBoundary/ErrorBoundary'; export { ErrorWithStack } from './ErrorBoundary/ErrorWithStack'; export { DataSourceHttpSettings } from './DataSourceSettings/DataSourceHttpSettings'; +export { CustomHeadersSettings } from './DataSourceSettings/CustomHeadersSettings'; export { AlertingSettings } from './DataSourceSettings/AlertingSettings'; export { SecureSocksProxySettings } from './DataSourceSettings/SecureSocksProxySettings'; export { TLSAuthSettings } from './DataSourceSettings/TLSAuthSettings'; From 3c073b00ec384515c725284b00b2b46ed527262b Mon Sep 17 00:00:00 2001 From: Selene Date: Wed, 25 Jan 2023 18:03:46 +0100 Subject: [PATCH 039/172] Schema: Fix interfaces with blank spaces (#62144) Fixes interfaces with blank spaces --- public/app/plugins/gen.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/gen.go b/public/app/plugins/gen.go index 9baebddf60b..f79e6d6c869 100644 --- a/public/app/plugins/gen.go +++ b/public/app/plugins/gen.go @@ -86,7 +86,7 @@ func main() { func adaptToPipeline(j codejen.OneToOne[corecodegen.SchemaForGen]) codejen.OneToOne[*pfs.PluginDecl] { return codejen.AdaptOneToOne(j, func(pd *pfs.PluginDecl) corecodegen.SchemaForGen { return corecodegen.SchemaForGen{ - Name: pd.PluginMeta.Name, + Name: strings.ReplaceAll(pd.PluginMeta.Name, " ", ""), Schema: pd.Lineage.Latest(), IsGroup: pd.SchemaInterface.IsGroup(), } From f99523048a8357e5ac13d1422be30b314b1f751a Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Wed, 25 Jan 2023 18:07:00 +0100 Subject: [PATCH 040/172] Schema: Covering service accounts Core Kind (#62091) * Adding service accounts schema Co-authored-by: Eric Leijonmarck * Change metadata field Co-authored-by: Eric Leijonmarck * Merging the two DTOs into one schema Co-authored-by: Eric Leijonmarck * Eric dedup things Co-authored-by: Eric Leijonmarck * Add optional to teams/created/updated Co-authored-by: Eric Leijonmarck * Lower case the OrgId Co-authored-by: Eric Leijonmarck Co-authored-by: Eric Leijonmarck --- .../core/serviceaccount/schema-reference.md | 38 ++++++ kinds/serviceaccount/serviceaccount_kind.cue | 46 +++++++ packages/grafana-schema/src/index.gen.ts | 9 ++ .../x/serviceaccount_types.gen.ts | 71 +++++++++++ .../serviceaccount/serviceaccount_kind_gen.go | 113 ++++++++++++++++++ .../serviceaccount_types_gen.go | 64 ++++++++++ pkg/kindsys/report.go | 2 +- pkg/kindsys/report.json | 29 +++-- pkg/registry/corekind/base_gen.go | 22 +++- 9 files changed, 377 insertions(+), 17 deletions(-) create mode 100644 docs/sources/developers/kinds/core/serviceaccount/schema-reference.md create mode 100644 kinds/serviceaccount/serviceaccount_kind.cue create mode 100644 packages/grafana-schema/src/raw/serviceaccount/x/serviceaccount_types.gen.ts create mode 100644 pkg/kinds/serviceaccount/serviceaccount_kind_gen.go create mode 100644 pkg/kinds/serviceaccount/serviceaccount_types_gen.go diff --git a/docs/sources/developers/kinds/core/serviceaccount/schema-reference.md b/docs/sources/developers/kinds/core/serviceaccount/schema-reference.md new file mode 100644 index 00000000000..e668f64aa2f --- /dev/null +++ b/docs/sources/developers/kinds/core/serviceaccount/schema-reference.md @@ -0,0 +1,38 @@ +--- +keywords: + - grafana + - schema +title: Serviceaccount kind +--- +> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. + +# Serviceaccount kind + +## Maturity: merged +## Version: 0.0 + +## Properties + +| Property | Type | Required | Description | +|-----------------|--------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------| +| `avatarUrl` | string | **Yes** | AvatarUrl is the service account's avatar URL. It allows the frontend to display a picture in front
of the service account. | +| `id` | integer | **Yes** | ID is the unique identifier of the service account in the database. | +| `isDisabled` | boolean | **Yes** | IsDisabled indicates if the service account is disabled. | +| `login` | string | **Yes** | Login of the service account. | +| `name` | string | **Yes** | Name of the service account. | +| `orgId` | integer | **Yes** | OrgId is the ID of an organisation the service account belongs to. | +| `role` | string | **Yes** | OrgRole is a Grafana Organization Role which can be 'Viewer', 'Editor', 'Admin'. Possible values are: `Admin`, `Editor`, `Viewer`. | +| `tokens` | integer | **Yes** | Tokens is the number of active tokens for the service account.
Tokens are used to authenticate the service account against Grafana. | +| `accessControl` | [object](#accesscontrol) | No | AccessControl metadata associated with a given resource. | +| `created` | integer | No | Created indicates when the service account was created. | +| `teams` | string[] | No | Teams is a list of teams the service account belongs to. | +| `updated` | integer | No | Updated indicates when the service account was updated. | + +## accessControl + +AccessControl metadata associated with a given resource. + +| Property | Type | Required | Description | +|----------|------|----------|-------------| + + diff --git a/kinds/serviceaccount/serviceaccount_kind.cue b/kinds/serviceaccount/serviceaccount_kind.cue new file mode 100644 index 00000000000..befc567bc9a --- /dev/null +++ b/kinds/serviceaccount/serviceaccount_kind.cue @@ -0,0 +1,46 @@ +package kind + +name: "Serviceaccount" +maturity: "merged" + +lineage: seqs: [ + { + schemas: [ + // v0.0 + { + // ID is the unique identifier of the service account in the database. + id: int64 @grafanamaturity(ToMetadata="sys") + // OrgId is the ID of an organisation the service account belongs to. + orgId: int64 @grafanamaturity(ToMetadata="sys") + // Name of the service account. + name: string + // Login of the service account. + login: string + // IsDisabled indicates if the service account is disabled. + isDisabled: bool + // Role is the Grafana organization role of the service account which can be 'Viewer', 'Editor', 'Admin'. + role: #OrgRole @grafanamaturity(ToMetadata="kind") + // Tokens is the number of active tokens for the service account. + // Tokens are used to authenticate the service account against Grafana. + tokens: int64 @grafanamaturity(ToMetadata="kind") + // AvatarUrl is the service account's avatar URL. It allows the frontend to display a picture in front + // of the service account. + avatarUrl: string @grafanamaturity(ToMetadata="kind") + // AccessControl metadata associated with a given resource. + accessControl?: { + [string]: bool @grafanamaturity(ToMetadata="sys") + } + + // Teams is a list of teams the service account belongs to. + teams?: [...string] @grafanamaturity(ToMetadata="sys") + // Created indicates when the service account was created. + created?: int64 @grafanamaturity(ToMetadata="sys") + // Updated indicates when the service account was updated. + updated?: int64 @grafanamaturity(ToMetadata="sys") + + // OrgRole is a Grafana Organization Role which can be 'Viewer', 'Editor', 'Admin'. + #OrgRole: "Admin" | "Editor" | "Viewer" @cuetsy(kind="type") + }, + ] + }, +] diff --git a/packages/grafana-schema/src/index.gen.ts b/packages/grafana-schema/src/index.gen.ts index 9fdf476a0e4..6e9378c10c7 100644 --- a/packages/grafana-schema/src/index.gen.ts +++ b/packages/grafana-schema/src/index.gen.ts @@ -95,6 +95,15 @@ export type { // Raw generated enums and default consts from playlist kind. export { defaultPlaylist } from './raw/playlist/x/playlist_types.gen'; +// Raw generated types from Serviceaccount kind. +export type { + Serviceaccount, + OrgRole +} from './raw/serviceaccount/x/serviceaccount_types.gen'; + +// Raw generated enums and default consts from serviceaccount kind. +export { defaultServiceaccount } from './raw/serviceaccount/x/serviceaccount_types.gen'; + // Raw generated types from Team kind. export type { Team } from './raw/team/x/team_types.gen'; diff --git a/packages/grafana-schema/src/raw/serviceaccount/x/serviceaccount_types.gen.ts b/packages/grafana-schema/src/raw/serviceaccount/x/serviceaccount_types.gen.ts new file mode 100644 index 00000000000..4abfa4f7cbc --- /dev/null +++ b/packages/grafana-schema/src/raw/serviceaccount/x/serviceaccount_types.gen.ts @@ -0,0 +1,71 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. +// +// Generated by: +// kinds/gen.go +// Using jennies: +// TSTypesJenny +// LatestMajorsOrXJenny +// +// Run 'make gen-cue' from repository root to regenerate. + +/** + * OrgRole is a Grafana Organization Role which can be 'Viewer', 'Editor', 'Admin'. + */ +export type OrgRole = ('Admin' | 'Editor' | 'Viewer'); + +export interface Serviceaccount { + /** + * AccessControl metadata associated with a given resource. + */ + accessControl?: Record; + /** + * AvatarUrl is the service account's avatar URL. It allows the frontend to display a picture in front + * of the service account. + */ + avatarUrl: string; + /** + * Created indicates when the service account was created. + */ + created?: number; + /** + * ID is the unique identifier of the service account in the database. + */ + id: number; + /** + * IsDisabled indicates if the service account is disabled. + */ + isDisabled: boolean; + /** + * Login of the service account. + */ + login: string; + /** + * Name of the service account. + */ + name: string; + /** + * OrgId is the ID of an organisation the service account belongs to. + */ + orgId: number; + /** + * Role is the Grafana organization role of the service account which can be 'Viewer', 'Editor', 'Admin'. + */ + role: OrgRole; + /** + * Teams is a list of teams the service account belongs to. + */ + teams?: Array; + /** + * Tokens is the number of active tokens for the service account. + * Tokens are used to authenticate the service account against Grafana. + */ + tokens: number; + /** + * Updated indicates when the service account was updated. + */ + updated?: number; +} + +export const defaultServiceaccount: Partial = { + teams: [], +}; diff --git a/pkg/kinds/serviceaccount/serviceaccount_kind_gen.go b/pkg/kinds/serviceaccount/serviceaccount_kind_gen.go new file mode 100644 index 00000000000..097d21c12c1 --- /dev/null +++ b/pkg/kinds/serviceaccount/serviceaccount_kind_gen.go @@ -0,0 +1,113 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. +// +// Generated by: +// kinds/gen.go +// Using jennies: +// CoreKindJenny +// +// Run 'make gen-cue' from repository root to regenerate. + +package serviceaccount + +import ( + "github.com/grafana/grafana/pkg/kindsys" + "github.com/grafana/thema" + "github.com/grafana/thema/vmux" +) + +// rootrel is the relative path from the grafana repository root to the +// directory containing the .cue files in which this kind is declared. Necessary +// for runtime errors related to the declaration and/or lineage to provide +// a real path to the correct .cue file. +const rootrel string = "kinds/serviceaccount" + +// TODO standard generated docs +type Kind struct { + lin thema.ConvergentLineage[*Serviceaccount] + jcodec vmux.Codec + valmux vmux.ValueMux[*Serviceaccount] + decl kindsys.Decl[kindsys.CoreProperties] +} + +// type guard +var _ kindsys.Core = &Kind{} + +// TODO standard generated docs +func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) { + decl, err := kindsys.LoadCoreKind(rootrel, rt.Context(), nil) + if err != nil { + return nil, err + } + k := &Kind{ + decl: decl, + } + + lin, err := decl.Some().BindKindLineage(rt, opts...) + if err != nil { + return nil, err + } + + // Get the thema.Schema that the meta says is in the current version (which + // codegen ensures is always the latest) + cursch := thema.SchemaP(lin, k.decl.Properties.CurrentVersion) + tsch, err := thema.BindType[*Serviceaccount](cursch, &Serviceaccount{}) + if err != nil { + // Should be unreachable, modulo bugs in the Thema->Go code generator + return nil, err + } + + k.jcodec = vmux.NewJSONCodec("serviceaccount.json") + k.lin = tsch.ConvergentLineage() + k.valmux = vmux.NewValueMux(k.lin.TypedSchema(), k.jcodec) + return k, nil +} + +// TODO standard generated docs +func (k *Kind) Name() string { + return "serviceaccount" +} + +// TODO standard generated docs +func (k *Kind) MachineName() string { + return "serviceaccount" +} + +// TODO standard generated docs +func (k *Kind) Lineage() thema.Lineage { + return k.lin +} + +// TODO standard generated docs +func (k *Kind) ConvergentLineage() thema.ConvergentLineage[*Serviceaccount] { + return k.lin +} + +// JSONValueMux is a version multiplexer that maps a []byte containing JSON data +// at any schematized dashboard version to an instance of Serviceaccount. +// +// Validation and translation errors emitted from this func will identify the +// input bytes as "dashboard.json". +// +// This is a thin wrapper around Thema's [vmux.ValueMux]. +func (k *Kind) JSONValueMux(b []byte) (*Serviceaccount, thema.TranslationLacunas, error) { + return k.valmux(b) +} + +// TODO standard generated docs +func (k *Kind) Maturity() kindsys.Maturity { + return k.decl.Properties.Maturity +} + +// Decl returns the [kindsys.Decl] containing both CUE and Go representations of the +// serviceaccount declaration in .cue files. +func (k *Kind) Decl() kindsys.Decl[kindsys.CoreProperties] { + return k.decl +} + +// Props returns a [kindsys.SomeKindProps], with underlying type [kindsys.CoreProperties], +// representing the static properties declared in the serviceaccount kind. +// +// This method is identical to calling Decl().Props. It is provided to satisfy [kindsys.Interface]. +func (k *Kind) Props() kindsys.SomeKindProperties { + return k.decl.Properties +} diff --git a/pkg/kinds/serviceaccount/serviceaccount_types_gen.go b/pkg/kinds/serviceaccount/serviceaccount_types_gen.go new file mode 100644 index 00000000000..c539cf24d1b --- /dev/null +++ b/pkg/kinds/serviceaccount/serviceaccount_types_gen.go @@ -0,0 +1,64 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. +// +// Generated by: +// kinds/gen.go +// Using jennies: +// GoTypesJenny +// LatestJenny +// +// Run 'make gen-cue' from repository root to regenerate. + +package serviceaccount + +// Defines values for OrgRole. +const ( + OrgRoleAdmin OrgRole = "Admin" + + OrgRoleEditor OrgRole = "Editor" + + OrgRoleViewer OrgRole = "Viewer" +) + +// OrgRole is a Grafana Organization Role which can be 'Viewer', 'Editor', 'Admin'. +type OrgRole string + +// Serviceaccount defines model for serviceaccount. +type Serviceaccount struct { + // AccessControl metadata associated with a given resource. + AccessControl map[string]bool `json:"accessControl,omitempty"` + + // AvatarUrl is the service account's avatar URL. It allows the frontend to display a picture in front + // of the service account. + AvatarUrl string `json:"avatarUrl"` + + // Created indicates when the service account was created. + Created *int64 `json:"created,omitempty"` + + // ID is the unique identifier of the service account in the database. + Id int64 `json:"id"` + + // IsDisabled indicates if the service account is disabled. + IsDisabled bool `json:"isDisabled"` + + // Login of the service account. + Login string `json:"login"` + + // Name of the service account. + Name string `json:"name"` + + // OrgId is the ID of an organisation the service account belongs to. + OrgId int64 `json:"orgId"` + + // OrgRole is a Grafana Organization Role which can be 'Viewer', 'Editor', 'Admin'. + Role OrgRole `json:"role"` + + // Teams is a list of teams the service account belongs to. + Teams *[]string `json:"teams,omitempty"` + + // Tokens is the number of active tokens for the service account. + // Tokens are used to authenticate the service account against Grafana. + Tokens int64 `json:"tokens"` + + // Updated indicates when the service account was updated. + Updated *int64 `json:"updated,omitempty"` +} diff --git a/pkg/kindsys/report.go b/pkg/kindsys/report.go index b853abbe97d..97f12d894fd 100644 --- a/pkg/kindsys/report.go +++ b/pkg/kindsys/report.go @@ -71,7 +71,7 @@ var plannedCoreKinds = []string{ "Folder", "DataSource", "APIKey", - "ServiceAccount", + "Serviceaccount", "Thumb", "Query", "QueryHistory", diff --git a/pkg/kindsys/report.json b/pkg/kindsys/report.json index 7ed9ccdc6b4..35d314c27c9 100644 --- a/pkg/kindsys/report.json +++ b/pkg/kindsys/report.json @@ -1299,24 +1299,29 @@ }, "serviceaccount": { "category": "core", - "codeowners": [], + "codeowners": [ + "grafana/grafana-as-code", + "grafana/grafana-bi-squad", + "grafana/plugins-platform-frontend", + "grafana/user-essentials" + ], "currentVersion": [ 0, 0 ], - "grafanaMaturityCount": 0, + "grafanaMaturityCount": 9, "lineageIsGroup": false, "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" + "docs": "https://grafana.com/docs/grafana/next/developers/kinds/core/serviceaccount/schema-reference", + "go": "https://github.com/grafana/grafana/tree/main/pkg/kinds/serviceaccount", + "schema": "https://github.com/grafana/grafana/tree/main/kinds/serviceaccount/serviceaccount_kind.cue", + "ts": "https://github.com/grafana/grafana/tree/main/packages/grafana-schema/src/raw/serviceaccount/x/serviceaccount_types.gen.ts" }, "machineName": "serviceaccount", - "maturity": "planned", - "name": "ServiceAccount", + "maturity": "merged", + "name": "Serviceaccount", "pluralMachineName": "serviceaccounts", - "pluralName": "ServiceAccounts" + "pluralName": "Serviceaccounts" }, "statpanelcfg": { "category": "composable", @@ -1772,9 +1777,10 @@ "items": [ "alertgroupspanelcfg", "playlist", + "serviceaccount", "team" ], - "count": 3 + "count": 4 }, "planned": { "name": "planned", @@ -1826,7 +1832,6 @@ "prometheusdatasourcecfg", "query", "queryhistory", - "serviceaccount", "tableoldpanelcfg", "tempodataquery", "tempodatasourcecfg", @@ -1840,7 +1845,7 @@ "zipkindataquery", "zipkindatasourcecfg" ], - "count": 60 + "count": 59 }, "stable": { "name": "stable", diff --git a/pkg/registry/corekind/base_gen.go b/pkg/registry/corekind/base_gen.go index 2e9753ed1a6..f3e52a08564 100644 --- a/pkg/registry/corekind/base_gen.go +++ b/pkg/registry/corekind/base_gen.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/kinds/dashboard" "github.com/grafana/grafana/pkg/kinds/playlist" + "github.com/grafana/grafana/pkg/kinds/serviceaccount" "github.com/grafana/grafana/pkg/kinds/team" "github.com/grafana/grafana/pkg/kindsys" "github.com/grafana/thema" @@ -30,16 +31,18 @@ import ( // Prefer All*() methods when performing operations generically across all kinds. // For example, a validation HTTP middleware for any kind-schematized object type. type Base struct { - all []kindsys.Core - dashboard *dashboard.Kind - playlist *playlist.Kind - team *team.Kind + all []kindsys.Core + dashboard *dashboard.Kind + playlist *playlist.Kind + serviceaccount *serviceaccount.Kind + team *team.Kind } // type guards var ( _ kindsys.Core = &dashboard.Kind{} _ kindsys.Core = &playlist.Kind{} + _ kindsys.Core = &serviceaccount.Kind{} _ kindsys.Core = &team.Kind{} ) @@ -53,6 +56,11 @@ func (b *Base) Playlist() *playlist.Kind { return b.playlist } +// Serviceaccount returns the [kindsys.Interface] implementation for the serviceaccount kind. +func (b *Base) Serviceaccount() *serviceaccount.Kind { + return b.serviceaccount +} + // Team returns the [kindsys.Interface] implementation for the team kind. func (b *Base) Team() *team.Kind { return b.team @@ -74,6 +82,12 @@ func doNewBase(rt *thema.Runtime) *Base { } reg.all = append(reg.all, reg.playlist) + reg.serviceaccount, err = serviceaccount.NewKind(rt) + if err != nil { + panic(fmt.Sprintf("error while initializing the serviceaccount Kind: %s", err)) + } + reg.all = append(reg.all, reg.serviceaccount) + reg.team, err = team.NewKind(rt) if err != nil { panic(fmt.Sprintf("error while initializing the team Kind: %s", err)) From 1f55a5454355d00ffbca62330bc7aa987f152688 Mon Sep 17 00:00:00 2001 From: Hamas Shafiq Date: Wed, 25 Jan 2023 17:12:59 +0000 Subject: [PATCH 041/172] Tempo: Create separate functions for querying the v1/v2 API for tag values (#61998) --- .../tempo/QueryEditor/NativeSearch.test.tsx | 4 ++-- .../tempo/QueryEditor/NativeSearch.tsx | 2 +- .../tempo/QueryEditor/TagsField/autocomplete.ts | 2 +- .../datasource/tempo/language_provider.ts | 16 +++++++++++++--- .../tempo/traceql/autocomplete.test.ts | 8 ++++---- .../datasource/tempo/traceql/autocomplete.ts | 2 +- 6 files changed, 22 insertions(+), 12 deletions(-) diff --git a/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.test.tsx b/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.test.tsx index 65ff13695d5..11e859853e7 100644 --- a/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.test.tsx +++ b/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.test.tsx @@ -7,7 +7,7 @@ import { TempoQuery } from '../types'; import NativeSearch from './NativeSearch'; -const getOptions = jest.fn().mockImplementation(() => { +const getOptionsV1 = jest.fn().mockImplementation(() => { return new Promise((resolve) => { setTimeout(() => { resolve([ @@ -26,7 +26,7 @@ const getOptions = jest.fn().mockImplementation(() => { jest.mock('../language_provider', () => { return jest.fn().mockImplementation(() => { - return { getOptions }; + return { getOptionsV1 }; }); }); diff --git a/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.tsx b/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.tsx index eb46bc551e5..7ea74ab8f4a 100644 --- a/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.tsx +++ b/public/app/plugins/datasource/tempo/QueryEditor/NativeSearch.tsx @@ -45,7 +45,7 @@ const NativeSearch = ({ datasource, query, onChange, onBlur, onRunQuery }: Props setIsLoading((prevValue) => ({ ...prevValue, [name]: true })); try { - const options = await languageProvider.getOptions(lpName); + const options = await languageProvider.getOptionsV1(lpName); const filteredOptions = options.filter((item) => (item.value ? fuzzyMatch(item.value, query).found : false)); return filteredOptions; } catch (error) { diff --git a/public/app/plugins/datasource/tempo/QueryEditor/TagsField/autocomplete.ts b/public/app/plugins/datasource/tempo/QueryEditor/TagsField/autocomplete.ts index a8ac9d088e3..93f51e9ad29 100644 --- a/public/app/plugins/datasource/tempo/QueryEditor/TagsField/autocomplete.ts +++ b/public/app/plugins/datasource/tempo/QueryEditor/TagsField/autocomplete.ts @@ -78,7 +78,7 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP if (this.cachedValues.hasOwnProperty(tagName)) { tagValues = this.cachedValues[tagName]; } else { - tagValues = await this.languageProvider.getOptions(tagName); + tagValues = await this.languageProvider.getOptionsV1(tagName); this.cachedValues[tagName] = tagValues; } return tagValues; diff --git a/public/app/plugins/datasource/tempo/language_provider.ts b/public/app/plugins/datasource/tempo/language_provider.ts index 117118e6e4f..d0186053e77 100644 --- a/public/app/plugins/datasource/tempo/language_provider.ts +++ b/public/app/plugins/datasource/tempo/language_provider.ts @@ -87,10 +87,21 @@ export default class TempoLanguageProvider extends LanguageProvider { return { suggestions }; } - async getOptions(tag: string): Promise>> { + async getOptionsV1(tag: string): Promise>> { + const response = await this.request(`/api/search/tag/${tag}/values`); + let options: Array> = []; + if (response && response.tagValues) { + options = response.tagValues.map((v: string) => ({ + value: v, + label: v, + })); + } + return options; + } + + async getOptionsV2(tag: string): Promise>> { const response = await this.request(`/api/v2/search/tag/${tag}/values`); let options: Array> = []; - if (response && response.tagValues) { options = response.tagValues.map((v: { type: string; value: string }) => ({ type: v.type, @@ -98,7 +109,6 @@ export default class TempoLanguageProvider extends LanguageProvider { label: v.value, })); } - return options; } } diff --git a/public/app/plugins/datasource/tempo/traceql/autocomplete.test.ts b/public/app/plugins/datasource/tempo/traceql/autocomplete.test.ts index 516aed301de..1533b21cb76 100644 --- a/public/app/plugins/datasource/tempo/traceql/autocomplete.test.ts +++ b/public/app/plugins/datasource/tempo/traceql/autocomplete.test.ts @@ -29,7 +29,7 @@ describe('CompletionProvider', () => { it('does not wrap the tag value in quotes if the type in the response is something other than "string"', async () => { const { provider, model } = setup('{foo=}', 5, defaultTags); - jest.spyOn(provider.languageProvider, 'getOptions').mockImplementation( + jest.spyOn(provider.languageProvider, 'getOptionsV2').mockImplementation( () => new Promise((resolve) => { resolve([ @@ -54,7 +54,7 @@ describe('CompletionProvider', () => { it('wraps the tag value in quotes if the type in the response is set to "string"', async () => { const { provider, model } = setup('{foo=}', 5, defaultTags); - jest.spyOn(provider.languageProvider, 'getOptions').mockImplementation( + jest.spyOn(provider.languageProvider, 'getOptionsV2').mockImplementation( () => new Promise((resolve) => { resolve([ @@ -79,7 +79,7 @@ describe('CompletionProvider', () => { it('inserts the tag value without quotes if the user has entered quotes', async () => { const { provider, model } = setup('{foo="}', 6, defaultTags); - jest.spyOn(provider.languageProvider, 'getOptions').mockImplementation( + jest.spyOn(provider.languageProvider, 'getOptionsV2').mockImplementation( () => new Promise((resolve) => { resolve([ @@ -171,7 +171,7 @@ describe('CompletionProvider', () => { it('suggests tag values after a space inside a string', async () => { const { provider, model } = setup('{foo="bar test " }', 15, defaultTags); - jest.spyOn(provider.languageProvider, 'getOptions').mockImplementation( + jest.spyOn(provider.languageProvider, 'getOptionsV2').mockImplementation( () => new Promise((resolve) => { resolve([ diff --git a/public/app/plugins/datasource/tempo/traceql/autocomplete.ts b/public/app/plugins/datasource/tempo/traceql/autocomplete.ts index f7ea65a181b..dc0eab1c857 100644 --- a/public/app/plugins/datasource/tempo/traceql/autocomplete.ts +++ b/public/app/plugins/datasource/tempo/traceql/autocomplete.ts @@ -98,7 +98,7 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP if (this.cachedValues.hasOwnProperty(tagName)) { tagValues = this.cachedValues[tagName]; } else { - tagValues = await this.languageProvider.getOptions(tagName); + tagValues = await this.languageProvider.getOptionsV2(tagName); this.cachedValues[tagName] = tagValues; } return tagValues; From 046a9bb7c1d1aa946347e17ccdfab67ca5cddd25 Mon Sep 17 00:00:00 2001 From: Alexander Weaver Date: Wed, 25 Jan 2023 11:29:57 -0600 Subject: [PATCH 042/172] Alerting: Copy rule definitions into state history (#62032) * Copy rules instead of accepting pointer * Deep-copy the rule, for even more guarantees * Create struct just for needed fields * Move RuleMeta to historian/model package, iron out package dependencies * Move tests for dash ID parsing to model package along with code --- .../ngalert/state/historian/annotation.go | 7 +- pkg/services/ngalert/state/historian/core.go | 17 ++-- .../ngalert/state/historian/core_test.go | 67 ---------------- pkg/services/ngalert/state/historian/loki.go | 7 +- .../ngalert/state/historian/model/rule.go | 46 +++++++++++ .../state/historian/model/rule_test.go | 77 +++++++++++++++++++ pkg/services/ngalert/state/historian/noop.go | 4 +- pkg/services/ngalert/state/historian/sql.go | 3 +- pkg/services/ngalert/state/manager.go | 3 +- pkg/services/ngalert/state/persist.go | 3 +- pkg/services/ngalert/state/testing.go | 3 +- 11 files changed, 146 insertions(+), 91 deletions(-) create mode 100644 pkg/services/ngalert/state/historian/model/rule.go create mode 100644 pkg/services/ngalert/state/historian/model/rule_test.go diff --git a/pkg/services/ngalert/state/historian/annotation.go b/pkg/services/ngalert/state/historian/annotation.go index 03f1b73faaa..188c9010371 100644 --- a/pkg/services/ngalert/state/historian/annotation.go +++ b/pkg/services/ngalert/state/historian/annotation.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/eval" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" + history_model "github.com/grafana/grafana/pkg/services/ngalert/state/historian/model" ) // AnnotationBackend is an implementation of state.Historian that uses Grafana Annotations as the backing datastore. @@ -40,7 +41,7 @@ func NewAnnotationBackend(annotations annotations.Repository, dashboards dashboa } // RecordStates writes a number of state transitions for a given rule to state history. -func (h *AnnotationBackend) RecordStatesAsync(ctx context.Context, rule *ngmodels.AlertRule, states []state.StateTransition) <-chan error { +func (h *AnnotationBackend) RecordStatesAsync(ctx context.Context, rule history_model.RuleMeta, states []state.StateTransition) <-chan error { logger := h.log.FromContext(ctx) // Build annotations before starting goroutine, to make sure all data is copied and won't mutate underneath us. annotations := h.buildAnnotations(rule, states, logger) @@ -136,7 +137,7 @@ func (h *AnnotationBackend) QueryStates(ctx context.Context, query ngmodels.Hist return frame, nil } -func (h *AnnotationBackend) buildAnnotations(rule *ngmodels.AlertRule, states []state.StateTransition, logger log.Logger) []annotations.Item { +func (h *AnnotationBackend) buildAnnotations(rule history_model.RuleMeta, states []state.StateTransition, logger log.Logger) []annotations.Item { items := make([]annotations.Item, 0, len(states)) for _, state := range states { if !shouldRecord(state) { @@ -184,7 +185,7 @@ func (h *AnnotationBackend) recordAnnotationsSync(ctx context.Context, panel *pa return nil } -func buildAnnotationTextAndData(rule *ngmodels.AlertRule, currentState *state.State) (string, *simplejson.Json) { +func buildAnnotationTextAndData(rule history_model.RuleMeta, currentState *state.State) (string, *simplejson.Json) { jsonData := simplejson.New() var value string diff --git a/pkg/services/ngalert/state/historian/core.go b/pkg/services/ngalert/state/historian/core.go index eef8e7e7236..d8d49868413 100644 --- a/pkg/services/ngalert/state/historian/core.go +++ b/pkg/services/ngalert/state/historian/core.go @@ -1,7 +1,6 @@ package historian import ( - "strconv" "strings" "github.com/grafana/grafana-plugin-sdk-go/data" @@ -9,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" + history_model "github.com/grafana/grafana/pkg/services/ngalert/state/historian/model" ) func shouldRecord(transition state.StateTransition) bool { @@ -37,19 +37,12 @@ type panelKey struct { } // panelKey attempts to get the key of the panel attached to the given rule. Returns nil if the rule is not attached to a panel. -func parsePanelKey(rule *models.AlertRule, logger log.Logger) *panelKey { - dashUID, ok := rule.Annotations[models.DashboardUIDAnnotation] - if ok { - panelAnno := rule.Annotations[models.PanelIDAnnotation] - panelID, err := strconv.ParseInt(panelAnno, 10, 64) - if err != nil { - logger.Error("Error parsing panelUID for alert annotation", "actual", panelAnno, "error", err) - return nil - } +func parsePanelKey(rule history_model.RuleMeta, logger log.Logger) *panelKey { + if rule.DashboardUID != "" { return &panelKey{ orgID: rule.OrgID, - dashUID: dashUID, - panelID: panelID, + dashUID: rule.DashboardUID, + panelID: rule.PanelID, } } return nil diff --git a/pkg/services/ngalert/state/historian/core_test.go b/pkg/services/ngalert/state/historian/core_test.go index b9a2cfd1108..0eae1303640 100644 --- a/pkg/services/ngalert/state/historian/core_test.go +++ b/pkg/services/ngalert/state/historian/core_test.go @@ -7,7 +7,6 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" @@ -153,69 +152,3 @@ func TestRemovePrivateLabels(t *testing.T) { }) } } - -func TestParsePanelKey(t *testing.T) { - logger := log.NewNopLogger() - - type testCase struct { - name string - in models.AlertRule - exp *panelKey - } - - cases := []testCase{ - { - name: "no dash UID", - in: models.AlertRule{ - OrgID: 1, - Annotations: map[string]string{ - models.PanelIDAnnotation: "123", - }, - }, - exp: nil, - }, - { - name: "no panel ID", - in: models.AlertRule{ - OrgID: 1, - Annotations: map[string]string{ - models.DashboardUIDAnnotation: "abcd-uid", - }, - }, - exp: nil, - }, - { - name: "invalid panel ID", - in: models.AlertRule{ - OrgID: 1, - Annotations: map[string]string{ - models.DashboardUIDAnnotation: "abcd-uid", - models.PanelIDAnnotation: "bad-id", - }, - }, - exp: nil, - }, - { - name: "success", - in: models.AlertRule{ - OrgID: 1, - Annotations: map[string]string{ - models.DashboardUIDAnnotation: "abcd-uid", - models.PanelIDAnnotation: "123", - }, - }, - exp: &panelKey{ - orgID: 1, - dashUID: "abcd-uid", - panelID: 123, - }, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - res := parsePanelKey(&tc.in, logger) - require.Equal(t, tc.exp, res) - }) - } -} diff --git a/pkg/services/ngalert/state/historian/loki.go b/pkg/services/ngalert/state/historian/loki.go index dcb6dd2da67..a37f6b40bac 100644 --- a/pkg/services/ngalert/state/historian/loki.go +++ b/pkg/services/ngalert/state/historian/loki.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" + history_model "github.com/grafana/grafana/pkg/services/ngalert/state/historian/model" ) const ( @@ -43,7 +44,7 @@ func (h *RemoteLokiBackend) TestConnection() error { return h.client.ping() } -func (h *RemoteLokiBackend) RecordStatesAsync(ctx context.Context, rule *models.AlertRule, states []state.StateTransition) <-chan error { +func (h *RemoteLokiBackend) RecordStatesAsync(ctx context.Context, rule history_model.RuleMeta, states []state.StateTransition) <-chan error { logger := h.log.FromContext(ctx) streams := h.statesToStreams(rule, states, logger) return h.recordStreamsAsync(ctx, streams, logger) @@ -53,7 +54,7 @@ func (h *RemoteLokiBackend) QueryStates(ctx context.Context, query models.Histor return data.NewFrame("states"), nil } -func (h *RemoteLokiBackend) statesToStreams(rule *models.AlertRule, states []state.StateTransition, logger log.Logger) []stream { +func (h *RemoteLokiBackend) statesToStreams(rule history_model.RuleMeta, states []state.StateTransition, logger log.Logger) []stream { buckets := make(map[string][]row) // label repr -> entries for _, state := range states { if !shouldRecord(state) { @@ -63,7 +64,7 @@ func (h *RemoteLokiBackend) statesToStreams(rule *models.AlertRule, states []sta labels := removePrivateLabels(state.State.Labels) labels[OrgIDLabel] = fmt.Sprint(rule.OrgID) labels[RuleUIDLabel] = fmt.Sprint(rule.UID) - labels[GroupLabel] = fmt.Sprint(rule.RuleGroup) + labels[GroupLabel] = fmt.Sprint(rule.Group) labels[FolderUIDLabel] = fmt.Sprint(rule.NamespaceUID) repr := labels.String() diff --git a/pkg/services/ngalert/state/historian/model/rule.go b/pkg/services/ngalert/state/historian/model/rule.go new file mode 100644 index 00000000000..a63656c7560 --- /dev/null +++ b/pkg/services/ngalert/state/historian/model/rule.go @@ -0,0 +1,46 @@ +package model + +import ( + "strconv" + + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/ngalert/models" +) + +// RuleMeta is the metadata about a rule that is needed by state history. +type RuleMeta struct { + ID int64 + OrgID int64 + UID string + Title string + Group string + NamespaceUID string + DashboardUID string + PanelID int64 +} + +func NewRuleMeta(r *models.AlertRule, log log.Logger) RuleMeta { + dashUID, ok := r.Annotations[models.DashboardUIDAnnotation] + var panelID int64 + if ok { + panelAnno := r.Annotations[models.PanelIDAnnotation] + pid, err := strconv.ParseInt(panelAnno, 10, 64) + if err != nil { + logger.Error("Error parsing panelUID for alert annotation", "ruleID", r.ID, "dash", dashUID, "actual", panelAnno, "error", err) + pid = 0 + dashUID = "" + } + panelID = pid + } + return RuleMeta{ + ID: r.ID, + OrgID: r.OrgID, + UID: r.UID, + Title: r.Title, + Group: r.RuleGroup, + NamespaceUID: r.NamespaceUID, + DashboardUID: dashUID, + PanelID: panelID, + } +} diff --git a/pkg/services/ngalert/state/historian/model/rule_test.go b/pkg/services/ngalert/state/historian/model/rule_test.go new file mode 100644 index 00000000000..228e9537307 --- /dev/null +++ b/pkg/services/ngalert/state/historian/model/rule_test.go @@ -0,0 +1,77 @@ +package model + +import ( + "testing" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/stretchr/testify/require" +) + +func TestNewRuleMeta(t *testing.T) { + logger := log.NewNopLogger() + + type testCase struct { + name string + in models.AlertRule + expDash string + expPanel int64 + } + + cases := []testCase{ + { + name: "no dash UID", + in: models.AlertRule{ + OrgID: 1, + Annotations: map[string]string{ + models.PanelIDAnnotation: "123", + }, + }, + expDash: "", + expPanel: 0, + }, + { + name: "no panel ID", + in: models.AlertRule{ + OrgID: 1, + Annotations: map[string]string{ + models.DashboardUIDAnnotation: "abcd-uid", + }, + }, + expDash: "", + expPanel: 0, + }, + { + name: "invalid panel ID", + in: models.AlertRule{ + OrgID: 1, + Annotations: map[string]string{ + models.DashboardUIDAnnotation: "abcd-uid", + models.PanelIDAnnotation: "bad-id", + }, + }, + expDash: "", + expPanel: 0, + }, + { + name: "success", + in: models.AlertRule{ + OrgID: 1, + Annotations: map[string]string{ + models.DashboardUIDAnnotation: "abcd-uid", + models.PanelIDAnnotation: "123", + }, + }, + expDash: "abcd-uid", + expPanel: 123, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + res := NewRuleMeta(&tc.in, logger) + require.Equal(t, tc.expDash, res.DashboardUID) + require.Equal(t, tc.expPanel, res.PanelID) + }) + } +} diff --git a/pkg/services/ngalert/state/historian/noop.go b/pkg/services/ngalert/state/historian/noop.go index a4b7ce1126b..2acee1ace1f 100644 --- a/pkg/services/ngalert/state/historian/noop.go +++ b/pkg/services/ngalert/state/historian/noop.go @@ -3,8 +3,8 @@ package historian import ( "context" - "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" + history_model "github.com/grafana/grafana/pkg/services/ngalert/state/historian/model" ) // NoOpHistorian is a state.Historian that does nothing with the resulting data, to be used in contexts where history is not needed. @@ -14,7 +14,7 @@ func NewNopHistorian() *NoOpHistorian { return &NoOpHistorian{} } -func (f *NoOpHistorian) RecordStatesAsync(ctx context.Context, _ *models.AlertRule, _ []state.StateTransition) <-chan error { +func (f *NoOpHistorian) RecordStatesAsync(ctx context.Context, _ history_model.RuleMeta, _ []state.StateTransition) <-chan error { errCh := make(chan error) close(errCh) return errCh diff --git a/pkg/services/ngalert/state/historian/sql.go b/pkg/services/ngalert/state/historian/sql.go index 8e5d0cf2294..64e716204b8 100644 --- a/pkg/services/ngalert/state/historian/sql.go +++ b/pkg/services/ngalert/state/historian/sql.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" + history_model "github.com/grafana/grafana/pkg/services/ngalert/state/historian/model" ) type SqlBackend struct { @@ -19,7 +20,7 @@ func NewSqlBackend() *SqlBackend { } } -func (h *SqlBackend) RecordStatesAsync(ctx context.Context, _ *models.AlertRule, _ []state.StateTransition) <-chan error { +func (h *SqlBackend) RecordStatesAsync(ctx context.Context, _ history_model.RuleMeta, _ []state.StateTransition) <-chan error { errCh := make(chan error) close(errCh) return errCh diff --git a/pkg/services/ngalert/state/manager.go b/pkg/services/ngalert/state/manager.go index 41b06c37f41..779066d64da 100644 --- a/pkg/services/ngalert/state/manager.go +++ b/pkg/services/ngalert/state/manager.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/metrics" ngModels "github.com/grafana/grafana/pkg/services/ngalert/models" + history_model "github.com/grafana/grafana/pkg/services/ngalert/state/historian/model" ) var ( @@ -197,7 +198,7 @@ func (st *Manager) ProcessEvalResults(ctx context.Context, evaluatedAt time.Time allChanges := append(states, staleStates...) if st.historian != nil { - st.historian.RecordStatesAsync(ctx, alertRule, allChanges) + st.historian.RecordStatesAsync(ctx, history_model.NewRuleMeta(alertRule, logger), allChanges) } return allChanges } diff --git a/pkg/services/ngalert/state/persist.go b/pkg/services/ngalert/state/persist.go index 1665444d248..237235f2627 100644 --- a/pkg/services/ngalert/state/persist.go +++ b/pkg/services/ngalert/state/persist.go @@ -4,6 +4,7 @@ import ( "context" "github.com/grafana/grafana/pkg/services/ngalert/models" + history_model "github.com/grafana/grafana/pkg/services/ngalert/state/historian/model" ) // InstanceStore represents the ability to fetch and write alert instances. @@ -25,7 +26,7 @@ type Historian interface { // RecordStates writes a number of state transitions for a given rule to state history. It returns a channel that // is closed when writing the state transitions has completed. If an error has occurred, the channel will contain a // non-nil error. - RecordStatesAsync(ctx context.Context, rule *models.AlertRule, states []StateTransition) <-chan error + RecordStatesAsync(ctx context.Context, rule history_model.RuleMeta, states []StateTransition) <-chan error } // ImageCapturer captures images. diff --git a/pkg/services/ngalert/state/testing.go b/pkg/services/ngalert/state/testing.go index 199d07d7060..dcb1f31e362 100644 --- a/pkg/services/ngalert/state/testing.go +++ b/pkg/services/ngalert/state/testing.go @@ -5,6 +5,7 @@ import ( "sync" "github.com/grafana/grafana/pkg/services/ngalert/models" + history_model "github.com/grafana/grafana/pkg/services/ngalert/state/historian/model" "github.com/grafana/grafana/pkg/services/screenshot" ) @@ -62,7 +63,7 @@ func (f *FakeRuleReader) ListAlertRules(_ context.Context, q *models.ListAlertRu type FakeHistorian struct{} -func (f *FakeHistorian) RecordStatesAsync(ctx context.Context, rule *models.AlertRule, states []StateTransition) <-chan error { +func (f *FakeHistorian) RecordStatesAsync(ctx context.Context, rule history_model.RuleMeta, states []StateTransition) <-chan error { errCh := make(chan error) close(errCh) return errCh From dd147a3c310b332478df3818a0ccff855778b402 Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Wed, 25 Jan 2023 12:43:22 -0500 Subject: [PATCH 043/172] chore: move entity models into entity store service (#62145) --- pkg/services/export/entity_store.go | 15 +++-- pkg/services/export/export_sys_playlists.go | 4 +- .../playlist/playlistimpl/entity_store.go | 13 ++--- pkg/services/searchV2/bluge.go | 14 ++--- pkg/services/searchV2/filter.go | 13 +++-- pkg/services/searchV2/index.go | 6 +- pkg/services/searchV2/index_test.go | 55 ++++++++++--------- .../store/entity/models.go} | 16 +----- .../store/entity/sqlstash/folder_support.go | 5 +- .../entity/sqlstash/sql_storage_server.go | 5 +- .../store/entity/sqlstash/summary_handler.go | 9 ++- .../entity/tests/server_integration_test.go | 12 ++-- .../store/kind/dashboard/reference.go | 14 ++--- pkg/services/store/kind/dashboard/summary.go | 36 ++++++------ pkg/services/store/kind/dataframe/summary.go | 17 +++--- pkg/services/store/kind/dummy/summary.go | 14 ++--- pkg/services/store/kind/folder/summary.go | 16 +++--- pkg/services/store/kind/geojson/summary.go | 16 +++--- pkg/services/store/kind/jsonobj/summary.go | 16 +++--- pkg/services/store/kind/playlist/summary.go | 18 +++--- pkg/services/store/kind/png/summary.go | 16 +++--- pkg/services/store/kind/registry.go | 54 +++++++++--------- pkg/services/store/kind/registry_test.go | 4 +- pkg/services/store/kind/snapshot/summary.go | 20 +++---- pkg/services/store/kind/svg/summary.go | 16 +++--- pkg/services/store/resolver/service.go | 16 +++--- pkg/services/store/resolver/service_test.go | 27 ++++----- 27 files changed, 227 insertions(+), 240 deletions(-) rename pkg/{models/entity.go => services/store/entity/models.go} (92%) diff --git a/pkg/services/export/entity_store.go b/pkg/services/export/entity_store.go index 57bd79eae94..9e956f4adff 100644 --- a/pkg/services/export/entity_store.go +++ b/pkg/services/export/entity_store.go @@ -10,7 +10,6 @@ import ( "github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboardsnapshots" "github.com/grafana/grafana/pkg/services/playlist" "github.com/grafana/grafana/pkg/services/sqlstore/session" @@ -105,7 +104,7 @@ func (e *entityStoreJob) start(ctx context.Context) { } ctx = appcontext.WithUser(ctx, rowUser) - what := models.StandardKindFolder + what := entity.StandardKindFolder e.status.Count[what] = 0 folders := make(map[int64]string) @@ -133,7 +132,7 @@ func (e *entityStoreJob) start(ctx context.Context) { _, err = e.store.AdminWrite(ctx, &entity.AdminWriteEntityRequest{ GRN: &entity.GRN{ UID: dash.UID, - Kind: models.StandardKindFolder, + Kind: entity.StandardKindFolder, }, ClearHistory: true, CreatedAt: dash.Created.UnixMilli(), @@ -158,7 +157,7 @@ func (e *entityStoreJob) start(ctx context.Context) { e.broadcaster(e.status) } - what = models.StandardKindDashboard + what = entity.StandardKindDashboard e.status.Count[what] = 0 // TODO paging etc @@ -181,7 +180,7 @@ func (e *entityStoreJob) start(ctx context.Context) { _, err = e.store.AdminWrite(ctx, &entity.AdminWriteEntityRequest{ GRN: &entity.GRN{ UID: dash.UID, - Kind: models.StandardKindDashboard, + Kind: entity.StandardKindDashboard, }, ClearHistory: true, Version: fmt.Sprintf("%d", dash.Version), @@ -208,7 +207,7 @@ func (e *entityStoreJob) start(ctx context.Context) { } // Playlists - what = models.StandardKindPlaylist + what = entity.StandardKindPlaylist e.status.Count[what] = 0 rowUser.OrgID = 1 rowUser.UserID = 1 @@ -233,7 +232,7 @@ func (e *entityStoreJob) start(ctx context.Context) { _, err = e.store.Write(ctx, &entity.WriteEntityRequest{ GRN: &entity.GRN{ UID: playlist.Uid, - Kind: models.StandardKindPlaylist, + Kind: entity.StandardKindPlaylist, }, Body: prettyJSON(playlist), Comment: "export from playlists", @@ -297,7 +296,7 @@ func (e *entityStoreJob) start(ctx context.Context) { _, err = e.store.Write(ctx, &entity.WriteEntityRequest{ GRN: &entity.GRN{ UID: dto.Key, - Kind: models.StandardKindSnapshot, + Kind: entity.StandardKindSnapshot, }, Body: prettyJSON(m), Comment: "export from snapshtts", diff --git a/pkg/services/export/export_sys_playlists.go b/pkg/services/export/export_sys_playlists.go index 285f8b85ce9..c9007c8e4d9 100644 --- a/pkg/services/export/export_sys_playlists.go +++ b/pkg/services/export/export_sys_playlists.go @@ -5,8 +5,8 @@ import ( "path/filepath" "time" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/playlist" + "github.com/grafana/grafana/pkg/services/store/entity" ) func exportSystemPlaylists(helper *commitHelper, job *gitExportJob) error { @@ -41,7 +41,7 @@ func exportSystemPlaylists(helper *commitHelper, job *gitExportJob) error { fpath: filepath.Join( helper.orgDir, "entity", - models.StandardKindPlaylist, + entity.StandardKindPlaylist, fmt.Sprintf("%s.json", playlist.Uid)), body: prettyJSON(playlist), }) diff --git a/pkg/services/playlist/playlistimpl/entity_store.go b/pkg/services/playlist/playlistimpl/entity_store.go index 068302975dc..d341994d817 100644 --- a/pkg/services/playlist/playlistimpl/entity_store.go +++ b/pkg/services/playlist/playlistimpl/entity_store.go @@ -6,7 +6,6 @@ import ( "fmt" "github.com/grafana/grafana/pkg/infra/appcontext" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/playlist" "github.com/grafana/grafana/pkg/services/sqlstore/session" "github.com/grafana/grafana/pkg/services/store/entity" @@ -59,7 +58,7 @@ func (s *entityStoreImpl) sync() { GRN: &entity.GRN{ TenantId: info.OrgID, UID: info.UID, - Kind: models.StandardKindPlaylist, + Kind: entity.StandardKindPlaylist, }, Body: body, }) @@ -75,7 +74,7 @@ func (s *entityStoreImpl) Create(ctx context.Context, cmd *playlist.CreatePlayli } _, err = s.store.Write(ctx, &entity.WriteEntityRequest{ GRN: &entity.GRN{ - Kind: models.StandardKindPlaylist, + Kind: entity.StandardKindPlaylist, UID: rsp.UID, }, Body: body, @@ -97,7 +96,7 @@ func (s *entityStoreImpl) Update(ctx context.Context, cmd *playlist.UpdatePlayli _, err = s.store.Write(ctx, &entity.WriteEntityRequest{ GRN: &entity.GRN{ UID: rsp.Uid, - Kind: models.StandardKindPlaylist, + Kind: entity.StandardKindPlaylist, }, Body: body, }) @@ -114,7 +113,7 @@ func (s *entityStoreImpl) Delete(ctx context.Context, cmd *playlist.DeletePlayli _, err = s.store.Delete(ctx, &entity.DeleteEntityRequest{ GRN: &entity.GRN{ UID: cmd.UID, - Kind: models.StandardKindPlaylist, + Kind: entity.StandardKindPlaylist, }, }) if err != nil { @@ -145,7 +144,7 @@ func (s *entityStoreImpl) Get(ctx context.Context, q *playlist.GetPlaylistByUidQ rsp, err := s.store.Read(ctx, &entity.ReadEntityRequest{ GRN: &entity.GRN{ UID: q.UID, - Kind: models.StandardKindPlaylist, + Kind: entity.StandardKindPlaylist, }, WithBody: true, }) @@ -166,7 +165,7 @@ func (s *entityStoreImpl) Search(ctx context.Context, q *playlist.GetPlaylistsQu playlists := make(playlist.Playlists, 0) rsp, err := s.store.Search(ctx, &entity.EntitySearchRequest{ - Kind: []string{models.StandardKindPlaylist}, + Kind: []string{entity.StandardKindPlaylist}, WithBody: true, Limit: 1000, }) diff --git a/pkg/services/searchV2/bluge.go b/pkg/services/searchV2/bluge.go index 93ecbb54ad2..7fece83e1b3 100644 --- a/pkg/services/searchV2/bluge.go +++ b/pkg/services/searchV2/bluge.go @@ -16,7 +16,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/slugify" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/store/entity" ) const ( @@ -170,7 +170,7 @@ func getNonFolderDashboardDoc(dash dashboard, location string) *bluge.Document { } for _, ref := range dash.summary.References { - if ref.Kind == models.StandardKindDataSource { + if ref.Kind == entity.StandardKindDataSource { if ref.Type != "" { doc.AddField(bluge.NewKeywordField(documentFieldDSType, ref.Type). StoreValue(). @@ -210,7 +210,7 @@ func getDashboardPanelDocs(dash dashboard, location string) []*bluge.Document { for _, ref := range dash.summary.References { switch ref.Kind { - case models.StandardKindDashboard: + case entity.StandardKindDashboard: if ref.Type != "" { doc.AddField(bluge.NewKeywordField(documentFieldDSType, ref.Type). StoreValue(). @@ -223,12 +223,12 @@ func getDashboardPanelDocs(dash dashboard, location string) []*bluge.Document { Aggregatable(). SearchTermPositions()) } - case models.ExternalEntityReferencePlugin: - if ref.Type == models.StandardKindPanel && ref.UID != "" { + case entity.ExternalEntityReferencePlugin: + if ref.Type == entity.StandardKindPanel && ref.UID != "" { doc.AddField(bluge.NewKeywordField(documentFieldPanelType, ref.UID).Aggregatable().StoreValue()) } - case models.ExternalEntityReferenceRuntime: - if ref.Type == models.ExternalEntityReferenceRuntime_Transformer && ref.UID != "" { + case entity.ExternalEntityReferenceRuntime: + if ref.Type == entity.ExternalEntityReferenceRuntime_Transformer && ref.UID != "" { doc.AddField(bluge.NewKeywordField(documentFieldTransformer, ref.UID).Aggregatable()) } } diff --git a/pkg/services/searchV2/filter.go b/pkg/services/searchV2/filter.go index 1a22f60c595..b64d9c2adae 100644 --- a/pkg/services/searchV2/filter.go +++ b/pkg/services/searchV2/filter.go @@ -7,8 +7,9 @@ import ( "github.com/blugelabs/bluge/search" "github.com/blugelabs/bluge/search/searcher" "github.com/blugelabs/bluge/search/similarity" + "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/store/entity" ) type PermissionFilter struct { @@ -19,11 +20,11 @@ type PermissionFilter struct { type entityKind string const ( - entityKindPanel entityKind = models.StandardKindPanel - entityKindDashboard entityKind = models.StandardKindDashboard - entityKindFolder entityKind = models.StandardKindFolder - entityKindDatasource entityKind = models.StandardKindDataSource - entityKindQuery entityKind = models.StandardKindQuery + entityKindPanel entityKind = entity.StandardKindPanel + entityKindDashboard entityKind = entity.StandardKindDashboard + entityKindFolder entityKind = entity.StandardKindFolder + entityKindDatasource entityKind = entity.StandardKindDataSource + entityKindQuery entityKind = entity.StandardKindQuery ) func (r entityKind) IsValid() bool { diff --git a/pkg/services/searchV2/index.go b/pkg/services/searchV2/index.go index 0e92a96993e..b3efdb079ee 100644 --- a/pkg/services/searchV2/index.go +++ b/pkg/services/searchV2/index.go @@ -19,9 +19,9 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/store" + "github.com/grafana/grafana/pkg/services/store/entity" kdash "github.com/grafana/grafana/pkg/services/store/kind/dashboard" "github.com/grafana/grafana/pkg/setting" ) @@ -54,7 +54,7 @@ type dashboard struct { updated time.Time // Use generic structure - summary *models.EntitySummary + summary *entity.EntitySummary } // buildSignal is sent when search index is accessed in organization for which @@ -912,7 +912,7 @@ func (l sqlDashboardLoader) LoadDashboards(ctx context.Context, orgID int64, das slug: "", created: time.Now(), updated: time.Now(), - summary: &models.EntitySummary{ + summary: &entity.EntitySummary{ //ID: 0, Name: "General", }, diff --git a/pkg/services/searchV2/index_test.go b/pkg/services/searchV2/index_test.go index 1e56e77ff7c..431b43d6d6e 100644 --- a/pkg/services/searchV2/index_test.go +++ b/pkg/services/searchV2/index_test.go @@ -8,9 +8,10 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/infra/log" @@ -113,14 +114,14 @@ var testDashboards = []dashboard{ { id: 1, uid: "1", - summary: &models.EntitySummary{ + summary: &entity.EntitySummary{ Name: "test", }, }, { id: 2, uid: "2", - summary: &models.EntitySummary{ + summary: &entity.EntitySummary{ Name: "boom", }, }, @@ -162,7 +163,7 @@ func TestDashboardIndexUpdates(t *testing.T) { err := index.updateDashboard(context.Background(), testOrgID, orgIdx, dashboard{ id: 3, uid: "3", - summary: &models.EntitySummary{ + summary: &entity.EntitySummary{ Name: "created", }, }) @@ -181,7 +182,7 @@ func TestDashboardIndexUpdates(t *testing.T) { err := index.updateDashboard(context.Background(), testOrgID, orgIdx, dashboard{ id: 2, uid: "2", - summary: &models.EntitySummary{ + summary: &entity.EntitySummary{ Name: "nginx", }, }) @@ -197,14 +198,14 @@ var testSortDashboards = []dashboard{ { id: 1, uid: "1", - summary: &models.EntitySummary{ + summary: &entity.EntitySummary{ Name: "a-test", }, }, { id: 2, uid: "2", - summary: &models.EntitySummary{ + summary: &entity.EntitySummary{ Name: "z-test", }, }, @@ -288,14 +289,14 @@ var testPrefixDashboards = []dashboard{ { id: 1, uid: "1", - summary: &models.EntitySummary{ + summary: &entity.EntitySummary{ Name: "Archer Data System", }, }, { id: 2, uid: "2", - summary: &models.EntitySummary{ + summary: &entity.EntitySummary{ Name: "Document Sync repo", }, }, @@ -366,7 +367,7 @@ var longPrefixDashboards = []dashboard{ { id: 1, uid: "1", - summary: &models.EntitySummary{ + summary: &entity.EntitySummary{ Name: "Eyjafjallajökull Eruption data", }, }, @@ -385,14 +386,14 @@ var scatteredTokensDashboards = []dashboard{ { id: 1, uid: "1", - summary: &models.EntitySummary{ + summary: &entity.EntitySummary{ Name: "Three can keep a secret, if two of them are dead (Benjamin Franklin)", }, }, { id: 3, uid: "2", - summary: &models.EntitySummary{ + summary: &entity.EntitySummary{ Name: "A secret is powerful when it is empty (Umberto Eco)", }, }, @@ -418,7 +419,7 @@ var dashboardsWithFolders = []dashboard{ id: 1, uid: "1", isFolder: true, - summary: &models.EntitySummary{ + summary: &entity.EntitySummary{ Name: "My folder", }, }, @@ -426,9 +427,9 @@ var dashboardsWithFolders = []dashboard{ id: 2, uid: "2", folderID: 1, - summary: &models.EntitySummary{ + summary: &entity.EntitySummary{ Name: "Dashboard in folder 1", - Nested: []*models.EntitySummary{ + Nested: []*entity.EntitySummary{ newNestedPanel(1, "Panel 1"), newNestedPanel(2, "Panel 2"), }, @@ -438,9 +439,9 @@ var dashboardsWithFolders = []dashboard{ id: 3, uid: "3", folderID: 1, - summary: &models.EntitySummary{ + summary: &entity.EntitySummary{ Name: "Dashboard in folder 2", - Nested: []*models.EntitySummary{ + Nested: []*entity.EntitySummary{ newNestedPanel(3, "Panel 3"), }, }, @@ -448,9 +449,9 @@ var dashboardsWithFolders = []dashboard{ { id: 4, uid: "4", - summary: &models.EntitySummary{ + summary: &entity.EntitySummary{ Name: "One more dash", - Nested: []*models.EntitySummary{ + Nested: []*entity.EntitySummary{ newNestedPanel(4, "Panel 4"), }, }, @@ -505,9 +506,9 @@ var dashboardsWithPanels = []dashboard{ { id: 1, uid: "1", - summary: &models.EntitySummary{ + summary: &entity.EntitySummary{ Name: "My Dash", - Nested: []*models.EntitySummary{ + Nested: []*entity.EntitySummary{ newNestedPanel(1, "Panel 1"), newNestedPanel(2, "Panel 2"), }, @@ -515,8 +516,8 @@ var dashboardsWithPanels = []dashboard{ }, } -func newNestedPanel(id int64, name string) *models.EntitySummary { - summary := &models.EntitySummary{ +func newNestedPanel(id int64, name string) *entity.EntitySummary { + summary := &entity.EntitySummary{ Kind: "panel", UID: fmt.Sprintf("???#%d", id), } @@ -553,14 +554,14 @@ var punctuationSplitNgramDashboards = []dashboard{ { id: 1, uid: "1", - summary: &models.EntitySummary{ + summary: &entity.EntitySummary{ Name: "heat-torkel", }, }, { id: 2, uid: "2", - summary: &models.EntitySummary{ + summary: &entity.EntitySummary{ Name: "topology heatmap", }, }, @@ -586,7 +587,7 @@ var camelCaseNgramDashboards = []dashboard{ { id: 1, uid: "1", - summary: &models.EntitySummary{ + summary: &entity.EntitySummary{ Name: "heatTorkel", }, }, @@ -608,7 +609,7 @@ func dashboardsWithTitles(names ...string) []dashboard { out = append(out, dashboard{ id: no, uid: fmt.Sprintf("%d", no), - summary: &models.EntitySummary{ + summary: &entity.EntitySummary{ Name: name, }, }) diff --git a/pkg/models/entity.go b/pkg/services/store/entity/models.go similarity index 92% rename from pkg/models/entity.go rename to pkg/services/store/entity/models.go index 78ba745f4d4..a4c7388388f 100644 --- a/pkg/models/entity.go +++ b/pkg/services/store/entity/models.go @@ -1,4 +1,4 @@ -package models +package entity //----------------------------------------------------------------------------------------------------- // NOTE: the object store is in heavy development, and the locations will likely continue to move @@ -20,7 +20,7 @@ const ( // Standalone panel is not an object kind yet -- library panel, or nested in dashboard StandardKindPanel = "panel" - // StandardKindSVG SVG file support + // entity.StandardKindSVG SVG file support StandardKindSVG = "svg" // StandardKindPNG PNG file support @@ -110,18 +110,6 @@ type EntitySummary struct { _ interface{} } -// This will likely get replaced with a more general error framework. -type EntityErrorInfo struct { - // TODO: Match an error code registry? - Code int64 `json:"code,omitempty"` - - // Simple error display - Message string `json:"message,omitempty"` - - // Error details - Details interface{} `json:"details,omitempty"` -} - // Reference to another object outside itself // This message is derived from the object body and can be used to search for references. // This does not represent a method to declare a reference to another object. diff --git a/pkg/services/store/entity/sqlstash/folder_support.go b/pkg/services/store/entity/sqlstash/folder_support.go index cf5f115eef1..84e2b60c021 100644 --- a/pkg/services/store/entity/sqlstash/folder_support.go +++ b/pkg/services/store/entity/sqlstash/folder_support.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/sqlstore/session" "github.com/grafana/grafana/pkg/services/store/entity" ) @@ -41,7 +40,7 @@ func updateFolderTree(ctx context.Context, tx *session.SessionTx, tenant int64) all := []*folderInfo{} rows, err := tx.Query(ctx, "SELECT uid,folder,name,slug FROM entity WHERE kind=? AND tenant_id=? ORDER BY slug asc;", - models.StandardKindFolder, tenant) + entity.StandardKindFolder, tenant) if err != nil { return err } @@ -135,7 +134,7 @@ func setMPTTOrder(folder *folderInfo, stack []*folderInfo, idx int32) (int32, er func insertFolderInfo(ctx context.Context, tx *session.SessionTx, tenant int64, folder *folderInfo, isDetached bool) error { js, _ := json.Marshal(folder.stack) - grn := entity.GRN{TenantId: tenant, Kind: models.StandardKindFolder, UID: folder.UID} + grn := entity.GRN{TenantId: tenant, Kind: entity.StandardKindFolder, UID: folder.UID} _, err := tx.Exec(ctx, `INSERT INTO entity_folder `+ "(grn, tenant_id, uid, slug_path, tree, depth, left, right, detached) "+ diff --git a/pkg/services/store/entity/sqlstash/sql_storage_server.go b/pkg/services/store/entity/sqlstash/sql_storage_server.go index 92fa784a419..6fc34ef8cd9 100644 --- a/pkg/services/store/entity/sqlstash/sql_storage_server.go +++ b/pkg/services/store/entity/sqlstash/sql_storage_server.go @@ -13,7 +13,6 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/slugify" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/grpcserver" "github.com/grafana/grafana/pkg/services/sqlstore/session" "github.com/grafana/grafana/pkg/services/store" @@ -447,7 +446,7 @@ func (s *sqlEntityServer) AdminWrite(ctx context.Context, r *entity.AdminWriteEn origin.Source, origin.Key, origin.Time, ) } - if err == nil && models.StandardKindFolder == r.GRN.Kind { + if err == nil && entity.StandardKindFolder == r.GRN.Kind { err = updateFolderTree(ctx, tx, grn.TenantId) } if err == nil { @@ -663,7 +662,7 @@ func doDelete(ctx context.Context, tx *session.SessionTx, grn *entity.GRN) (bool return false, err } - if grn.Kind == models.StandardKindFolder { + if grn.Kind == entity.StandardKindFolder { err = updateFolderTree(ctx, tx, grn.TenantId) } return rows > 0, err diff --git a/pkg/services/store/entity/sqlstash/summary_handler.go b/pkg/services/store/entity/sqlstash/summary_handler.go index d78d982dd26..95c7b20b61f 100644 --- a/pkg/services/store/entity/sqlstash/summary_handler.go +++ b/pkg/services/store/entity/sqlstash/summary_handler.go @@ -3,12 +3,11 @@ package sqlstash import ( "encoding/json" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/store/entity" ) type summarySupport struct { - model *models.EntitySummary + model *entity.EntitySummary name string description *string // null or empty slug *string // null or empty @@ -23,7 +22,7 @@ type summarySupport struct { isNested bool // set when this is for a nested item } -func newSummarySupport(summary *models.EntitySummary) (*summarySupport, error) { +func newSummarySupport(summary *entity.EntitySummary) (*summarySupport, error) { var err error var js []byte s := &summarySupport{ @@ -72,9 +71,9 @@ func newSummarySupport(summary *models.EntitySummary) (*summarySupport, error) { return s, err } -func (s summarySupport) toEntitySummary() (*models.EntitySummary, error) { +func (s summarySupport) toEntitySummary() (*entity.EntitySummary, error) { var err error - summary := &models.EntitySummary{ + summary := &entity.EntitySummary{ Name: s.name, } if s.description != nil { diff --git a/pkg/services/store/entity/tests/server_integration_test.go b/pkg/services/store/entity/tests/server_integration_test.go index 29d7762e1d3..0fab91445fc 100644 --- a/pkg/services/store/entity/tests/server_integration_test.go +++ b/pkg/services/store/entity/tests/server_integration_test.go @@ -8,12 +8,12 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/models" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/metadata" + "github.com/grafana/grafana/pkg/services/store" "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/util" - "github.com/stretchr/testify/require" - "google.golang.org/grpc/metadata" ) var ( @@ -133,7 +133,7 @@ func TestIntegrationEntityServer(t *testing.T) { fakeUser := store.GetUserIDString(testCtx.user) firstVersion := "1" - kind := models.StandardKindJSONObj + kind := entity.StandardKindJSONObj grn := &entity.GRN{ Kind: kind, UID: "my-test-entity", @@ -314,7 +314,7 @@ func TestIntegrationEntityServer(t *testing.T) { uid2 := "uid2" uid3 := "uid3" uid4 := "uid4" - kind2 := models.StandardKindPlaylist + kind2 := entity.StandardKindPlaylist w1, err := testCtx.client.Write(ctx, &entity.WriteEntityRequest{ GRN: grn, Body: body, @@ -394,7 +394,7 @@ func TestIntegrationEntityServer(t *testing.T) { }) t.Run("should be able to filter objects based on their labels", func(t *testing.T) { - kind := models.StandardKindDashboard + kind := entity.StandardKindDashboard _, err := testCtx.client.Write(ctx, &entity.WriteEntityRequest{ GRN: &entity.GRN{ Kind: kind, diff --git a/pkg/services/store/kind/dashboard/reference.go b/pkg/services/store/kind/dashboard/reference.go index 25b78c35f2f..332078228f3 100644 --- a/pkg/services/store/kind/dashboard/reference.go +++ b/pkg/services/store/kind/dashboard/reference.go @@ -4,7 +4,7 @@ import ( "fmt" "sort" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/store/entity" ) // A reference accumulator can combine @@ -13,24 +13,24 @@ type ReferenceAccumulator interface { Add(kind string, subtype string, uid string) // Returns the set of distinct references in a sorted order - Get() []*models.EntityExternalReference + Get() []*entity.EntityExternalReference } func NewReferenceAccumulator() ReferenceAccumulator { return &referenceAccumulator{ - refs: make(map[string]*models.EntityExternalReference), + refs: make(map[string]*entity.EntityExternalReference), } } type referenceAccumulator struct { - refs map[string]*models.EntityExternalReference + refs map[string]*entity.EntityExternalReference } func (x *referenceAccumulator) Add(kind string, sub string, uid string) { key := fmt.Sprintf("%s/%s/%s", kind, sub, uid) _, ok := x.refs[key] if !ok { - x.refs[key] = &models.EntityExternalReference{ + x.refs[key] = &entity.EntityExternalReference{ Kind: kind, Type: sub, UID: uid, @@ -38,14 +38,14 @@ func (x *referenceAccumulator) Add(kind string, sub string, uid string) { } } -func (x *referenceAccumulator) Get() []*models.EntityExternalReference { +func (x *referenceAccumulator) Get() []*entity.EntityExternalReference { keys := make([]string, 0, len(x.refs)) for k := range x.refs { keys = append(keys, k) } sort.Strings(keys) - refs := make([]*models.EntityExternalReference, len(keys)) + refs := make([]*entity.EntityExternalReference, len(keys)) for i, key := range keys { refs[i] = x.refs[key] } diff --git a/pkg/services/store/kind/dashboard/summary.go b/pkg/services/store/kind/dashboard/summary.go index 7038db0c306..51a1193a8b7 100644 --- a/pkg/services/store/kind/dashboard/summary.go +++ b/pkg/services/store/kind/dashboard/summary.go @@ -6,29 +6,29 @@ import ( "encoding/json" "strconv" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/services/store/entity" ) -func GetEntityKindInfo() models.EntityKindInfo { - return models.EntityKindInfo{ - ID: models.StandardKindDashboard, +func GetEntityKindInfo() entity.EntityKindInfo { + return entity.EntityKindInfo{ + ID: entity.StandardKindDashboard, Name: "Dashboard", Description: "Define a grafana dashboard layout", } } // This summary does not resolve old name as UID -func GetEntitySummaryBuilder() models.EntitySummaryBuilder { +func GetEntitySummaryBuilder() entity.EntitySummaryBuilder { builder := NewStaticDashboardSummaryBuilder(&directLookup{}, true) - return func(ctx context.Context, uid string, body []byte) (*models.EntitySummary, []byte, error) { + return func(ctx context.Context, uid string, body []byte) (*entity.EntitySummary, []byte, error) { return builder(ctx, uid, body) } } // This implementation moves datasources referenced by internal ID or name to UID -func NewStaticDashboardSummaryBuilder(lookup DatasourceLookup, sanitize bool) models.EntitySummaryBuilder { - return func(ctx context.Context, uid string, body []byte) (*models.EntitySummary, []byte, error) { +func NewStaticDashboardSummaryBuilder(lookup DatasourceLookup, sanitize bool) entity.EntitySummaryBuilder { + return func(ctx context.Context, uid string, body []byte) (*entity.EntitySummary, []byte, error) { var parsed map[string]interface{} if sanitize { @@ -42,14 +42,14 @@ func NewStaticDashboardSummaryBuilder(lookup DatasourceLookup, sanitize bool) mo // slug? (derived from title) } - summary := &models.EntitySummary{ + summary := &entity.EntitySummary{ Labels: make(map[string]string), Fields: make(map[string]interface{}), } stream := bytes.NewBuffer(body) dash, err := readDashboard(stream, lookup) if err != nil { - summary.Error = &models.EntityErrorInfo{ + summary.Error = &entity.EntityErrorInfo{ Message: err.Error(), } return summary, body, err @@ -68,7 +68,7 @@ func NewStaticDashboardSummaryBuilder(lookup DatasourceLookup, sanitize bool) mo for _, panel := range dash.Panels { panelRefs := NewReferenceAccumulator() - p := &models.EntitySummary{ + p := &entity.EntitySummary{ UID: uid + "#" + strconv.FormatInt(panel.ID, 10), Kind: "panel", } @@ -78,19 +78,19 @@ func NewStaticDashboardSummaryBuilder(lookup DatasourceLookup, sanitize bool) mo p.Fields["type"] = panel.Type if panel.Type != "row" { - panelRefs.Add(models.ExternalEntityReferencePlugin, string(plugins.Panel), panel.Type) - dashboardRefs.Add(models.ExternalEntityReferencePlugin, string(plugins.Panel), panel.Type) + panelRefs.Add(entity.ExternalEntityReferencePlugin, string(plugins.Panel), panel.Type) + dashboardRefs.Add(entity.ExternalEntityReferencePlugin, string(plugins.Panel), panel.Type) } for _, v := range panel.Datasource { - dashboardRefs.Add(models.StandardKindDataSource, v.Type, v.UID) - panelRefs.Add(models.StandardKindDataSource, v.Type, v.UID) + dashboardRefs.Add(entity.StandardKindDataSource, v.Type, v.UID) + panelRefs.Add(entity.StandardKindDataSource, v.Type, v.UID) if v.Type != "" { - dashboardRefs.Add(models.ExternalEntityReferencePlugin, string(plugins.DataSource), v.Type) + dashboardRefs.Add(entity.ExternalEntityReferencePlugin, string(plugins.DataSource), v.Type) } } for _, v := range panel.Transformer { - panelRefs.Add(models.ExternalEntityReferenceRuntime, models.ExternalEntityReferenceRuntime_Transformer, v) - dashboardRefs.Add(models.ExternalEntityReferenceRuntime, models.ExternalEntityReferenceRuntime_Transformer, v) + panelRefs.Add(entity.ExternalEntityReferenceRuntime, entity.ExternalEntityReferenceRuntime_Transformer, v) + dashboardRefs.Add(entity.ExternalEntityReferenceRuntime, entity.ExternalEntityReferenceRuntime_Transformer, v) } p.References = panelRefs.Get() summary.Nested = append(summary.Nested, p) diff --git a/pkg/services/store/kind/dataframe/summary.go b/pkg/services/store/kind/dataframe/summary.go index 88c5df709b9..cc1abf69656 100644 --- a/pkg/services/store/kind/dataframe/summary.go +++ b/pkg/services/store/kind/dataframe/summary.go @@ -5,20 +5,21 @@ import ( "encoding/json" "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/store" + "github.com/grafana/grafana/pkg/services/store/entity" ) -func GetEntityKindInfo() models.EntityKindInfo { - return models.EntityKindInfo{ - ID: models.StandardKindDataFrame, +func GetEntityKindInfo() entity.EntityKindInfo { + return entity.EntityKindInfo{ + ID: entity.StandardKindDataFrame, Name: "Data frame", Description: "Data frame", } } -func GetEntitySummaryBuilder() models.EntitySummaryBuilder { - return func(ctx context.Context, uid string, body []byte) (*models.EntitySummary, []byte, error) { +func GetEntitySummaryBuilder() entity.EntitySummaryBuilder { + return func(ctx context.Context, uid string, body []byte) (*entity.EntitySummary, []byte, error) { df := &data.Frame{} err := json.Unmarshal(body, df) if err != nil { @@ -33,8 +34,8 @@ func GetEntitySummaryBuilder() models.EntitySummaryBuilder { if err != nil { return nil, nil, err } - summary := &models.EntitySummary{ - Kind: models.StandardKindDataFrame, + summary := &entity.EntitySummary{ + Kind: entity.StandardKindDataFrame, Name: df.Name, UID: uid, Fields: map[string]interface{}{ diff --git a/pkg/services/store/kind/dummy/summary.go b/pkg/services/store/kind/dummy/summary.go index cdf88f97530..a7ea51019ba 100644 --- a/pkg/services/store/kind/dummy/summary.go +++ b/pkg/services/store/kind/dummy/summary.go @@ -5,11 +5,11 @@ import ( "fmt" "time" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/store/entity" ) -func GetEntityKindInfo(kind string) models.EntityKindInfo { - return models.EntityKindInfo{ +func GetEntityKindInfo(kind string) entity.EntityKindInfo { + return entity.EntityKindInfo{ ID: kind, Name: kind, Description: "Dummy kind used for testing.", @@ -17,9 +17,9 @@ func GetEntityKindInfo(kind string) models.EntityKindInfo { } } -func GetEntitySummaryBuilder(kind string) models.EntitySummaryBuilder { - return func(ctx context.Context, uid string, body []byte) (*models.EntitySummary, []byte, error) { - summary := &models.EntitySummary{ +func GetEntitySummaryBuilder(kind string) entity.EntitySummaryBuilder { + return func(ctx context.Context, uid string, body []byte) (*entity.EntitySummary, []byte, error) { + summary := &entity.EntitySummary{ Name: fmt.Sprintf("Dummy: %s", kind), Kind: kind, Description: fmt.Sprintf("Wrote at %s", time.Now().Local().String()), @@ -35,7 +35,7 @@ func GetEntitySummaryBuilder(kind string) models.EntitySummaryBuilder { }, Error: nil, // ignore for now Nested: nil, // ignore for now - References: []*models.EntityExternalReference{ + References: []*entity.EntityExternalReference{ { Kind: "ds", Type: "influx", diff --git a/pkg/services/store/kind/folder/summary.go b/pkg/services/store/kind/folder/summary.go index 0e24a95fa38..a328e139ace 100644 --- a/pkg/services/store/kind/folder/summary.go +++ b/pkg/services/store/kind/folder/summary.go @@ -4,8 +4,8 @@ import ( "context" "encoding/json" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/store" + "github.com/grafana/grafana/pkg/services/store/entity" ) type Model struct { @@ -13,15 +13,15 @@ type Model struct { Description string `json:"description,omitempty"` } -func GetEntityKindInfo() models.EntityKindInfo { - return models.EntityKindInfo{ - ID: models.StandardKindFolder, +func GetEntityKindInfo() entity.EntityKindInfo { + return entity.EntityKindInfo{ + ID: entity.StandardKindFolder, Name: "Folder", } } -func GetEntitySummaryBuilder() models.EntitySummaryBuilder { - return func(ctx context.Context, uid string, body []byte) (*models.EntitySummary, []byte, error) { +func GetEntitySummaryBuilder() entity.EntitySummaryBuilder { + return func(ctx context.Context, uid string, body []byte) (*entity.EntitySummary, []byte, error) { obj := &Model{} err := json.Unmarshal(body, obj) if err != nil { @@ -32,8 +32,8 @@ func GetEntitySummaryBuilder() models.EntitySummaryBuilder { obj.Name = store.GuessNameFromUID(uid) } - summary := &models.EntitySummary{ - Kind: models.StandardKindFolder, + summary := &entity.EntitySummary{ + Kind: entity.StandardKindFolder, Name: obj.Name, Description: obj.Description, UID: uid, diff --git a/pkg/services/store/kind/geojson/summary.go b/pkg/services/store/kind/geojson/summary.go index 55c7c115929..2a4b410814d 100644 --- a/pkg/services/store/kind/geojson/summary.go +++ b/pkg/services/store/kind/geojson/summary.go @@ -5,13 +5,13 @@ import ( "encoding/json" "fmt" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/store" + "github.com/grafana/grafana/pkg/services/store/entity" ) -func GetEntityKindInfo() models.EntityKindInfo { - return models.EntityKindInfo{ - ID: models.StandardKindGeoJSON, +func GetEntityKindInfo() entity.EntityKindInfo { + return entity.EntityKindInfo{ + ID: entity.StandardKindGeoJSON, Name: "GeoJSON", Description: "JSON formatted spatial data", FileExtension: ".geojson", @@ -20,8 +20,8 @@ func GetEntityKindInfo() models.EntityKindInfo { } // Very basic geojson validator -func GetEntitySummaryBuilder() models.EntitySummaryBuilder { - return func(ctx context.Context, uid string, body []byte) (*models.EntitySummary, []byte, error) { +func GetEntitySummaryBuilder() entity.EntitySummaryBuilder { + return func(ctx context.Context, uid string, body []byte) (*entity.EntitySummary, []byte, error) { var geojson map[string]interface{} err := json.Unmarshal(body, &geojson) if err != nil { @@ -38,8 +38,8 @@ func GetEntitySummaryBuilder() models.EntitySummaryBuilder { return nil, nil, err } - summary := &models.EntitySummary{ - Kind: models.StandardKindGeoJSON, + summary := &entity.EntitySummary{ + Kind: entity.StandardKindGeoJSON, Name: store.GuessNameFromUID(uid), UID: uid, Fields: map[string]interface{}{ diff --git a/pkg/services/store/kind/jsonobj/summary.go b/pkg/services/store/kind/jsonobj/summary.go index 7bbe5051778..30d13e949b3 100644 --- a/pkg/services/store/kind/jsonobj/summary.go +++ b/pkg/services/store/kind/jsonobj/summary.go @@ -4,20 +4,20 @@ import ( "context" "encoding/json" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/store" + "github.com/grafana/grafana/pkg/services/store/entity" ) -func GetEntityKindInfo() models.EntityKindInfo { - return models.EntityKindInfo{ - ID: models.StandardKindJSONObj, +func GetEntityKindInfo() entity.EntityKindInfo { + return entity.EntityKindInfo{ + ID: entity.StandardKindJSONObj, Name: "JSON Object", Description: "JSON Object", } } -func GetEntitySummaryBuilder() models.EntitySummaryBuilder { - return func(ctx context.Context, uid string, body []byte) (*models.EntitySummary, []byte, error) { +func GetEntitySummaryBuilder() entity.EntitySummaryBuilder { + return func(ctx context.Context, uid string, body []byte) (*entity.EntitySummary, []byte, error) { v := make(map[string]interface{}) err := json.Unmarshal(body, &v) if err != nil { @@ -28,8 +28,8 @@ func GetEntitySummaryBuilder() models.EntitySummaryBuilder { if err != nil { return nil, nil, err } - return &models.EntitySummary{ - Kind: models.StandardKindJSONObj, + return &entity.EntitySummary{ + Kind: entity.StandardKindJSONObj, Name: store.GuessNameFromUID(uid), UID: uid, }, out, err diff --git a/pkg/services/store/kind/playlist/summary.go b/pkg/services/store/kind/playlist/summary.go index 52c03ab1eb4..9cb27362710 100644 --- a/pkg/services/store/kind/playlist/summary.go +++ b/pkg/services/store/kind/playlist/summary.go @@ -6,22 +6,22 @@ import ( "fmt" "github.com/grafana/grafana/pkg/kinds/playlist" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/store/entity" ) -func GetEntityKindInfo() models.EntityKindInfo { - return models.EntityKindInfo{ - ID: models.StandardKindPlaylist, +func GetEntityKindInfo() entity.EntityKindInfo { + return entity.EntityKindInfo{ + ID: entity.StandardKindPlaylist, Name: "Playlist", Description: "Cycle though a collection of dashboards automatically", } } -func GetEntitySummaryBuilder() models.EntitySummaryBuilder { +func GetEntitySummaryBuilder() entity.EntitySummaryBuilder { return summaryBuilder } -func summaryBuilder(ctx context.Context, uid string, body []byte) (*models.EntitySummary, []byte, error) { +func summaryBuilder(ctx context.Context, uid string, body []byte) (*entity.EntitySummary, []byte, error) { obj := &playlist.Playlist{} err := json.Unmarshal(body, obj) if err != nil { @@ -35,7 +35,7 @@ func summaryBuilder(ctx context.Context, uid string, body []byte) (*models.Entit } obj.Uid = uid // make sure they are consistent - summary := &models.EntitySummary{ + summary := &entity.EntitySummary{ UID: uid, Name: obj.Name, Description: fmt.Sprintf("%d items, refreshed every %s", len(*obj.Items), obj.Interval), @@ -44,7 +44,7 @@ func summaryBuilder(ctx context.Context, uid string, body []byte) (*models.Entit for _, item := range *obj.Items { switch item.Type { case playlist.ItemTypeDashboardByUid: - summary.References = append(summary.References, &models.EntityExternalReference{ + summary.References = append(summary.References, &entity.EntityExternalReference{ Kind: "dashboard", UID: item.Value, }) @@ -57,7 +57,7 @@ func summaryBuilder(ctx context.Context, uid string, body []byte) (*models.Entit case playlist.ItemTypeDashboardById: // obviously insufficient long term... but good to have an example :) - summary.Error = &models.EntityErrorInfo{ + summary.Error = &entity.EntityErrorInfo{ Message: "Playlist uses deprecated internal id system", } } diff --git a/pkg/services/store/kind/png/summary.go b/pkg/services/store/kind/png/summary.go index cfa325b5096..c7d7dadbc6d 100644 --- a/pkg/services/store/kind/png/summary.go +++ b/pkg/services/store/kind/png/summary.go @@ -5,13 +5,13 @@ import ( "context" "image/png" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/store" + "github.com/grafana/grafana/pkg/services/store/entity" ) -func GetEntityKindInfo() models.EntityKindInfo { - return models.EntityKindInfo{ - ID: models.StandardKindPNG, +func GetEntityKindInfo() entity.EntityKindInfo { + return entity.EntityKindInfo{ + ID: entity.StandardKindPNG, Name: "PNG", Description: "PNG Image file", IsRaw: true, @@ -21,16 +21,16 @@ func GetEntityKindInfo() models.EntityKindInfo { } // SVG sanitizer based on the rendering service -func GetEntitySummaryBuilder() models.EntitySummaryBuilder { - return func(ctx context.Context, uid string, body []byte) (*models.EntitySummary, []byte, error) { +func GetEntitySummaryBuilder() entity.EntitySummaryBuilder { + return func(ctx context.Context, uid string, body []byte) (*entity.EntitySummary, []byte, error) { img, err := png.Decode(bytes.NewReader(body)) if err != nil { return nil, nil, err } size := img.Bounds().Size() - summary := &models.EntitySummary{ - Kind: models.StandardKindSVG, + summary := &entity.EntitySummary{ + Kind: entity.StandardKindSVG, Name: store.GuessNameFromUID(uid), UID: uid, Fields: map[string]interface{}{ diff --git a/pkg/services/store/kind/registry.go b/pkg/services/store/kind/registry.go index 7c76f106e3e..d977511ea4c 100644 --- a/pkg/services/store/kind/registry.go +++ b/pkg/services/store/kind/registry.go @@ -5,8 +5,8 @@ import ( "sort" "sync" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/rendering" + "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/services/store/kind/dashboard" "github.com/grafana/grafana/pkg/services/store/kind/dataframe" "github.com/grafana/grafana/pkg/services/store/kind/folder" @@ -20,44 +20,44 @@ import ( ) type KindRegistry interface { - Register(info models.EntityKindInfo, builder models.EntitySummaryBuilder) error - GetSummaryBuilder(kind string) models.EntitySummaryBuilder - GetInfo(kind string) (models.EntityKindInfo, error) - GetFromExtension(suffix string) (models.EntityKindInfo, error) - GetKinds() []models.EntityKindInfo + Register(info entity.EntityKindInfo, builder entity.EntitySummaryBuilder) error + GetSummaryBuilder(kind string) entity.EntitySummaryBuilder + GetInfo(kind string) (entity.EntityKindInfo, error) + GetFromExtension(suffix string) (entity.EntityKindInfo, error) + GetKinds() []entity.EntityKindInfo } func NewKindRegistry() KindRegistry { kinds := make(map[string]*kindValues) - kinds[models.StandardKindPlaylist] = &kindValues{ + kinds[entity.StandardKindPlaylist] = &kindValues{ info: playlist.GetEntityKindInfo(), builder: playlist.GetEntitySummaryBuilder(), } - kinds[models.StandardKindDashboard] = &kindValues{ + kinds[entity.StandardKindDashboard] = &kindValues{ info: dashboard.GetEntityKindInfo(), builder: dashboard.GetEntitySummaryBuilder(), } - kinds[models.StandardKindSnapshot] = &kindValues{ + kinds[entity.StandardKindSnapshot] = &kindValues{ info: snapshot.GetEntityKindInfo(), builder: snapshot.GetEntitySummaryBuilder(), } - kinds[models.StandardKindFolder] = &kindValues{ + kinds[entity.StandardKindFolder] = &kindValues{ info: folder.GetEntityKindInfo(), builder: folder.GetEntitySummaryBuilder(), } - kinds[models.StandardKindPNG] = &kindValues{ + kinds[entity.StandardKindPNG] = &kindValues{ info: png.GetEntityKindInfo(), builder: png.GetEntitySummaryBuilder(), } - kinds[models.StandardKindGeoJSON] = &kindValues{ + kinds[entity.StandardKindGeoJSON] = &kindValues{ info: geojson.GetEntityKindInfo(), builder: geojson.GetEntitySummaryBuilder(), } - kinds[models.StandardKindDataFrame] = &kindValues{ + kinds[entity.StandardKindDataFrame] = &kindValues{ info: dataframe.GetEntityKindInfo(), builder: dataframe.GetEntitySummaryBuilder(), } - kinds[models.StandardKindJSONObj] = &kindValues{ + kinds[entity.StandardKindJSONObj] = &kindValues{ info: jsonobj.GetEntityKindInfo(), builder: jsonobj.GetEntitySummaryBuilder(), } @@ -86,20 +86,20 @@ func ProvideService(cfg *setting.Cfg, renderer rendering.Service) KindRegistry { } type kindValues struct { - info models.EntityKindInfo - builder models.EntitySummaryBuilder + info entity.EntityKindInfo + builder entity.EntitySummaryBuilder } type registry struct { mutex sync.RWMutex kinds map[string]*kindValues - info []models.EntityKindInfo - suffix map[string]models.EntityKindInfo + info []entity.EntityKindInfo + suffix map[string]entity.EntityKindInfo } func (r *registry) updateInfoArray() { - suffix := make(map[string]models.EntityKindInfo) - info := make([]models.EntityKindInfo, 0, len(r.kinds)) + suffix := make(map[string]entity.EntityKindInfo) + info := make([]entity.EntityKindInfo, 0, len(r.kinds)) for _, v := range r.kinds { info = append(info, v.info) if v.info.FileExtension != "" { @@ -113,7 +113,7 @@ func (r *registry) updateInfoArray() { r.suffix = suffix } -func (r *registry) Register(info models.EntityKindInfo, builder models.EntitySummaryBuilder) error { +func (r *registry) Register(info entity.EntityKindInfo, builder entity.EntitySummaryBuilder) error { if info.ID == "" || builder == nil { return fmt.Errorf("invalid kind") } @@ -134,7 +134,7 @@ func (r *registry) Register(info models.EntityKindInfo, builder models.EntitySum } // GetSummaryBuilder returns a builder or nil if not found -func (r *registry) GetSummaryBuilder(kind string) models.EntitySummaryBuilder { +func (r *registry) GetSummaryBuilder(kind string) entity.EntitySummaryBuilder { r.mutex.RLock() defer r.mutex.RUnlock() @@ -146,7 +146,7 @@ func (r *registry) GetSummaryBuilder(kind string) models.EntitySummaryBuilder { } // GetInfo returns the registered info -func (r *registry) GetInfo(kind string) (models.EntityKindInfo, error) { +func (r *registry) GetInfo(kind string) (entity.EntityKindInfo, error) { r.mutex.RLock() defer r.mutex.RUnlock() @@ -154,11 +154,11 @@ func (r *registry) GetInfo(kind string) (models.EntityKindInfo, error) { if ok { return v.info, nil } - return models.EntityKindInfo{}, fmt.Errorf("not found") + return entity.EntityKindInfo{}, fmt.Errorf("not found") } // GetInfo returns the registered info -func (r *registry) GetFromExtension(suffix string) (models.EntityKindInfo, error) { +func (r *registry) GetFromExtension(suffix string) (entity.EntityKindInfo, error) { r.mutex.RLock() defer r.mutex.RUnlock() @@ -166,11 +166,11 @@ func (r *registry) GetFromExtension(suffix string) (models.EntityKindInfo, error if ok { return v, nil } - return models.EntityKindInfo{}, fmt.Errorf("not found") + return entity.EntityKindInfo{}, fmt.Errorf("not found") } // GetSummaryBuilder returns a builder or nil if not found -func (r *registry) GetKinds() []models.EntityKindInfo { +func (r *registry) GetKinds() []entity.EntityKindInfo { r.mutex.RLock() defer r.mutex.RUnlock() diff --git a/pkg/services/store/kind/registry_test.go b/pkg/services/store/kind/registry_test.go index 3b92361cfff..419b029e64e 100644 --- a/pkg/services/store/kind/registry_test.go +++ b/pkg/services/store/kind/registry_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/services/store/kind/dummy" ) @@ -31,7 +31,7 @@ func TestKindRegistry(t *testing.T) { }, ids) // Check playlist exists - info, err := registry.GetInfo(models.StandardKindPlaylist) + info, err := registry.GetInfo(entity.StandardKindPlaylist) require.NoError(t, err) require.Equal(t, "Playlist", info.Name) require.False(t, info.IsRaw) diff --git a/pkg/services/store/kind/snapshot/summary.go b/pkg/services/store/kind/snapshot/summary.go index 7994851fe49..9907b10e21a 100644 --- a/pkg/services/store/kind/snapshot/summary.go +++ b/pkg/services/store/kind/snapshot/summary.go @@ -5,7 +5,7 @@ import ( "encoding/json" "fmt" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/store/entity" ) // A snapshot is a dashboard with no external queries and a few additional properties @@ -19,15 +19,15 @@ type Model struct { Snapshot json.RawMessage `json:"snapshot,omitempty"` } -func GetEntityKindInfo() models.EntityKindInfo { - return models.EntityKindInfo{ - ID: models.StandardKindSnapshot, +func GetEntityKindInfo() entity.EntityKindInfo { + return entity.EntityKindInfo{ + ID: entity.StandardKindSnapshot, Name: "Snapshot", } } -func GetEntitySummaryBuilder() models.EntitySummaryBuilder { - return func(ctx context.Context, uid string, body []byte) (*models.EntitySummary, []byte, error) { +func GetEntitySummaryBuilder() entity.EntitySummaryBuilder { + return func(ctx context.Context, uid string, body []byte) (*entity.EntitySummary, []byte, error) { obj := &Model{} err := json.Unmarshal(body, obj) if err != nil { @@ -41,8 +41,8 @@ func GetEntitySummaryBuilder() models.EntitySummaryBuilder { return nil, nil, fmt.Errorf("expected delete key") } - summary := &models.EntitySummary{ - Kind: models.StandardKindFolder, + summary := &entity.EntitySummary{ + Kind: entity.StandardKindFolder, Name: obj.Name, Description: obj.Description, UID: uid, @@ -51,8 +51,8 @@ func GetEntitySummaryBuilder() models.EntitySummaryBuilder { "externalURL": obj.ExternalURL, "expires": obj.Expires, }, - References: []*models.EntityExternalReference{ - {Kind: models.StandardKindDashboard, UID: obj.DashboardUID}, + References: []*entity.EntityExternalReference{ + {Kind: entity.StandardKindDashboard, UID: obj.DashboardUID}, }, } diff --git a/pkg/services/store/kind/svg/summary.go b/pkg/services/store/kind/svg/summary.go index a13c51ca2ad..474a81fb5e2 100644 --- a/pkg/services/store/kind/svg/summary.go +++ b/pkg/services/store/kind/svg/summary.go @@ -5,13 +5,13 @@ import ( "fmt" "strings" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/rendering" + "github.com/grafana/grafana/pkg/services/store/entity" ) -func GetEntityKindInfo() models.EntityKindInfo { - return models.EntityKindInfo{ - ID: models.StandardKindSVG, +func GetEntityKindInfo() entity.EntityKindInfo { + return entity.EntityKindInfo{ + ID: entity.StandardKindSVG, Name: "SVG", Description: "Scalable Vector Graphics", IsRaw: true, @@ -21,8 +21,8 @@ func GetEntityKindInfo() models.EntityKindInfo { } // SVG sanitizer based on the rendering service -func GetEntitySummaryBuilder(allowUnsanitizedSvgUpload bool, renderer rendering.Service) models.EntitySummaryBuilder { - return func(ctx context.Context, uid string, body []byte) (*models.EntitySummary, []byte, error) { +func GetEntitySummaryBuilder(allowUnsanitizedSvgUpload bool, renderer rendering.Service) entity.EntitySummaryBuilder { + return func(ctx context.Context, uid string, body []byte) (*entity.EntitySummary, []byte, error) { if !IsSVG(body) { return nil, nil, fmt.Errorf("invalid svg") } @@ -45,8 +45,8 @@ func GetEntitySummaryBuilder(allowUnsanitizedSvgUpload bool, renderer rendering. sanitized = body } - return &models.EntitySummary{ - Kind: models.StandardKindSVG, + return &entity.EntitySummary{ + Kind: entity.StandardKindSVG, Name: guessNameFromUID(uid), UID: uid, }, sanitized, nil diff --git a/pkg/services/store/resolver/service.go b/pkg/services/store/resolver/service.go index 4160edbea5b..e8343f3180a 100644 --- a/pkg/services/store/resolver/service.go +++ b/pkg/services/store/resolver/service.go @@ -5,9 +5,9 @@ import ( "fmt" "time" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/store/entity" ) const ( @@ -28,7 +28,7 @@ type ResolutionInfo struct { } type EntityReferenceResolver interface { - Resolve(ctx context.Context, ref *models.EntityExternalReference) (ResolutionInfo, error) + Resolve(ctx context.Context, ref *entity.EntityExternalReference) (ResolutionInfo, error) } func ProvideEntityReferenceResolver(ds datasources.DataSourceService, pluginStore plugins.Store) EntityReferenceResolver { @@ -46,19 +46,19 @@ type standardReferenceResolver struct { ds dsCache } -func (r *standardReferenceResolver) Resolve(ctx context.Context, ref *models.EntityExternalReference) (ResolutionInfo, error) { +func (r *standardReferenceResolver) Resolve(ctx context.Context, ref *entity.EntityExternalReference) (ResolutionInfo, error) { if ref == nil { return ResolutionInfo{OK: false, Timestamp: getNow()}, fmt.Errorf("ref is nil") } switch ref.Kind { - case models.StandardKindDataSource: + case entity.StandardKindDataSource: return r.resolveDatasource(ctx, ref) - case models.ExternalEntityReferencePlugin: + case entity.ExternalEntityReferencePlugin: return r.resolvePlugin(ctx, ref) - // case models.ExternalEntityReferenceRuntime: + // case entity.ExternalEntityReferenceRuntime: // return ResolutionInfo{ // OK: false, // Timestamp: getNow(), @@ -73,7 +73,7 @@ func (r *standardReferenceResolver) Resolve(ctx context.Context, ref *models.Ent }, nil } -func (r *standardReferenceResolver) resolveDatasource(ctx context.Context, ref *models.EntityExternalReference) (ResolutionInfo, error) { +func (r *standardReferenceResolver) resolveDatasource(ctx context.Context, ref *entity.EntityExternalReference) (ResolutionInfo, error) { ds, err := r.ds.getDS(ctx, ref.UID) if err != nil || ds == nil || ds.UID == "" { return ResolutionInfo{ @@ -99,7 +99,7 @@ func (r *standardReferenceResolver) resolveDatasource(ctx context.Context, ref * return res, nil } -func (r *standardReferenceResolver) resolvePlugin(ctx context.Context, ref *models.EntityExternalReference) (ResolutionInfo, error) { +func (r *standardReferenceResolver) resolvePlugin(ctx context.Context, ref *entity.EntityExternalReference) (ResolutionInfo, error) { p, ok := r.pluginStore.Plugin(ctx, ref.UID) if !ok { return ResolutionInfo{ diff --git a/pkg/services/store/resolver/service_test.go b/pkg/services/store/resolver/service_test.go index b420b27a508..d6cf4d1a8e5 100644 --- a/pkg/services/store/resolver/service_test.go +++ b/pkg/services/store/resolver/service_test.go @@ -4,13 +4,14 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/appcontext" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/datasources" fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes" + "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/services/user" - "github.com/stretchr/testify/require" ) func TestResolver(t *testing.T) { @@ -49,15 +50,15 @@ func TestResolver(t *testing.T) { scenarios := []struct { name string - given *models.EntityExternalReference + given *entity.EntityExternalReference expect ResolutionInfo err string ctx context.Context }{ { name: "Missing datasource without type", - given: &models.EntityExternalReference{ - Kind: models.StandardKindDataSource, + given: &entity.EntityExternalReference{ + Kind: entity.StandardKindDataSource, UID: "xyz", }, expect: ResolutionInfo{OK: false}, @@ -65,8 +66,8 @@ func TestResolver(t *testing.T) { }, { name: "OK datasource", - given: &models.EntityExternalReference{ - Kind: models.StandardKindDataSource, + given: &entity.EntityExternalReference{ + Kind: entity.StandardKindDataSource, Type: "influx", UID: "influx-uid", }, @@ -75,8 +76,8 @@ func TestResolver(t *testing.T) { }, { name: "Get the default datasource", - given: &models.EntityExternalReference{ - Kind: models.StandardKindDataSource, + given: &entity.EntityExternalReference{ + Kind: entity.StandardKindDataSource, }, expect: ResolutionInfo{ OK: true, @@ -87,8 +88,8 @@ func TestResolver(t *testing.T) { }, { name: "Get the default datasource (with type)", - given: &models.EntityExternalReference{ - Kind: models.StandardKindDataSource, + given: &entity.EntityExternalReference{ + Kind: entity.StandardKindDataSource, Type: "influx", }, expect: ResolutionInfo{ @@ -99,8 +100,8 @@ func TestResolver(t *testing.T) { }, { name: "Lookup by name", - given: &models.EntityExternalReference{ - Kind: models.StandardKindDataSource, + given: &entity.EntityExternalReference{ + Kind: entity.StandardKindDataSource, UID: "Influx2", }, expect: ResolutionInfo{ From 8b574e22b53aa4c5a35032a58844fd4aaaa12f5f Mon Sep 17 00:00:00 2001 From: Nathan Marrs Date: Wed, 25 Jan 2023 11:37:29 -0700 Subject: [PATCH 044/172] SVG: Add dompurify preprocessor step (#62143) Co-authored-by: Ryan McKinley --- package.json | 2 ++ .../grafana-ui/src/components/Icon/Icon.tsx | 6 ++++- .../app/core/components/SVG/SanitizedSVG.tsx | 18 +++++++++++++ public/app/features/canvas/elements/icon.tsx | 4 +-- .../dimensions/editors/FileUploader.tsx | 5 ++-- .../dimensions/editors/ResourceCards.tsx | 4 +-- .../dimensions/editors/ResourcePicker.tsx | 6 ++--- .../dimensions/editors/URLPickerTab.tsx | 4 +-- .../components/QueryEditorDrawerHeader.tsx | 4 +-- public/app/features/storage/FileView.tsx | 4 +-- .../panel/geomap/components/MarkersLegend.tsx | 4 +-- .../app/plugins/panel/geomap/style/markers.ts | 3 +++ yarn.lock | 25 +++++++++++++++++++ 13 files changed, 71 insertions(+), 18 deletions(-) create mode 100644 public/app/core/components/SVG/SanitizedSVG.tsx diff --git a/package.json b/package.json index a9079fa8025..c9255b114bd 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,7 @@ "@types/d3-force": "^2.1.0", "@types/d3-scale-chromatic": "1.3.1", "@types/debounce-promise": "3.1.5", + "@types/dompurify": "^2", "@types/eslint": "8.4.9", "@types/file-saver": "2.0.5", "@types/glob": "^8.0.0", @@ -316,6 +317,7 @@ "dangerously-set-html-content": "1.0.9", "date-fns": "2.29.3", "debounce-promise": "3.1.2", + "dompurify": "^2.4.1", "emotion": "11.0.0", "eventemitter3": "4.0.7", "fast-deep-equal": "^3.1.3", diff --git a/packages/grafana-ui/src/components/Icon/Icon.tsx b/packages/grafana-ui/src/components/Icon/Icon.tsx index c601c17adc1..a411c06a6a1 100644 --- a/packages/grafana-ui/src/components/Icon/Icon.tsx +++ b/packages/grafana-ui/src/components/Icon/Icon.tsx @@ -57,9 +57,13 @@ export const Icon = React.forwardRef( console.warn('Icon component passed an invalid icon name', name); } + if (!name || name.includes('..')) { + return
invalid icon name
; + } + const svgSize = getSvgSize(size); const svgHgt = svgSize; - const svgWid = name?.startsWith('gf-bar-align') ? 16 : name?.startsWith('gf-interp') ? 30 : svgSize; + const svgWid = name.startsWith('gf-bar-align') ? 16 : name.startsWith('gf-interp') ? 30 : svgSize; const subDir = getIconSubDir(name, type); const svgPath = `${iconRoot}${subDir}/${name}.svg`; diff --git a/public/app/core/components/SVG/SanitizedSVG.tsx b/public/app/core/components/SVG/SanitizedSVG.tsx new file mode 100644 index 00000000000..9a2628cd63d --- /dev/null +++ b/public/app/core/components/SVG/SanitizedSVG.tsx @@ -0,0 +1,18 @@ +import * as DOMPurify from 'dompurify'; +import React from 'react'; +import SVG, { Props } from 'react-inlinesvg'; + +export const SanitizedSVG = (props: Props) => { + return ; +}; + +let cache = new Map(); + +function getCleanSVG(code: string): string { + let clean = cache.get(code); + if (!clean) { + clean = DOMPurify.sanitize(code, { USE_PROFILES: { svg: true, svgFilters: true } }); + cache.set(code, clean); + } + return clean; +} diff --git a/public/app/features/canvas/elements/icon.tsx b/public/app/features/canvas/elements/icon.tsx index cb4920e8948..8d7c8a4c876 100644 --- a/public/app/features/canvas/elements/icon.tsx +++ b/public/app/features/canvas/elements/icon.tsx @@ -1,8 +1,8 @@ import { css } from '@emotion/css'; import { isString } from 'lodash'; import React, { CSSProperties } from 'react'; -import SVG from 'react-inlinesvg'; +import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG'; import { ColorDimensionConfig, ResourceDimensionConfig, @@ -59,7 +59,7 @@ export function IconDisplay(props: CanvasElementProps) { }; return ( - >; mediaType: MediaType; @@ -36,7 +37,7 @@ export const FileUploader = ({ mediaType, setFormData, setUpload, error }: Props const Preview = () => (
- {mediaType === MediaType.Icon && } + {mediaType === MediaType.Icon && } {mediaType === MediaType.Image && Preview of the uploaded file}
diff --git a/public/app/features/dimensions/editors/ResourceCards.tsx b/public/app/features/dimensions/editors/ResourceCards.tsx index 49aa6ea804c..57d01604cda 100644 --- a/public/app/features/dimensions/editors/ResourceCards.tsx +++ b/public/app/features/dimensions/editors/ResourceCards.tsx @@ -1,11 +1,11 @@ import { css, cx } from '@emotion/css'; import React, { memo, CSSProperties } from 'react'; -import SVG from 'react-inlinesvg'; import AutoSizer from 'react-virtualized-auto-sizer'; import { areEqual, FixedSizeGrid as Grid } from 'react-window'; import { GrafanaTheme2 } from '@grafana/data'; import { useTheme2, stylesFactory } from '@grafana/ui'; +import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG'; import { ResourceItem } from './FolderPickerTab'; @@ -38,7 +38,7 @@ function Cell(props: CellProps) { onClick={() => onChange(card.value)} > {card.imgUrl.endsWith('.svg') ? ( - + ) : ( )} diff --git a/public/app/features/dimensions/editors/ResourcePicker.tsx b/public/app/features/dimensions/editors/ResourcePicker.tsx index 30246d6aa0d..ecfb63f3ef4 100644 --- a/public/app/features/dimensions/editors/ResourcePicker.tsx +++ b/public/app/features/dimensions/editors/ResourcePicker.tsx @@ -1,6 +1,5 @@ import { css } from '@emotion/css'; import React, { createRef } from 'react'; -import SVG from 'react-inlinesvg'; import { GrafanaTheme2 } from '@grafana/data'; import { @@ -15,6 +14,7 @@ import { useTheme2, } from '@grafana/ui'; import { closePopover } from '@grafana/ui/src/utils/closePopover'; +import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG'; import { getPublicOrAbsoluteUrl } from '../resource'; import { MediaType, ResourceFolderName, ResourcePickerSize } from '../types'; @@ -56,7 +56,7 @@ export const ResourcePicker = (props: Props) => { const renderSmallResourcePicker = () => { if (value && sanitizedSrc) { - return ; + return ; } else { return ( @@ -73,7 +73,7 @@ export const ResourcePicker = (props: Props) => { value={name} placeholder={placeholder} readOnly={true} - prefix={sanitizedSrc && } + prefix={sanitizedSrc && } suffix={ + +
+ + {modKey}+k +
); @@ -73,6 +84,16 @@ const getStyles = (theme: GrafanaTheme2) => { wrapper: baseStyles.wrapper, inputWrapper: baseStyles.inputWrapper, prefix: baseStyles.prefix, + suffix: css([ + baseStyles.suffix, + { + display: 'flex', + gap: theme.spacing(0.5), + }, + ]), + shortcut: css({ + fontSize: theme.typography.bodySmall.fontSize, + }), fakeInput: css([ baseStyles.input, { diff --git a/public/app/core/components/help/HelpModal.tsx b/public/app/core/components/help/HelpModal.tsx index f51900e9571..3d9f0a5afa9 100644 --- a/public/app/core/components/help/HelpModal.tsx +++ b/public/app/core/components/help/HelpModal.tsx @@ -1,10 +1,11 @@ import { css } from '@emotion/css'; -import React from 'react'; +import React, { useMemo } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Modal, useStyles2 } from '@grafana/ui'; +import { getModKey } from 'app/core/utils/browser'; -const shortcuts = { +const getShortcuts = (modKey: string) => ({ Global: [ { keys: ['g', 'h'], description: 'Go to Home Dashboard' }, { keys: ['g', 'e'], description: 'Go to Explore' }, @@ -12,11 +13,11 @@ const shortcuts = { { keys: ['s', 'o'], description: 'Open search' }, { keys: ['esc'], description: 'Exit edit/setting views' }, { keys: ['h'], description: 'Show all keyboard shortcuts' }, - { keys: ['mod+k'], description: 'Open command palette' }, + { keys: [`${modKey}+k`], description: 'Open command palette' }, { keys: ['c', 't'], description: 'Change theme' }, ], Dashboard: [ - { keys: ['mod+s'], description: 'Save dashboard' }, + { keys: [`${modKey}+s`], description: 'Save dashboard' }, { keys: ['d', 'r'], description: 'Refresh all panels' }, { keys: ['d', 's'], description: 'Dashboard settings' }, { keys: ['d', 'v'], description: 'Toggle in-active / view mode' }, @@ -24,7 +25,7 @@ const shortcuts = { { keys: ['d', 'E'], description: 'Expand all rows' }, { keys: ['d', 'C'], description: 'Collapse all rows' }, { keys: ['d', 'a'], description: 'Toggle auto fit panels (experimental feature)' }, - { keys: ['mod+o'], description: 'Toggle shared graph crosshair' }, + { keys: [`${modKey}+o`], description: 'Toggle shared graph crosshair' }, { keys: ['d', 'l'], description: 'Toggle all panel legends' }, ], 'Focused Panel': [ @@ -50,7 +51,7 @@ const shortcuts = { description: 'Make time range absolute/permanent', }, ], -}; +}); export interface HelpModalProps { onDismiss: () => void; @@ -58,11 +59,10 @@ export interface HelpModalProps { export const HelpModal = ({ onDismiss }: HelpModalProps): JSX.Element => { const styles = useStyles2(getStyles); + const modKey = useMemo(() => getModKey(), []); + const shortcuts = useMemo(() => getShortcuts(modKey), [modKey]); return ( -
- mod = CTRL on windows or linux and CMD key on Mac -
{Object.entries(shortcuts).map(([category, shortcuts], i) => (
diff --git a/public/app/core/utils/browser.ts b/public/app/core/utils/browser.ts index 346dee58ee9..d47b8745e4b 100644 --- a/public/app/core/utils/browser.ts +++ b/public/app/core/utils/browser.ts @@ -32,3 +32,12 @@ export function checkBrowserCompatibility() { return true; } + +export function userAgentIsApple() { + const appleRe = /(iPhone|iPad|Mac)/; + return appleRe.test(navigator.platform); +} + +export function getModKey() { + return userAgentIsApple() ? 'cmd' : 'ctrl'; +} From f7d92ab8411ae3488b15f0912c992e733d599271 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Thu, 26 Jan 2023 12:07:43 +0000 Subject: [PATCH 073/172] Navigation: only show the `img` for a section root if both `img` and `icon` are present (#62127) only show an img for a section root if both img and icon are present --- .../PageNew/SectionNavItem.test.tsx | 25 +++++++++++++++++++ .../components/PageNew/SectionNavItem.tsx | 11 ++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 public/app/core/components/PageNew/SectionNavItem.test.tsx diff --git a/public/app/core/components/PageNew/SectionNavItem.test.tsx b/public/app/core/components/PageNew/SectionNavItem.test.tsx new file mode 100644 index 00000000000..90d92e75462 --- /dev/null +++ b/public/app/core/components/PageNew/SectionNavItem.test.tsx @@ -0,0 +1,25 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { NavModelItem } from '@grafana/data'; + +import { SectionNavItem } from './SectionNavItem'; + +describe('SectionNavItem', () => { + it('should only show the img for a section root if both img and icon are present', () => { + const item: NavModelItem = { + text: 'Test', + icon: 'k6', + img: 'img', + children: [ + { + text: 'Child', + }, + ], + }; + + render(); + expect(screen.getByTestId('section-image')).toBeInTheDocument(); + expect(screen.queryByTestId('section-icon')).not.toBeInTheDocument(); + }); +}); diff --git a/public/app/core/components/PageNew/SectionNavItem.tsx b/public/app/core/components/PageNew/SectionNavItem.tsx index 148a3b386f1..9a85a66ee76 100644 --- a/public/app/core/components/PageNew/SectionNavItem.tsx +++ b/public/app/core/components/PageNew/SectionNavItem.tsx @@ -28,6 +28,14 @@ export function SectionNavItem({ item, isSectionRoot = false }: Props) { [styles.noRootMargin]: noRootMargin, }); + let icon: React.ReactNode | null = null; + + if (item.img) { + icon = ; + } else if (item.icon) { + icon = ; + } + return ( <> - {isSectionRoot && item.icon && } - {isSectionRoot && item.img && {`logo} + {isSectionRoot && icon} {getNavTitle(item.id) ?? item.text} {item.tabSuffix && } From bfbc8c3c4fd059591e3d09fe1f3d14399939e3c6 Mon Sep 17 00:00:00 2001 From: Ludovic Viaud Date: Thu, 26 Jan 2023 13:10:05 +0100 Subject: [PATCH 074/172] Transforms: Add join by fields (#61322) --- .../transforms/join-by-field.json | 414 ++++++------------ .../transformers/ensureColumns.ts | 13 +- .../transformers/joinByField.test.ts | 98 ++++- .../transformers/joinByField.ts | 15 +- .../transformers/joinDataFrames.test.ts | 99 +++++ .../transformers/joinDataFrames.ts | 21 +- .../editors/JoinByFieldTransformerEditor.tsx | 86 +++- 7 files changed, 392 insertions(+), 354 deletions(-) diff --git a/devenv/dev-dashboards/transforms/join-by-field.json b/devenv/dev-dashboards/transforms/join-by-field.json index a86b3b89a89..907cee7e5d1 100644 --- a/devenv/dev-dashboards/transforms/join-by-field.json +++ b/devenv/dev-dashboards/transforms/join-by-field.json @@ -24,7 +24,6 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, - "id": 1351, "links": [], "liveNow": false, "panels": [ @@ -38,7 +37,7 @@ }, "id": 9, "panels": [], - "title": "Join by time", + "title": "Input", "type": "row" }, { @@ -49,37 +48,14 @@ "fieldConfig": { "defaults": { "color": { - "mode": "palette-classic" + "mode": "thresholds" }, "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false + "align": "auto", + "cellOptions": { + "type": "auto" }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } + "inspect": false }, "mappings": [], "thresholds": { @@ -99,41 +75,41 @@ }, "gridPos": { "h": 8, - "w": 12, + "w": 8, "x": 0, "y": 1 }, "id": 11, "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false }, - "tooltip": { - "mode": "single", - "sort": "none" - } + "showHeader": true }, + "pluginVersion": "9.4.0-pre", "targets": [ { "datasource": { "type": "testdata", "uid": "PD8C576611E62080A" }, - "refId": "A", - "scenarioId": "random_walk", - "seriesCount": 4 + "rawFrameContent": "[{\r\n \"name\": \"tags\",\r\n \"fields\": [\r\n { \"name\": \"tags__time\", \"values\": [100, 101, 200] },\r\n { \"name\": \"tags__name\", \"values\": [\"v1.2\", \"v1.2b\", \"v1.3\"] }\r\n ]\r\n}]", + "refId": "tags", + "scenarioId": "raw_frame" } ], - "title": "Timeseries data", - "type": "timeseries" + "title": "tags", + "type": "table" }, { "datasource": { - "type": "datasource", - "uid": "-- Dashboard --" + "type": "testdata", + "uid": "PD8C576611E62080A" }, "fieldConfig": { "defaults": { @@ -142,7 +118,9 @@ }, "custom": { "align": "auto", - "displayMode": "auto", + "cellOptions": { + "type": "auto" + }, "inspect": false }, "mappings": [], @@ -163,13 +141,14 @@ }, "gridPos": { "h": 8, - "w": 12, - "x": 12, + "w": 8, + "x": 8, "y": 1 }, "id": 13, "options": { "footer": { + "countRows": false, "fields": "", "reducer": [ "sum" @@ -178,24 +157,25 @@ }, "showHeader": true }, - "pluginVersion": "9.2.0-pre", + "pluginVersion": "9.4.0-pre", "targets": [ { "datasource": { - "type": "datasource", - "uid": "-- Dashboard --" + "type": "testdata", + "uid": "PD8C576611E62080A" }, - "panelId": 11, - "refId": "A" + "rawFrameContent": "[{\r\n \"name\": \"releases\",\r\n\"fields\": [\r\n { \"name\": \"releases__time\", \"values\": [150, 250] },\r\n { \"name\": \"releases__tag\", \"values\": [\"v1.2\", \"v1.3\"] }\r\n]}]", + "refId": "releases", + "scenarioId": "raw_frame" } ], - "title": "Same data (as a table)", + "title": "releases", "type": "table" }, { "datasource": { - "type": "datasource", - "uid": "-- Dashboard --" + "type": "testdata", + "uid": "PD8C576611E62080A" }, "fieldConfig": { "defaults": { @@ -204,7 +184,9 @@ }, "custom": { "align": "auto", - "displayMode": "auto", + "cellOptions": { + "type": "auto" + }, "inspect": false }, "mappings": [], @@ -224,14 +206,15 @@ "overrides": [] }, "gridPos": { - "h": 5, - "w": 24, - "x": 0, - "y": 9 + "h": 8, + "w": 8, + "x": 16, + "y": 1 }, - "id": 16, + "id": 19, "options": { "footer": { + "countRows": false, "fields": "", "reducer": [ "sum" @@ -240,24 +223,19 @@ }, "showHeader": true }, - "pluginVersion": "9.2.0-pre", + "pluginVersion": "9.4.0-pre", "targets": [ { "datasource": { - "type": "datasource", - "uid": "-- Dashboard --" + "type": "testdata", + "uid": "PD8C576611E62080A" }, - "panelId": 11, - "refId": "A" - } - ], - "title": "OUTER join on time (default)", - "transformations": [ - { - "id": "joinByField", - "options": {} + "rawFrameContent": "[{\r\n \"name\": \"features\",\r\n\"fields\": [\r\n { \"name\": \"features__name\", \"values\": [\"A\", \"B\", \"C\", \"D\", \"E\"] },\r\n { \"name\": \"features__tag\", \"values\": [\"v1.2\", \"v1.3\", \"v1.2b\", \"v1.3\", \"v1.2\"] }\r\n]}]", + "refId": "features", + "scenarioId": "raw_frame" } ], + "title": "features", "type": "table" }, { @@ -266,11 +244,11 @@ "h": 1, "w": 24, "x": 0, - "y": 14 + "y": 9 }, - "id": 5, + "id": 21, "panels": [], - "title": "Join by string field", + "title": "Output", "type": "row" }, { @@ -285,7 +263,9 @@ }, "custom": { "align": "auto", - "displayMode": "auto", + "cellOptions": { + "type": "auto" + }, "inspect": false }, "mappings": [], @@ -308,202 +288,61 @@ "h": 8, "w": 12, "x": 0, - "y": 15 + "y": 10 }, - "id": 2, + "id": 23, "options": { "footer": { + "countRows": false, "fields": "", "reducer": [ "sum" ], "show": false }, - "frameIndex": 0, "showHeader": true }, - "pluginVersion": "9.2.0-pre", + "pluginVersion": "9.4.0-pre", "targets": [ { - "csvContent": "OrderID,CustomerID,Time\n100,A,10000\n101,B,20000\n102,C,30000", "datasource": { "type": "testdata", "uid": "PD8C576611E62080A" }, - "refId": "Orders", - "scenarioId": "csv_content" + "rawFrameContent": "[{\r\n \"name\": \"tags\",\r\n \"fields\": [\r\n { \"name\": \"tags__time\", \"values\": [100, 101, 200] },\r\n { \"name\": \"tags__name\", \"values\": [\"v1.2\", \"v1.2b\", \"v1.3\"] }\r\n ]\r\n}]", + "refId": "tags", + "scenarioId": "raw_frame" }, { - "csvContent": "CustomerID,Name,Country\nA,Customer A,USA\nB,Customer B,Germany\nC,Customer C,Spain\nD,Customer D,Canada", "datasource": { "type": "testdata", "uid": "PD8C576611E62080A" }, - "hide": false, - "refId": "Customers", - "scenarioId": "csv_content" - } - ], - "title": "Orders", - "transformations": [], - "type": "table" - }, - { - "datasource": { - "type": "datasource", - "uid": "-- Dashboard --" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "displayMode": "auto", - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } + "rawFrameContent": "[{\r\n \"name\": \"releases\",\r\n \"fields\": [\r\n { \"name\": \"releases__time\", \"values\": [150, 250] },\r\n { \"name\": \"releases__tag\", \"values\": [\"v1.2\", \"v1.3\"] }\r\n]}]", + "refId": "releases", + "scenarioId": "raw_frame" }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 15 - }, - "id": 3, - "options": { - "footer": { - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, - "frameIndex": 1, - "showHeader": true - }, - "pluginVersion": "9.2.0-pre", - "targets": [ { "datasource": { - "type": "datasource", - "uid": "-- Dashboard --" + "type": "testdata", + "uid": "PD8C576611E62080A" }, - "panelId": 2, - "refId": "A" + "rawFrameContent": "[{\r\n \"name\": \"features\",\r\n \"fields\": [\r\n { \"name\": \"features__name\", \"values\": [\"A\", \"B\", \"C\", \"D\", \"E\"] },\r\n { \"name\": \"features__tag\", \"values\": [\"v1.2\", \"v1.3\", \"v1.2b\", \"v1.3\", \"v1.2\"] }\r\n]}]", + "refId": "features", + "scenarioId": "raw_frame" } ], - "title": "Customers", - "transformations": [], - "type": "table" - }, - { - "datasource": { - "type": "datasource", - "uid": "-- Dashboard --" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "custom": { - "align": "auto", - "displayMode": "auto", - "inspect": false - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "CustomerID" - }, - "properties": [ - { - "id": "custom.width", - "value": 101 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "OrderID" - }, - "properties": [ - { - "id": "custom.width", - "value": 89 - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 23 - }, - "id": 6, - "options": { - "footer": { - "fields": "", - "reducer": [ - "sum" - ], - "show": false - }, - "frameIndex": 0, - "showHeader": true, - "sortBy": [] - }, - "pluginVersion": "9.2.0-pre", - "targets": [ - { - "datasource": { - "type": "datasource", - "uid": "-- Dashboard --" - }, - "panelId": 2, - "refId": "A" - } - ], - "title": "OUTER join on CustomerID (keeps missing values)", + "title": "OUTER JOIN", "transformations": [ { "id": "joinByField", "options": { - "byField": "CustomerID", + "fields": { + "A": "features__name", + "features": "features__tag", + "releases": "releases__tag", + "tags": "tags__name" + }, "mode": "outer" } } @@ -512,8 +351,8 @@ }, { "datasource": { - "type": "datasource", - "uid": "-- Dashboard --" + "type": "testdata", + "uid": "PD8C576611E62080A" }, "fieldConfig": { "defaults": { @@ -522,7 +361,9 @@ }, "custom": { "align": "auto", - "displayMode": "auto", + "cellOptions": { + "type": "auto" + }, "inspect": false }, "mappings": [], @@ -539,69 +380,67 @@ ] } }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "CustomerID" - }, - "properties": [ - { - "id": "custom.width", - "value": 101 - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "OrderID" - }, - "properties": [ - { - "id": "custom.width", - "value": 89 - } - ] - } - ] + "overrides": [] }, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 23 + "y": 10 }, - "id": 7, + "id": 24, "options": { "footer": { + "countRows": false, "fields": "", "reducer": [ "sum" ], "show": false }, - "frameIndex": 0, - "showHeader": true, - "sortBy": [] + "showHeader": true }, - "pluginVersion": "9.2.0-pre", + "pluginVersion": "9.4.0-pre", "targets": [ { "datasource": { - "type": "datasource", - "uid": "-- Dashboard --" + "type": "testdata", + "uid": "PD8C576611E62080A" }, - "panelId": 2, - "refId": "A" + "rawFrameContent": "[{\r\n \"name\": \"tags\",\r\n \"fields\": [\r\n { \"name\": \"tags__time\", \"values\": [100, 101, 200] },\r\n { \"name\": \"tags__name\", \"values\": [\"v1.2\", \"v1.2b\", \"v1.3\"] }\r\n ]\r\n}]", + "refId": "tags", + "scenarioId": "raw_frame" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "rawFrameContent": "[{\r\n \"name\": \"releases\",\r\n \"fields\": [\r\n { \"name\": \"releases__time\", \"values\": [150, 250] },\r\n { \"name\": \"releases__tag\", \"values\": [\"v1.2\", \"v1.3\"] }\r\n]}]", + "refId": "releases", + "scenarioId": "raw_frame" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "rawFrameContent": "[{\r\n \"name\": \"features\",\r\n \"fields\": [\r\n { \"name\": \"features__name\", \"values\": [\"A\", \"B\", \"C\", \"D\", \"E\"] },\r\n { \"name\": \"features__tag\", \"values\": [\"v1.2\", \"v1.3\", \"v1.2b\", \"v1.3\", \"v1.2\"] }\r\n]}]", + "refId": "features", + "scenarioId": "raw_frame" } ], - "title": "INNER join on CustomerID ", + "title": "INNER JOIN", "transformations": [ { "id": "joinByField", "options": { - "byField": "CustomerID", + "fields": { + "A": "features__name", + "features": "features__tag", + "releases": "releases__tag", + "tags": "tags__name" + }, "mode": "inner" } } @@ -609,7 +448,8 @@ "type": "table" } ], - "schemaVersion": 37, + "revision": 1, + "schemaVersion": 38, "style": "dark", "tags": [ "gdev", @@ -626,6 +466,6 @@ "timezone": "", "title": "Join by field", "uid": "gw0K4rmVz", - "version": 6, + "version": 1, "weekStart": "" -} +} \ No newline at end of file diff --git a/packages/grafana-data/src/transformations/transformers/ensureColumns.ts b/packages/grafana-data/src/transformations/transformers/ensureColumns.ts index 4968336c236..9cca6fead37 100644 --- a/packages/grafana-data/src/transformations/transformers/ensureColumns.ts +++ b/packages/grafana-data/src/transformations/transformers/ensureColumns.ts @@ -20,12 +20,13 @@ export const ensureColumnsTransformer: SynchronousDataTransformerInfo = { const timeFieldName = findConsistentTimeFieldName(frames); if (frames.length > 1 && timeFieldName) { - return joinByFieldTransformer.transformer( - { - byField: timeFieldName, - }, - ctx - )(frames); + const fields: { [key: string]: string } = {}; + for (const frame of frames) { + if (frame.refId) { + fields[frame.refId] = timeFieldName; + } + } + return joinByFieldTransformer.transformer({ fields }, ctx)(frames); } return frames; }, diff --git a/packages/grafana-data/src/transformations/transformers/joinByField.test.ts b/packages/grafana-data/src/transformations/transformers/joinByField.test.ts index 1a1026017b3..5326e86da46 100644 --- a/packages/grafana-data/src/transformations/transformers/joinByField.test.ts +++ b/packages/grafana-data/src/transformations/transformers/joinByField.test.ts @@ -15,6 +15,7 @@ describe('JOIN Transformer', () => { describe('outer join', () => { const everySecondSeries = toDataFrame({ name: 'even', + refId: 'even', fields: [ { name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] }, { name: 'temperature', type: FieldType.number, values: [10.3, 10.4, 10.5, 10.6] }, @@ -24,6 +25,7 @@ describe('JOIN Transformer', () => { const everyOtherSecondSeries = toDataFrame({ name: 'odd', + refId: 'odd', fields: [ { name: 'time', type: FieldType.time, values: [1000, 3000, 5000, 7000] }, { name: 'temperature', type: FieldType.number, values: [11.1, 11.3, 11.5, 11.7] }, @@ -33,9 +35,12 @@ describe('JOIN Transformer', () => { it('joins by time field', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.seriesToColumns, + id: DataTransformerID.joinByField, options: { - byField: 'time', + fields: { + even: 'time', + odd: 'time', + }, }, }; @@ -135,9 +140,12 @@ describe('JOIN Transformer', () => { it('joins by temperature field', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.seriesToColumns, + id: DataTransformerID.joinByField, options: { - byField: 'temperature', + fields: { + even: 'temperature', + odd: 'temperature', + }, }, }; @@ -145,6 +153,7 @@ describe('JOIN Transformer', () => { (received) => { const data = received[0]; const filtered = data[0]; + expect(filtered.fields).toMatchInlineSnapshot(` [ { @@ -251,9 +260,12 @@ describe('JOIN Transformer', () => { it('joins by time field in reverse order', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.seriesToColumns, + id: DataTransformerID.joinByField, options: { - byField: 'time', + fields: { + even: 'time', + odd: 'time', + }, }, }; @@ -265,6 +277,7 @@ describe('JOIN Transformer', () => { (received) => { const data = received[0]; const filtered = data[0]; + expect(filtered.fields).toMatchInlineSnapshot(` [ { @@ -376,9 +389,12 @@ describe('JOIN Transformer', () => { it('when dataframe and field share the same name then use the field name', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.seriesToColumns, + id: DataTransformerID.joinByField, options: { - byField: 'time', + fields: { + even: 'time', + odd: 'time', + }, }, }; @@ -439,9 +455,12 @@ describe('JOIN Transformer', () => { it('joins if fields are missing', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.seriesToColumns, + id: DataTransformerID.joinByField, options: { - byField: 'time', + fields: { + even: 'time', + odd: 'time', + }, }, }; @@ -517,9 +536,12 @@ describe('JOIN Transformer', () => { it('handles duplicate field name', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.seriesToColumns, + id: DataTransformerID.joinByField, options: { - byField: 'time', + fields: { + even: 'time', + odd: 'time', + }, }, }; @@ -580,6 +602,7 @@ describe('JOIN Transformer', () => { describe('inner join', () => { const seriesA = toDataFrame({ name: 'A', + refId: 'A', fields: [ { name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] }, { name: 'temperature', type: FieldType.number, values: [10.3, 10.4, 10.5, 10.6] }, @@ -589,6 +612,7 @@ describe('JOIN Transformer', () => { const seriesB = toDataFrame({ name: 'B', + refId: 'B', fields: [ { name: 'time', type: FieldType.time, values: [1000, 3000, 5000, 7000] }, { name: 'temperature', type: FieldType.number, values: [11.1, 10.3, 10.5, 11.7] }, @@ -598,9 +622,12 @@ describe('JOIN Transformer', () => { it('inner joins by time field', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.seriesToColumns, + id: DataTransformerID.joinByField, options: { - byField: 'time', + fields: { + A: 'time', + B: 'time', + }, mode: JoinMode.inner, }, }; @@ -679,9 +706,12 @@ describe('JOIN Transformer', () => { it('inner joins by temperature field', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.seriesToColumns, + id: DataTransformerID.joinByField, options: { - byField: 'temperature', + fields: { + A: 'temperature', + B: 'temperature', + }, mode: JoinMode.inner, }, }; @@ -764,9 +794,12 @@ describe('JOIN Transformer', () => { it('inner joins by time field in reverse order', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.seriesToColumns, + id: DataTransformerID.joinByField, options: { - byField: 'time', + fields: { + A: 'time', + B: 'time', + }, mode: JoinMode.inner, }, }; @@ -852,6 +885,7 @@ describe('JOIN Transformer', () => { describe('Field names', () => { const seriesWithSameFieldAndDataFrameName = toDataFrame({ name: 'temperature', + refId: 'temperature', fields: [ { name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] }, { name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] }, @@ -860,6 +894,7 @@ describe('JOIN Transformer', () => { const seriesB = toDataFrame({ name: 'B', + refId: 'B', fields: [ { name: 'time', type: FieldType.time, values: [1000, 2000, 3000, 4000] }, { name: 'temperature', type: FieldType.number, values: [2, 4, 6, 8] }, @@ -868,9 +903,12 @@ describe('JOIN Transformer', () => { it('when dataframe and field share the same name then use the field name', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.seriesToColumns, + id: DataTransformerID.joinByField, options: { - byField: 'time', + fields: { + temperature: 'time', + B: 'time', + }, mode: JoinMode.inner, }, }; @@ -932,15 +970,20 @@ describe('JOIN Transformer', () => { it('joins if fields are missing', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.seriesToColumns, + id: DataTransformerID.joinByField, options: { - byField: 'time', + fields: { + A: 'time', + B: 'time', + C: 'time', + }, mode: JoinMode.inner, }, }; const frame1 = toDataFrame({ name: 'A', + refId: 'A', fields: [ { name: 'time', type: FieldType.time, values: [1, 2, 3] }, { name: 'temperature', type: FieldType.number, values: [10, 11, 12] }, @@ -949,11 +992,13 @@ describe('JOIN Transformer', () => { const frame2 = toDataFrame({ name: 'B', + refId: 'B', fields: [], }); const frame3 = toDataFrame({ name: 'C', + refId: 'C', fields: [ { name: 'time', type: FieldType.time, values: [1, 2, 3] }, { name: 'temperature', type: FieldType.number, values: [20, 22, 24] }, @@ -1011,14 +1056,18 @@ describe('JOIN Transformer', () => { it('handles duplicate field name', async () => { const cfg: DataTransformerConfig = { - id: DataTransformerID.seriesToColumns, + id: DataTransformerID.joinByField, options: { - byField: 'time', + fields: { + frame1: 'time', + frame2: 'time', + }, mode: JoinMode.inner, }, }; const frame1 = toDataFrame({ + refId: 'frame1', fields: [ { name: 'time', type: FieldType.time, values: [1] }, { name: 'temperature', type: FieldType.number, values: [10] }, @@ -1026,6 +1075,7 @@ describe('JOIN Transformer', () => { }); const frame2 = toDataFrame({ + refId: 'frame2', fields: [ { name: 'time', type: FieldType.time, values: [1] }, { name: 'temperature', type: FieldType.number, values: [20] }, diff --git a/packages/grafana-data/src/transformations/transformers/joinByField.ts b/packages/grafana-data/src/transformations/transformers/joinByField.ts index e6c1d386fa6..2946dd4125b 100644 --- a/packages/grafana-data/src/transformations/transformers/joinByField.ts +++ b/packages/grafana-data/src/transformations/transformers/joinByField.ts @@ -1,8 +1,6 @@ import { map } from 'rxjs/operators'; -import { DataFrame, SynchronousDataTransformerInfo, FieldMatcher } from '../../types'; -import { fieldMatchers } from '../matchers'; -import { FieldMatcherID } from '../matchers/ids'; +import { DataFrame, SynchronousDataTransformerInfo } from '../../types'; import { DataTransformerID } from './ids'; import { joinDataFrames } from './joinDataFrames'; @@ -13,7 +11,7 @@ export enum JoinMode { } export interface JoinByFieldOptions { - byField?: string; // empty will pick the field automatically + fields?: { [key: string]: string }; // empty will pick the field automatically mode?: JoinMode; } @@ -24,7 +22,7 @@ export const joinByFieldTransformer: SynchronousDataTransformerInfo joinByFieldTransformer.transformer(options, ctx)(data))), transformer: (options: JoinByFieldOptions) => { - let joinBy: FieldMatcher | undefined = undefined; return (data: DataFrame[]) => { if (data.length > 1) { - if (options.byField && !joinBy) { - joinBy = fieldMatchers.get(FieldMatcherID.byName).get(options.byField); - } - const joined = joinDataFrames({ frames: data, joinBy, mode: options.mode }); + const joined = joinDataFrames({ frames: data, mode: options.mode, fields: options.fields }); if (joined) { return [joined]; } } + return data; }; }, diff --git a/packages/grafana-data/src/transformations/transformers/joinDataFrames.test.ts b/packages/grafana-data/src/transformations/transformers/joinDataFrames.test.ts index 5f4ef23427b..83dd9469f6b 100644 --- a/packages/grafana-data/src/transformations/transformers/joinDataFrames.test.ts +++ b/packages/grafana-data/src/transformations/transformers/joinDataFrames.test.ts @@ -359,4 +359,103 @@ describe('align frames', () => { expect(isLikelyAscendingVector(new ArrayVector([null, 1, null]), 3)).toBeTruthy(); }); }); + + describe('should perform a join on custom fields', () => { + const tags = toDataFrame({ + refId: 'tags', + fields: [ + { name: 'tags__time', type: FieldType.time, values: [100, 101, 200] }, + { name: 'tags__name', type: FieldType.string, values: ['v1.2', 'v1.2b', 'v1.3'] }, + ], + }); + + const releases = toDataFrame({ + refId: 'releases', + fields: [ + { name: 'releases__time', type: FieldType.time, values: [150, 250] }, + { name: 'releases__tag', type: FieldType.string, values: ['v1.2', 'v1.3'] }, + ], + }); + + const features = toDataFrame({ + refId: 'features', + fields: [ + { name: 'features__name', type: FieldType.string, values: ['A', 'B', 'C', 'D', 'E'] }, + { name: 'features__tag', type: FieldType.time, values: ['v1.2', 'v1.3', 'v1.2b', 'v1.3', 'v1.2'] }, + ], + }); + + it('should perform an outer join', () => { + const out = joinDataFrames({ + frames: [tags, releases, features], + fields: { + tags: 'tags__name', + releases: 'releases__tag', + features: 'features__tag', + }, + })!; + + expect( + out.fields.map((f) => ({ + name: f.name, + values: f.values.toArray(), + })) + ).toEqual([ + { + name: 'tags__name', + values: ['v1.2', 'v1.2b', 'v1.3'], + }, + { + name: 'tags__time', + values: [100, 101, 200], + }, + { + name: 'releases__time', + values: [150, undefined, 250], + }, + { + name: 'features__name', + values: ['E', 'C', 'D'], + }, + ]); + }); + + it('should perform an inner join', () => { + const out = joinDataFrames({ + frames: [tags, releases, features], + fields: { + tags: 'tags__name', + releases: 'releases__tag', + features: 'features__tag', + }, + mode: JoinMode.inner, + })!; + + const mappedOut = out.fields.map((f) => ({ + name: f.name, + values: f.values.toArray(), + })); + + const expected = [ + { + name: 'tags__name', + values: ['v1.2', 'v1.3'], + }, + { + name: 'tags__time', + values: [100, 200], + }, + { + name: 'releases__time', + values: [150, 250], + }, + { + name: 'features__name', + values: ['E', 'D'], + }, + ]; + + expect(JSON.stringify(mappedOut)).toEqual(JSON.stringify(expected)); + }); + }); }); diff --git a/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts b/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts index 767161d2f9a..dd09c373bfb 100644 --- a/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts +++ b/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts @@ -47,6 +47,11 @@ export interface JoinOptions { */ joinBy?: FieldMatcher; + /** + * The fields to join on + */ + fields?: { [key: string]: string }; + /** * Optionally filter the non-join fields */ @@ -63,8 +68,16 @@ export interface JoinOptions { mode?: JoinMode; } -function getJoinMatcher(options: JoinOptions): FieldMatcher { - return options.joinBy ?? pickBestJoinField(options.frames); +function getJoinMatcher(options: JoinOptions, refId: string | undefined): FieldMatcher { + if (options.joinBy) { + return options.joinBy; + } + + if (!options.fields || !refId) { + return pickBestJoinField(options.frames); + } + + return fieldMatchers.get(FieldMatcherID.byName).get(options.fields[refId]); } /** @@ -95,7 +108,7 @@ export function joinDataFrames(options: JoinOptions): DataFrame | undefined { let frame = options.frames[0]; let frameCopy = frame; - const joinFieldMatcher = getJoinMatcher(options); + const joinFieldMatcher = getJoinMatcher(options, frame.refId); let joinIndex = frameCopy.fields.findIndex((f) => joinFieldMatcher(f, frameCopy, options.frames)); if (options.keepOriginIndices) { @@ -152,10 +165,10 @@ export function joinDataFrames(options: JoinOptions): DataFrame | undefined { const nullModes: JoinNullMode[][] = []; const allData: AlignedData[] = []; const originalFields: Field[] = []; - const joinFieldMatcher = getJoinMatcher(options); for (let frameIndex = 0; frameIndex < options.frames.length; frameIndex++) { const frame = options.frames[frameIndex]; + const joinFieldMatcher = getJoinMatcher(options, frame.refId); if (!frame || !frame.fields?.length) { continue; // skip the frame diff --git a/public/app/features/transformers/editors/JoinByFieldTransformerEditor.tsx b/public/app/features/transformers/editors/JoinByFieldTransformerEditor.tsx index cffa84015e0..1b0977455d6 100644 --- a/public/app/features/transformers/editors/JoinByFieldTransformerEditor.tsx +++ b/public/app/features/transformers/editors/JoinByFieldTransformerEditor.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { DataTransformerID, @@ -6,11 +6,10 @@ import { standardTransformers, TransformerRegistryItem, TransformerUIProps, + DataFrame, } from '@grafana/data'; import { JoinByFieldOptions, JoinMode } from '@grafana/data/src/transformations/transformers/joinByField'; -import { Select, InlineFieldRow, InlineField } from '@grafana/ui'; - -import { useAllFieldNamesFromDataFrames } from '../utils'; +import { Select, InlineFieldRow, InlineField, Checkbox, HorizontalGroup } from '@grafana/ui'; const modes = [ { value: JoinMode.outer, label: 'OUTER', description: 'Keep all rows from any table with a value' }, @@ -18,14 +17,44 @@ const modes = [ ]; export function SeriesToFieldsTransformerEditor({ input, options, onChange }: TransformerUIProps) { - const fieldNames = useAllFieldNamesFromDataFrames(input).map((item: string) => ({ label: item, value: item })); + useEffect(() => { + if (options.fields && !Object.keys(options.fields).length && input.length && input[0].refId) { + options.fields[input[0].refId] = input[0].fields[0].name; + onChange({ ...options }); + } + }, [onChange, options, input]); + + const onToggleDataFrame = useCallback( + (dataFrame: DataFrame) => { + if (!dataFrame.refId) { + return; + } + + if (options.fields) { + if (dataFrame.refId in options.fields) { + if (Object.keys(options.fields).length === 1) { + return; + } + + delete options.fields[dataFrame.refId]; + } else { + options.fields[dataFrame.refId] = dataFrame.fields[0].name; + } + } + + onChange({ ...options }); + }, + [onChange, options] + ); const onSelectField = useCallback( - (value: SelectableValue) => { - onChange({ - ...options, - byField: value?.value, - }); + (queryRefId: string | undefined, fieldName: SelectableValue) => { + if (queryRefId && fieldName.value) { + onChange({ + ...options, + fields: { ...options.fields, [queryRefId]: fieldName.value }, + }); + } }, [onChange, options] ); @@ -34,7 +63,7 @@ export function SeriesToFieldsTransformerEditor({ input, options, onChange }: Tr (value: SelectableValue) => { onChange({ ...options, - mode: value?.value, + mode: value?.value || JoinMode.outer, }); }, [onChange, options] @@ -44,20 +73,31 @@ export function SeriesToFieldsTransformerEditor({ input, options, onChange }: Tr <> - + ({ label: field.name, value: field.name }))} + value={dataFrame.refId ? (options.fields || {})[dataFrame.refId] : dataFrame.fields[0].name} + onChange={(fieldName) => onSelectField(dataFrame.refId, fieldName)} + /> + +
+
+ ))} ); } From a20d1eec325f41d68932f43b404084c5a46dc36e Mon Sep 17 00:00:00 2001 From: Virginia Cepeda Date: Thu, 26 Jan 2023 09:27:08 -0300 Subject: [PATCH 075/172] Alerting: validate alert condition on saving rule (#61958) * Validate alert condition on saving rule * Remove unused const --- .../components/rule-editor/AlertRuleForm.tsx | 13 +++++- .../QueryAndExpressionsStep.tsx | 45 ++++++++++++++----- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx index e250922e515..83b9c87c8bf 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx @@ -122,7 +122,18 @@ export const AlertRuleForm: FC = ({ existing, prefill }) => { const submitState = useUnifiedAlertingSelector((state) => state.ruleForm.saveRule) || initialAsyncRequestState; useCleanup((state) => (state.unifiedAlerting.ruleForm.saveRule = initialAsyncRequestState)); + const [conditionErrorMsg, setConditionErrorMsg] = useState(''); + + const checkAlertCondition = (msg = '') => { + setConditionErrorMsg(msg); + }; + const submit = (values: RuleFormValues, exitOnSave: boolean) => { + if (conditionErrorMsg !== '') { + notifyApp.error(conditionErrorMsg); + return; + } + dispatch( saveRuleFormAction({ values: { @@ -236,7 +247,7 @@ export const AlertRuleForm: FC = ({ existing, prefill }) => {
- + {showStep2 && ( <> {type === RuleFormType.grafana ? ( diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx index dc7ea4c1377..4b426163f84 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx @@ -16,7 +16,7 @@ import { ExpressionEditor } from '../ExpressionEditor'; import { ExpressionsEditor } from '../ExpressionsEditor'; import { QueryEditor } from '../QueryEditor'; import { RuleEditorSection } from '../RuleEditorSection'; -import { refIdExists } from '../util'; +import { errorFromSeries, refIdExists } from '../util'; import { AlertType } from './AlertType'; import { @@ -35,9 +35,10 @@ import { interface Props { editingExistingRule: boolean; + onDataChange: (error: string) => void; } -export const QueryAndExpressionsStep: FC = ({ editingExistingRule }) => { +export const QueryAndExpressionsStep: FC = ({ editingExistingRule, onDataChange }) => { const runner = useRef(new AlertingQueryRunner()); const { setValue, @@ -104,6 +105,30 @@ export const QueryAndExpressionsStep: FC = ({ editingExistingRule }) => { const emptyQueries = queries.length === 0; + useEffect(() => { + const currentCondition = getValues('condition'); + + if (!currentCondition) { + return; + } + + const error = errorFromSeries(panelData[currentCondition]?.series || []); + onDataChange(error?.message || ''); + }, [panelData, getValues, onDataChange]); + + const handleSetCondition = useCallback( + (refId: string | null) => { + if (!refId) { + return; + } + + runQueries(); //we need to run the queries to know if the condition is valid + + setValue('condition', refId); + }, + [runQueries, setValue] + ); + const onUpdateRefId = useCallback( (oldRefId: string, newRefId: string) => { const newRefIdExists = refIdExists(queries, newRefId); @@ -116,10 +141,10 @@ export const QueryAndExpressionsStep: FC = ({ editingExistingRule }) => { // update condition too if refId was updated if (condition === oldRefId) { - setValue('condition', newRefId); + handleSetCondition(newRefId); } }, - [condition, queries, setValue] + [condition, queries, handleSetCondition] ); const onChangeQueries = useCallback( @@ -147,9 +172,9 @@ export const QueryAndExpressionsStep: FC = ({ editingExistingRule }) => { useEffect(() => { if (!refIdExists(queries, condition)) { const lastRefId = queries.at(-1)?.refId ?? null; - setValue('condition', lastRefId); + handleSetCondition(lastRefId); } - }, [condition, queries, setValue]); + }, [condition, queries, handleSetCondition]); return ( @@ -189,18 +214,14 @@ export const QueryAndExpressionsStep: FC = ({ editingExistingRule }) => { onDuplicateQuery={onDuplicateQuery} panelData={panelData} condition={condition} - onSetCondition={(refId) => { - setValue('condition', refId); - }} + onSetCondition={handleSetCondition} /> {/* Expression Queries */} { - setValue('condition', refId); - }} + onSetCondition={handleSetCondition} onRemoveExpression={(refId) => { dispatch(removeExpression(refId)); }} From 94dca85b30e67bf981dcad74bbdd7f884eb1c521 Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Thu, 26 Jan 2023 13:35:08 +0100 Subject: [PATCH 076/172] Auth: Fix error check (#62192) --- pkg/models/context.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/models/context.go b/pkg/models/context.go index c6099f423cc..97763e39b7e 100644 --- a/pkg/models/context.go +++ b/pkg/models/context.go @@ -110,8 +110,8 @@ func (ctx *ReqContext) writeErrOrFallback(status int, message string, err error) var logMessage string logger := ctx.Logger.Warn - var gfErr *errutil.Error - if errors.As(err, gfErr) { + gfErr := errutil.Error{} + if errors.As(err, &gfErr) { logger = gfErr.LogLevel.LogFunc(ctx.Logger) publicErr := gfErr.Public() From e8dd01df35aac7f4fb2e69bcaca946fd0c4b4aec Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Thu, 26 Jan 2023 13:44:14 +0100 Subject: [PATCH 077/172] Alerting: Alert rules search improvements (#61398) Co-authored-by: Gilles De Mey --- .betterer.results | 3 +- .../alerting/unified/RuleList.test.tsx | 10 +- .../features/alerting/unified/RuleList.tsx | 13 +- .../alerting/unified/components/Tokenize.tsx | 17 +- .../unified/components/rules/RulesFilter.tsx | 258 +++++++++++------- .../unified/hooks/useFilteredRules.test.ts | 163 +++++++++++ .../unified/hooks/useFilteredRules.ts | 182 +++++++++--- public/app/features/alerting/unified/mocks.ts | 35 ++- .../alerting/unified/search/README.md | 27 ++ .../unified/search/rulesSearchParser.test.ts | 183 +++++++++++++ .../unified/search/rulesSearchParser.ts | 100 +++++++ .../alerting/unified/search/search.grammar | 54 ++++ .../alerting/unified/search/search.js | 30 ++ .../alerting/unified/search/search.terms.js | 21 ++ .../alerting/unified/search/searchParser.ts | 144 ++++++++++ .../features/alerting/unified/utils/rules.ts | 21 ++ .../features/alerting/unified/utils/search.ts | 9 + public/app/types/unified-alerting-dto.ts | 4 + 18 files changed, 1108 insertions(+), 166 deletions(-) create mode 100644 public/app/features/alerting/unified/hooks/useFilteredRules.test.ts create mode 100644 public/app/features/alerting/unified/search/README.md create mode 100644 public/app/features/alerting/unified/search/rulesSearchParser.test.ts create mode 100644 public/app/features/alerting/unified/search/rulesSearchParser.ts create mode 100644 public/app/features/alerting/unified/search/search.grammar create mode 100644 public/app/features/alerting/unified/search/search.js create mode 100644 public/app/features/alerting/unified/search/search.terms.js create mode 100644 public/app/features/alerting/unified/search/searchParser.ts create mode 100644 public/app/features/alerting/unified/utils/search.ts diff --git a/.betterer.results b/.betterer.results index 84e8e04d72f..e0b69715f9d 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3000,8 +3000,7 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/features/alerting/unified/components/rules/RulesFilter.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"] + [0, 0, 0, "Do not use any type assertions.", "0"] ], "public/app/features/alerting/unified/components/silences/SilencesEditor.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] diff --git a/public/app/features/alerting/unified/RuleList.test.tsx b/public/app/features/alerting/unified/RuleList.test.tsx index 534aa43628f..44a8508b672 100644 --- a/public/app/features/alerting/unified/RuleList.test.tsx +++ b/public/app/features/alerting/unified/RuleList.test.tsx @@ -498,10 +498,10 @@ describe('RuleList', () => { expect(groups).toHaveLength(2); const filterInput = ui.rulesFilterInput.get(); - await userEvent.type(filterInput, '{{foo="bar"}'); + await userEvent.type(filterInput, 'label:foo=bar'); // Input is debounced so wait for it to be visible - await waitFor(() => expect(filterInput).toHaveValue('{foo="bar"}')); + await waitFor(() => expect(filterInput).toHaveValue('label:foo=bar')); // Group doesn't contain matching labels await waitFor(() => expect(ui.ruleGroup.queryAll()).toHaveLength(1)); @@ -517,17 +517,17 @@ describe('RuleList', () => { // Check for different label matchers await userEvent.clear(filterInput); - await userEvent.type(filterInput, '{{foo!="bar",foo!="baz"}'); + await userEvent.type(filterInput, 'label:foo!=bar label:foo!=baz'); // Group doesn't contain matching labels await waitFor(() => expect(ui.ruleGroup.queryAll()).toHaveLength(1)); await waitFor(() => expect(ui.ruleGroup.get()).toHaveTextContent('group-2')); await userEvent.clear(filterInput); - await userEvent.type(filterInput, '{{foo=~"b.+"}'); + await userEvent.type(filterInput, 'label:"foo=~b.+"'); await waitFor(() => expect(ui.ruleGroup.queryAll()).toHaveLength(2)); await userEvent.clear(filterInput); - await userEvent.type(filterInput, '{{region="US"}'); + await userEvent.type(filterInput, 'label:region=US'); await waitFor(() => expect(ui.ruleGroup.queryAll()).toHaveLength(1)); await waitFor(() => expect(ui.ruleGroup.get()).toHaveTextContent('group-2')); }); diff --git a/public/app/features/alerting/unified/RuleList.tsx b/public/app/features/alerting/unified/RuleList.tsx index a46ddfc9523..c819a0b81bc 100644 --- a/public/app/features/alerting/unified/RuleList.tsx +++ b/public/app/features/alerting/unified/RuleList.tsx @@ -20,13 +20,12 @@ import { RuleListStateView } from './components/rules/RuleListStateView'; import { RuleStats } from './components/rules/RuleStats'; import RulesFilter from './components/rules/RulesFilter'; import { useCombinedRuleNamespaces } from './hooks/useCombinedRuleNamespaces'; -import { useFilteredRules } from './hooks/useFilteredRules'; +import { useFilteredRules, useRulesFilter } from './hooks/useFilteredRules'; import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector'; import { fetchAllPromAndRulerRulesAction } from './state/actions'; import { useRulesAccess } from './utils/accessControlHooks'; import { RULE_LIST_POLL_INTERVAL_MS } from './utils/constants'; import { getAllRulesSourceNames } from './utils/datasource'; -import { getFiltersFromUrlParams } from './utils/misc'; const VIEWS = { groups: RuleListGroupView, @@ -42,8 +41,7 @@ const RuleList = withErrorBoundary( const [expandAll, setExpandAll] = useState(false); const [queryParams] = useQueryParams(); - const filters = getFiltersFromUrlParams(queryParams); - const filtersActive = Object.values(filters).some((filter) => filter !== undefined); + const { filterState, hasActiveFilters } = useRulesFilter(); const { canCreateGrafanaRules, canCreateCloudRules } = useRulesAccess(); @@ -83,19 +81,20 @@ const RuleList = withErrorBoundary( const hasNoAlertRulesCreatedYet = allPromLoaded && allPromEmpty && promRequests.length > 0; const combinedNamespaces: CombinedRuleNamespace[] = useCombinedRuleNamespaces(); - const filteredNamespaces = useFilteredRules(combinedNamespaces); + const filteredNamespaces = useFilteredRules(combinedNamespaces, filterState); + return ( // We don't want to show the Loading... indicator for the whole page. // We show separate indicators for Grafana-managed and Cloud rules - + setExpandAll(false)} /> {!hasNoAlertRulesCreatedYet && ( <>
- {view === 'groups' && filtersActive && ( + {view === 'groups' && hasActiveFilters && ( -
- )} -
+ + +
+ + +
+ + {hasActiveFilters && ( +
+ +
+ )} + +
); }; @@ -181,32 +194,65 @@ const RulesFilter = () => { const getStyles = (theme: GrafanaTheme2) => { return { container: css` - display: flex; - flex-direction: column; - padding-bottom: ${theme.spacing(1)}; margin-bottom: ${theme.spacing(1)}; `, - inputWidth: css` - width: 340px; + dsPickerContainer: css` + width: 250px; flex-grow: 0; + margin: 0; `, - flexRow: css` - display: flex; - flex-direction: row; - align-items: flex-end; - width: 100%; - flex-wrap: wrap; - `, - spaceBetween: css` - justify-content: space-between; - `, - rowChild: css` - margin: ${theme.spacing(0, 1, 0, 0)}; - `, - clearButton: css` - margin-top: ${theme.spacing(1)}; + searchInput: css` + flex: 1; + margin: 0; `, }; }; +function SearchQueryHelp() { + const styles = useStyles2(helpStyles); + + return ( +
+
Search syntax allows to query alert rules by the parameters defined below.
+
+
+
Filter type
+
Expression
+ + + + + + + + +
+
+ ); +} + +function HelpRow({ title, expr }: { title: string; expr: string }) { + const styles = useStyles2(helpStyles); + + return ( + <> +
{title}
+ {expr} + + ); +} + +const helpStyles = (theme: GrafanaTheme2) => ({ + grid: css` + display: grid; + grid-template-columns: max-content auto; + gap: ${theme.spacing(1)}; + align-items: center; + `, + code: css` + display: block; + text-align: center; + `, +}); + export default RulesFilter; diff --git a/public/app/features/alerting/unified/hooks/useFilteredRules.test.ts b/public/app/features/alerting/unified/hooks/useFilteredRules.test.ts new file mode 100644 index 00000000000..a809e148a61 --- /dev/null +++ b/public/app/features/alerting/unified/hooks/useFilteredRules.test.ts @@ -0,0 +1,163 @@ +import { setDataSourceSrv } from '@grafana/runtime'; + +import { PromAlertingRuleState } from '../../../../types/unified-alerting-dto'; +import { + mockAlertQuery, + mockCombinedRule, + mockCombinedRuleGroup, + mockCombinedRuleNamespace, + mockDataSource, + MockDataSourceSrv, + mockPromAlert, + mockPromAlertingRule, + mockRulerGrafanaRule, +} from '../mocks'; +import { RuleHealth } from '../search/rulesSearchParser'; +import { getFilter } from '../utils/search'; + +import { filterRules } from './useFilteredRules'; + +const dataSources = { + prometheus: mockDataSource({ uid: 'prom-1', name: 'prometheus' }), + loki: mockDataSource({ uid: 'loki-1', name: 'loki' }), +}; +beforeAll(() => { + setDataSourceSrv(new MockDataSourceSrv(dataSources)); +}); + +describe('filterRules', function () { + it('should filter out rules by name filter', function () { + const rules = [mockCombinedRule({ name: 'High CPU usage' }), mockCombinedRule({ name: 'Memory too low' })]; + + const ns = mockCombinedRuleNamespace({ + groups: [mockCombinedRuleGroup('Resources usage group', rules)], + }); + + const filtered = filterRules([ns], getFilter({ ruleName: 'cpu' })); + + expect(filtered[0].groups[0].rules).toHaveLength(1); + expect(filtered[0].groups[0].rules[0].name).toBe('High CPU usage'); + }); + + it('should filter out rules by evaluation group name', function () { + const ns = mockCombinedRuleNamespace({ + groups: [ + mockCombinedRuleGroup('Performance group', [mockCombinedRule({ name: 'High CPU usage' })]), + mockCombinedRuleGroup('Availability group', [mockCombinedRule({ name: 'Memory too low' })]), + ], + }); + + const filtered = filterRules([ns], getFilter({ groupName: 'availability' })); + + expect(filtered[0].groups).toHaveLength(1); + expect(filtered[0].groups[0].rules[0].name).toBe('Memory too low'); + }); + + it('should filter out rules by label filter', function () { + const rules = [ + mockCombinedRule({ name: 'High CPU usage', labels: { severity: 'warning' } }), + mockCombinedRule({ name: 'Memory too low', labels: { severity: 'critical' } }), + ]; + + const ns = mockCombinedRuleNamespace({ + groups: [mockCombinedRuleGroup('Resources usage group', rules)], + }); + + const filtered = filterRules([ns], getFilter({ labels: ['severity=critical'] })); + + expect(filtered[0].groups[0].rules).toHaveLength(1); + expect(filtered[0].groups[0].rules[0].name).toBe('Memory too low'); + }); + + it('should filter out rules by alert instance labels', function () { + const rules = [ + mockCombinedRule({ + name: 'High CPU usage', + promRule: mockPromAlertingRule({ alerts: [mockPromAlert({ labels: { severity: 'warning' } })] }), + }), + mockCombinedRule({ + name: 'Memory too low', + promRule: mockPromAlertingRule({ labels: { severity: 'critical' }, alerts: [] }), + }), + ]; + + const ns = mockCombinedRuleNamespace({ + groups: [mockCombinedRuleGroup('Resources usage group', rules)], + }); + + const filtered = filterRules([ns], getFilter({ labels: ['severity=warning'] })); + + expect(filtered[0].groups[0].rules).toHaveLength(1); + expect(filtered[0].groups[0].rules[0].name).toBe('High CPU usage'); + }); + + it('should filter out rules by state filter', function () { + const rules = [ + mockCombinedRule({ + name: 'High CPU usage', + promRule: mockPromAlertingRule({ state: PromAlertingRuleState.Inactive }), + }), + mockCombinedRule({ + name: 'Memory too low', + promRule: mockPromAlertingRule({ state: PromAlertingRuleState.Firing }), + }), + ]; + + const ns = mockCombinedRuleNamespace({ + groups: [mockCombinedRuleGroup('Resources usage group', rules)], + }); + + const filtered = filterRules([ns], getFilter({ ruleState: PromAlertingRuleState.Firing })); + + expect(filtered[0].groups[0].rules).toHaveLength(1); + expect(filtered[0].groups[0].rules[0].name).toBe('Memory too low'); + }); + + it('should filter out rules by health filter', function () { + const rules = [ + mockCombinedRule({ + name: 'High CPU usage', + promRule: mockPromAlertingRule({ health: RuleHealth.Ok }), + }), + mockCombinedRule({ + name: 'Memory too low', + promRule: mockPromAlertingRule({ health: RuleHealth.Error }), + }), + ]; + + const ns = mockCombinedRuleNamespace({ + groups: [mockCombinedRuleGroup('Resources usage group', rules)], + }); + + const filtered = filterRules([ns], getFilter({ ruleHealth: RuleHealth.Error })); + + expect(filtered[0].groups[0].rules).toHaveLength(1); + expect(filtered[0].groups[0].rules[0].name).toBe('Memory too low'); + }); + + it('should filter out rules by datasource', function () { + const rules = [ + mockCombinedRule({ + name: 'High CPU usage', + rulerRule: mockRulerGrafanaRule(undefined, { + data: [mockAlertQuery({ datasourceUid: dataSources.prometheus.uid })], + }), + }), + mockCombinedRule({ + name: 'Memory too low', + rulerRule: mockRulerGrafanaRule(undefined, { + data: [mockAlertQuery({ datasourceUid: dataSources.loki.uid })], + }), + }), + ]; + + const ns = mockCombinedRuleNamespace({ + groups: [mockCombinedRuleGroup('Resources usage group', rules)], + }); + + const filtered = filterRules([ns], getFilter({ dataSourceName: 'loki' })); + + expect(filtered[0].groups[0].rules).toHaveLength(1); + expect(filtered[0].groups[0].rules[0].name).toBe('Memory too low'); + }); +}); diff --git a/public/app/features/alerting/unified/hooks/useFilteredRules.ts b/public/app/features/alerting/unified/hooks/useFilteredRules.ts index 20cf18caf17..061c90a7ead 100644 --- a/public/app/features/alerting/unified/hooks/useFilteredRules.ts +++ b/public/app/features/alerting/unified/hooks/useFilteredRules.ts @@ -1,35 +1,107 @@ -import { useMemo } from 'react'; +import produce from 'immer'; +import { compact, isEmpty } from 'lodash'; +import { useCallback, useEffect, useMemo } from 'react'; import { getDataSourceSrv } from '@grafana/runtime'; -import { useQueryParams } from 'app/core/hooks/useQueryParams'; -import { CombinedRuleGroup, CombinedRuleNamespace, FilterState } from 'app/types/unified-alerting'; -import { PromRuleType, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto'; +import { Matcher } from 'app/plugins/datasource/alertmanager/types'; +import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting'; +import { isPromAlertingRuleState, PromRuleType, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto'; -import { labelsMatchMatchers, parseMatchers } from '../utils/alertmanager'; +import { getSearchFilterFromQuery, RulesFilter, applySearchFilterToQuery } from '../search/rulesSearchParser'; +import { labelsMatchMatchers, matcherToMatcherField, parseMatcher, parseMatchers } from '../utils/alertmanager'; import { isCloudRulesSource } from '../utils/datasource'; -import { getFiltersFromUrlParams } from '../utils/misc'; -import { isAlertingRule, isGrafanaRulerRule } from '../utils/rules'; +import { getRuleHealth, isAlertingRule, isGrafanaRulerRule, isPromRuleType } from '../utils/rules'; -export const useFilteredRules = (namespaces: CombinedRuleNamespace[]) => { - const [queryParams] = useQueryParams(); - const filters = getFiltersFromUrlParams(queryParams); +import { useURLSearchParams } from './useURLSearchParams'; - return useMemo(() => { - const filteredNamespaces = namespaces - // Filter by data source - // TODO: filter by multiple data sources for grafana-managed alerts - .filter(({ rulesSource }) => - filters.dataSource && isCloudRulesSource(rulesSource) ? rulesSource.name === filters.dataSource : true - ) - // If a namespace and group have rules that match the rules filters then keep them. - .reduce(reduceNamespaces(filters), [] as CombinedRuleNamespace[]); - return filteredNamespaces; - }, [namespaces, filters]); +export function useRulesFilter() { + const [queryParams, updateQueryParams] = useURLSearchParams(); + const searchQuery = queryParams.get('search') ?? ''; + + const filterState = getSearchFilterFromQuery(searchQuery); + const hasActiveFilters = Object.values(filterState).some((filter) => !isEmpty(filter)); + + const updateFilters = useCallback( + (newFilter: RulesFilter) => { + const newSearchQuery = applySearchFilterToQuery(searchQuery, newFilter); + updateQueryParams({ search: newSearchQuery }); + }, + [searchQuery, updateQueryParams] + ); + + const setSearchQuery = useCallback( + (newSearchQuery: string | undefined) => { + updateQueryParams({ search: newSearchQuery }); + }, + [updateQueryParams] + ); + + // Handle legacy filters + useEffect(() => { + const legacyFilters = { + dataSource: queryParams.get('dataSource') ?? undefined, + alertState: queryParams.get('alertState') ?? undefined, + ruleType: queryParams.get('ruleType') ?? undefined, + labels: parseMatchers(queryParams.get('queryString') ?? '').map(matcherToMatcherField), + }; + + const hasLegacyFilters = Object.values(legacyFilters).some((legacyFilter) => !isEmpty(legacyFilter)); + if (hasLegacyFilters) { + updateQueryParams({ dataSource: undefined, alertState: undefined, ruleType: undefined, queryString: undefined }); + // Existing query filters takes precedence over legacy ones + updateFilters( + produce(filterState, (draft) => { + draft.dataSourceName ??= legacyFilters.dataSource; + if (legacyFilters.alertState && isPromAlertingRuleState(legacyFilters.alertState)) { + draft.ruleState ??= legacyFilters.alertState; + } + if (legacyFilters.ruleType && isPromRuleType(legacyFilters.ruleType)) { + draft.ruleType ??= legacyFilters.ruleType; + } + if (draft.labels.length === 0 && legacyFilters.labels.length > 0) { + const legacyLabelsAsStrings = legacyFilters.labels.map( + ({ name, operator, value }) => `${name}${operator}${value}` + ); + draft.labels.push(...legacyLabelsAsStrings); + } + }) + ); + } + }, [queryParams, updateFilters, filterState, updateQueryParams]); + + return { filterState, hasActiveFilters, searchQuery, setSearchQuery, updateFilters }; +} + +export const useFilteredRules = (namespaces: CombinedRuleNamespace[], filterState: RulesFilter) => { + return useMemo(() => filterRules(namespaces, filterState), [namespaces, filterState]); }; -const reduceNamespaces = (filters: FilterState) => { +export const filterRules = ( + namespaces: CombinedRuleNamespace[], + filterState: RulesFilter = { labels: [], freeFormWords: [] } +): CombinedRuleNamespace[] => { + return ( + namespaces + .filter((ns) => + filterState.namespace ? ns.name.toLowerCase().includes(filterState.namespace.toLowerCase()) : true + ) + .filter(({ rulesSource }) => + filterState.dataSourceName && isCloudRulesSource(rulesSource) + ? rulesSource.name === filterState.dataSourceName + : true + ) + // If a namespace and group have rules that match the rules filters then keep them. + .reduce(reduceNamespaces(filterState), [] as CombinedRuleNamespace[]) + ); +}; + +const reduceNamespaces = (filterStateFilters: RulesFilter) => { return (namespaceAcc: CombinedRuleNamespace[], namespace: CombinedRuleNamespace) => { - const groups = namespace.groups.reduce(reduceGroups(filters), [] as CombinedRuleGroup[]); + const groups = namespace.groups + .filter((g) => + filterStateFilters.groupName ? g.name.toLowerCase().includes(filterStateFilters.groupName.toLowerCase()) : true + ) + .reduce(reduceGroups(filterStateFilters), [] as CombinedRuleGroup[]); if (groups.length) { namespaceAcc.push({ @@ -43,35 +115,56 @@ const reduceNamespaces = (filters: FilterState) => { }; // Reduces groups to only groups that have rules matching the filters -const reduceGroups = (filters: FilterState) => { +const reduceGroups = (filterState: RulesFilter) => { return (groupAcc: CombinedRuleGroup[], group: CombinedRuleGroup) => { const rules = group.rules.filter((rule) => { - if (filters.ruleType && filters.ruleType !== rule.promRule?.type) { + if (filterState.ruleType && filterState.ruleType !== rule.promRule?.type) { return false; } - if (filters.dataSource && isGrafanaRulerRule(rule.rulerRule) && !isQueryingDataSource(rule.rulerRule, filters)) { - return false; - } - // Query strings can match alert name, label keys, and label values - if (filters.queryString) { - const normalizedQueryString = filters.queryString.toLocaleLowerCase(); - const doesNameContainsQueryString = rule.name?.toLocaleLowerCase().includes(normalizedQueryString); - const matchers = parseMatchers(filters.queryString); - const doRuleLabelsMatchQuery = labelsMatchMatchers(rule.labels, matchers); + const doesNotQueryDs = isGrafanaRulerRule(rule.rulerRule) && !isQueryingDataSource(rule.rulerRule, filterState); + if (filterState.dataSourceName && doesNotQueryDs) { + return false; + } + + const ruleNameLc = rule.name?.toLocaleLowerCase(); + // Free Form Query is used to filter by rule name + if ( + filterState.freeFormWords.length > 0 && + !filterState.freeFormWords.every((w) => ruleNameLc.includes(w.toLocaleLowerCase())) + ) { + return false; + } + + if (filterState.ruleName && !rule.name?.toLocaleLowerCase().includes(filterState.ruleName.toLocaleLowerCase())) { + return false; + } + + if (filterState.ruleHealth && rule.promRule) { + const ruleHealth = getRuleHealth(rule.promRule.health); + return filterState.ruleHealth === ruleHealth; + } + + // Query strings can match alert name, label keys, and label values + if (filterState.labels.length > 0) { + // const matchers = parseMatchers(filters.queryString); + const matchers = compact(filterState.labels.map(looseParseMatcher)); + + const doRuleLabelsMatchQuery = matchers.length > 0 && labelsMatchMatchers(rule.labels, matchers); const doAlertsContainMatchingLabels = + matchers.length > 0 && rule.promRule && rule.promRule.type === PromRuleType.Alerting && rule.promRule.alerts && rule.promRule.alerts.some((alert) => labelsMatchMatchers(alert.labels, matchers)); - if (!(doesNameContainsQueryString || doRuleLabelsMatchQuery || doAlertsContainMatchingLabels)) { + if (!(doRuleLabelsMatchQuery || doAlertsContainMatchingLabels)) { return false; } } if ( - filters.alertState && - !(rule.promRule && isAlertingRule(rule.promRule) && rule.promRule.state === filters.alertState) + filterState.ruleState && + !(rule.promRule && isAlertingRule(rule.promRule) && rule.promRule.state === filterState.ruleState) ) { return false; } @@ -88,8 +181,17 @@ const reduceGroups = (filters: FilterState) => { }; }; -const isQueryingDataSource = (rulerRule: RulerGrafanaRuleDTO, filter: FilterState): boolean => { - if (!filter.dataSource) { +function looseParseMatcher(matcherQuery: string): Matcher | undefined { + try { + return parseMatcher(matcherQuery); + } catch { + // Try to createa a matcher than matches all values for a given key + return { name: matcherQuery, value: '', isRegex: true, isEqual: true }; + } +} + +const isQueryingDataSource = (rulerRule: RulerGrafanaRuleDTO, filterState: RulesFilter): boolean => { + if (!filterState.dataSourceName) { return true; } @@ -98,6 +200,6 @@ const isQueryingDataSource = (rulerRule: RulerGrafanaRuleDTO, filter: FilterStat return false; } const ds = getDataSourceSrv().getInstanceSettings(query.datasourceUid); - return ds?.name === filter.dataSource; + return ds?.name === filterState.dataSourceName; }); }; diff --git a/public/app/features/alerting/unified/mocks.ts b/public/app/features/alerting/unified/mocks.ts index e0f524eb91e..2148209f3f7 100644 --- a/public/app/features/alerting/unified/mocks.ts +++ b/public/app/features/alerting/unified/mocks.ts @@ -23,8 +23,18 @@ import { } from 'app/plugins/datasource/alertmanager/types'; import { configureStore } from 'app/store/configureStore'; import { AccessControlAction, FolderDTO, StoreState } from 'app/types'; -import { Alert, AlertingRule, CombinedRule, RecordingRule, RuleGroup, RuleNamespace } from 'app/types/unified-alerting'; import { + Alert, + AlertingRule, + CombinedRule, + CombinedRuleGroup, + CombinedRuleNamespace, + RecordingRule, + RuleGroup, + RuleNamespace, +} from 'app/types/unified-alerting'; +import { + AlertQuery, GrafanaAlertStateDecision, GrafanaRuleDefinition, PromAlertingRuleState, @@ -517,6 +527,29 @@ export function mockStore(recipe: (state: StoreState) => void) { return configureStore(produce(defaultState, recipe)); } +export function mockAlertQuery(query: Partial): AlertQuery { + return { + datasourceUid: '--uid--', + refId: 'A', + queryType: '', + model: { refId: 'A' }, + ...query, + }; +} + +export function mockCombinedRuleGroup(name: string, rules: CombinedRule[]): CombinedRuleGroup { + return { name, rules }; +} + +export function mockCombinedRuleNamespace(namespace: Partial): CombinedRuleNamespace { + return { + name: 'Grafana', + groups: [], + rulesSource: 'grafana', + ...namespace, + }; +} + export function getGrafanaRule(override?: Partial) { return mockCombinedRule({ namespace: { diff --git a/public/app/features/alerting/unified/search/README.md b/public/app/features/alerting/unified/search/README.md new file mode 100644 index 00000000000..c550edffe6c --- /dev/null +++ b/public/app/features/alerting/unified/search/README.md @@ -0,0 +1,27 @@ +# Alerting search syntax + +## Lezer grammar + +Alerting uses the [Lezer](https://lezer.codemirror.net/) parser system to create a search syntax grammar. + +File [search.grammar](search.grammar) describes the search grammar. + +`@lezer/generator` package is used to generate [search.js](search.js) and [search.terms.js](search.terms.js) files which include a JS grammar parser. + +## Changing the grammar + +After making changes in the `search.grammar` file, a new version of the parser needs to be generated. +To do that, the following command needs to be run in the `public/app/features/alerting/unified/search` directory + +```sh +yarn dlx @lezer/generator search.grammar -o search.js +``` + +The command will re-create [search.js](search.js) and [search.terms.js](search.terms.js) files which are the files containing grammar parser. + +## Extensibility + +The `search.grammar` uses the [dialects feature](https://lezer.codemirror.net/docs/guide/#dialects) of Lezer to enable parsing of each filter term separately. + +This will allow us to have a single grammar file for handling filter expressions for all of our filters (e.g. Rules, Silences, Notification policies). +Then we can configure the required set of filters dynamically in the JS code using the parser. diff --git a/public/app/features/alerting/unified/search/rulesSearchParser.test.ts b/public/app/features/alerting/unified/search/rulesSearchParser.test.ts new file mode 100644 index 00000000000..4520655095c --- /dev/null +++ b/public/app/features/alerting/unified/search/rulesSearchParser.test.ts @@ -0,0 +1,183 @@ +import { PromAlertingRuleState, PromRuleType } from '../../../../types/unified-alerting-dto'; +import { getFilter } from '../utils/search'; + +import { applySearchFilterToQuery, getSearchFilterFromQuery, RuleHealth } from './rulesSearchParser'; + +describe('Alert rules searchParser', () => { + describe('getSearchFilterFromQuery', () => { + it.each(['datasource:prometheus'])('should parse data source filter from "%s" query', (query) => { + const filter = getSearchFilterFromQuery(query); + expect(filter.dataSourceName).toBe('prometheus'); + }); + + it.each(['namespace:integrations-node'])('should parse namespace filter from "%s" query', (query) => { + const filter = getSearchFilterFromQuery(query); + expect(filter.namespace).toBe('integrations-node'); + }); + + it.each(['label:team label:region=emea'])('should parse label filter from "%s" query', (query) => { + const filter = getSearchFilterFromQuery(query); + expect(filter.labels).toHaveLength(2); + expect(filter.labels).toContain('team'); + expect(filter.labels).toContain('region=emea'); + }); + + it.each(['group:cpu-utilization'])('should parse group filter from "%s" query', (query) => { + const filter = getSearchFilterFromQuery(query); + expect(filter.groupName).toBe('cpu-utilization'); + }); + + it.each(['rule:cpu-80%-alert'])('should parse rule name filter from "%s" query', (query) => { + const filter = getSearchFilterFromQuery(query); + expect(filter.ruleName).toBe('cpu-80%-alert'); + }); + + it.each([ + { query: 'state:firing', expectedFilter: PromAlertingRuleState.Firing }, + { query: 'state:inactive', expectedFilter: PromAlertingRuleState.Inactive }, + { query: 'state:pending', expectedFilter: PromAlertingRuleState.Pending }, + ])('should parse $expectedFilter rule state filter from "$query" query', ({ query, expectedFilter }) => { + const filter = getSearchFilterFromQuery(query); + expect(filter.ruleState).toBe(expectedFilter); + }); + + it.each([ + { query: 'type:alerting', expectedFilter: PromRuleType.Alerting }, + { query: 'type:recording', expectedFilter: PromRuleType.Recording }, + ])('should parse $expectedFilter rule type filter from "$query" input', ({ query, expectedFilter }) => { + const filter = getSearchFilterFromQuery(query); + expect(filter.ruleType).toBe(expectedFilter); + }); + + it.each([ + { query: 'health:ok', expectedFilter: RuleHealth.Ok }, + { query: 'health:nodata', expectedFilter: RuleHealth.NoData }, + { query: 'health:error', expectedFilter: RuleHealth.Error }, + ])('should parse RuleHealth $expectedFilter filter from "$query" query', ({ query, expectedFilter }) => { + const filter = getSearchFilterFromQuery(query); + expect(filter.ruleHealth).toBe(expectedFilter); + }); + + it('should parse non-filtering words as free form query', () => { + const filter = getSearchFilterFromQuery('cpu usage rule'); + expect(filter.freeFormWords).toHaveLength(3); + expect(filter.freeFormWords).toContain('cpu'); + expect(filter.freeFormWords).toContain('usage'); + expect(filter.freeFormWords).toContain('rule'); + }); + + it('should parse free words with quotes', () => { + const query = '"hello world" hello world'; + const filter = getSearchFilterFromQuery(query); + + expect(filter.freeFormWords).toEqual(['hello world', 'hello', 'world']); + }); + + it('should parse filter values with whitespaces when in quotes', () => { + const query = + 'datasource:"prom dev" namespace:"node one" label:"team=frontend us" group:"cpu alerts" rule:"cpu failure"'; + const filter = getSearchFilterFromQuery(query); + + expect(filter.dataSourceName).toBe('prom dev'); + expect(filter.namespace).toBe('node one'); + expect(filter.labels).toContain('team=frontend us'); + expect(filter.groupName).toContain('cpu alerts'); + expect(filter.ruleName).toContain('cpu failure'); + }); + + it('should parse filter values with special characters', () => { + const query = + 'datasource:prom::dev/linux>>; namespace:"[{node}] (#20+)" label:_region=apac|emea\\nasa group:$20.00%$ rule:"cpu!! & memory.,?"'; + const filter = getSearchFilterFromQuery(query); + + expect(filter.dataSourceName).toBe('prom::dev/linux>>;'); + expect(filter.namespace).toBe('[{node}] (#20+)'); + expect(filter.labels).toContain('_region=apac|emea\\nasa'); + expect(filter.groupName).toContain('$20.00%$'); + expect(filter.ruleName).toContain('cpu!! & memory.,?'); + }); + + it('should parse non-filter terms with colon as free form words', () => { + const query = 'cpu:high-utilization memory:overload'; + const filter = getSearchFilterFromQuery(query); + + expect(filter.freeFormWords).toContain('cpu:high-utilization'); + expect(filter.freeFormWords).toContain('memory:overload'); + }); + + it('should parse mixed free form words and filters', () => { + const query = 'datasource:prometheus utilization label:team cpu'; + const filter = getSearchFilterFromQuery(query); + + expect(filter.dataSourceName).toBe('prometheus'); + expect(filter.labels).toContain('team'); + expect(filter.freeFormWords).toContain('utilization'); + expect(filter.freeFormWords).toContain('cpu'); + }); + + it('should parse labels containing matchers', () => { + const query = 'label:region!=US label:"team=~fe.*devs" label:cluster!~ba.+'; + const filter = getSearchFilterFromQuery(query); + + expect(filter.labels).toContain('region!=US'); + expect(filter.labels).toContain('team=~fe.*devs'); + expect(filter.labels).toContain('cluster!~ba.+'); + }); + }); + + describe('applySearchFilterToQuery', () => { + it('should apply filters to an empty query', () => { + const filter = getFilter({ + freeFormWords: ['cpu', 'eighty'], + dataSourceName: 'Mimir Dev', + namespace: '/etc/prometheus', + labels: ['team', 'region=apac'], + groupName: 'cpu-usage', + ruleName: 'cpu > 80%', + ruleType: PromRuleType.Alerting, + ruleState: PromAlertingRuleState.Firing, + ruleHealth: RuleHealth.Ok, + }); + + const query = applySearchFilterToQuery('', filter); + + expect(query).toBe( + 'datasource:"Mimir Dev" namespace:/etc/prometheus group:cpu-usage rule:"cpu > 80%" state:firing type:alerting health:ok label:team label:region=apac cpu eighty' + ); + }); + + it('should update filters in existing query', () => { + const filter = getFilter({ + dataSourceName: 'Mimir Dev', + namespace: '/etc/prometheus', + labels: ['team', 'region=apac'], + groupName: 'cpu-usage', + ruleName: 'cpu > 80%', + }); + + const baseQuery = 'datasource:prometheus namespace:mimir-global group:memory rule:"mem > 90% label:severity"'; + const query = applySearchFilterToQuery(baseQuery, filter); + + expect(query).toBe( + 'datasource:"Mimir Dev" namespace:/etc/prometheus group:cpu-usage rule:"cpu > 80%" label:team label:region=apac' + ); + }); + + it('should preserve the order of parameters when updating', () => { + const filter = getFilter({ + dataSourceName: 'Mimir Dev', + namespace: '/etc/prometheus', + labels: ['region=emea'], + groupName: 'cpu-usage', + ruleName: 'cpu > 80%', + }); + + const baseQuery = 'label:region=apac rule:"mem > 90%" group:memory namespace:mimir-global datasource:prometheus'; + const query = applySearchFilterToQuery(baseQuery, filter); + + expect(query).toBe( + 'label:region=emea rule:"cpu > 80%" group:cpu-usage namespace:/etc/prometheus datasource:"Mimir Dev"' + ); + }); + }); +}); diff --git a/public/app/features/alerting/unified/search/rulesSearchParser.ts b/public/app/features/alerting/unified/search/rulesSearchParser.ts new file mode 100644 index 00000000000..c74bd0871bf --- /dev/null +++ b/public/app/features/alerting/unified/search/rulesSearchParser.ts @@ -0,0 +1,100 @@ +import { isPromAlertingRuleState, PromAlertingRuleState, PromRuleType } from '../../../../types/unified-alerting-dto'; +import { getRuleHealth, isPromRuleType } from '../utils/rules'; + +import * as terms from './search.terms'; +import { + applyFiltersToQuery, + FilterExpr, + FilterSupportedTerm, + parseQueryToFilter, + QueryFilterMapper, +} from './searchParser'; + +export interface RulesFilter { + freeFormWords: string[]; + namespace?: string; + groupName?: string; + ruleName?: string; + ruleState?: PromAlertingRuleState; + ruleType?: PromRuleType; + dataSourceName?: string; + labels: string[]; + ruleHealth?: RuleHealth; +} + +const filterSupportedTerms: FilterSupportedTerm[] = [ + FilterSupportedTerm.dataSource, + FilterSupportedTerm.nameSpace, + FilterSupportedTerm.label, + FilterSupportedTerm.group, + FilterSupportedTerm.rule, + FilterSupportedTerm.state, + FilterSupportedTerm.type, + FilterSupportedTerm.health, +]; + +export enum RuleHealth { + Ok = 'ok', + Error = 'error', + NoData = 'nodata', + Unknown = 'unknown', +} + +// Define how to map parsed tokens into the filter object +export function getSearchFilterFromQuery(query: string): RulesFilter { + const filter: RulesFilter = { labels: [], freeFormWords: [] }; + + const tokenToFilterMap: QueryFilterMapper = { + [terms.DataSourceToken]: (value) => (filter.dataSourceName = value), + [terms.NameSpaceToken]: (value) => (filter.namespace = value), + [terms.GroupToken]: (value) => (filter.groupName = value), + [terms.RuleToken]: (value) => (filter.ruleName = value), + [terms.LabelToken]: (value) => filter.labels.push(value), + [terms.StateToken]: (value) => (isPromAlertingRuleState(value) ? (filter.ruleState = value) : undefined), + [terms.TypeToken]: (value) => (isPromRuleType(value) ? (filter.ruleType = value) : undefined), + [terms.HealthToken]: (value) => (filter.ruleHealth = getRuleHealth(value)), + [terms.FreeFormExpression]: (value) => filter.freeFormWords.push(value), + }; + + parseQueryToFilter(query, filterSupportedTerms, tokenToFilterMap); + + return filter; +} + +// Reverse of the previous function +// Describes how to map the object into an array of tokens and values +export function applySearchFilterToQuery(query: string, filter: RulesFilter): string { + const filterStateArray: FilterExpr[] = []; + + // Convert filter object into an array + // It allows to pick filters from the array in the same order as they were applied in the original query + if (filter.dataSourceName) { + filterStateArray.push({ type: terms.DataSourceToken, value: filter.dataSourceName }); + } + if (filter.namespace) { + filterStateArray.push({ type: terms.NameSpaceToken, value: filter.namespace }); + } + if (filter.groupName) { + filterStateArray.push({ type: terms.GroupToken, value: filter.groupName }); + } + if (filter.ruleName) { + filterStateArray.push({ type: terms.RuleToken, value: filter.ruleName }); + } + if (filter.ruleState) { + filterStateArray.push({ type: terms.StateToken, value: filter.ruleState }); + } + if (filter.ruleType) { + filterStateArray.push({ type: terms.TypeToken, value: filter.ruleType }); + } + if (filter.ruleHealth) { + filterStateArray.push({ type: terms.HealthToken, value: filter.ruleHealth }); + } + if (filter.labels) { + filterStateArray.push(...filter.labels.map((l) => ({ type: terms.LabelToken, value: l }))); + } + if (filter.freeFormWords) { + filterStateArray.push(...filter.freeFormWords.map((word) => ({ type: terms.FreeFormExpression, value: word }))); + } + + return applyFiltersToQuery(query, filterSupportedTerms, filterStateArray); +} diff --git a/public/app/features/alerting/unified/search/search.grammar b/public/app/features/alerting/unified/search/search.grammar new file mode 100644 index 00000000000..a8d64b6dceb --- /dev/null +++ b/public/app/features/alerting/unified/search/search.grammar @@ -0,0 +1,54 @@ +@top AlertRuleSearch { expression+ } + +@dialects { dataSourceFilter, nameSpaceFilter, labelFilter, groupFilter, ruleFilter, stateFilter, typeFilter, healthFilter } + +expression { (FilterExpression | FreeFormExpression) expression } + +FreeFormExpression { word (colon word)* | stringWithQuotes } + +FilterExpression { + filter | + filter | + filter | + filter | + filter | + filter | + filter | + filter +} + +filter { token FilterValue } + +@tokens { + colon { ":" } + + // Special characters (except colon, quotes and space), Latin characters, extended latin and emoji + allowedInputChar { $[!#$%&'()*+,-./] | $[\u{0030}-\u{0039}] | $[\u{003b}-\u{1eff}] | $[\u{2030}-\u{1faff}] } + word { allowedInputChar+ } + + allowedInputCharOrColon { allowedInputChar | colon } + allowedInputCharOrColonOrWhitespace { allowedInputCharOrColon | @whitespace } + stringWithQuotes { ("\"" allowedInputCharOrColonOrWhitespace+ "\"") } + + FilterValue { allowedInputCharOrColon+ | stringWithQuotes } + filterToken { type colon } + + DataSourceToken[@dialect=dataSourceFilter] { filterToken<"datasource"> } + NameSpaceToken[@dialect=nameSpaceFilter] { filterToken<"namespace"> } + LabelToken[@dialect=labelFilter] { filterToken<"label"> } + GroupToken[@dialect=groupFilter] { filterToken<"group"> } + RuleToken[@dialect=ruleFilter] { filterToken<"rule"> } + StateToken[@dialect=stateFilter] { filterToken<"state"> } + TypeToken[@dialect=typeFilter] { filterToken<"type"> } + HealthToken[@dialect=healthFilter] { filterToken<"health"> } + + @precedence { DataSourceToken, word } + @precedence { NameSpaceToken, word } + @precedence { LabelToken, word } + @precedence { GroupToken, word } + @precedence { RuleToken, word } + @precedence { StateToken, word } + @precedence { TypeToken, word } + @precedence { HealthToken, word } +} + diff --git a/public/app/features/alerting/unified/search/search.js b/public/app/features/alerting/unified/search/search.js new file mode 100644 index 00000000000..8ee40ac1d7c --- /dev/null +++ b/public/app/features/alerting/unified/search/search.js @@ -0,0 +1,30 @@ +// This file was generated by lezer-generator. You probably shouldn't edit it. +import { LRParser } from '@lezer/lr'; +export const parser = LRParser.deserialize({ + version: 14, + states: + "!vOQOPOOOrOPO'#ChOOOO'#Ch'#ChOQOPO'#ClOOOO'#Ci'#CiQQOPOOO!gOQO'#C^O!lOPO'#CjO!qOPO,59SOOOO,59W,59WOOOO-E6g-E6gOOOO,58x,58xOOOO,59U,59UOOOO-E6h-E6h", + stateData: + '$O~ORUOTUOUUOVUOWUOXUOYUOZUOaPOcQO~ObVOR[XT[XU[XV[XW[XX[XY[XZ[Xa[Xc[X~OSZO~Oa[O~ObVOR[aT[aU[aV[aW[aX[aY[aZ[aa[ac[a~OR~T~U~V~W~Y~Z~RZYXWVUTa~', + goto: 'zaPPbPPPPPPPPPbgmPsVRORTQTORYTQWPR]WSSOTRXR', + nodeNames: + '⚠ AlertRuleSearch FilterExpression DataSourceToken FilterValue NameSpaceToken LabelToken GroupToken RuleToken StateToken TypeToken HealthToken FreeFormExpression', + maxTerm: 19, + skippedNodes: [0], + repeatNodeCount: 2, + tokenData: + "#$QRRqqr#Yrs&fst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]+h!]#W#Y#W#X,z#X#Z#Y#Z#[=b#[#]F[#]#`#Y#`#a!!n#a#b#Y#b#c!+h#c#f#Y#f#g!:f#g#h!Av#h#i!Jp#i$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR#acSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YQ$qcSQqr$lst$ltu$luv$lvw$lwx$lxy$lyz$lz{$l{|$l|!P$l!P!Q$l!Q![$l![!]$l!]$Ch$l$JU;'S$l;'S;(d%|;(d;(e&S<%lO$lQ&PP;=`<%l$lQ&VP;=`;NQ$lR&]P;=`<%l#YR&cP;=`;NQ#YR&irX^(spq(sqr(sst(stu(suv(svw(swx(sxy(syz(sz{(s{|(s|!P(s!P!Q(s!Q![(s![!](s!]#y(s#y#z(s#z$f(s$f$g(s$g#BY(s#BY#BZ(s#BZ$Ch(s$IS$I_(s$I|$JO(s$JT$JU(s$JU$KV(s$KV$KW(s$KW&FU(s&FU&FV(s&FV;'S(s;'S;(d+[;(d;(e+b<%lO(sR(vsX^(spq(sqr(srs+Tst(stu(suv(svw(swx(sxy(syz(sz{(s{|(s|!P(s!P!Q(s!Q![(s![!](s!]#y(s#y#z(s#z$f(s$f$g(s$g#BY(s#BY#BZ(s#BZ$Ch(s$IS$I_(s$I|$JO(s$JT$JU(s$JU$KV(s$KV$KW(s$KW&FU(s&FU&FV(s&FV;'S(s;'S;(d+[;(d;(e+b<%lO(sR+[OSQcPR+_P;=`<%l(sR+eP;=`;NQ(sR+ocSQbPqr$lst$ltu$luv$lvw$lwx$lxy$lyz$lz{$l{|$l|!P$l!P!Q$l!Q![$l![!]$l!]$Ch$l$JU;'S$l;'S;(d%|;(d;(e&S<%lO$lR-ReSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#T#Y#T#U.d#U$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR.keSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#h#Y#h#i/|#i$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR0TeSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#T#Y#T#U1f#U$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR1meSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#g#Y#g#h3O#h$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR3VeSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#c#Y#c#d4h#d$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR4oeSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#i#Y#i#j6Q#j$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR6XeSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#f#Y#f#g7j#g$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR7qeSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#V#Y#V#W9S#W$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR9ZeSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#X#Y#X#Y:l#Y$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR:scSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]z#g$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR?ReSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#c#Y#c#d@d#d$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR@keSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#i#Y#i#jA|#j$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YRBTeSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#d#Y#d#eCf#e$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YRCmcSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]Dx!]$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YREPcSQVPqr$lst$ltu$luv$lvw$lwx$lxy$lyz$lz{$l{|$l|!P$l!P!Q$l!Q![$l![!]$l!]$Ch$l$JU;'S$l;'S;(d%|;(d;(e&S<%lO$lRFceSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#X#Y#X#YGt#Y$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YRG{eSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#T#Y#T#UI^#U$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YRIeeSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#`#Y#`#aJv#a$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YRJ}eSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#h#Y#h#iL`#i$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YRLgeSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#[#Y#[#]Mx#]$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YRNPcSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]! [!]$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR! ccSQZPqr$lst$ltu$luv$lvw$lwx$lxy$lyz$lz{$l{|$l|!P$l!P!Q$l!Q![$l![!]$l!]$Ch$l$JU;'S$l;'S;(d%|;(d;(e&S<%lO$lR!!ueSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#T#Y#T#U!$W#U$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR!$_eSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#U#Y#U#V!%p#V$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR!%weSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#X#Y#X#Y!'Y#Y$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR!'aeSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#`#Y#`#a!(r#a$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR!(ycSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]!*U!]$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR!*]cSQUPqr$lst$ltu$luv$lvw$lwx$lxy$lyz$lz{$l{|$l|!P$l!P!Q$l!Q![$l![!]$l!]$Ch$l$JU;'S$l;'S;(d%|;(d;(e&S<%lO$lR!+oeSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#T#Y#T#U!-Q#U$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR!-XeSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#a#Y#a#b!.j#b$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR!.qeSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#X#Y#X#Y!0S#Y$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR!0ZeSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#g#Y#g#h!1l#h$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR!1seSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#d#Y#d#e!3U#e$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR!3]eSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#T#Y#T#U!4n#U$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR!4ueSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#V#Y#V#W!6W#W$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR!6_eSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#X#Y#X#Y!7p#Y$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR!7wcSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]!9S!]$Ch#Y$JU;'S#Y;'S;(d&Y;(d;(e&`<%lO#YR!9ZcSQTPqr$lst$ltu$luv$lvw$lwx$lxy$lyz$lz{$l{|$l|!P$l!P!Q$l!Q![$l![!]$l!]$Ch$l$JU;'S$l;'S;(d%|;(d;(e&S<%lO$lR!:meSQaPqr#Yst#Ytu#Yuv#Yvw#Ywx#Yxy#Yyz#Yz{#Y{|#Y|!P#Y!P!Q#Y!Q![#Y![!]$l!]#i#Y#i#j! = { + [terms.DataSourceToken]: 'datasource', + [terms.NameSpaceToken]: 'namespace', + [terms.LabelToken]: 'label', + [terms.RuleToken]: 'rule', + [terms.GroupToken]: 'group', + [terms.StateToken]: 'state', + [terms.TypeToken]: 'type', + [terms.HealthToken]: 'health', +}; + +// This enum allows to configure parser behavior +// Depending on our needs we can enable and disable only selected filters +// Thanks to that we can create multiple different filters having the same search grammar +export enum FilterSupportedTerm { + dataSource = 'dataSourceFilter', + nameSpace = 'nameSpaceFilter', + label = 'labelFilter', + group = 'groupFilter', + rule = 'ruleFilter', + state = 'stateFilter', + type = 'typeFilter', + health = 'healthFilter', +} + +export type QueryFilterMapper = Record void>; + +export interface FilterExpr { + type: number; + value: string; +} + +export function parseQueryToFilter( + query: string, + supportedTerms: FilterSupportedTerm[], + filterMapper: QueryFilterMapper +) { + traverseNodeTree(query, supportedTerms, (node) => { + if (node.type.id === terms.FilterExpression) { + const filter = getFilterFromSyntaxNode(query, node); + + if (filter.type && filter.value) { + const filterHandler = filterMapper[filter.type]; + if (filterHandler) { + filterHandler(filter.value); + } + } + } else if (node.type.id === terms.FreeFormExpression) { + const filterHandler = filterMapper[terms.FreeFormExpression]; + if (filterHandler) { + filterHandler(getNodeContent(query, node)); + } + } + }); +} + +function getFilterFromSyntaxNode(query: string, filterExpressionNode: SyntaxNode): { type?: number; value?: string } { + if (filterExpressionNode.type.id !== terms.FilterExpression) { + throw new Error('Invalid node provided. Only FilterExpression nodes are supported'); + } + + const filterTokenNode = filterExpressionNode.firstChild; + if (!filterTokenNode) { + return { type: undefined, value: undefined }; + } + + const filterValueNode = filterExpressionNode.getChild(terms.FilterValue); + const filterValue = filterValueNode ? trim(getNodeContent(query, filterValueNode), '"') : undefined; + + return { type: filterTokenNode.type.id, value: filterValue }; +} + +function getNodeContent(query: string, node: SyntaxNode) { + return query.slice(node.from, node.to).trim().replace(/\"/g, ''); +} + +export function applyFiltersToQuery( + query: string, + supportedTerms: FilterSupportedTerm[], + filters: FilterExpr[] +): string { + const existingFilterNodes: SyntaxNode[] = []; + traverseNodeTree(query, supportedTerms, (node) => { + if (node.type.id === terms.FilterExpression && node.firstChild) { + existingFilterNodes.push(node.firstChild); + } + if (node.type.id === terms.FreeFormExpression) { + existingFilterNodes.push(node); + } + }); + + let newQueryExpressions: string[] = []; + + // Apply filters from filterState in the same order as they appear in the search query + // This allows to remain the order of filters in the search input during changes + existingFilterNodes.forEach((filterNode) => { + const matchingFilterIdx = filters.findIndex((f) => f.type === filterNode.type.id); + if (matchingFilterIdx === -1) { + return; + } + + if (filterNode.parent?.type.is(terms.FilterExpression)) { + const filterToken = filterTokenToTypeMap[filterNode.type.id]; + const filterItem = filters.splice(matchingFilterIdx, 1)[0]; + newQueryExpressions.push(`${filterToken}:${getSafeFilterValue(filterItem.value)}`); + } + + if (filterNode.type.is(terms.FreeFormExpression)) { + const freeFormWordNode = filters.splice(matchingFilterIdx, 1)[0]; + newQueryExpressions.push(freeFormWordNode.value); + } + }); + + // Apply new filters that hasn't been in the query yet + filters.forEach((fs) => { + if (fs.type === terms.FreeFormExpression) { + newQueryExpressions.push(fs.value); + } else { + newQueryExpressions.push(`${filterTokenToTypeMap[fs.type]}:${getSafeFilterValue(fs.value)}`); + } + }); + + return newQueryExpressions.join(' '); +} + +function traverseNodeTree(query: string, supportedTerms: FilterSupportedTerm[], visit: (node: SyntaxNode) => void) { + const dialect = supportedTerms.join(' '); + const parsed = parser.configure({ dialect }).parse(query); + let cursor = parsed.cursor(); + do { + visit(cursor.node); + } while (cursor.next()); +} + +function getSafeFilterValue(filterValue: string) { + const containsWhiteSpaces = /\s/.test(filterValue); + return containsWhiteSpaces ? `\"${filterValue}\"` : filterValue; +} diff --git a/public/app/features/alerting/unified/utils/rules.ts b/public/app/features/alerting/unified/utils/rules.ts index 6386541891a..c44ba0869d4 100644 --- a/public/app/features/alerting/unified/utils/rules.ts +++ b/public/app/features/alerting/unified/utils/rules.ts @@ -27,6 +27,7 @@ import { } from 'app/types/unified-alerting-dto'; import { State } from '../components/StateTag'; +import { RuleHealth } from '../search/rulesSearchParser'; import { RULER_NOT_SUPPORTED_MSG } from './constants'; import { AsyncRequestState } from './redux'; @@ -67,10 +68,30 @@ export function isCloudRuleIdentifier(identifier: RuleIdentifier): identifier is return 'rulerRuleHash' in identifier; } +export function isPromRuleType(ruleType: string): ruleType is PromRuleType { + return Object.values(PromRuleType).includes(ruleType); +} + export function isPrometheusRuleIdentifier(identifier: RuleIdentifier): identifier is PrometheusRuleIdentifier { return 'ruleHash' in identifier; } +export function getRuleHealth(health: string): RuleHealth | undefined { + switch (health) { + case 'ok': + return RuleHealth.Ok; + case 'nodata': + return RuleHealth.NoData; + case 'error': + case 'err': // Prometheus-compat data sources + return RuleHealth.Error; + case 'unknown': + return RuleHealth.Unknown; + default: + return undefined; + } +} + export function alertStateToReadable(state: PromAlertingRuleState | GrafanaAlertStateWithReason | AlertState): string { if (state === PromAlertingRuleState.Inactive) { return 'Normal'; diff --git a/public/app/features/alerting/unified/utils/search.ts b/public/app/features/alerting/unified/utils/search.ts new file mode 100644 index 00000000000..846da556b01 --- /dev/null +++ b/public/app/features/alerting/unified/utils/search.ts @@ -0,0 +1,9 @@ +import { RulesFilter } from '../search/rulesSearchParser'; + +export function getFilter(filter: Partial): RulesFilter { + return { + freeFormWords: [], + labels: [], + ...filter, + }; +} diff --git a/public/app/types/unified-alerting-dto.ts b/public/app/types/unified-alerting-dto.ts index 9c07deff217..4d00880782e 100644 --- a/public/app/types/unified-alerting-dto.ts +++ b/public/app/types/unified-alerting-dto.ts @@ -23,6 +23,10 @@ type GrafanaAlertStateReason = ` (${string})` | ''; export type GrafanaAlertStateWithReason = `${GrafanaAlertState}${GrafanaAlertStateReason}`; +export function isPromAlertingRuleState(state: string): state is PromAlertingRuleState { + return Object.values(PromAlertingRuleState).includes(state); +} + export function isGrafanaAlertState(state: string): state is GrafanaAlertState { return Object.values(GrafanaAlertState).some((promState) => promState === state); } From 0bfe150928284bebd3208b9387651499cc8b7667 Mon Sep 17 00:00:00 2001 From: gotjosh Date: Thu, 26 Jan 2023 12:54:03 +0000 Subject: [PATCH 078/172] Alerting: Fix Test Receivers when settings are non-strings (#62156) * Alerting: Fix Test Receivers when settings are non-strings As part of the Alerting extraction, we want to make sure we don't have circular depedencies. As such, I had to move `PostableGrafanaReceiver` to a new struct in `grafana/alerting` called `GrafanaReceiver`. `PostableGrafanaReceiver` has an attribute called `Settings` that uses a Grafana-propietary struct called `RawMessage`, this struct shadows `json.RawMessage`. When I created `GrafanaReceiver`, I turned settings into a `map[string]string` thinking all settings would end up as strings. This was a mistake, and this test proves that it doesn't work, and breaks the API. --- go.mod | 2 +- go.sum | 4 ++-- pkg/services/ngalert/notifier/receivers.go | 2 +- pkg/tests/api/alerting/api_notification_channel_test.go | 3 ++- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 7595bdc6861..108a98b42c1 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,7 @@ require ( github.com/google/uuid v1.3.0 github.com/google/wire v0.5.0 github.com/gorilla/websocket v1.5.0 - github.com/grafana/alerting v0.0.0-20230124145916-c6a7791d037e + github.com/grafana/alerting v0.0.0-20230125210216-facc6b27b9e0 github.com/grafana/cuetsy v0.1.5 github.com/grafana/grafana-aws-sdk v0.12.0 github.com/grafana/grafana-azure-sdk-go v1.5.1 diff --git a/go.sum b/go.sum index 7c6fb1c4d0f..7e2c44649b6 100644 --- a/go.sum +++ b/go.sum @@ -1394,8 +1394,8 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/alerting v0.0.0-20230124145916-c6a7791d037e h1:YCxvmaPXHGiaQdWy6qeQExivxWzpwUyM0sh73vudWhw= -github.com/grafana/alerting v0.0.0-20230124145916-c6a7791d037e/go.mod h1:NoSLbfmUwE+omWFReFrLtbtOItmvTbuQERJ6XFYp9ME= +github.com/grafana/alerting v0.0.0-20230125210216-facc6b27b9e0 h1:BzkQNnj+eevX30EMqJiUS1w3CPoGc8kp7pDf/ari/4Y= +github.com/grafana/alerting v0.0.0-20230125210216-facc6b27b9e0/go.mod h1:NoSLbfmUwE+omWFReFrLtbtOItmvTbuQERJ6XFYp9ME= github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw= github.com/grafana/codejen v0.0.3/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s= github.com/grafana/cuetsy v0.1.5 h1:mnFwAXdbqCsyL8r7kkdUMJ4kOAR26cxIPmrZj7JzTeY= diff --git a/pkg/services/ngalert/notifier/receivers.go b/pkg/services/ngalert/notifier/receivers.go index 02478c589bd..2a964b1e5d0 100644 --- a/pkg/services/ngalert/notifier/receivers.go +++ b/pkg/services/ngalert/notifier/receivers.go @@ -60,7 +60,7 @@ func (am *Alertmanager) TestReceivers(ctx context.Context, c apimodels.TestRecei for _, r := range c.Receivers { greceivers := make([]*alerting.GrafanaReceiver, 0, len(r.GrafanaManagedReceivers)) for _, gr := range r.PostableGrafanaReceivers.GrafanaManagedReceivers { - var settings map[string]string + var settings map[string]interface{} //TODO: We shouldn't need to do this marshalling. j, err := gr.Settings.MarshalJSON() if err != nil { diff --git a/pkg/tests/api/alerting/api_notification_channel_test.go b/pkg/tests/api/alerting/api_notification_channel_test.go index 7396e24dbaf..ce6323085c5 100644 --- a/pkg/tests/api/alerting/api_notification_channel_test.go +++ b/pkg/tests/api/alerting/api_notification_channel_test.go @@ -98,7 +98,8 @@ func TestIntegrationTestReceivers(t *testing.T) { "type": "email", "disableResolveMessage": false, "settings": { - "addresses":"example@email.com" + "addresses":"example@email.com", + "singleEmail": true }, "secureFields": {} } From 928e2c9c9e4013c5ca66d90fe3cbc2537d2dc0f4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Jan 2023 13:24:35 +0000 Subject: [PATCH 079/172] Update dependency glob to v8.1.0 (#62186) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index b5f2b51f72c..fd64121aa27 100644 --- a/package.json +++ b/package.json @@ -196,7 +196,7 @@ "expose-loader": "4.0.0", "fork-ts-checker-webpack-plugin": "7.3.0", "fs-extra": "10.1.0", - "glob": "8.0.3", + "glob": "8.1.0", "html-loader": "4.2.0", "html-webpack-plugin": "5.5.0", "http-server": "14.1.1", diff --git a/yarn.lock b/yarn.lock index 92702e6a5ec..79ac59c8a3c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21438,7 +21438,20 @@ __metadata: languageName: node linkType: hard -"glob@npm:8.0.3, glob@npm:^8.0.1, glob@npm:^8.0.3": +"glob@npm:8.1.0": + version: 8.1.0 + resolution: "glob@npm:8.1.0" + dependencies: + fs.realpath: ^1.0.0 + inflight: ^1.0.4 + inherits: 2 + minimatch: ^5.0.1 + once: ^1.3.0 + checksum: 92fbea3221a7d12075f26f0227abac435de868dd0736a17170663783296d0dd8d3d532a5672b4488a439bf5d7fb85cdd07c11185d6cd39184f0385cbdfb86a47 + languageName: node + linkType: hard + +"glob@npm:^8.0.1, glob@npm:^8.0.3": version: 8.0.3 resolution: "glob@npm:8.0.3" dependencies: @@ -21810,7 +21823,7 @@ __metadata: fork-ts-checker-webpack-plugin: 7.3.0 framework-utils: ^1.1.0 fs-extra: 10.1.0 - glob: 8.0.3 + glob: 8.1.0 history: 4.10.1 hoist-non-react-statics: 3.3.2 html-loader: 4.2.0 From 7d8ec6199d2c74f18fa0640e89c3e04a3b36e888 Mon Sep 17 00:00:00 2001 From: "lean.dev" <34773040+leandro-deveikis@users.noreply.github.com> Date: Thu, 26 Jan 2023 10:28:11 -0300 Subject: [PATCH 080/172] Snapshots: Add snapshot enable config (#61587) * Add config to remove Snapshot functionality (frontend is hidden and validation in the backend) * Add test cases * Remove unused mock on the test * Moving Snapshot config from globar variables to settings.Cfg * Removing warnings on code --- conf/defaults.ini | 3 + conf/sample.ini | 3 + packages/grafana-data/src/types/config.ts | 1 + packages/grafana-runtime/src/config.ts | 1 + pkg/api/api.go | 2 +- pkg/api/dashboard_snapshot.go | 86 +++++++++----- pkg/api/dashboard_snapshot_test.go | 107 ++++++++++++++---- pkg/api/frontendsettings.go | 1 + .../dashboardsnapshots/database/database.go | 7 +- .../database/database_test.go | 6 +- .../service/service_test.go | 2 +- pkg/services/navtree/navtreeimpl/navtree.go | 16 +-- pkg/setting/setting.go | 22 ++-- .../components/ShareModal/ShareModal.tsx | 33 +++--- 14 files changed, 191 insertions(+), 99 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 56fe0791c0d..d67385d1db5 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -366,6 +366,9 @@ data_keys_cache_cleanup_interval = 1m #################################### Snapshots ########################### [snapshots] +# set to false to remove snapshot functionality +enabled = true + # snapshot sharing options external_enabled = true external_snapshot_url = https://snapshots.raintank.io diff --git a/conf/sample.ini b/conf/sample.ini index 799151eb864..546f828ac45 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -372,6 +372,9 @@ #################################### Snapshots ########################### [snapshots] +# set to false to remove snapshot functionality +;enabled = true + # snapshot sharing options ;external_enabled = true ;external_snapshot_url = https://snapshots.raintank.io diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index dfbde50d60a..804fa0b2601 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -152,6 +152,7 @@ export interface BootData { */ export interface GrafanaConfig { isPublicDashboardView: boolean; + snapshotEnabled: boolean; datasources: { [str: string]: DataSourceInstanceSettings }; panels: { [key: string]: PanelPluginMeta }; auth: AuthSettings; diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index a9ef4dbf9b3..1382f8c6aba 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -27,6 +27,7 @@ export interface AzureSettings { export class GrafanaBootConfig implements GrafanaConfig { isPublicDashboardView: boolean; + snapshotEnabled = true; datasources: { [str: string]: DataSourceInstanceSettings } = {}; panels: { [key: string]: PanelPluginMeta } = {}; auth: AuthSettings = {}; diff --git a/pkg/api/api.go b/pkg/api/api.go index 18fc62a9d84..807c33fe786 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -701,7 +701,7 @@ func (hs *HTTPServer) registerRoutes() { // Snapshots r.Post("/api/snapshots/", reqSnapshotPublicModeOrSignedIn, hs.CreateDashboardSnapshot) - r.Get("/api/snapshot/shared-options/", reqSignedIn, GetSharingOptions) + r.Get("/api/snapshot/shared-options/", reqSignedIn, hs.GetSharingOptions) r.Get("/api/snapshots/:key", routing.Wrap(hs.GetDashboardSnapshot)) r.Get("/api/snapshots-delete/:deleteKey", reqSnapshotPublicModeOrSignedIn, routing.Wrap(hs.DeleteDashboardSnapshotByDeleteKey)) r.Delete("/api/snapshots/:key", reqSignedIn, routing.Wrap(hs.DeleteDashboardSnapshot)) diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index aa8033a2d5c..5d017a82838 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -33,11 +33,12 @@ var client = &http.Client{ // Responses: // 200: getSharingOptionsResponse // 401: unauthorisedError -func GetSharingOptions(c *models.ReqContext) { +func (hs *HTTPServer) GetSharingOptions(c *models.ReqContext) { c.JSON(http.StatusOK, util.DynMap{ - "externalSnapshotURL": setting.ExternalSnapshotUrl, - "externalSnapshotName": setting.ExternalSnapshotName, - "externalEnabled": setting.ExternalEnabled, + "snapshotEnabled": hs.Cfg.SnapshotEnabled, + "externalSnapshotURL": hs.Cfg.ExternalSnapshotUrl, + "externalSnapshotName": hs.Cfg.ExternalSnapshotName, + "externalEnabled": hs.Cfg.ExternalEnabled, }) } @@ -48,7 +49,7 @@ type CreateExternalSnapshotResponse struct { DeleteUrl string `json:"deleteUrl"` } -func createExternalDashboardSnapshot(cmd dashboardsnapshots.CreateDashboardSnapshotCommand) (*CreateExternalSnapshotResponse, error) { +func createExternalDashboardSnapshot(cmd dashboardsnapshots.CreateDashboardSnapshotCommand, externalSnapshotUrl string) (*CreateExternalSnapshotResponse, error) { var createSnapshotResponse CreateExternalSnapshotResponse message := map[string]interface{}{ "name": cmd.Name, @@ -63,28 +64,28 @@ func createExternalDashboardSnapshot(cmd dashboardsnapshots.CreateDashboardSnaps return nil, err } - response, err := client.Post(setting.ExternalSnapshotUrl+"/api/snapshots", "application/json", bytes.NewBuffer(messageBytes)) + resp, err := client.Post(externalSnapshotUrl+"/api/snapshots", "application/json", bytes.NewBuffer(messageBytes)) if err != nil { return nil, err } defer func() { - if err := response.Body.Close(); err != nil { + if err := resp.Body.Close(); err != nil { plog.Warn("Failed to close response body", "err", err) } }() - if response.StatusCode != 200 { - return nil, fmt.Errorf("create external snapshot response status code %d", response.StatusCode) + if resp.StatusCode != 200 { + return nil, fmt.Errorf("create external snapshot response status code %d", resp.StatusCode) } - if err := json.NewDecoder(response.Body).Decode(&createSnapshotResponse); err != nil { + if err := json.NewDecoder(resp.Body).Decode(&createSnapshotResponse); err != nil { return nil, err } return &createSnapshotResponse, nil } -func createOriginalDashboardURL(appURL string, cmd *dashboardsnapshots.CreateDashboardSnapshotCommand) (string, error) { +func createOriginalDashboardURL(cmd *dashboardsnapshots.CreateDashboardSnapshotCommand) (string, error) { dashUID := cmd.Dashboard.Get("uid").MustString("") if ok := util.IsValidShortUID(dashUID); !ok { return "", fmt.Errorf("invalid dashboard UID") @@ -105,6 +106,11 @@ func createOriginalDashboardURL(appURL string, cmd *dashboardsnapshots.CreateDas // 403: forbiddenError // 500: internalServerError func (hs *HTTPServer) CreateDashboardSnapshot(c *models.ReqContext) response.Response { + if !hs.Cfg.SnapshotEnabled { + c.JsonApiErr(http.StatusForbidden, "Dashboard Snapshots are disabled", nil) + return nil + } + cmd := dashboardsnapshots.CreateDashboardSnapshotCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) @@ -117,28 +123,28 @@ func (hs *HTTPServer) CreateDashboardSnapshot(c *models.ReqContext) response.Res cmd.ExternalURL = "" cmd.OrgID = c.OrgID cmd.UserID = c.UserID - originalDashboardURL, err := createOriginalDashboardURL(hs.Cfg.AppURL, &cmd) + originalDashboardURL, err := createOriginalDashboardURL(&cmd) if err != nil { return response.Error(http.StatusInternalServerError, "Invalid app URL", err) } if cmd.External { - if !setting.ExternalEnabled { + if !hs.Cfg.ExternalEnabled { c.JsonApiErr(http.StatusForbidden, "External dashboard creation is disabled", nil) return nil } - response, err := createExternalDashboardSnapshot(cmd) + resp, err := createExternalDashboardSnapshot(cmd, hs.Cfg.ExternalSnapshotUrl) if err != nil { c.JsonApiErr(http.StatusInternalServerError, "Failed to create external snapshot", err) return nil } - snapshotUrl = response.Url - cmd.Key = response.Key - cmd.DeleteKey = response.DeleteKey - cmd.ExternalURL = response.Url - cmd.ExternalDeleteURL = response.DeleteUrl + snapshotUrl = resp.Url + cmd.Key = resp.Key + cmd.DeleteKey = resp.DeleteKey + cmd.ExternalURL = resp.Url + cmd.ExternalDeleteURL = resp.DeleteUrl cmd.Dashboard = simplejson.New() metrics.MApiDashboardSnapshotExternal.Inc() @@ -195,6 +201,11 @@ func (hs *HTTPServer) CreateDashboardSnapshot(c *models.ReqContext) response.Res // 404: notFoundError // 500: internalServerError func (hs *HTTPServer) GetDashboardSnapshot(c *models.ReqContext) response.Response { + if !hs.Cfg.SnapshotEnabled { + c.JsonApiErr(http.StatusForbidden, "Dashboard Snapshots are disabled", nil) + return nil + } + key := web.Params(c.Req)[":key"] if len(key) == 0 { return response.Error(http.StatusBadRequest, "Empty snapshot key", nil) @@ -230,26 +241,26 @@ func (hs *HTTPServer) GetDashboardSnapshot(c *models.ReqContext) response.Respon } func deleteExternalDashboardSnapshot(externalUrl string) error { - response, err := client.Get(externalUrl) + resp, err := client.Get(externalUrl) if err != nil { return err } defer func() { - if err := response.Body.Close(); err != nil { + if err := resp.Body.Close(); err != nil { plog.Warn("Failed to close response body", "err", err) } }() - if response.StatusCode == 200 { + if resp.StatusCode == 200 { return nil } // Gracefully ignore "snapshot not found" errors as they could have already // been removed either via the cleanup script or by request. - if response.StatusCode == 500 { + if resp.StatusCode == 500 { var respJson map[string]interface{} - if err := json.NewDecoder(response.Body).Decode(&respJson); err != nil { + if err := json.NewDecoder(resp.Body).Decode(&respJson); err != nil { return err } @@ -258,7 +269,7 @@ func deleteExternalDashboardSnapshot(externalUrl string) error { } } - return fmt.Errorf("unexpected response when deleting external snapshot, status code: %d", response.StatusCode) + return fmt.Errorf("unexpected response when deleting external snapshot, status code: %d", resp.StatusCode) } // swagger:route GET /snapshots-delete/{deleteKey} snapshots deleteDashboardSnapshotByDeleteKey @@ -274,6 +285,11 @@ func deleteExternalDashboardSnapshot(externalUrl string) error { // 404: notFoundError // 500: internalServerError func (hs *HTTPServer) DeleteDashboardSnapshotByDeleteKey(c *models.ReqContext) response.Response { + if !hs.Cfg.SnapshotEnabled { + c.JsonApiErr(http.StatusForbidden, "Dashboard Snapshots are disabled", nil) + return nil + } + key := web.Params(c.Req)[":deleteKey"] if len(key) == 0 { return response.Error(404, "Snapshot not found", nil) @@ -314,6 +330,11 @@ func (hs *HTTPServer) DeleteDashboardSnapshotByDeleteKey(c *models.ReqContext) r // 404: notFoundError // 500: internalServerError func (hs *HTTPServer) DeleteDashboardSnapshot(c *models.ReqContext) response.Response { + if !hs.Cfg.SnapshotEnabled { + c.JsonApiErr(http.StatusForbidden, "Dashboard Snapshots are disabled", nil) + return nil + } + key := web.Params(c.Req)[":key"] if len(key) == 0 { return response.Error(http.StatusNotFound, "Snapshot not found", nil) @@ -343,12 +364,12 @@ func (hs *HTTPServer) DeleteDashboardSnapshot(c *models.ReqContext) response.Res dashboardID := queryResult.Dashboard.Get("id").MustInt64() if dashboardID != 0 { - guardian, err := guardian.New(c.Req.Context(), dashboardID, c.OrgID, c.SignedInUser) + g, err := guardian.New(c.Req.Context(), dashboardID, c.OrgID, c.SignedInUser) if err != nil { return response.Err(err) } - canEdit, err := guardian.CanEdit() + canEdit, err := g.CanEdit() // check for permissions only if the dashboard is found if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) { return response.Error(http.StatusInternalServerError, "Error while checking permissions for snapshot", err) @@ -379,6 +400,11 @@ func (hs *HTTPServer) DeleteDashboardSnapshot(c *models.ReqContext) response.Res // 200: searchDashboardSnapshotsResponse // 500: internalServerError func (hs *HTTPServer) SearchDashboardSnapshots(c *models.ReqContext) response.Response { + if !hs.Cfg.SnapshotEnabled { + c.JsonApiErr(http.StatusForbidden, "Dashboard Snapshots are disabled", nil) + return nil + } + query := c.Query("query") limit := c.QueryInt("limit") @@ -398,9 +424,9 @@ func (hs *HTTPServer) SearchDashboardSnapshots(c *models.ReqContext) response.Re return response.Error(500, "Search failed", err) } - dtos := make([]*dashboardsnapshots.DashboardSnapshotDTO, len(searchQueryResult)) + dto := make([]*dashboardsnapshots.DashboardSnapshotDTO, len(searchQueryResult)) for i, snapshot := range searchQueryResult { - dtos[i] = &dashboardsnapshots.DashboardSnapshotDTO{ + dto[i] = &dashboardsnapshots.DashboardSnapshotDTO{ ID: snapshot.ID, Name: snapshot.Name, Key: snapshot.Key, @@ -414,7 +440,7 @@ func (hs *HTTPServer) SearchDashboardSnapshots(c *models.ReqContext) response.Re } } - return response.JSON(http.StatusOK, dtos) + return response.JSON(http.StatusOK, dto) } // swagger:parameters createDashboardSnapshot diff --git a/pkg/api/dashboard_snapshot_test.go b/pkg/api/dashboard_snapshot_test.go index a80eec50d8b..d0e142c391f 100644 --- a/pkg/api/dashboard_snapshot_test.go +++ b/pkg/api/dashboard_snapshot_test.go @@ -21,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/team/teamtest" + "github.com/grafana/grafana/pkg/setting" ) func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { @@ -64,7 +65,8 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { t.Run("When user has editor role and is not in the ACL", func(t *testing.T) { loggedInUserScenarioWithRole(t, "Should not be able to delete snapshot when calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", org.RoleEditor, func(sc *scenarioContext) { - hs := &HTTPServer{dashboardsnapshotsService: setUpSnapshotTest(t, 0, "")} + d := setUpSnapshotTest(t, 0, "") + hs := buildHttpServer(d, true) sc.handlerFunc = hs.DeleteDashboardSnapshot teamSvc := &teamtest.FakeService{} @@ -95,7 +97,8 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { rw.WriteHeader(200) externalRequest = req }) - hs := &HTTPServer{dashboardsnapshotsService: setUpSnapshotTest(t, 0, ts.URL)} + d := setUpSnapshotTest(t, 0, ts.URL) + hs := buildHttpServer(d, true) sc.handlerFunc = hs.DeleteDashboardSnapshotByDeleteKey sc.fakeReqWithParams("GET", sc.url, map[string]string{"deleteKey": "12345"}).exec() @@ -138,7 +141,9 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { } dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResultACL, nil) guardian.InitLegacyGuardian(sc.sqlStore, dashSvc, teamSvc) - hs := &HTTPServer{dashboardsnapshotsService: setUpSnapshotTest(t, 0, ts.URL), DashboardService: dashSvc} + d := setUpSnapshotTest(t, 0, ts.URL) + hs := buildHttpServer(d, true) + hs.DashboardService = dashSvc sc.handlerFunc = hs.DeleteDashboardSnapshot sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() @@ -159,7 +164,8 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { d := setUpSnapshotTest(t, testUserID, "") dashSvc := dashboards.NewFakeDashboardService(t) - hs := &HTTPServer{dashboardsnapshotsService: d, DashboardService: dashSvc} + hs := buildHttpServer(d, true) + hs.DashboardService = dashSvc sc.handlerFunc = hs.DeleteDashboardSnapshot sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() @@ -183,7 +189,9 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { }) dashSvc := dashboards.NewFakeDashboardService(t) - hs := &HTTPServer{dashboardsnapshotsService: setUpSnapshotTest(t, testUserID, ts.URL), DashboardService: dashSvc} + d := setUpSnapshotTest(t, testUserID, ts.URL) + hs := buildHttpServer(d, true) + hs.DashboardService = dashSvc sc.handlerFunc = hs.DeleteDashboardSnapshot sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() @@ -204,7 +212,8 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { rw.WriteHeader(500) _, writeErr = rw.Write([]byte(`{"message":"Unexpected"}`)) }) - hs := &HTTPServer{dashboardsnapshotsService: setUpSnapshotTest(t, testUserID, ts.URL)} + d := setUpSnapshotTest(t, testUserID, ts.URL) + hs := buildHttpServer(d, true) sc.handlerFunc = hs.DeleteDashboardSnapshot sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() @@ -218,7 +227,8 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(404) }) - hs := &HTTPServer{dashboardsnapshotsService: setUpSnapshotTest(t, testUserID, ts.URL)} + d := setUpSnapshotTest(t, testUserID, ts.URL) + hs := buildHttpServer(d, true) sc.handlerFunc = hs.DeleteDashboardSnapshot sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() @@ -227,7 +237,8 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { loggedInUserScenarioWithRole(t, "Should be able to read a snapshot's unencrypted data when calling GET on", "GET", "/api/snapshots/12345", "/api/snapshots/:key", org.RoleEditor, func(sc *scenarioContext) { - hs := &HTTPServer{dashboardsnapshotsService: setUpSnapshotTest(t, 0, "")} + d := setUpSnapshotTest(t, 0, "") + hs := buildHttpServer(d, true) sc.handlerFunc = hs.GetDashboardSnapshot sc.fakeReqWithParams("GET", sc.url, map[string]string{"key": "12345"}).exec() @@ -262,7 +273,7 @@ func TestGetDashboardSnapshotNotFound(t *testing.T) { "GET /snapshots/{key} should return 404 when the snapshot does not exist", "GET", "/api/snapshots/12345", "/api/snapshots/:key", org.RoleEditor, func(sc *scenarioContext) { d := setUpSnapshotTest(t) - hs := &HTTPServer{dashboardsnapshotsService: d} + hs := buildHttpServer(d, true) sc.handlerFunc = hs.GetDashboardSnapshot sc.fakeReqWithParams("GET", sc.url, map[string]string{"key": "12345"}).exec() @@ -273,7 +284,7 @@ func TestGetDashboardSnapshotNotFound(t *testing.T) { "DELETE /snapshots/{key} should return 404 when the snapshot does not exist", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", org.RoleEditor, func(sc *scenarioContext) { d := setUpSnapshotTest(t) - hs := &HTTPServer{dashboardsnapshotsService: d} + hs := buildHttpServer(d, true) sc.handlerFunc = hs.DeleteDashboardSnapshot sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() @@ -284,7 +295,7 @@ func TestGetDashboardSnapshotNotFound(t *testing.T) { "GET /snapshots-delete/{deleteKey} should return 404 when the snapshot does not exist", "DELETE", "/api/snapshots-delete/12345", "/api/snapshots-delete/:deleteKey", org.RoleEditor, func(sc *scenarioContext) { d := setUpSnapshotTest(t) - hs := &HTTPServer{dashboardsnapshotsService: d} + hs := buildHttpServer(d, true) sc.handlerFunc = hs.DeleteDashboardSnapshotByDeleteKey sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"deleteKey": "12345"}).exec() @@ -295,48 +306,94 @@ func TestGetDashboardSnapshotNotFound(t *testing.T) { func TestGetDashboardSnapshotFailure(t *testing.T) { sqlmock := dbtest.NewFakeDB() - setUpSnapshotTest := func(t *testing.T) dashboardsnapshots.Service { + setUpSnapshotTest := func(t *testing.T, shouldMockDashSnapServ bool) dashboardsnapshots.Service { t.Helper() - dashSnapSvc := dashboardsnapshots.NewMockService(t) - dashSnapSvc. - On("GetDashboardSnapshot", mock.Anything, mock.AnythingOfType("*dashboardsnapshots.GetDashboardSnapshotQuery")). - Run(func(args mock.Arguments) {}). - Return(nil, errors.New("something went wrong")) - - return dashSnapSvc + if shouldMockDashSnapServ { + dashSnapSvc := dashboardsnapshots.NewMockService(t) + dashSnapSvc. + On("GetDashboardSnapshot", mock.Anything, mock.AnythingOfType("*dashboardsnapshots.GetDashboardSnapshotQuery")). + Run(func(args mock.Arguments) {}). + Return(nil, errors.New("something went wrong")) + return dashSnapSvc + } else { + return nil + } } loggedInUserScenarioWithRole(t, "GET /snapshots/{key} should return 404 when the snapshot does not exist", "GET", "/api/snapshots/12345", "/api/snapshots/:key", org.RoleEditor, func(sc *scenarioContext) { - d := setUpSnapshotTest(t) - hs := &HTTPServer{dashboardsnapshotsService: d} + d := setUpSnapshotTest(t, true) + hs := buildHttpServer(d, true) sc.handlerFunc = hs.GetDashboardSnapshot sc.fakeReqWithParams("GET", sc.url, map[string]string{"key": "12345"}).exec() assert.Equal(t, http.StatusInternalServerError, sc.resp.Code) }, sqlmock) + loggedInUserScenarioWithRole(t, + "GET /snapshots/{key} should return 403 when snapshot is disabled", "GET", + "/api/snapshots/12345", "/api/snapshots/:key", org.RoleEditor, func(sc *scenarioContext) { + d := setUpSnapshotTest(t, false) + hs := buildHttpServer(d, false) + sc.handlerFunc = hs.GetDashboardSnapshot + sc.fakeReqWithParams("GET", sc.url, map[string]string{"key": "12345"}).exec() + + assert.Equal(t, http.StatusForbidden, sc.resp.Code) + }, sqlmock) + loggedInUserScenarioWithRole(t, "DELETE /snapshots/{key} should return 404 when the snapshot does not exist", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", org.RoleEditor, func(sc *scenarioContext) { - d := setUpSnapshotTest(t) - hs := &HTTPServer{dashboardsnapshotsService: d} + d := setUpSnapshotTest(t, true) + hs := buildHttpServer(d, true) sc.handlerFunc = hs.DeleteDashboardSnapshot sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() assert.Equal(t, http.StatusInternalServerError, sc.resp.Code) }, sqlmock) + loggedInUserScenarioWithRole(t, + "DELETE /snapshots/{key} should return 403 when snapshot is disabled", "DELETE", + "/api/snapshots/12345", "/api/snapshots/:key", org.RoleEditor, func(sc *scenarioContext) { + d := setUpSnapshotTest(t, false) + hs := buildHttpServer(d, false) + sc.handlerFunc = hs.DeleteDashboardSnapshot + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() + + assert.Equal(t, http.StatusForbidden, sc.resp.Code) + }, sqlmock) + loggedInUserScenarioWithRole(t, "GET /snapshots-delete/{deleteKey} should return 404 when the snapshot does not exist", "DELETE", "/api/snapshots-delete/12345", "/api/snapshots-delete/:deleteKey", org.RoleEditor, func(sc *scenarioContext) { - d := setUpSnapshotTest(t) - hs := &HTTPServer{dashboardsnapshotsService: d} + d := setUpSnapshotTest(t, true) + hs := buildHttpServer(d, true) sc.handlerFunc = hs.DeleteDashboardSnapshotByDeleteKey sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"deleteKey": "12345"}).exec() assert.Equal(t, http.StatusInternalServerError, sc.resp.Code) }, sqlmock) + + loggedInUserScenarioWithRole(t, + "GET /snapshots-delete/{deleteKey} should return 403 when snapshot is disabled", "DELETE", + "/api/snapshots-delete/12345", "/api/snapshots-delete/:deleteKey", org.RoleEditor, func(sc *scenarioContext) { + d := setUpSnapshotTest(t, false) + hs := buildHttpServer(d, false) + sc.handlerFunc = hs.DeleteDashboardSnapshotByDeleteKey + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"deleteKey": "12345"}).exec() + + assert.Equal(t, http.StatusForbidden, sc.resp.Code) + }, sqlmock) +} + +func buildHttpServer(d dashboardsnapshots.Service, snapshotEnabled bool) *HTTPServer { + hs := &HTTPServer{ + dashboardsnapshotsService: d, + Cfg: &setting.Cfg{ + SnapshotEnabled: snapshotEnabled, + }, + } + return hs } diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 089b8f21a67..3c65148c382 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -209,6 +209,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i "samlEnabled": hs.samlEnabled(), "samlName": hs.samlName(), "tokenExpirationDayLimit": hs.Cfg.SATokenExpirationDayLimit, + "snapshotEnabled": hs.Cfg.SnapshotEnabled, } if hs.ThumbService != nil { diff --git a/pkg/services/dashboardsnapshots/database/database.go b/pkg/services/dashboardsnapshots/database/database.go index a303b3810a1..dab53376535 100644 --- a/pkg/services/dashboardsnapshots/database/database.go +++ b/pkg/services/dashboardsnapshots/database/database.go @@ -15,13 +15,14 @@ import ( type DashboardSnapshotStore struct { store db.DB log log.Logger + cfg *setting.Cfg } // DashboardStore implements the Store interface var _ dashboardsnapshots.Store = (*DashboardSnapshotStore)(nil) -func ProvideStore(db db.DB) *DashboardSnapshotStore { - return &DashboardSnapshotStore{store: db, log: log.New("dashboardsnapshot.store")} +func ProvideStore(db db.DB, cfg *setting.Cfg) *DashboardSnapshotStore { + return &DashboardSnapshotStore{store: db, log: log.New("dashboardsnapshot.store"), cfg: cfg} } // DeleteExpiredSnapshots removes snapshots with old expiry dates. @@ -29,7 +30,7 @@ func ProvideStore(db db.DB) *DashboardSnapshotStore { // Snapshot expiry is decided by the user when they share the snapshot. func (d *DashboardSnapshotStore) DeleteExpiredSnapshots(ctx context.Context, cmd *dashboardsnapshots.DeleteExpiredSnapshotsCommand) error { return d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error { - if !setting.SnapShotRemoveExpired { + if !d.cfg.SnapShotRemoveExpired { d.log.Warn("[Deprecated] The snapshot_remove_expired setting is outdated. Please remove from your config.") return nil } diff --git a/pkg/services/dashboardsnapshots/database/database_test.go b/pkg/services/dashboardsnapshots/database/database_test.go index 6df8b42bcca..63f90107a6f 100644 --- a/pkg/services/dashboardsnapshots/database/database_test.go +++ b/pkg/services/dashboardsnapshots/database/database_test.go @@ -23,7 +23,7 @@ func TestIntegrationDashboardSnapshotDBAccess(t *testing.T) { t.Skip("skipping integration test") } sqlstore := db.InitTestDB(t) - dashStore := ProvideStore(sqlstore) + dashStore := ProvideStore(sqlstore, setting.NewCfg()) origSecret := setting.SecretKey setting.SecretKey = "dashboard_snapshot_testing" @@ -154,10 +154,10 @@ func TestIntegrationDeleteExpiredSnapshots(t *testing.T) { t.Skip("skipping integration test") } sqlstore := db.InitTestDB(t) - dashStore := ProvideStore(sqlstore) + dashStore := ProvideStore(sqlstore, setting.NewCfg()) t.Run("Testing dashboard snapshots clean up", func(t *testing.T) { - setting.SnapShotRemoveExpired = true + dashStore.cfg.SnapShotRemoveExpired = true nonExpiredSnapshot := createTestSnapshot(t, dashStore, "key1", 48000) createTestSnapshot(t, dashStore, "key2", -1200) diff --git a/pkg/services/dashboardsnapshots/service/service_test.go b/pkg/services/dashboardsnapshots/service/service_test.go index 27ef9761518..d18e77cea67 100644 --- a/pkg/services/dashboardsnapshots/service/service_test.go +++ b/pkg/services/dashboardsnapshots/service/service_test.go @@ -17,7 +17,7 @@ import ( func TestDashboardSnapshotsService(t *testing.T) { sqlStore := db.InitTestDB(t) - dsStore := dashsnapdb.ProvideStore(sqlStore) + dsStore := dashsnapdb.ProvideStore(sqlStore, setting.NewCfg()) secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(sqlStore)) s := ProvideService(dsStore, secretsService) diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index e7b48fe4a04..00740daa92c 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -376,13 +376,15 @@ func (s *ServiceImpl) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm b }) if c.IsSignedIn { - dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{ - Text: "Snapshots", - SubTitle: "Interactive, publically available, point-in-time representations of dashboards", - Id: "dashboards/snapshots", - Url: s.cfg.AppSubURL + "/dashboard/snapshots", - Icon: "camera", - }) + if s.cfg.SnapshotEnabled { + dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{ + Text: "Snapshots", + SubTitle: "Interactive, publically available, point-in-time representations of dashboards", + Id: "dashboards/snapshots", + Url: s.cfg.AppSubURL + "/dashboard/snapshots", + Icon: "camera", + }) + } dashboardChildNavs = append(dashboardChildNavs, &navtree.NavLink{ Text: "Library panels", diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 80830297702..e4f6d211b68 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -87,12 +87,6 @@ var ( CookieSameSiteDisabled bool CookieSameSiteMode http.SameSite - // Snapshots - ExternalSnapshotUrl string - ExternalSnapshotName string - ExternalEnabled bool - SnapShotRemoveExpired bool - // Dashboard history DashboardVersionsToKeep int MinRefreshInterval string @@ -407,6 +401,12 @@ type Cfg struct { DataSourceLimit int // Snapshots + SnapshotEnabled bool + ExternalSnapshotUrl string + ExternalSnapshotName string + ExternalEnabled bool + SnapShotRemoveExpired bool + SnapshotPublicMode bool ErrTemplateName string @@ -1702,11 +1702,13 @@ func IsLegacyAlertingEnabled() bool { func readSnapshotsSettings(cfg *Cfg, iniFile *ini.File) error { snapshots := iniFile.Section("snapshots") - ExternalSnapshotUrl = valueAsString(snapshots, "external_snapshot_url", "") - ExternalSnapshotName = valueAsString(snapshots, "external_snapshot_name", "") + cfg.SnapshotEnabled = snapshots.Key("enabled").MustBool(true) - ExternalEnabled = snapshots.Key("external_enabled").MustBool(true) - SnapShotRemoveExpired = snapshots.Key("snapshot_remove_expired").MustBool(true) + cfg.ExternalSnapshotUrl = valueAsString(snapshots, "external_snapshot_url", "") + cfg.ExternalSnapshotName = valueAsString(snapshots, "external_snapshot_name", "") + + cfg.ExternalEnabled = snapshots.Key("external_enabled").MustBool(true) + cfg.SnapShotRemoveExpired = snapshots.Key("snapshot_remove_expired").MustBool(true) cfg.SnapshotPublicMode = snapshots.Key("public_mode").MustBool(false) return nil diff --git a/public/app/features/dashboard/components/ShareModal/ShareModal.tsx b/public/app/features/dashboard/components/ShareModal/ShareModal.tsx index 8064761de1e..357effdee2f 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareModal.tsx +++ b/public/app/features/dashboard/components/ShareModal/ShareModal.tsx @@ -27,22 +27,11 @@ export function addPanelShareTab(tab: ShareModalTabModel) { customPanelTabs.push(tab); } -function getInitialState(props: Props): State { - const { tabs, activeTab } = getTabs(props); - - return { - tabs, - activeTab, - }; -} - -function getTabs(props: Props) { - const { panel, activeTab } = props; - +function getTabs(panel?: PanelModel, activeTab?: string) { const linkLabel = t('share-modal.tab-title.link', 'Link'); const tabs: ShareModalTabModel[] = [{ label: linkLabel, value: 'link', component: ShareLink }]; - if (contextSrv.isSignedIn) { + if (contextSrv.isSignedIn && config.snapshotEnabled) { const snapshotLabel = t('share-modal.tab-title.snapshot', 'Snapshot'); tabs.push({ label: snapshotLabel, value: 'snapshot', component: ShareSnapshot }); } @@ -87,6 +76,15 @@ interface State { activeTab: string; } +function getInitialState(props: Props): State { + const { tabs, activeTab } = getTabs(props.panel, props.activeTab); + + return { + tabs, + activeTab, + }; +} + export class ShareModal extends React.Component { constructor(props: Props) { super(props); @@ -98,13 +96,9 @@ export class ShareModal extends React.Component { } onSelectTab = (t: any) => { - this.setState({ activeTab: t.value }); + this.setState((prevState) => ({ ...prevState, activeTab: t.value })); }; - getTabs() { - return getTabs(this.props).tabs; - } - getActiveTab() { const { tabs, activeTab } = this.state; return tabs.find((t) => t.value === activeTab)!; @@ -114,12 +108,13 @@ export class ShareModal extends React.Component { const { panel } = this.props; const { activeTab } = this.state; const title = panel ? t('share-modal.panel.title', 'Share Panel') : t('share-modal.dashboard.title', 'Share'); + const tabs = getTabs(this.props.panel, this.state.activeTab).tabs; return ( From c5cb5be3cc078cc99b0324ae0db429646e0603ad Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Thu, 26 Jan 2023 14:42:50 +0100 Subject: [PATCH 081/172] Auth: Fix catch both both ErrInvalidAPIKey for context with APIKey (#62193) * fix: capture both ErrInvalidAPIKey * rename of variable --- pkg/services/contexthandler/contexthandler.go | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index f2c49b26bf9..d9351fca9f4 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -306,13 +306,13 @@ func (h *ContextHandler) initContextWithAPIKey(reqContext *models.ReqContext) bo *reqContext.Req = *reqContext.Req.WithContext(ctx) var ( - apikey *apikey.APIKey + apiKey *apikey.APIKey errKey error ) if strings.HasPrefix(keyString, apikeygenprefix.GrafanaPrefix) { - apikey, errKey = h.getPrefixedAPIKey(reqContext.Req.Context(), keyString) // decode prefixed key + apiKey, errKey = h.getPrefixedAPIKey(reqContext.Req.Context(), keyString) // decode prefixed key } else { - apikey, errKey = h.getAPIKey(reqContext.Req.Context(), keyString) // decode legacy api key + apiKey, errKey = h.getAPIKey(reqContext.Req.Context(), keyString) // decode legacy api key } if errKey != nil { @@ -320,6 +320,11 @@ func (h *ContextHandler) initContextWithAPIKey(reqContext *models.ReqContext) bo if errors.Is(errKey, apikeygen.ErrInvalidApiKey) { status = http.StatusUnauthorized } + // this is when the getPrefixAPIKey return error form the apikey package instead of the apikeygen + // when called in the sqlx store methods + if errors.Is(errKey, apikey.ErrInvalid) { + status = http.StatusUnauthorized + } reqContext.JsonApiErr(status, InvalidAPIKey, errKey) return true } @@ -329,12 +334,12 @@ func (h *ContextHandler) initContextWithAPIKey(reqContext *models.ReqContext) bo if getTime == nil { getTime = time.Now } - if apikey.Expires != nil && *apikey.Expires <= getTime().Unix() { + if apiKey.Expires != nil && *apiKey.Expires <= getTime().Unix() { reqContext.JsonApiErr(http.StatusUnauthorized, "Expired API key", nil) return true } - if apikey.IsRevoked != nil && *apikey.IsRevoked { + if apiKey.IsRevoked != nil && *apiKey.IsRevoked { reqContext.JsonApiErr(http.StatusUnauthorized, "Revoked token", nil) return true @@ -350,15 +355,15 @@ func (h *ContextHandler) initContextWithAPIKey(reqContext *models.ReqContext) bo if err := h.apiKeyService.UpdateAPIKeyLastUsedDate(context.Background(), id); err != nil { reqContext.Logger.Warn("failed to update last use date for api key", "id", id) } - }(apikey.Id) + }(apiKey.Id) - if apikey.ServiceAccountId == nil || *apikey.ServiceAccountId < 1 { //There is no service account attached to the apikey + if apiKey.ServiceAccountId == nil || *apiKey.ServiceAccountId < 1 { //There is no service account attached to the apikey // Use the old APIkey method. This provides backwards compatibility. // will probably have to be supported for a long time. reqContext.SignedInUser = &user.SignedInUser{} - reqContext.OrgRole = apikey.Role - reqContext.ApiKeyID = apikey.Id - reqContext.OrgID = apikey.OrgId + reqContext.OrgRole = apiKey.Role + reqContext.ApiKeyID = apiKey.Id + reqContext.OrgID = apiKey.OrgId reqContext.IsSignedIn = true return true } @@ -366,7 +371,7 @@ func (h *ContextHandler) initContextWithAPIKey(reqContext *models.ReqContext) bo //There is a service account attached to the API key //Use service account linked to API key as the signed in user - querySignedInUser := user.GetSignedInUserQuery{UserID: *apikey.ServiceAccountId, OrgID: apikey.OrgId} + querySignedInUser := user.GetSignedInUserQuery{UserID: *apiKey.ServiceAccountId, OrgID: apiKey.OrgId} querySignedInUserResult, err := h.userService.GetSignedInUserWithCacheCtx(reqContext.Req.Context(), &querySignedInUser) if err != nil { reqContext.Logger.Error( From e8b8a9e2761335e327ad8e6f46b699d15fa44e6e Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Thu, 26 Jan 2023 08:46:30 -0500 Subject: [PATCH 082/172] chore: move dashboard_acl models into dashboard service (#62151) --- pkg/api/alerting.go | 2 +- pkg/api/annotations_test.go | 4 +- pkg/api/dashboard_permission.go | 10 +- pkg/api/dashboard_permission_test.go | 44 ++-- pkg/api/dashboard_snapshot_test.go | 9 +- pkg/api/dashboard_test.go | 16 +- pkg/api/dtos/acl.go | 4 +- pkg/api/folder.go | 2 +- pkg/api/folder_permission.go | 12 +- pkg/api/folder_permission_test.go | 42 +-- pkg/api/org_users_test.go | 5 +- pkg/api/search.go | 4 +- pkg/api/team.go | 3 +- pkg/api/team_members.go | 3 +- pkg/api/team_members_test.go | 18 +- pkg/infra/db/sqlbuilder.go | 4 +- pkg/infra/db/sqlbuilder_test.go | 65 +++-- pkg/models/search.go | 21 -- .../accesscontrol/database/database_test.go | 6 +- .../ossaccesscontrol/permissions_services.go | 3 +- pkg/services/alerting/store.go | 4 +- .../annotations/annotationsimpl/xorm_store.go | 4 +- pkg/services/dashboards/dashboard.go | 7 +- .../dashboards}/dashboard_acl.go | 2 +- .../dashboards}/dashboard_acl_test.go | 2 +- .../dashboards/dashboard_service_mock.go | 12 +- pkg/services/dashboards/database/acl.go | 5 +- pkg/services/dashboards/database/acl_test.go | 23 +- pkg/services/dashboards/database/database.go | 7 +- .../database/database_folder_test.go | 55 ++-- .../dashboards/database/database_test.go | 28 +- pkg/services/dashboards/models.go | 56 ++-- .../dashboards/service/dashboard_service.go | 18 +- pkg/services/dashboards/store_mock.go | 16 +- pkg/services/folder/folderimpl/folder.go | 13 +- .../guardian/accesscontrol_guardian.go | 11 +- pkg/services/guardian/guardian.go | 27 +- pkg/services/guardian/guardian_test.go | 249 +++++++++--------- pkg/services/guardian/guardian_util_test.go | 15 +- pkg/services/libraryelements/database.go | 6 +- .../libraryelements_permissions_test.go | 16 +- .../libraryelements/libraryelements_test.go | 2 +- .../librarypanels/librarypanels_test.go | 2 +- pkg/services/ngalert/store/alert_rule.go | 5 +- pkg/services/search/service.go | 4 +- pkg/services/search/service_test.go | 4 +- pkg/services/searchV2/auth.go | 6 +- .../accesscontrol/dashboard_permissions.go | 33 ++- .../accesscontrol/team_membership.go | 8 +- .../migrations/accesscontrol/test/ac_test.go | 8 +- .../sqlstore/migrations/ualert/permissions.go | 6 +- .../sqlstore/permissions/dashboard.go | 9 +- .../sqlstore/permissions/dashboard_test.go | 26 +- .../permissions/dashboards_bench_test.go | 8 +- .../sqlstore/searchstore/search_test.go | 3 +- pkg/services/stats/statsimpl/stats.go | 11 +- pkg/services/team/model.go | 60 ++--- pkg/services/team/team.go | 4 +- pkg/services/team/teamimpl/store.go | 16 +- pkg/services/team/teamimpl/store_test.go | 23 +- pkg/services/team/teamimpl/team.go | 4 +- pkg/services/team/teamtest/team.go | 4 +- pkg/services/teamguardian/manager/service.go | 4 +- .../teamguardian/manager/service_test.go | 11 +- pkg/services/user/userimpl/store_test.go | 11 +- 65 files changed, 553 insertions(+), 572 deletions(-) rename pkg/{models => services/dashboards}/dashboard_acl.go (98%) rename pkg/{models => services/dashboards}/dashboard_acl_test.go (96%) diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index e8f0404498f..86fa1884d80 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -110,7 +110,7 @@ func (hs *HTTPServer) GetAlerts(c *models.ReqContext) response.Response { DashboardIds: dashboardIDs, Type: string(models.DashHitDB), FolderIds: folderIDs, - Permission: models.PERMISSION_VIEW, + Permission: dashboards.PERMISSION_VIEW, } err := hs.SearchService.SearchHandler(c.Req.Context(), &searchQuery) diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go index b8c190c62be..f5c015f03ff 100644 --- a/pkg/api/annotations_test.go +++ b/pkg/api/annotations_test.go @@ -681,8 +681,8 @@ func setUpACL() { teamSvc := &teamtest.FakeService{} dashSvc := &dashboards.FakeDashboardService{} qResult := []*dashboards.DashboardACLInfoDTO{ - {Role: &viewerRole, Permission: models.PERMISSION_VIEW}, - {Role: &editorRole, Permission: models.PERMISSION_EDIT}, + {Role: &viewerRole, Permission: dashboards.PERMISSION_VIEW}, + {Role: &editorRole, Permission: dashboards.PERMISSION_EDIT}, } dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Run(func(args mock.Arguments) { // q := args.Get(1).(*dashboards.GetDashboardACLInfoListQuery) diff --git a/pkg/api/dashboard_permission.go b/pkg/api/dashboard_permission.go index 6c4608bb141..6548547ee14 100644 --- a/pkg/api/dashboard_permission.go +++ b/pkg/api/dashboard_permission.go @@ -176,7 +176,7 @@ func (hs *HTTPServer) UpdateDashboardPermissions(c *models.ReqContext) response. } items = append(items, hiddenACL...) - if okToUpdate, err := g.CheckPermissionBeforeUpdate(models.PERMISSION_ADMIN, items); err != nil || !okToUpdate { + if okToUpdate, err := g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, items); err != nil || !okToUpdate { if err != nil { if errors.Is(err, guardian.ErrGuardianPermissionExists) || errors.Is(err, guardian.ErrGuardianOverride) { return response.Error(400, err.Error(), err) @@ -200,8 +200,8 @@ func (hs *HTTPServer) UpdateDashboardPermissions(c *models.ReqContext) response. } if err := hs.DashboardService.UpdateDashboardACL(c.Req.Context(), dashID, items); err != nil { - if errors.Is(err, models.ErrDashboardACLInfoMissing) || - errors.Is(err, models.ErrDashboardPermissionDashboardEmpty) { + if errors.Is(err, dashboards.ErrDashboardACLInfoMissing) || + errors.Is(err, dashboards.ErrDashboardPermissionDashboardEmpty) { return response.Error(409, err.Error(), err) } return response.Error(500, "Failed to create permission", err) @@ -275,11 +275,11 @@ func (hs *HTTPServer) updateDashboardAccessControl(ctx context.Context, orgID in func validatePermissionsUpdate(apiCmd dtos.UpdateDashboardACLCommand) error { for _, item := range apiCmd.Items { if item.UserID > 0 && item.TeamID > 0 { - return models.ErrPermissionsWithUserAndTeamNotAllowed + return dashboards.ErrPermissionsWithUserAndTeamNotAllowed } if (item.UserID > 0 || item.TeamID > 0) && item.Role != nil { - return models.ErrPermissionsWithRoleNotAllowed + return dashboards.ErrPermissionsWithRoleNotAllowed } } return nil diff --git a/pkg/api/dashboard_permission_test.go b/pkg/api/dashboard_permission_test.go index 965dacc464a..4ae44e3a02e 100644 --- a/pkg/api/dashboard_permission_test.go +++ b/pkg/api/dashboard_permission_test.go @@ -67,7 +67,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { cmd := dtos.UpdateDashboardACLCommand{ Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, Permission: models.PERMISSION_ADMIN}, + {UserID: 1000, Permission: dashboards.PERMISSION_ADMIN}, }, } @@ -94,11 +94,11 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { CanAdminValue: true, CheckPermissionBeforeUpdateValue: true, GetACLValue: []*dashboards.DashboardACLInfoDTO{ - {OrgID: 1, DashboardID: 1, UserID: 2, Permission: models.PERMISSION_VIEW}, - {OrgID: 1, DashboardID: 1, UserID: 3, Permission: models.PERMISSION_EDIT}, - {OrgID: 1, DashboardID: 1, UserID: 4, Permission: models.PERMISSION_ADMIN}, - {OrgID: 1, DashboardID: 1, TeamID: 1, Permission: models.PERMISSION_VIEW}, - {OrgID: 1, DashboardID: 1, TeamID: 2, Permission: models.PERMISSION_ADMIN}, + {OrgID: 1, DashboardID: 1, UserID: 2, Permission: dashboards.PERMISSION_VIEW}, + {OrgID: 1, DashboardID: 1, UserID: 3, Permission: dashboards.PERMISSION_EDIT}, + {OrgID: 1, DashboardID: 1, UserID: 4, Permission: dashboards.PERMISSION_ADMIN}, + {OrgID: 1, DashboardID: 1, TeamID: 1, Permission: dashboards.PERMISSION_VIEW}, + {OrgID: 1, DashboardID: 1, TeamID: 2, Permission: dashboards.PERMISSION_ADMIN}, }, }) @@ -113,12 +113,12 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { assert.Len(t, resp, 5) assert.Equal(t, int64(2), resp[0].UserID) - assert.Equal(t, models.PERMISSION_VIEW, resp[0].Permission) + assert.Equal(t, dashboards.PERMISSION_VIEW, resp[0].Permission) }, mockSQLStore) cmd := dtos.UpdateDashboardACLCommand{ Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, Permission: models.PERMISSION_ADMIN}, + {UserID: 1000, Permission: dashboards.PERMISSION_ADMIN}, }, } @@ -147,7 +147,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { cmd := dtos.UpdateDashboardACLCommand{ Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, TeamID: 1, Permission: models.PERMISSION_ADMIN}, + {UserID: 1000, TeamID: 1, Permission: dashboards.PERMISSION_ADMIN}, }, } @@ -161,7 +161,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { assert.Equal(t, 400, sc.resp.Code) respJSON, err := jsonMap(sc.resp.Body.Bytes()) require.NoError(t, err) - assert.Equal(t, models.ErrPermissionsWithUserAndTeamNotAllowed.Error(), respJSON["error"]) + assert.Equal(t, dashboards.ErrPermissionsWithUserAndTeamNotAllowed.Error(), respJSON["error"]) }, }, hs) }) @@ -179,7 +179,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { cmd := dtos.UpdateDashboardACLCommand{ Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, Permission: models.PERMISSION_ADMIN}, + {UserID: 1000, Permission: dashboards.PERMISSION_ADMIN}, }, } @@ -200,12 +200,12 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { cmds := []dtos.UpdateDashboardACLCommand{ { Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, Permission: models.PERMISSION_ADMIN, Role: &role}, + {UserID: 1000, Permission: dashboards.PERMISSION_ADMIN, Role: &role}, }, }, { Items: []dtos.DashboardACLUpdateItem{ - {TeamID: 1000, Permission: models.PERMISSION_ADMIN, Role: &role}, + {TeamID: 1000, Permission: dashboards.PERMISSION_ADMIN, Role: &role}, }, }, } @@ -221,7 +221,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { assert.Equal(t, 400, sc.resp.Code) respJSON, err := jsonMap(sc.resp.Body.Bytes()) require.NoError(t, err) - assert.Equal(t, models.ErrPermissionsWithRoleNotAllowed.Error(), respJSON["error"]) + assert.Equal(t, dashboards.ErrPermissionsWithRoleNotAllowed.Error(), respJSON["error"]) }, }, hs) } @@ -241,7 +241,7 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { cmd := dtos.UpdateDashboardACLCommand{ Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, Permission: models.PERMISSION_ADMIN}, + {UserID: 1000, Permission: dashboards.PERMISSION_ADMIN}, }, } @@ -277,12 +277,12 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { CanAdminValue: true, CheckPermissionBeforeUpdateValue: true, GetACLValue: []*dashboards.DashboardACLInfoDTO{ - {OrgID: 1, DashboardID: 1, UserID: 2, UserLogin: "hiddenUser", Permission: models.PERMISSION_VIEW}, - {OrgID: 1, DashboardID: 1, UserID: 3, UserLogin: testUserLogin, Permission: models.PERMISSION_EDIT}, - {OrgID: 1, DashboardID: 1, UserID: 4, UserLogin: "user_1", Permission: models.PERMISSION_ADMIN}, + {OrgID: 1, DashboardID: 1, UserID: 2, UserLogin: "hiddenUser", Permission: dashboards.PERMISSION_VIEW}, + {OrgID: 1, DashboardID: 1, UserID: 3, UserLogin: testUserLogin, Permission: dashboards.PERMISSION_EDIT}, + {OrgID: 1, DashboardID: 1, UserID: 4, UserLogin: "user_1", Permission: dashboards.PERMISSION_ADMIN}, }, GetHiddenACLValue: []*dashboards.DashboardACL{ - {OrgID: 1, DashboardID: 1, UserID: 2, Permission: models.PERMISSION_VIEW}, + {OrgID: 1, DashboardID: 1, UserID: 2, Permission: dashboards.PERMISSION_VIEW}, }, }) @@ -294,14 +294,14 @@ func TestDashboardPermissionAPIEndpoint(t *testing.T) { assert.Len(t, resp, 2) assert.Equal(t, int64(3), resp[0].UserID) - assert.Equal(t, models.PERMISSION_EDIT, resp[0].Permission) + assert.Equal(t, dashboards.PERMISSION_EDIT, resp[0].Permission) assert.Equal(t, int64(4), resp[1].UserID) - assert.Equal(t, models.PERMISSION_ADMIN, resp[1].Permission) + assert.Equal(t, dashboards.PERMISSION_ADMIN, resp[1].Permission) }, mockSQLStore) cmd := dtos.UpdateDashboardACLCommand{ Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, Permission: models.PERMISSION_ADMIN}, + {UserID: 1000, Permission: dashboards.PERMISSION_ADMIN}, }, } for _, acl := range resp { diff --git a/pkg/api/dashboard_snapshot_test.go b/pkg/api/dashboard_snapshot_test.go index d0e142c391f..9512506ef7c 100644 --- a/pkg/api/dashboard_snapshot_test.go +++ b/pkg/api/dashboard_snapshot_test.go @@ -15,7 +15,6 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db/dbtest" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboardsnapshots" "github.com/grafana/grafana/pkg/services/guardian" @@ -120,8 +119,8 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { teamSvc := &teamtest.FakeService{} dashSvc := &dashboards.FakeDashboardService{} qResult := []*dashboards.DashboardACLInfoDTO{ - {Role: &viewerRole, Permission: models.PERMISSION_VIEW}, - {Role: &editorRole, Permission: models.PERMISSION_EDIT}, + {Role: &viewerRole, Permission: dashboards.PERMISSION_VIEW}, + {Role: &editorRole, Permission: dashboards.PERMISSION_EDIT}, } dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult, nil) @@ -136,8 +135,8 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { qResult := &dashboards.Dashboard{} dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(qResult, nil).Maybe() qResultACL := []*dashboards.DashboardACLInfoDTO{ - {Role: &viewerRole, Permission: models.PERMISSION_VIEW}, - {Role: &editorRole, Permission: models.PERMISSION_EDIT}, + {Role: &viewerRole, Permission: dashboards.PERMISSION_VIEW}, + {Role: &editorRole, Permission: dashboards.PERMISSION_EDIT}, } dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResultACL, nil) guardian.InitLegacyGuardian(sc.sqlStore, dashSvc, teamSvc) diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index e9cb683e200..0623d413707 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -159,8 +159,8 @@ func TestDashboardAPIEndpoint(t *testing.T) { viewerRole := org.RoleViewer editorRole := org.RoleEditor qResult := []*dashboards.DashboardACLInfoDTO{ - {Role: &viewerRole, Permission: models.PERMISSION_VIEW}, - {Role: &editorRole, Permission: models.PERMISSION_EDIT}, + {Role: &viewerRole, Permission: dashboards.PERMISSION_VIEW}, + {Role: &editorRole, Permission: dashboards.PERMISSION_EDIT}, } dashboardService.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult, nil) guardian.InitLegacyGuardian(mockSQLStore, dashboardService, teamService) @@ -248,7 +248,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { qResult := []*dashboards.DashboardACLInfoDTO{ { DashboardID: 1, - Permission: models.PERMISSION_EDIT, + Permission: dashboards.PERMISSION_EDIT, UserID: 200, }, } @@ -376,7 +376,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { dashboardService := dashboards.NewFakeDashboardService(t) qResult := []*dashboards.DashboardACLInfoDTO{ - {OrgID: 1, DashboardID: 2, UserID: 1, Permission: models.PERMISSION_EDIT}, + {OrgID: 1, DashboardID: 2, UserID: 1, Permission: dashboards.PERMISSION_EDIT}, } dashboardService.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult, nil) guardian.InitLegacyGuardian(mockSQLStore, dashboardService, teamService) @@ -434,7 +434,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { dashboardService := dashboards.NewFakeDashboardService(t) qResult := []*dashboards.DashboardACLInfoDTO{ - {OrgID: 1, DashboardID: 2, UserID: 1, Permission: models.PERMISSION_VIEW}, + {OrgID: 1, DashboardID: 2, UserID: 1, Permission: dashboards.PERMISSION_VIEW}, } dashboardService.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult, nil) guardian.InitLegacyGuardian(mockSQLStore, dashboardService, teamService) @@ -472,7 +472,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { dashboardService := dashboards.NewFakeDashboardService(t) qResult := []*dashboards.DashboardACLInfoDTO{ - {OrgID: 1, DashboardID: 2, UserID: 1, Permission: models.PERMISSION_ADMIN}, + {OrgID: 1, DashboardID: 2, UserID: 1, Permission: dashboards.PERMISSION_ADMIN}, } dashboardService.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult, nil) guardian.InitLegacyGuardian(mockSQLStore, dashboardService, teamService) @@ -521,7 +521,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { setUpInner := func() { dashboardService := dashboards.NewFakeDashboardService(t) qResult := []*dashboards.DashboardACLInfoDTO{ - {OrgID: 1, DashboardID: 2, UserID: 1, Permission: models.PERMISSION_VIEW}, + {OrgID: 1, DashboardID: 2, UserID: 1, Permission: dashboards.PERMISSION_VIEW}, } dashboardService.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult, nil) guardian.InitLegacyGuardian(mockSQLStore, dashboardService, teamService) @@ -910,7 +910,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { require.NoError(t, err) qResult := &dashboards.Dashboard{ID: 1, Data: dataValue} dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(qResult, nil) - qResult2 := []*dashboards.DashboardACLInfoDTO{{OrgID: testOrgID, DashboardID: 1, UserID: testUserID, Permission: models.PERMISSION_EDIT}} + qResult2 := []*dashboards.DashboardACLInfoDTO{{OrgID: testOrgID, DashboardID: 1, UserID: testUserID, Permission: dashboards.PERMISSION_EDIT}} dashboardService.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult2, nil) guardian.InitLegacyGuardian(mockSQLStore, dashboardService, teamService) diff --git a/pkg/api/dtos/acl.go b/pkg/api/dtos/acl.go index ae0f8e9f266..d6f4114157a 100644 --- a/pkg/api/dtos/acl.go +++ b/pkg/api/dtos/acl.go @@ -1,7 +1,7 @@ package dtos import ( - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" ) @@ -21,5 +21,5 @@ type DashboardACLUpdateItem struct { // * `2` - Edit // * `4` - Admin // Enum: 1,2,4 - Permission models.PermissionType `json:"permission"` + Permission dashboards.PermissionType `json:"permission"` } diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 1a9aa95afcf..20cede8a574 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -301,7 +301,7 @@ func (hs *HTTPServer) searchFolders(c *models.ReqContext) ([]*folder.Folder, err Limit: c.QueryInt64("limit"), OrgId: c.OrgID, Type: "dash-folder", - Permission: models.PERMISSION_VIEW, + Permission: dashboards.PERMISSION_VIEW, Page: c.QueryInt64("page"), } diff --git a/pkg/api/folder_permission.go b/pkg/api/folder_permission.go index ae1ad246c56..530d0a4fe17 100644 --- a/pkg/api/folder_permission.go +++ b/pkg/api/folder_permission.go @@ -132,7 +132,7 @@ func (hs *HTTPServer) UpdateFolderPermissions(c *models.ReqContext) response.Res } items = append(items, hiddenACL...) - if okToUpdate, err := g.CheckPermissionBeforeUpdate(models.PERMISSION_ADMIN, items); err != nil || !okToUpdate { + if okToUpdate, err := g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, items); err != nil || !okToUpdate { if err != nil { if errors.Is(err, guardian.ErrGuardianPermissionExists) || errors.Is(err, guardian.ErrGuardianOverride) { @@ -157,14 +157,14 @@ func (hs *HTTPServer) UpdateFolderPermissions(c *models.ReqContext) response.Res } if err := hs.DashboardService.UpdateDashboardACL(c.Req.Context(), folder.ID, items); err != nil { - if errors.Is(err, models.ErrDashboardACLInfoMissing) { - err = models.ErrFolderACLInfoMissing + if errors.Is(err, dashboards.ErrDashboardACLInfoMissing) { + err = dashboards.ErrFolderACLInfoMissing } - if errors.Is(err, models.ErrDashboardPermissionDashboardEmpty) { - err = models.ErrFolderPermissionFolderEmpty + if errors.Is(err, dashboards.ErrDashboardPermissionDashboardEmpty) { + err = dashboards.ErrFolderPermissionFolderEmpty } - if errors.Is(err, models.ErrFolderACLInfoMissing) || errors.Is(err, models.ErrFolderPermissionFolderEmpty) { + if errors.Is(err, dashboards.ErrFolderACLInfoMissing) || errors.Is(err, dashboards.ErrFolderPermissionFolderEmpty) { return response.Error(409, err.Error(), err) } diff --git a/pkg/api/folder_permission_test.go b/pkg/api/folder_permission_test.go index 57ce79e2cf4..9f30eb92f24 100644 --- a/pkg/api/folder_permission_test.go +++ b/pkg/api/folder_permission_test.go @@ -64,7 +64,7 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { cmd := dtos.UpdateDashboardACLCommand{ Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, Permission: models.PERMISSION_ADMIN}, + {UserID: 1000, Permission: dashboards.PERMISSION_ADMIN}, }, } @@ -98,7 +98,7 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { cmd := dtos.UpdateDashboardACLCommand{ Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, Permission: models.PERMISSION_ADMIN}, + {UserID: 1000, Permission: dashboards.PERMISSION_ADMIN}, }, } @@ -124,11 +124,11 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { CanAdminValue: true, CheckPermissionBeforeUpdateValue: true, GetACLValue: []*dashboards.DashboardACLInfoDTO{ - {OrgID: 1, DashboardID: 1, UserID: 2, Permission: models.PERMISSION_VIEW}, - {OrgID: 1, DashboardID: 1, UserID: 3, Permission: models.PERMISSION_EDIT}, - {OrgID: 1, DashboardID: 1, UserID: 4, Permission: models.PERMISSION_ADMIN}, - {OrgID: 1, DashboardID: 1, TeamID: 1, Permission: models.PERMISSION_VIEW}, - {OrgID: 1, DashboardID: 1, TeamID: 2, Permission: models.PERMISSION_ADMIN}, + {OrgID: 1, DashboardID: 1, UserID: 2, Permission: dashboards.PERMISSION_VIEW}, + {OrgID: 1, DashboardID: 1, UserID: 3, Permission: dashboards.PERMISSION_EDIT}, + {OrgID: 1, DashboardID: 1, UserID: 4, Permission: dashboards.PERMISSION_ADMIN}, + {OrgID: 1, DashboardID: 1, TeamID: 1, Permission: dashboards.PERMISSION_VIEW}, + {OrgID: 1, DashboardID: 1, TeamID: 2, Permission: dashboards.PERMISSION_ADMIN}, }, }) @@ -146,12 +146,12 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { assert.Len(t, resp, 5) assert.Equal(t, int64(2), resp[0].UserID) - assert.Equal(t, models.PERMISSION_VIEW, resp[0].Permission) + assert.Equal(t, dashboards.PERMISSION_VIEW, resp[0].Permission) }, mockSQLStore) cmd := dtos.UpdateDashboardACLCommand{ Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, Permission: models.PERMISSION_ADMIN}, + {UserID: 1000, Permission: dashboards.PERMISSION_ADMIN}, }, } @@ -193,7 +193,7 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { cmd := dtos.UpdateDashboardACLCommand{ Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, Permission: models.PERMISSION_ADMIN}, + {UserID: 1000, Permission: dashboards.PERMISSION_ADMIN}, }, } @@ -214,12 +214,12 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { cmds := []dtos.UpdateDashboardACLCommand{ { Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, Permission: models.PERMISSION_ADMIN, Role: &role}, + {UserID: 1000, Permission: dashboards.PERMISSION_ADMIN, Role: &role}, }, }, { Items: []dtos.DashboardACLUpdateItem{ - {TeamID: 1000, Permission: models.PERMISSION_ADMIN, Role: &role}, + {TeamID: 1000, Permission: dashboards.PERMISSION_ADMIN, Role: &role}, }, }, } @@ -235,7 +235,7 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { assert.Equal(t, 400, sc.resp.Code) respJSON, err := jsonMap(sc.resp.Body.Bytes()) require.NoError(t, err) - assert.Equal(t, models.ErrPermissionsWithRoleNotAllowed.Error(), respJSON["error"]) + assert.Equal(t, dashboards.ErrPermissionsWithRoleNotAllowed.Error(), respJSON["error"]) }, }, hs) } @@ -257,7 +257,7 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { cmd := dtos.UpdateDashboardACLCommand{ Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, Permission: models.PERMISSION_ADMIN}, + {UserID: 1000, Permission: dashboards.PERMISSION_ADMIN}, }, } @@ -288,12 +288,12 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { CanAdminValue: true, CheckPermissionBeforeUpdateValue: true, GetACLValue: []*dashboards.DashboardACLInfoDTO{ - {OrgID: 1, DashboardID: 1, UserID: 2, UserLogin: "hiddenUser", Permission: models.PERMISSION_VIEW}, - {OrgID: 1, DashboardID: 1, UserID: 3, UserLogin: testUserLogin, Permission: models.PERMISSION_EDIT}, - {OrgID: 1, DashboardID: 1, UserID: 4, UserLogin: "user_1", Permission: models.PERMISSION_ADMIN}, + {OrgID: 1, DashboardID: 1, UserID: 2, UserLogin: "hiddenUser", Permission: dashboards.PERMISSION_VIEW}, + {OrgID: 1, DashboardID: 1, UserID: 3, UserLogin: testUserLogin, Permission: dashboards.PERMISSION_EDIT}, + {OrgID: 1, DashboardID: 1, UserID: 4, UserLogin: "user_1", Permission: dashboards.PERMISSION_ADMIN}, }, GetHiddenACLValue: []*dashboards.DashboardACL{ - {OrgID: 1, DashboardID: 1, UserID: 2, Permission: models.PERMISSION_VIEW}, + {OrgID: 1, DashboardID: 1, UserID: 2, Permission: dashboards.PERMISSION_VIEW}, }, }) @@ -315,14 +315,14 @@ func TestFolderPermissionAPIEndpoint(t *testing.T) { assert.Len(t, resp, 2) assert.Equal(t, int64(3), resp[0].UserID) - assert.Equal(t, models.PERMISSION_EDIT, resp[0].Permission) + assert.Equal(t, dashboards.PERMISSION_EDIT, resp[0].Permission) assert.Equal(t, int64(4), resp[1].UserID) - assert.Equal(t, models.PERMISSION_ADMIN, resp[1].Permission) + assert.Equal(t, dashboards.PERMISSION_ADMIN, resp[1].Permission) }, mockSQLStore) cmd := dtos.UpdateDashboardACLCommand{ Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, Permission: models.PERMISSION_ADMIN}, + {UserID: 1000, Permission: dashboards.PERMISSION_ADMIN}, }, } for _, acl := range resp { diff --git a/pkg/api/org_users_test.go b/pkg/api/org_users_test.go index 5b704ee8bb7..47902f5045d 100644 --- a/pkg/api/org_users_test.go +++ b/pkg/api/org_users_test.go @@ -17,7 +17,6 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/localcache" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -224,7 +223,7 @@ func TestOrgUsersAPIEndpoint_LegacyAccessControl_FolderAdmin(t *testing.T) { DashboardID: folder.ID, OrgID: testOrgID, UserID: testUserID, - Permission: models.PERMISSION_ADMIN, + Permission: dashboards.PERMISSION_ADMIN, Created: time.Now(), Updated: time.Now(), }, @@ -245,7 +244,7 @@ func TestOrgUsersAPIEndpoint_LegacyAccessControl_TeamAdmin(t *testing.T) { // Setup store teams team1, err := sc.teamService.CreateTeam("testteam1", "testteam1@example.org", testOrgID) require.NoError(t, err) - err = sc.teamService.AddTeamMember(testUserID, testOrgID, team1.ID, false, models.PERMISSION_ADMIN) + err = sc.teamService.AddTeamMember(testUserID, testOrgID, team1.ID, false, dashboards.PERMISSION_ADMIN) require.NoError(t, err) response := callAPI(sc.server, http.MethodGet, "/api/org/users/lookup", nil, t) diff --git a/pkg/api/search.go b/pkg/api/search.go index 290d11d3f05..5ebf630f606 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -28,14 +28,14 @@ func (hs *HTTPServer) Search(c *models.ReqContext) response.Response { page := c.QueryInt64("page") dashboardType := c.Query("type") sort := c.Query("sort") - permission := models.PERMISSION_VIEW + permission := dashboards.PERMISSION_VIEW if limit > 5000 { return response.Error(422, "Limit is above maximum allowed (5000), use page parameter to access hits beyond limit", nil) } if c.Query("permission") == "Edit" { - permission = models.PERMISSION_EDIT + permission = dashboards.PERMISSION_EDIT } dbIDs := make([]int64, 0) diff --git a/pkg/api/team.go b/pkg/api/team.go index 9bed235a498..d845b4f6ab1 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/util" @@ -53,7 +54,7 @@ func (hs *HTTPServer) CreateTeam(c *models.ReqContext) response.Response { // the SignedInUser is an empty struct therefore // an additional check whether it is an actual user is required if c.SignedInUser.IsRealUser() { - if err := addOrUpdateTeamMember(c.Req.Context(), hs.teamPermissionsService, c.SignedInUser.UserID, c.OrgID, t.ID, models.PERMISSION_ADMIN.String()); err != nil { + if err := addOrUpdateTeamMember(c.Req.Context(), hs.teamPermissionsService, c.SignedInUser.UserID, c.OrgID, t.ID, dashboards.PERMISSION_ADMIN.String()); err != nil { c.Logger.Error("Could not add creator to team", "error", err) } } else { diff --git a/pkg/api/team_members.go b/pkg/api/team_members.go index 17748226a90..630d99b8d85 100644 --- a/pkg/api/team_members.go +++ b/pkg/api/team_members.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/util" @@ -160,7 +161,7 @@ func (hs *HTTPServer) UpdateTeamMember(c *models.ReqContext) response.Response { return response.Success("Team member updated") } -func getPermissionName(permission models.PermissionType) string { +func getPermissionName(permission dashboards.PermissionType) string { permissionName := permission.String() // Team member permission is 0, which maps to an empty string. // However, we want the team permission service to display "Member" for team members. This is a hack to make it work. diff --git a/pkg/api/team_members_test.go b/pkg/api/team_members_test.go index fd42bdef9c9..5d7643c3f56 100644 --- a/pkg/api/team_members_test.go +++ b/pkg/api/team_members_test.go @@ -8,21 +8,21 @@ import ( "strings" "testing" - "github.com/grafana/grafana/pkg/services/accesscontrol/actest" - "github.com/grafana/grafana/pkg/services/team" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" - "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/actest" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotaimpl" "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/services/team/teamimpl" "github.com/grafana/grafana/pkg/services/team/teamtest" "github.com/grafana/grafana/pkg/services/teamguardian/database" @@ -134,8 +134,8 @@ func TestAddTeamMembersAPIEndpoint_LegacyAccessControl(t *testing.T) { hs.teamService = teamtest.NewFakeService() store := &database.TeamGuardianStoreMock{} store.On("GetTeamMembers", mock.Anything, mock.Anything).Return([]*team.TeamMemberDTO{ - {UserID: 2, Permission: models.PERMISSION_ADMIN}, - {UserID: 3, Permission: models.PERMISSION_VIEW}, + {UserID: 2, Permission: dashboards.PERMISSION_ADMIN}, + {UserID: 3, Permission: dashboards.PERMISSION_VIEW}, }, nil).Maybe() hs.teamGuardian = manager.ProvideService(store) hs.teamPermissionsService = &actest.FakePermissionsService{} @@ -254,8 +254,8 @@ func TestUpdateTeamMembersAPIEndpoint_LegacyAccessControl(t *testing.T) { hs.teamService = &teamtest.FakeService{ExpectedIsMember: true} store := &database.TeamGuardianStoreMock{} store.On("GetTeamMembers", mock.Anything, mock.Anything).Return([]*team.TeamMemberDTO{ - {UserID: 2, Permission: models.PERMISSION_ADMIN}, - {UserID: 3, Permission: models.PERMISSION_VIEW}, + {UserID: 2, Permission: dashboards.PERMISSION_ADMIN}, + {UserID: 3, Permission: dashboards.PERMISSION_VIEW}, }, nil).Maybe() hs.teamGuardian = manager.ProvideService(store) hs.teamPermissionsService = &actest.FakePermissionsService{} @@ -344,8 +344,8 @@ func TestDeleteTeamMembersAPIEndpoint_LegacyAccessControl(t *testing.T) { hs.teamService = &teamtest.FakeService{ExpectedIsMember: true} store := &database.TeamGuardianStoreMock{} store.On("GetTeamMembers", mock.Anything, mock.Anything).Return([]*team.TeamMemberDTO{ - {UserID: 2, Permission: models.PERMISSION_ADMIN}, - {UserID: 3, Permission: models.PERMISSION_VIEW}, + {UserID: 2, Permission: dashboards.PERMISSION_ADMIN}, + {UserID: 3, Permission: dashboards.PERMISSION_VIEW}, }, nil).Maybe() hs.teamGuardian = manager.ProvideService(store) hs.teamPermissionsService = &actest.FakePermissionsService{} diff --git a/pkg/infra/db/sqlbuilder.go b/pkg/infra/db/sqlbuilder.go index 24c2ab2b7ac..1b3c8427879 100644 --- a/pkg/infra/db/sqlbuilder.go +++ b/pkg/infra/db/sqlbuilder.go @@ -3,8 +3,8 @@ package db import ( "bytes" - "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/sqlstore/permissions" "github.com/grafana/grafana/pkg/services/user" @@ -42,7 +42,7 @@ func (sb *SQLBuilder) AddParams(params ...interface{}) { sb.params = append(sb.params, params...) } -func (sb *SQLBuilder) WriteDashboardPermissionFilter(user *user.SignedInUser, permission models.PermissionType) { +func (sb *SQLBuilder) WriteDashboardPermissionFilter(user *user.SignedInUser, permission dashboards.PermissionType) { var ( sql string params []interface{} diff --git a/pkg/infra/db/sqlbuilder_test.go b/pkg/infra/db/sqlbuilder_test.go index 864cb16865c..8bc1d506627 100644 --- a/pkg/infra/db/sqlbuilder_test.go +++ b/pkg/infra/db/sqlbuilder_test.go @@ -12,7 +12,6 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" dashver "github.com/grafana/grafana/pkg/services/dashboardversion" "github.com/grafana/grafana/pkg/services/org" @@ -29,29 +28,29 @@ func TestIntegrationSQLBuilder(t *testing.T) { t.Run("user ACL", func(t *testing.T) { test(t, DashboardProps{}, - &DashboardPermission{User: true, Permission: models.PERMISSION_VIEW}, - Search{UserFromACL: true, RequiredPermission: models.PERMISSION_VIEW}, + &DashboardPermission{User: true, Permission: dashboards.PERMISSION_VIEW}, + Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_VIEW}, shouldFind, ) test(t, DashboardProps{}, - &DashboardPermission{User: true, Permission: models.PERMISSION_VIEW}, - Search{UserFromACL: true, RequiredPermission: models.PERMISSION_EDIT}, + &DashboardPermission{User: true, Permission: dashboards.PERMISSION_VIEW}, + Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_EDIT}, shouldNotFind, ) test(t, DashboardProps{}, - &DashboardPermission{User: true, Permission: models.PERMISSION_EDIT}, - Search{UserFromACL: true, RequiredPermission: models.PERMISSION_EDIT}, + &DashboardPermission{User: true, Permission: dashboards.PERMISSION_EDIT}, + Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_EDIT}, shouldFind, ) test(t, DashboardProps{}, - &DashboardPermission{User: true, Permission: models.PERMISSION_VIEW}, - Search{RequiredPermission: models.PERMISSION_VIEW}, + &DashboardPermission{User: true, Permission: dashboards.PERMISSION_VIEW}, + Search{RequiredPermission: dashboards.PERMISSION_VIEW}, shouldNotFind, ) }) @@ -59,29 +58,29 @@ func TestIntegrationSQLBuilder(t *testing.T) { t.Run("role ACL", func(t *testing.T) { test(t, DashboardProps{}, - &DashboardPermission{Role: org.RoleViewer, Permission: models.PERMISSION_VIEW}, - Search{UsersOrgRole: org.RoleViewer, RequiredPermission: models.PERMISSION_VIEW}, + &DashboardPermission{Role: org.RoleViewer, Permission: dashboards.PERMISSION_VIEW}, + Search{UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW}, shouldFind, ) test(t, DashboardProps{}, - &DashboardPermission{Role: org.RoleViewer, Permission: models.PERMISSION_VIEW}, - Search{UsersOrgRole: org.RoleViewer, RequiredPermission: models.PERMISSION_EDIT}, + &DashboardPermission{Role: org.RoleViewer, Permission: dashboards.PERMISSION_VIEW}, + Search{UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_EDIT}, shouldNotFind, ) test(t, DashboardProps{}, - &DashboardPermission{Role: org.RoleEditor, Permission: models.PERMISSION_VIEW}, - Search{UsersOrgRole: org.RoleViewer, RequiredPermission: models.PERMISSION_VIEW}, + &DashboardPermission{Role: org.RoleEditor, Permission: dashboards.PERMISSION_VIEW}, + Search{UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW}, shouldNotFind, ) test(t, DashboardProps{}, - &DashboardPermission{Role: org.RoleEditor, Permission: models.PERMISSION_VIEW}, - Search{UsersOrgRole: org.RoleViewer, RequiredPermission: models.PERMISSION_VIEW}, + &DashboardPermission{Role: org.RoleEditor, Permission: dashboards.PERMISSION_VIEW}, + Search{UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW}, shouldNotFind, ) }) @@ -89,29 +88,29 @@ func TestIntegrationSQLBuilder(t *testing.T) { t.Run("team ACL", func(t *testing.T) { test(t, DashboardProps{}, - &DashboardPermission{Team: true, Permission: models.PERMISSION_VIEW}, - Search{UserFromACL: true, RequiredPermission: models.PERMISSION_VIEW}, + &DashboardPermission{Team: true, Permission: dashboards.PERMISSION_VIEW}, + Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_VIEW}, shouldFind, ) test(t, DashboardProps{}, - &DashboardPermission{Team: true, Permission: models.PERMISSION_VIEW}, - Search{UserFromACL: true, RequiredPermission: models.PERMISSION_EDIT}, + &DashboardPermission{Team: true, Permission: dashboards.PERMISSION_VIEW}, + Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_EDIT}, shouldNotFind, ) test(t, DashboardProps{}, - &DashboardPermission{Team: true, Permission: models.PERMISSION_EDIT}, - Search{UserFromACL: true, RequiredPermission: models.PERMISSION_EDIT}, + &DashboardPermission{Team: true, Permission: dashboards.PERMISSION_EDIT}, + Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_EDIT}, shouldFind, ) test(t, DashboardProps{}, - &DashboardPermission{Team: true, Permission: models.PERMISSION_EDIT}, - Search{UserFromACL: false, RequiredPermission: models.PERMISSION_EDIT}, + &DashboardPermission{Team: true, Permission: dashboards.PERMISSION_EDIT}, + Search{UserFromACL: false, RequiredPermission: dashboards.PERMISSION_EDIT}, shouldNotFind, ) }) @@ -120,28 +119,28 @@ func TestIntegrationSQLBuilder(t *testing.T) { test(t, DashboardProps{}, nil, - Search{OrgId: -1, UsersOrgRole: org.RoleViewer, RequiredPermission: models.PERMISSION_VIEW}, + Search{OrgId: -1, UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW}, shouldNotFind, ) test(t, DashboardProps{OrgId: -1}, nil, - Search{OrgId: -1, UsersOrgRole: org.RoleViewer, RequiredPermission: models.PERMISSION_VIEW}, + Search{OrgId: -1, UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW}, shouldFind, ) test(t, DashboardProps{OrgId: -1}, nil, - Search{OrgId: -1, UsersOrgRole: org.RoleEditor, RequiredPermission: models.PERMISSION_EDIT}, + Search{OrgId: -1, UsersOrgRole: org.RoleEditor, RequiredPermission: dashboards.PERMISSION_EDIT}, shouldFind, ) test(t, DashboardProps{OrgId: -1}, nil, - Search{OrgId: -1, UsersOrgRole: org.RoleViewer, RequiredPermission: models.PERMISSION_EDIT}, + Search{OrgId: -1, UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_EDIT}, shouldNotFind, ) }) @@ -159,13 +158,13 @@ type DashboardPermission struct { User bool Team bool Role org.RoleType - Permission models.PermissionType + Permission dashboards.PermissionType } type Search struct { UsersOrgRole org.RoleType UserFromACL bool - RequiredPermission models.PermissionType + RequiredPermission dashboards.PermissionType OrgId int64 } @@ -401,11 +400,11 @@ func updateDashboardACL(t *testing.T, sqlStore *sqlstore.SQLStore, dashboardID i item.Created = time.Now() item.Updated = time.Now() if item.UserID == 0 && item.TeamID == 0 && (item.Role == nil || !item.Role.IsValid()) { - return models.ErrDashboardACLInfoMissing + return dashboards.ErrDashboardACLInfoMissing } if item.DashboardID == 0 { - return models.ErrDashboardPermissionDashboardEmpty + return dashboards.ErrDashboardPermissionDashboardEmpty } sess.Nullable("user_id", "team_id") diff --git a/pkg/models/search.go b/pkg/models/search.go index 6629620b99a..6bf91aca3fe 100644 --- a/pkg/models/search.go +++ b/pkg/models/search.go @@ -4,7 +4,6 @@ import ( "strings" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" - "github.com/grafana/grafana/pkg/services/user" ) type SortOption struct { @@ -20,26 +19,6 @@ type SortOptionFilter interface { searchstore.FilterOrderBy } -type FindPersistedDashboardsQuery struct { - Title string - OrgId int64 - SignedInUser *user.SignedInUser - IsStarred bool - DashboardIds []int64 - DashboardUIDs []string - Type string - FolderIds []int64 - Tags []string - Limit int64 - Page int64 - Permission PermissionType - Sort SortOption - - Filters []interface{} - - Result HitList -} - type HitType string const ( diff --git a/pkg/services/accesscontrol/database/database_test.go b/pkg/services/accesscontrol/database/database_test.go index 6f14b6fd50e..fa40140caa4 100644 --- a/pkg/services/accesscontrol/database/database_test.go +++ b/pkg/services/accesscontrol/database/database_test.go @@ -10,9 +10,9 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/localcache" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" rs "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotatest" @@ -229,7 +229,7 @@ func createUserAndTeam(t *testing.T, userSrv user.Service, teamSvc team.Service, team, err := teamSvc.CreateTeam("team", "", orgID) require.NoError(t, err) - err = teamSvc.AddTeamMember(user.ID, orgID, team.ID, false, models.PERMISSION_VIEW) + err = teamSvc.AddTeamMember(user.ID, orgID, team.ID, false, dashboards.PERMISSION_VIEW) require.NoError(t, err) return user, team @@ -276,7 +276,7 @@ func createUsersAndTeams(t *testing.T, svcs helperServices, orgID int64, users [ team, err := svcs.teamSvc.CreateTeam(fmt.Sprintf("team%v", i+1), "", orgID) require.NoError(t, err) - err = svcs.teamSvc.AddTeamMember(user.ID, orgID, team.ID, false, models.PERMISSION_VIEW) + err = svcs.teamSvc.AddTeamMember(user.ID, orgID, team.ID, false, dashboards.PERMISSION_VIEW) require.NoError(t, err) err = svcs.orgSvc.UpdateOrgUser(context.Background(), diff --git a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go index 2397e710fa2..81ed7e0ca01 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go @@ -8,7 +8,6 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" "github.com/grafana/grafana/pkg/services/dashboards" @@ -85,7 +84,7 @@ func ProvideTeamPermissions( case "Member": return teamimpl.AddOrUpdateTeamMemberHook(session, user.ID, orgID, teamId, user.IsExternal, 0) case "Admin": - return teamimpl.AddOrUpdateTeamMemberHook(session, user.ID, orgID, teamId, user.IsExternal, models.PERMISSION_ADMIN) + return teamimpl.AddOrUpdateTeamMemberHook(session, user.ID, orgID, teamId, user.IsExternal, dashboards.PERMISSION_ADMIN) case "": return teamimpl.RemoveTeamMemberHook(session, &team.RemoveTeamMemberCommand{ OrgID: orgID, diff --git a/pkg/services/alerting/store.go b/pkg/services/alerting/store.go index ffbe00f30c9..0ab879af213 100644 --- a/pkg/services/alerting/store.go +++ b/pkg/services/alerting/store.go @@ -9,8 +9,8 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" alertmodels "github.com/grafana/grafana/pkg/services/alerting/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/tag" "github.com/grafana/grafana/pkg/setting" @@ -159,7 +159,7 @@ func (ss *sqlStore) HandleAlertsQuery(ctx context.Context, query *alertmodels.Ge } if query.User.OrgRole != org.RoleAdmin { - builder.WriteDashboardPermissionFilter(query.User, models.PERMISSION_VIEW) + builder.WriteDashboardPermissionFilter(query.User, dashboards.PERMISSION_VIEW) } builder.Write(" ORDER BY name ASC") diff --git a/pkg/services/annotations/annotationsimpl/xorm_store.go b/pkg/services/annotations/annotationsimpl/xorm_store.go index 33f75061479..bcb4675411d 100644 --- a/pkg/services/annotations/annotationsimpl/xorm_store.go +++ b/pkg/services/annotations/annotationsimpl/xorm_store.go @@ -10,9 +10,9 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" - "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/dashboards" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/permissions" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" @@ -347,7 +347,7 @@ func getAccessControlFilter(user *user.SignedInUser) (string, []interface{}, err } // annotation read permission with scope annotations:type:dashboard allows listing annotations from dashboards which the user can view if t == annotations.Dashboard.String() { - dashboardFilter, dashboardParams := permissions.NewAccessControlDashboardPermissionFilter(user, models.PERMISSION_VIEW, searchstore.TypeDashboard).Where() + dashboardFilter, dashboardParams := permissions.NewAccessControlDashboardPermissionFilter(user, dashboards.PERMISSION_VIEW, searchstore.TypeDashboard).Where() filter := fmt.Sprintf("a.dashboard_id IN(SELECT id FROM dashboard WHERE %s)", dashboardFilter) filters = append(filters, filter) params = dashboardParams diff --git a/pkg/services/dashboards/dashboard.go b/pkg/services/dashboards/dashboard.go index a52b98244e5..54092578208 100644 --- a/pkg/services/dashboards/dashboard.go +++ b/pkg/services/dashboards/dashboard.go @@ -3,7 +3,6 @@ package dashboards import ( "context" - "github.com/grafana/grafana/pkg/models" alertmodels "github.com/grafana/grafana/pkg/services/alerting/models" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/quota" @@ -15,7 +14,7 @@ import ( type DashboardService interface { BuildSaveDashboardCommand(ctx context.Context, dto *SaveDashboardDTO, shouldValidateAlerts bool, validateProvisionedDashboard bool) (*SaveDashboardCommand, error) DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error - FindDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) + FindDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) GetDashboard(ctx context.Context, query *GetDashboardQuery) (*Dashboard, error) GetDashboardACLInfoList(ctx context.Context, query *GetDashboardACLInfoListQuery) ([]*DashboardACLInfoDTO, error) GetDashboards(ctx context.Context, query *GetDashboardsQuery) ([]*Dashboard, error) @@ -26,7 +25,7 @@ type DashboardService interface { ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*Dashboard, error) MakeUserAdmin(ctx context.Context, orgID int64, userID, dashboardID int64, setViewAndEditPermissions bool) error SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*Dashboard, error) - SearchDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) error + SearchDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) error UpdateDashboardACL(ctx context.Context, uid int64, items []*DashboardACL) error DeleteACLByUser(ctx context.Context, userID int64) error CountDashboardsInFolder(ctx context.Context, query *CountDashboardsInFolderQuery) (int64, error) @@ -57,7 +56,7 @@ type DashboardProvisioningService interface { type Store interface { DeleteDashboard(ctx context.Context, cmd *DeleteDashboardCommand) error DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *DeleteOrphanedProvisionedDashboardsCommand) error - FindDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) + FindDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) GetDashboard(ctx context.Context, query *GetDashboardQuery) (*Dashboard, error) GetDashboardACLInfoList(ctx context.Context, query *GetDashboardACLInfoListQuery) ([]*DashboardACLInfoDTO, error) GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) (*DashboardRef, error) diff --git a/pkg/models/dashboard_acl.go b/pkg/services/dashboards/dashboard_acl.go similarity index 98% rename from pkg/models/dashboard_acl.go rename to pkg/services/dashboards/dashboard_acl.go index f8cb7a3adf7..2068e7d5832 100644 --- a/pkg/models/dashboard_acl.go +++ b/pkg/services/dashboards/dashboard_acl.go @@ -1,4 +1,4 @@ -package models +package dashboards import ( "errors" diff --git a/pkg/models/dashboard_acl_test.go b/pkg/services/dashboards/dashboard_acl_test.go similarity index 96% rename from pkg/models/dashboard_acl_test.go rename to pkg/services/dashboards/dashboard_acl_test.go index f02b132fb9e..97ba21dd58a 100644 --- a/pkg/models/dashboard_acl_test.go +++ b/pkg/services/dashboards/dashboard_acl_test.go @@ -1,4 +1,4 @@ -package models +package dashboards import ( "fmt" diff --git a/pkg/services/dashboards/dashboard_service_mock.go b/pkg/services/dashboards/dashboard_service_mock.go index e0d3622ded3..3da367b22be 100644 --- a/pkg/services/dashboards/dashboard_service_mock.go +++ b/pkg/services/dashboards/dashboard_service_mock.go @@ -7,8 +7,6 @@ import ( folder "github.com/grafana/grafana/pkg/services/folder" mock "github.com/stretchr/testify/mock" - - models "github.com/grafana/grafana/pkg/models" ) // FakeDashboardService is an autogenerated mock type for the DashboardService type @@ -89,11 +87,11 @@ func (_m *FakeDashboardService) DeleteDashboard(ctx context.Context, dashboardId } // FindDashboards provides a mock function with given fields: ctx, query -func (_m *FakeDashboardService) FindDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) { +func (_m *FakeDashboardService) FindDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) { ret := _m.Called(ctx, query) var r0 []DashboardSearchProjection - if rf, ok := ret.Get(0).(func(context.Context, *models.FindPersistedDashboardsQuery) []DashboardSearchProjection); ok { + if rf, ok := ret.Get(0).(func(context.Context, *FindPersistedDashboardsQuery) []DashboardSearchProjection); ok { r0 = rf(ctx, query) } else { if ret.Get(0) != nil { @@ -102,7 +100,7 @@ func (_m *FakeDashboardService) FindDashboards(ctx context.Context, query *model } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *models.FindPersistedDashboardsQuery) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, *FindPersistedDashboardsQuery) error); ok { r1 = rf(ctx, query) } else { r1 = ret.Error(1) @@ -329,11 +327,11 @@ func (_m *FakeDashboardService) SaveDashboard(ctx context.Context, dto *SaveDash } // SearchDashboards provides a mock function with given fields: ctx, query -func (_m *FakeDashboardService) SearchDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) error { +func (_m *FakeDashboardService) SearchDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) error { ret := _m.Called(ctx, query) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *models.FindPersistedDashboardsQuery) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, *FindPersistedDashboardsQuery) error); ok { r0 = rf(ctx, query) } else { r0 = ret.Error(0) diff --git a/pkg/services/dashboards/database/acl.go b/pkg/services/dashboards/database/acl.go index 3e4185cea75..4e28b944622 100644 --- a/pkg/services/dashboards/database/acl.go +++ b/pkg/services/dashboards/database/acl.go @@ -4,7 +4,6 @@ import ( "context" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/org" @@ -108,7 +107,7 @@ func (d *DashboardStore) HasEditPermissionInFolders(ctx context.Context, query * builder := db.NewSqlBuilder(d.cfg, d.store.GetDialect()) builder.Write("SELECT COUNT(dashboard.id) AS count FROM dashboard WHERE dashboard.org_id = ? AND dashboard.is_folder = ?", query.SignedInUser.OrgID, d.store.GetDialect().BooleanStr(true)) - builder.WriteDashboardPermissionFilter(query.SignedInUser, models.PERMISSION_EDIT) + builder.WriteDashboardPermissionFilter(query.SignedInUser, dashboards.PERMISSION_EDIT) type folderCount struct { Count int64 @@ -140,7 +139,7 @@ func (d *DashboardStore) HasAdminPermissionInDashboardsOrFolders(ctx context.Con builder := db.NewSqlBuilder(d.cfg, d.store.GetDialect()) builder.Write("SELECT COUNT(dashboard.id) AS count FROM dashboard WHERE dashboard.org_id = ?", query.SignedInUser.OrgID) - builder.WriteDashboardPermissionFilter(query.SignedInUser, models.PERMISSION_ADMIN) + builder.WriteDashboardPermissionFilter(query.SignedInUser, dashboards.PERMISSION_ADMIN) type folderCount struct { Count int64 diff --git a/pkg/services/dashboards/database/acl_test.go b/pkg/services/dashboards/database/acl_test.go index dc505a5944e..6efeb8d3921 100644 --- a/pkg/services/dashboards/database/acl_test.go +++ b/pkg/services/dashboards/database/acl_test.go @@ -7,7 +7,6 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" @@ -45,9 +44,9 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { err := updateDashboardACL(t, dashboardStore, savedFolder.ID, dashboards.DashboardACL{ OrgID: 1, DashboardID: savedFolder.ID, - Permission: models.PERMISSION_EDIT, + Permission: dashboards.PERMISSION_EDIT, }) - require.Equal(t, models.ErrDashboardACLInfoMissing, err) + require.Equal(t, dashboards.ErrDashboardACLInfoMissing, err) }) t.Run("Folder acl should include default acl", func(t *testing.T) { @@ -103,7 +102,7 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { OrgID: 1, UserID: currentUser.ID, DashboardID: savedFolder.ID, - Permission: models.PERMISSION_EDIT, + Permission: dashboards.PERMISSION_EDIT, }) require.Nil(t, err) @@ -122,7 +121,7 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { OrgID: 1, UserID: currentUser.ID, DashboardID: childDash.ID, - Permission: models.PERMISSION_EDIT, + Permission: dashboards.PERMISSION_EDIT, }) require.Nil(t, err) @@ -147,7 +146,7 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { OrgID: 1, UserID: currentUser.ID, DashboardID: childDash.ID, - Permission: models.PERMISSION_EDIT, + Permission: dashboards.PERMISSION_EDIT, }) require.Nil(t, err) @@ -174,7 +173,7 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { OrgID: 1, UserID: currentUser.ID, DashboardID: savedFolder.ID, - Permission: models.PERMISSION_EDIT, + Permission: dashboards.PERMISSION_EDIT, }) require.Nil(t, err) @@ -183,7 +182,7 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { require.Nil(t, err) require.Equal(t, savedFolder.ID, q1Result[0].DashboardID) - require.Equal(t, models.PERMISSION_EDIT, q1Result[0].Permission) + require.Equal(t, dashboards.PERMISSION_EDIT, q1Result[0].Permission) require.Equal(t, "Edit", q1Result[0].PermissionName) require.Equal(t, currentUser.ID, q1Result[0].UserID) require.Equal(t, currentUser.Login, q1Result[0].UserLogin) @@ -208,7 +207,7 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { OrgID: 1, TeamID: team1.ID, DashboardID: savedFolder.ID, - Permission: models.PERMISSION_EDIT, + Permission: dashboards.PERMISSION_EDIT, }) require.Nil(t, err) @@ -216,7 +215,7 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { q1Result, err := dashboardStore.GetDashboardACLInfoList(context.Background(), q1) require.Nil(t, err) require.Equal(t, savedFolder.ID, q1Result[0].DashboardID) - require.Equal(t, models.PERMISSION_EDIT, q1Result[0].Permission) + require.Equal(t, dashboards.PERMISSION_EDIT, q1Result[0].Permission) require.Equal(t, team1.ID, q1Result[0].TeamID) }) @@ -229,7 +228,7 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { OrgID: 1, TeamID: team1.ID, DashboardID: savedFolder.ID, - Permission: models.PERMISSION_ADMIN, + Permission: dashboards.PERMISSION_ADMIN, }) require.Nil(t, err) @@ -238,7 +237,7 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { require.Nil(t, err) require.Equal(t, 1, len(q3Result)) require.Equal(t, savedFolder.ID, q3Result[0].DashboardID) - require.Equal(t, models.PERMISSION_ADMIN, q3Result[0].Permission) + require.Equal(t, dashboards.PERMISSION_ADMIN, q3Result[0].Permission) require.Equal(t, team1.ID, q3Result[0].TeamID) }) }) diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index 5190219f2ae..1d8b2bc0d47 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -11,7 +11,6 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" - "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" alertmodels "github.com/grafana/grafana/pkg/services/alerting/models" "github.com/grafana/grafana/pkg/services/dashboards" @@ -249,11 +248,11 @@ func (d *DashboardStore) UpdateDashboardACL(ctx context.Context, dashboardID int for _, item := range items { if item.UserID == 0 && item.TeamID == 0 && (item.Role == nil || !item.Role.IsValid()) { - return models.ErrDashboardACLInfoMissing + return dashboards.ErrDashboardACLInfoMissing } if item.DashboardID == 0 { - return models.ErrDashboardPermissionDashboardEmpty + return dashboards.ErrDashboardPermissionDashboardEmpty } sess.Nullable("user_id", "team_id") @@ -1003,7 +1002,7 @@ func (d *DashboardStore) GetDashboards(ctx context.Context, query *dashboards.Ge return dashboards, nil } -func (d *DashboardStore) FindDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) { +func (d *DashboardStore) FindDashboards(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) { filters := []interface{}{ permissions.DashboardPermissionFilter{ OrgRole: query.SignedInUser.OrgRole, diff --git a/pkg/services/dashboards/database/database_folder_test.go b/pkg/services/dashboards/database/database_folder_test.go index 59a42e8968c..a63517b83d4 100644 --- a/pkg/services/dashboards/database/database_folder_test.go +++ b/pkg/services/dashboards/database/database_folder_test.go @@ -8,7 +8,6 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" @@ -51,7 +50,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { t.Run("and no acls are set", func(t *testing.T) { t.Run("should return all dashboards", func(t *testing.T) { - query := &models.FindPersistedDashboardsQuery{ + query := &dashboards.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, DashboardIds: []int64{flder.ID, dashInRoot.ID}, @@ -70,12 +69,12 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { DashboardID: flder.ID, OrgID: 1, UserID: otherUser, - Permission: models.PERMISSION_EDIT, + Permission: dashboards.PERMISSION_EDIT, }) require.NoError(t, err) t.Run("should not return folder", func(t *testing.T) { - query := &models.FindPersistedDashboardsQuery{ + query := &dashboards.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, DashboardIds: []int64{flder.ID, dashInRoot.ID}, } @@ -88,12 +87,12 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { t.Run("when the user is given permission", func(t *testing.T) { err := updateDashboardACL(t, dashboardStore, flder.ID, dashboards.DashboardACL{ - DashboardID: flder.ID, OrgID: 1, UserID: currentUser.ID, Permission: models.PERMISSION_EDIT, + DashboardID: flder.ID, OrgID: 1, UserID: currentUser.ID, Permission: dashboards.PERMISSION_EDIT, }) require.NoError(t, err) t.Run("should be able to access folder", func(t *testing.T) { - query := &models.FindPersistedDashboardsQuery{ + query := &dashboards.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, DashboardIds: []int64{flder.ID, dashInRoot.ID}, @@ -108,7 +107,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { t.Run("when the user is an admin", func(t *testing.T) { t.Run("should be able to access folder", func(t *testing.T) { - query := &models.FindPersistedDashboardsQuery{ + query := &dashboards.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{ UserID: currentUser.ID, OrgID: 1, @@ -131,12 +130,12 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { err := updateDashboardACL(t, dashboardStore, flder.ID) require.NoError(t, err) err = updateDashboardACL(t, dashboardStore, childDash.ID, dashboards.DashboardACL{ - DashboardID: flder.ID, OrgID: 1, UserID: otherUser, Permission: models.PERMISSION_EDIT, + DashboardID: flder.ID, OrgID: 1, UserID: otherUser, Permission: dashboards.PERMISSION_EDIT, }) require.NoError(t, err) t.Run("should not return folder or child", func(t *testing.T) { - query := &models.FindPersistedDashboardsQuery{ + query := &dashboards.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, DashboardIds: []int64{flder.ID, childDash.ID, dashInRoot.ID}, } err := testSearchDashboards(dashboardStore, query) @@ -147,12 +146,12 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { t.Run("when the user is given permission to child", func(t *testing.T) { err := updateDashboardACL(t, dashboardStore, childDash.ID, dashboards.DashboardACL{ - DashboardID: childDash.ID, OrgID: 1, UserID: currentUser.ID, Permission: models.PERMISSION_EDIT, + DashboardID: childDash.ID, OrgID: 1, UserID: currentUser.ID, Permission: dashboards.PERMISSION_EDIT, }) require.NoError(t, err) t.Run("should be able to search for child dashboard but not folder", func(t *testing.T) { - query := &models.FindPersistedDashboardsQuery{SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, DashboardIds: []int64{flder.ID, childDash.ID, dashInRoot.ID}} + query := &dashboards.FindPersistedDashboardsQuery{SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, DashboardIds: []int64{flder.ID, childDash.ID, dashInRoot.ID}} err := testSearchDashboards(dashboardStore, query) require.NoError(t, err) require.Equal(t, len(query.Result), 2) @@ -163,7 +162,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { t.Run("when the user is an admin", func(t *testing.T) { t.Run("should be able to search for child dash and folder", func(t *testing.T) { - query := &models.FindPersistedDashboardsQuery{ + query := &dashboards.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{ UserID: currentUser.ID, OrgID: 1, @@ -206,7 +205,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { setup2() t.Run("and one folder is expanded, the other collapsed", func(t *testing.T) { t.Run("should return dashboards in root and expanded folder", func(t *testing.T) { - query := &models.FindPersistedDashboardsQuery{ + query := &dashboards.FindPersistedDashboardsQuery{ FolderIds: []int64{ rootFolderId, folder1.ID}, SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer, @@ -226,7 +225,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { t.Run("and acl is set for one dashboard folder", func(t *testing.T) { const otherUser int64 = 999 err := updateDashboardACL(t, dashboardStore, folder1.ID, dashboards.DashboardACL{ - DashboardID: folder1.ID, OrgID: 1, UserID: otherUser, Permission: models.PERMISSION_EDIT, + DashboardID: folder1.ID, OrgID: 1, UserID: otherUser, Permission: dashboards.PERMISSION_EDIT, }) require.NoError(t, err) @@ -234,7 +233,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { moveDashboard(t, dashboardStore, 1, childDash2.Data, folder1.ID) t.Run("should not return folder with acl or its children", func(t *testing.T) { - query := &models.FindPersistedDashboardsQuery{ + query := &dashboards.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, DashboardIds: []int64{folder1.ID, childDash1.ID, childDash2.ID, dashInRoot.ID}, @@ -250,7 +249,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { moveDashboard(t, dashboardStore, 1, childDash1.Data, folder2.ID) t.Run("should return folder without acl and its children", func(t *testing.T) { - query := &models.FindPersistedDashboardsQuery{ + query := &dashboards.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, DashboardIds: []int64{folder2.ID, childDash1.ID, childDash2.ID, dashInRoot.ID}, @@ -267,14 +266,14 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { t.Run("and a dashboard with an acl is moved to the folder without an acl", func(t *testing.T) { err := updateDashboardACL(t, dashboardStore, childDash1.ID, dashboards.DashboardACL{ - DashboardID: childDash1.ID, OrgID: 1, UserID: otherUser, Permission: models.PERMISSION_EDIT, + DashboardID: childDash1.ID, OrgID: 1, UserID: otherUser, Permission: dashboards.PERMISSION_EDIT, }) require.NoError(t, err) moveDashboard(t, dashboardStore, 1, childDash1.Data, folder2.ID) t.Run("should return folder without acl but not the dashboard with acl", func(t *testing.T) { - query := &models.FindPersistedDashboardsQuery{ + query := &dashboards.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, DashboardIds: []int64{folder2.ID, childDash1.ID, childDash2.ID, dashInRoot.ID}, @@ -313,10 +312,10 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { setup3() t.Run("Admin users", func(t *testing.T) { t.Run("Should have write access to all dashboard folders in their org", func(t *testing.T) { - query := models.FindPersistedDashboardsQuery{ + query := dashboards.FindPersistedDashboardsQuery{ OrgId: 1, SignedInUser: &user.SignedInUser{UserID: adminUser.ID, OrgRole: org.RoleAdmin, OrgID: 1}, - Permission: models.PERMISSION_VIEW, + Permission: dashboards.PERMISSION_VIEW, Type: "dash-folder", } @@ -348,10 +347,10 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { }) t.Run("Editor users", func(t *testing.T) { - query := models.FindPersistedDashboardsQuery{ + query := dashboards.FindPersistedDashboardsQuery{ OrgId: 1, SignedInUser: &user.SignedInUser{UserID: editorUser.ID, OrgRole: org.RoleEditor, OrgID: 1}, - Permission: models.PERMISSION_EDIT, + Permission: dashboards.PERMISSION_EDIT, } t.Run("Should have write access to all dashboard folders with default ACL", func(t *testing.T) { @@ -365,7 +364,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { t.Run("Should have write access to one dashboard folder if default role changed to view for one folder", func(t *testing.T) { err := updateDashboardACL(t, dashboardStore, folder1.ID, dashboards.DashboardACL{ - DashboardID: folder1.ID, OrgID: 1, UserID: editorUser.ID, Permission: models.PERMISSION_VIEW, + DashboardID: folder1.ID, OrgID: 1, UserID: editorUser.ID, Permission: dashboards.PERMISSION_VIEW, }) require.NoError(t, err) @@ -396,10 +395,10 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { }) t.Run("Viewer users", func(t *testing.T) { - query := models.FindPersistedDashboardsQuery{ + query := dashboards.FindPersistedDashboardsQuery{ OrgId: 1, SignedInUser: &user.SignedInUser{UserID: viewerUser.ID, OrgRole: org.RoleViewer, OrgID: 1}, - Permission: models.PERMISSION_EDIT, + Permission: dashboards.PERMISSION_EDIT, } t.Run("Should have no write access to any dashboard folders with default ACL", func(t *testing.T) { @@ -411,7 +410,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { t.Run("Should be able to get one dashboard folder if default role changed to edit for one folder", func(t *testing.T) { err := updateDashboardACL(t, dashboardStore, folder1.ID, dashboards.DashboardACL{ - DashboardID: folder1.ID, OrgID: 1, UserID: viewerUser.ID, Permission: models.PERMISSION_EDIT, + DashboardID: folder1.ID, OrgID: 1, UserID: viewerUser.ID, Permission: dashboards.PERMISSION_EDIT, }) require.NoError(t, err) @@ -444,7 +443,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { t.Run("and admin permission is given for user with org role viewer in one dashboard folder", func(t *testing.T) { err := updateDashboardACL(t, dashboardStore, folder1.ID, dashboards.DashboardACL{ - DashboardID: folder1.ID, OrgID: 1, UserID: viewerUser.ID, Permission: models.PERMISSION_ADMIN, + DashboardID: folder1.ID, OrgID: 1, UserID: viewerUser.ID, Permission: dashboards.PERMISSION_ADMIN, }) require.NoError(t, err) @@ -460,7 +459,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { t.Run("and edit permission is given for user with org role viewer in one dashboard folder", func(t *testing.T) { err := updateDashboardACL(t, dashboardStore, folder1.ID, dashboards.DashboardACL{ - DashboardID: folder1.ID, OrgID: 1, UserID: viewerUser.ID, Permission: models.PERMISSION_EDIT, + DashboardID: folder1.ID, OrgID: 1, UserID: viewerUser.ID, Permission: dashboards.PERMISSION_EDIT, }) require.NoError(t, err) diff --git a/pkg/services/dashboards/database/database_test.go b/pkg/services/dashboards/database/database_test.go index 18b03364180..7f643a8fc78 100644 --- a/pkg/services/dashboards/database/database_test.go +++ b/pkg/services/dashboards/database/database_test.go @@ -323,7 +323,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { err := dashboardStore.DeleteDashboard(context.Background(), deleteCmd) require.NoError(t, err) - query := models.FindPersistedDashboardsQuery{ + query := dashboards.FindPersistedDashboardsQuery{ OrgId: 1, FolderIds: []int64{savedFolder.ID}, SignedInUser: &user.SignedInUser{}, @@ -390,7 +390,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { t.Run("Should be able to find dashboard folder", func(t *testing.T) { setup() - query := models.FindPersistedDashboardsQuery{ + query := dashboards.FindPersistedDashboardsQuery{ Title: "1 test dash folder", OrgId: 1, SignedInUser: &user.SignedInUser{ @@ -414,7 +414,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { t.Run("Should be able to limit find results", func(t *testing.T) { setup() - query := models.FindPersistedDashboardsQuery{ + query := dashboards.FindPersistedDashboardsQuery{ OrgId: 1, Limit: 1, SignedInUser: &user.SignedInUser{ @@ -435,7 +435,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { t.Run("Should be able to find results beyond limit using paging", func(t *testing.T) { setup() - query := models.FindPersistedDashboardsQuery{ + query := dashboards.FindPersistedDashboardsQuery{ OrgId: 1, Limit: 1, Page: 2, @@ -460,7 +460,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { t.Run("Should be able to filter by tag and type", func(t *testing.T) { setup() - query := models.FindPersistedDashboardsQuery{ + query := dashboards.FindPersistedDashboardsQuery{ OrgId: 1, Type: "dash-db", Tags: []string{"prod"}, @@ -482,7 +482,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { t.Run("Should be able to find a dashboard folder's children", func(t *testing.T) { setup() - query := models.FindPersistedDashboardsQuery{ + query := dashboards.FindPersistedDashboardsQuery{ OrgId: 1, FolderIds: []int64{savedFolder.ID}, SignedInUser: &user.SignedInUser{ @@ -509,7 +509,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { t.Run("Should be able to find dashboards by ids", func(t *testing.T) { setup() - query := models.FindPersistedDashboardsQuery{ + query := dashboards.FindPersistedDashboardsQuery{ DashboardIds: []int64{savedDash.ID, savedDash2.ID}, SignedInUser: &user.SignedInUser{ OrgID: 1, @@ -547,7 +547,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { }) require.NoError(t, err) - query := models.FindPersistedDashboardsQuery{ + query := dashboards.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{ UserID: 10, OrgID: 1, @@ -623,7 +623,7 @@ func TestIntegrationDashboard_SortingOptions(t *testing.T) { dashA := insertTestDashboard(t, dashboardStore, "Alfa", 1, 0, false) assert.NotZero(t, dashA.ID) assert.Less(t, dashB.ID, dashA.ID) - qNoSort := &models.FindPersistedDashboardsQuery{ + qNoSort := &dashboards.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{ OrgID: 1, UserID: 1, @@ -639,7 +639,7 @@ func TestIntegrationDashboard_SortingOptions(t *testing.T) { assert.Equal(t, dashA.ID, results[0].ID) assert.Equal(t, dashB.ID, results[1].ID) - qSort := &models.FindPersistedDashboardsQuery{ + qSort := &dashboards.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{ OrgID: 1, UserID: 1, @@ -673,7 +673,7 @@ func TestIntegrationDashboard_Filter(t *testing.T) { require.NoError(t, err) insertTestDashboard(t, dashboardStore, "Alfa", 1, 0, false) dashB := insertTestDashboard(t, dashboardStore, "Beta", 1, 0, false) - qNoFilter := &models.FindPersistedDashboardsQuery{ + qNoFilter := &dashboards.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{ OrgID: 1, UserID: 1, @@ -687,7 +687,7 @@ func TestIntegrationDashboard_Filter(t *testing.T) { require.NoError(t, err) require.Len(t, results, 2) - qFilter := &models.FindPersistedDashboardsQuery{ + qFilter := &dashboards.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{ OrgID: 1, UserID: 1, @@ -840,7 +840,7 @@ func updateDashboardACL(t *testing.T, dashboardStore *DashboardStore, dashboardI // testSearchDashboards is a (near) copy of the dashboard service // SearchDashboards, which is a wrapper around FindDashboards. -func testSearchDashboards(d *DashboardStore, query *models.FindPersistedDashboardsQuery) error { +func testSearchDashboards(d *DashboardStore, query *dashboards.FindPersistedDashboardsQuery) error { res, err := d.FindDashboards(context.Background(), query) if err != nil { return err @@ -849,7 +849,7 @@ func testSearchDashboards(d *DashboardStore, query *models.FindPersistedDashboar return nil } -func makeQueryResult(query *models.FindPersistedDashboardsQuery, res []dashboards.DashboardSearchProjection) { +func makeQueryResult(query *dashboards.FindPersistedDashboardsQuery, res []dashboards.DashboardSearchProjection) { query.Result = make([]*models.Hit, 0) hits := make(map[int64]*models.Hit) diff --git a/pkg/services/dashboards/models.go b/pkg/services/dashboards/models.go index 8bc4c78dc05..b67cd20c33c 100644 --- a/pkg/services/dashboards/models.go +++ b/pkg/services/dashboards/models.go @@ -337,7 +337,7 @@ type DashboardACL struct { UserID int64 `xorm:"user_id"` TeamID int64 `xorm:"team_id"` Role *org.RoleType // pointer to be nullable - Permission models.PermissionType + Permission PermissionType Created time.Time Updated time.Time @@ -353,23 +353,23 @@ type DashboardACLInfoDTO struct { Created time.Time `json:"created"` Updated time.Time `json:"updated"` - UserID int64 `json:"userId" xorm:"user_id"` - UserLogin string `json:"userLogin"` - UserEmail string `json:"userEmail"` - UserAvatarURL string `json:"userAvatarUrl" xorm:"user_avatar_url"` - TeamID int64 `json:"teamId" xorm:"team_id"` - TeamEmail string `json:"teamEmail"` - TeamAvatarURL string `json:"teamAvatarUrl" xorm:"team_avatar_url"` - Team string `json:"team"` - Role *org.RoleType `json:"role,omitempty"` - Permission models.PermissionType `json:"permission"` - PermissionName string `json:"permissionName"` - UID string `json:"uid" xorm:"uid"` - Title string `json:"title"` - Slug string `json:"slug"` - IsFolder bool `json:"isFolder"` - URL string `json:"url" xorm:"url"` - Inherited bool `json:"inherited"` + UserID int64 `json:"userId" xorm:"user_id"` + UserLogin string `json:"userLogin"` + UserEmail string `json:"userEmail"` + UserAvatarURL string `json:"userAvatarUrl" xorm:"user_avatar_url"` + TeamID int64 `json:"teamId" xorm:"team_id"` + TeamEmail string `json:"teamEmail"` + TeamAvatarURL string `json:"teamAvatarUrl" xorm:"team_avatar_url"` + Team string `json:"team"` + Role *org.RoleType `json:"role,omitempty"` + Permission PermissionType `json:"permission"` + PermissionName string `json:"permissionName"` + UID string `json:"uid" xorm:"uid"` + Title string `json:"title"` + Slug string `json:"slug"` + IsFolder bool `json:"isFolder"` + URL string `json:"url" xorm:"url"` + Inherited bool `json:"inherited"` } func (dto *DashboardACLInfoDTO) hasSameRoleAs(other *DashboardACLInfoDTO) bool { @@ -398,3 +398,23 @@ type GetDashboardACLInfoListQuery struct { DashboardID int64 OrgID int64 } + +type FindPersistedDashboardsQuery struct { + Title string + OrgId int64 + SignedInUser *user.SignedInUser + IsStarred bool + DashboardIds []int64 + DashboardUIDs []string + Type string + FolderIds []int64 + Tags []string + Limit int64 + Page int64 + Permission PermissionType + Sort models.SortOption + + Filters []interface{} + + Result models.HitList +} diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index e234cb093b1..b4098316e02 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -401,7 +401,7 @@ func (dr *DashboardServiceImpl) MakeUserAdmin(ctx context.Context, orgID int64, OrgID: orgID, DashboardID: dashboardID, UserID: userID, - Permission: models.PERMISSION_ADMIN, + Permission: dashboards.PERMISSION_ADMIN, Created: time.Now(), Updated: time.Now(), }, @@ -413,7 +413,7 @@ func (dr *DashboardServiceImpl) MakeUserAdmin(ctx context.Context, orgID int64, OrgID: orgID, DashboardID: dashboardID, Role: &rtEditor, - Permission: models.PERMISSION_EDIT, + Permission: dashboards.PERMISSION_EDIT, Created: time.Now(), Updated: time.Now(), }, @@ -421,7 +421,7 @@ func (dr *DashboardServiceImpl) MakeUserAdmin(ctx context.Context, orgID int64, OrgID: orgID, DashboardID: dashboardID, Role: &rtViewer, - Permission: models.PERMISSION_VIEW, + Permission: dashboards.PERMISSION_VIEW, Created: time.Now(), Updated: time.Now(), }, @@ -497,14 +497,14 @@ func (dr *DashboardServiceImpl) setDefaultPermissions(ctx context.Context, dto * var permissions []accesscontrol.SetResourcePermissionCommand if !provisioned && dto.User.IsRealUser() && !dto.User.IsAnonymous { permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{ - UserID: dto.User.UserID, Permission: models.PERMISSION_ADMIN.String(), + UserID: dto.User.UserID, Permission: dashboards.PERMISSION_ADMIN.String(), }) } if !inFolder { permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{ - {BuiltinRole: string(org.RoleEditor), Permission: models.PERMISSION_EDIT.String()}, - {BuiltinRole: string(org.RoleViewer), Permission: models.PERMISSION_VIEW.String()}, + {BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_EDIT.String()}, + {BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_VIEW.String()}, }...) } @@ -538,11 +538,11 @@ func (dr *DashboardServiceImpl) GetDashboards(ctx context.Context, query *dashbo return dr.dashboardStore.GetDashboards(ctx, query) } -func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) { +func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) { return dr.dashboardStore.FindDashboards(ctx, query) } -func (dr *DashboardServiceImpl) SearchDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) error { +func (dr *DashboardServiceImpl) SearchDashboards(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) error { res, err := dr.FindDashboards(ctx, query) if err != nil { return err @@ -564,7 +564,7 @@ func getHitType(item dashboards.DashboardSearchProjection) models.HitType { return hitType } -func makeQueryResult(query *models.FindPersistedDashboardsQuery, res []dashboards.DashboardSearchProjection) { +func makeQueryResult(query *dashboards.FindPersistedDashboardsQuery, res []dashboards.DashboardSearchProjection) { query.Result = make([]*models.Hit, 0) hits := make(map[int64]*models.Hit) diff --git a/pkg/services/dashboards/store_mock.go b/pkg/services/dashboards/store_mock.go index d9dce524db4..f32268a86ce 100644 --- a/pkg/services/dashboards/store_mock.go +++ b/pkg/services/dashboards/store_mock.go @@ -1,16 +1,14 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. +// Code generated by mockery v2.16.0. DO NOT EDIT. package dashboards import ( context "context" - alertingmodels "github.com/grafana/grafana/pkg/services/alerting/models" folder "github.com/grafana/grafana/pkg/services/folder" - mock "github.com/stretchr/testify/mock" - models "github.com/grafana/grafana/pkg/models" + models "github.com/grafana/grafana/pkg/services/alerting/models" quota "github.com/grafana/grafana/pkg/services/quota" ) @@ -107,11 +105,11 @@ func (_m *FakeDashboardStore) DeleteOrphanedProvisionedDashboards(ctx context.Co } // FindDashboards provides a mock function with given fields: ctx, query -func (_m *FakeDashboardStore) FindDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) { +func (_m *FakeDashboardStore) FindDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) { ret := _m.Called(ctx, query) var r0 []DashboardSearchProjection - if rf, ok := ret.Get(0).(func(context.Context, *models.FindPersistedDashboardsQuery) []DashboardSearchProjection); ok { + if rf, ok := ret.Get(0).(func(context.Context, *FindPersistedDashboardsQuery) []DashboardSearchProjection); ok { r0 = rf(ctx, query) } else { if ret.Get(0) != nil { @@ -120,7 +118,7 @@ func (_m *FakeDashboardStore) FindDashboards(ctx context.Context, query *models. } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *models.FindPersistedDashboardsQuery) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, *FindPersistedDashboardsQuery) error); ok { r1 = rf(ctx, query) } else { r1 = ret.Error(1) @@ -379,11 +377,11 @@ func (_m *FakeDashboardStore) HasEditPermissionInFolders(ctx context.Context, qu } // SaveAlerts provides a mock function with given fields: ctx, dashID, alerts -func (_m *FakeDashboardStore) SaveAlerts(ctx context.Context, dashID int64, alerts []*alertingmodels.Alert) error { +func (_m *FakeDashboardStore) SaveAlerts(ctx context.Context, dashID int64, alerts []*models.Alert) error { ret := _m.Called(ctx, dashID, alerts) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, int64, []*alertingmodels.Alert) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, int64, []*models.Alert) error); ok { r0 = rf(ctx, dashID, alerts) } else { r0 = ret.Error(0) diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 9254fcfb753..f7dfb2e3266 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -11,7 +11,6 @@ import ( "github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/infra/db" "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/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -318,13 +317,13 @@ func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) ( var permissions []accesscontrol.SetResourcePermissionCommand if user.IsRealUser() && !user.IsAnonymous { permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{ - UserID: userID, Permission: models.PERMISSION_ADMIN.String(), + UserID: userID, Permission: dashboards.PERMISSION_ADMIN.String(), }) } permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{ - {BuiltinRole: string(org.RoleEditor), Permission: models.PERMISSION_EDIT.String()}, - {BuiltinRole: string(org.RoleViewer), Permission: models.PERMISSION_VIEW.String()}, + {BuiltinRole: string(org.RoleEditor), Permission: dashboards.PERMISSION_EDIT.String()}, + {BuiltinRole: string(org.RoleViewer), Permission: dashboards.PERMISSION_VIEW.String()}, }...) _, permissionErr = s.permissions.SetPermissions(ctx, cmd.OrgID, createdFolder.UID, permissions...) @@ -634,7 +633,7 @@ func (s *Service) MakeUserAdmin(ctx context.Context, orgID int64, userID, folder OrgID: orgID, DashboardID: folderID, UserID: userID, - Permission: models.PERMISSION_ADMIN, + Permission: dashboards.PERMISSION_ADMIN, Created: time.Now(), Updated: time.Now(), }, @@ -646,7 +645,7 @@ func (s *Service) MakeUserAdmin(ctx context.Context, orgID int64, userID, folder OrgID: orgID, DashboardID: folderID, Role: &rtEditor, - Permission: models.PERMISSION_EDIT, + Permission: dashboards.PERMISSION_EDIT, Created: time.Now(), Updated: time.Now(), }, @@ -654,7 +653,7 @@ func (s *Service) MakeUserAdmin(ctx context.Context, orgID int64, userID, folder OrgID: orgID, DashboardID: folderID, Role: &rtViewer, - Permission: models.PERMISSION_VIEW, + Permission: dashboards.PERMISSION_VIEW, Created: time.Now(), Updated: time.Now(), }, diff --git a/pkg/services/guardian/accesscontrol_guardian.go b/pkg/services/guardian/accesscontrol_guardian.go index b2d7bad4100..1a57bba508f 100644 --- a/pkg/services/guardian/accesscontrol_guardian.go +++ b/pkg/services/guardian/accesscontrol_guardian.go @@ -6,7 +6,6 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "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/dashboards" "github.com/grafana/grafana/pkg/services/org" @@ -14,10 +13,10 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -var permissionMap = map[string]models.PermissionType{ - "View": models.PERMISSION_VIEW, - "Edit": models.PERMISSION_EDIT, - "Admin": models.PERMISSION_ADMIN, +var permissionMap = map[string]dashboards.PermissionType{ + "View": dashboards.PERMISSION_VIEW, + "Edit": dashboards.PERMISSION_EDIT, + "Admin": dashboards.PERMISSION_ADMIN, } var _ DashboardGuardian = new(AccessControlDashboardGuardian) @@ -235,7 +234,7 @@ func (a *AccessControlDashboardGuardian) evaluate(evaluator accesscontrol.Evalua return ok, err } -func (a *AccessControlDashboardGuardian) CheckPermissionBeforeUpdate(permission models.PermissionType, updatePermissions []*dashboards.DashboardACL) (bool, error) { +func (a *AccessControlDashboardGuardian) CheckPermissionBeforeUpdate(permission dashboards.PermissionType, updatePermissions []*dashboards.DashboardACL) (bool, error) { // always true for access control return true, nil } diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index e64a1f7a763..f47fe6398e1 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -6,7 +6,6 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/team" @@ -30,7 +29,7 @@ type DashboardGuardian interface { CanAdmin() (bool, error) CanDelete() (bool, error) CanCreate(folderID int64, isFolder bool) (bool, error) - CheckPermissionBeforeUpdate(permission models.PermissionType, updatePermissions []*dashboards.DashboardACL) (bool, error) + CheckPermissionBeforeUpdate(permission dashboards.PermissionType, updatePermissions []*dashboards.DashboardACL) (bool, error) // GetACL returns ACL. GetACL() ([]*dashboards.DashboardACLInfoDTO, error) @@ -149,23 +148,23 @@ func newDashboardGuardianByDashboard(ctx context.Context, dash *dashboards.Dashb } func (g *dashboardGuardianImpl) CanSave() (bool, error) { - return g.HasPermission(models.PERMISSION_EDIT) + return g.HasPermission(dashboards.PERMISSION_EDIT) } func (g *dashboardGuardianImpl) CanEdit() (bool, error) { if setting.ViewersCanEdit { - return g.HasPermission(models.PERMISSION_VIEW) + return g.HasPermission(dashboards.PERMISSION_VIEW) } - return g.HasPermission(models.PERMISSION_EDIT) + return g.HasPermission(dashboards.PERMISSION_EDIT) } func (g *dashboardGuardianImpl) CanView() (bool, error) { - return g.HasPermission(models.PERMISSION_VIEW) + return g.HasPermission(dashboards.PERMISSION_VIEW) } func (g *dashboardGuardianImpl) CanAdmin() (bool, error) { - return g.HasPermission(models.PERMISSION_ADMIN) + return g.HasPermission(dashboards.PERMISSION_ADMIN) } func (g *dashboardGuardianImpl) CanDelete() (bool, error) { @@ -178,7 +177,7 @@ func (g *dashboardGuardianImpl) CanCreate(_ int64, _ bool) (bool, error) { return g.CanSave() } -func (g *dashboardGuardianImpl) HasPermission(permission models.PermissionType) (bool, error) { +func (g *dashboardGuardianImpl) HasPermission(permission dashboards.PermissionType) (bool, error) { if g.user.OrgRole == org.RoleAdmin { return g.logHasPermissionResult(permission, true, nil) } @@ -192,7 +191,7 @@ func (g *dashboardGuardianImpl) HasPermission(permission models.PermissionType) return g.logHasPermissionResult(permission, result, err) } -func (g *dashboardGuardianImpl) logHasPermissionResult(permission models.PermissionType, hasPermission bool, err error) (bool, error) { +func (g *dashboardGuardianImpl) logHasPermissionResult(permission dashboards.PermissionType, hasPermission bool, err error) (bool, error) { if err != nil { return hasPermission, err } @@ -206,7 +205,7 @@ func (g *dashboardGuardianImpl) logHasPermissionResult(permission models.Permiss return hasPermission, err } -func (g *dashboardGuardianImpl) checkACL(permission models.PermissionType, acl []*dashboards.DashboardACLInfoDTO) (bool, error) { +func (g *dashboardGuardianImpl) checkACL(permission dashboards.PermissionType, acl []*dashboards.DashboardACLInfoDTO) (bool, error) { orgRole := g.user.OrgRole teamACLItems := []*dashboards.DashboardACLInfoDTO{} @@ -254,10 +253,10 @@ func (g *dashboardGuardianImpl) checkACL(permission models.PermissionType, acl [ return false, nil } -func (g *dashboardGuardianImpl) CheckPermissionBeforeUpdate(permission models.PermissionType, updatePermissions []*dashboards.DashboardACL) (bool, error) { +func (g *dashboardGuardianImpl) CheckPermissionBeforeUpdate(permission dashboards.PermissionType, updatePermissions []*dashboards.DashboardACL) (bool, error) { acl := []*dashboards.DashboardACLInfoDTO{} adminRole := org.RoleAdmin - everyoneWithAdminRole := &dashboards.DashboardACLInfoDTO{DashboardID: g.dashId, UserID: 0, TeamID: 0, Role: &adminRole, Permission: models.PERMISSION_ADMIN} + everyoneWithAdminRole := &dashboards.DashboardACLInfoDTO{DashboardID: g.dashId, UserID: 0, TeamID: 0, Role: &adminRole, Permission: dashboards.PERMISSION_ADMIN} // validate that duplicate permissions don't exists for _, p := range updatePermissions { @@ -436,11 +435,11 @@ func (g *FakeDashboardGuardian) CanCreate(_ int64, _ bool) (bool, error) { return g.CanSaveValue, nil } -func (g *FakeDashboardGuardian) HasPermission(permission models.PermissionType) (bool, error) { +func (g *FakeDashboardGuardian) HasPermission(permission dashboards.PermissionType) (bool, error) { return g.HasPermissionValue, nil } -func (g *FakeDashboardGuardian) CheckPermissionBeforeUpdate(permission models.PermissionType, updatePermissions []*dashboards.DashboardACL) (bool, error) { +func (g *FakeDashboardGuardian) CheckPermissionBeforeUpdate(permission dashboards.PermissionType, updatePermissions []*dashboards.DashboardACL) (bool, error) { return g.CheckPermissionBeforeUpdateValue, g.CheckPermissionBeforeUpdateError } diff --git a/pkg/services/guardian/guardian_test.go b/pkg/services/guardian/guardian_test.go index 7da22aa0c6f..d7d3a8b5a9d 100644 --- a/pkg/services/guardian/guardian_test.go +++ b/pkg/services/guardian/guardian_test.go @@ -11,7 +11,6 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db/dbtest" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/team/teamtest" @@ -44,44 +43,44 @@ func TestGuardianAdmin(t *testing.T) { sc.defaultPermissionScenario(USER, FULL_ACCESS) // dashboard has user with permission - sc.dashboardPermissionScenario(USER, models.PERMISSION_ADMIN, FULL_ACCESS) - sc.dashboardPermissionScenario(USER, models.PERMISSION_EDIT, FULL_ACCESS) - sc.dashboardPermissionScenario(USER, models.PERMISSION_VIEW, FULL_ACCESS) + sc.dashboardPermissionScenario(USER, dashboards.PERMISSION_ADMIN, FULL_ACCESS) + sc.dashboardPermissionScenario(USER, dashboards.PERMISSION_EDIT, FULL_ACCESS) + sc.dashboardPermissionScenario(USER, dashboards.PERMISSION_VIEW, FULL_ACCESS) // dashboard has team with permission - sc.dashboardPermissionScenario(TEAM, models.PERMISSION_ADMIN, FULL_ACCESS) - sc.dashboardPermissionScenario(TEAM, models.PERMISSION_EDIT, FULL_ACCESS) - sc.dashboardPermissionScenario(TEAM, models.PERMISSION_VIEW, FULL_ACCESS) + sc.dashboardPermissionScenario(TEAM, dashboards.PERMISSION_ADMIN, FULL_ACCESS) + sc.dashboardPermissionScenario(TEAM, dashboards.PERMISSION_EDIT, FULL_ACCESS) + sc.dashboardPermissionScenario(TEAM, dashboards.PERMISSION_VIEW, FULL_ACCESS) // dashboard has editor role with permission - sc.dashboardPermissionScenario(EDITOR, models.PERMISSION_ADMIN, FULL_ACCESS) - sc.dashboardPermissionScenario(EDITOR, models.PERMISSION_EDIT, FULL_ACCESS) - sc.dashboardPermissionScenario(EDITOR, models.PERMISSION_VIEW, FULL_ACCESS) + sc.dashboardPermissionScenario(EDITOR, dashboards.PERMISSION_ADMIN, FULL_ACCESS) + sc.dashboardPermissionScenario(EDITOR, dashboards.PERMISSION_EDIT, FULL_ACCESS) + sc.dashboardPermissionScenario(EDITOR, dashboards.PERMISSION_VIEW, FULL_ACCESS) // dashboard has viewer role with permission - sc.dashboardPermissionScenario(VIEWER, models.PERMISSION_ADMIN, FULL_ACCESS) - sc.dashboardPermissionScenario(VIEWER, models.PERMISSION_EDIT, FULL_ACCESS) - sc.dashboardPermissionScenario(VIEWER, models.PERMISSION_VIEW, FULL_ACCESS) + sc.dashboardPermissionScenario(VIEWER, dashboards.PERMISSION_ADMIN, FULL_ACCESS) + sc.dashboardPermissionScenario(VIEWER, dashboards.PERMISSION_EDIT, FULL_ACCESS) + sc.dashboardPermissionScenario(VIEWER, dashboards.PERMISSION_VIEW, FULL_ACCESS) // parent folder has user with permission - sc.parentFolderPermissionScenario(USER, models.PERMISSION_ADMIN, FULL_ACCESS) - sc.parentFolderPermissionScenario(USER, models.PERMISSION_EDIT, FULL_ACCESS) - sc.parentFolderPermissionScenario(USER, models.PERMISSION_VIEW, FULL_ACCESS) + sc.parentFolderPermissionScenario(USER, dashboards.PERMISSION_ADMIN, FULL_ACCESS) + sc.parentFolderPermissionScenario(USER, dashboards.PERMISSION_EDIT, FULL_ACCESS) + sc.parentFolderPermissionScenario(USER, dashboards.PERMISSION_VIEW, FULL_ACCESS) // parent folder has team with permission - sc.parentFolderPermissionScenario(TEAM, models.PERMISSION_ADMIN, FULL_ACCESS) - sc.parentFolderPermissionScenario(TEAM, models.PERMISSION_EDIT, FULL_ACCESS) - sc.parentFolderPermissionScenario(TEAM, models.PERMISSION_VIEW, FULL_ACCESS) + sc.parentFolderPermissionScenario(TEAM, dashboards.PERMISSION_ADMIN, FULL_ACCESS) + sc.parentFolderPermissionScenario(TEAM, dashboards.PERMISSION_EDIT, FULL_ACCESS) + sc.parentFolderPermissionScenario(TEAM, dashboards.PERMISSION_VIEW, FULL_ACCESS) // parent folder has editor role with permission - sc.parentFolderPermissionScenario(EDITOR, models.PERMISSION_ADMIN, FULL_ACCESS) - sc.parentFolderPermissionScenario(EDITOR, models.PERMISSION_EDIT, FULL_ACCESS) - sc.parentFolderPermissionScenario(EDITOR, models.PERMISSION_VIEW, FULL_ACCESS) + sc.parentFolderPermissionScenario(EDITOR, dashboards.PERMISSION_ADMIN, FULL_ACCESS) + sc.parentFolderPermissionScenario(EDITOR, dashboards.PERMISSION_EDIT, FULL_ACCESS) + sc.parentFolderPermissionScenario(EDITOR, dashboards.PERMISSION_VIEW, FULL_ACCESS) // parent folder has viewer role with permission - sc.parentFolderPermissionScenario(VIEWER, models.PERMISSION_ADMIN, FULL_ACCESS) - sc.parentFolderPermissionScenario(VIEWER, models.PERMISSION_EDIT, FULL_ACCESS) - sc.parentFolderPermissionScenario(VIEWER, models.PERMISSION_VIEW, FULL_ACCESS) + sc.parentFolderPermissionScenario(VIEWER, dashboards.PERMISSION_ADMIN, FULL_ACCESS) + sc.parentFolderPermissionScenario(VIEWER, dashboards.PERMISSION_EDIT, FULL_ACCESS) + sc.parentFolderPermissionScenario(VIEWER, dashboards.PERMISSION_VIEW, FULL_ACCESS) }) } @@ -91,44 +90,44 @@ func TestGuardianEditor(t *testing.T) { sc.defaultPermissionScenario(USER, EDITOR_ACCESS) // dashboard has user with permission - sc.dashboardPermissionScenario(USER, models.PERMISSION_ADMIN, FULL_ACCESS) - sc.dashboardPermissionScenario(USER, models.PERMISSION_EDIT, EDITOR_ACCESS) - sc.dashboardPermissionScenario(USER, models.PERMISSION_VIEW, CAN_VIEW) + sc.dashboardPermissionScenario(USER, dashboards.PERMISSION_ADMIN, FULL_ACCESS) + sc.dashboardPermissionScenario(USER, dashboards.PERMISSION_EDIT, EDITOR_ACCESS) + sc.dashboardPermissionScenario(USER, dashboards.PERMISSION_VIEW, CAN_VIEW) // dashboard has team with permission - sc.dashboardPermissionScenario(TEAM, models.PERMISSION_ADMIN, FULL_ACCESS) - sc.dashboardPermissionScenario(TEAM, models.PERMISSION_EDIT, EDITOR_ACCESS) - sc.dashboardPermissionScenario(TEAM, models.PERMISSION_VIEW, CAN_VIEW) + sc.dashboardPermissionScenario(TEAM, dashboards.PERMISSION_ADMIN, FULL_ACCESS) + sc.dashboardPermissionScenario(TEAM, dashboards.PERMISSION_EDIT, EDITOR_ACCESS) + sc.dashboardPermissionScenario(TEAM, dashboards.PERMISSION_VIEW, CAN_VIEW) // dashboard has editor role with permission - sc.dashboardPermissionScenario(EDITOR, models.PERMISSION_ADMIN, FULL_ACCESS) - sc.dashboardPermissionScenario(EDITOR, models.PERMISSION_EDIT, EDITOR_ACCESS) - sc.dashboardPermissionScenario(EDITOR, models.PERMISSION_VIEW, VIEWER_ACCESS) + sc.dashboardPermissionScenario(EDITOR, dashboards.PERMISSION_ADMIN, FULL_ACCESS) + sc.dashboardPermissionScenario(EDITOR, dashboards.PERMISSION_EDIT, EDITOR_ACCESS) + sc.dashboardPermissionScenario(EDITOR, dashboards.PERMISSION_VIEW, VIEWER_ACCESS) // dashboard has viewer role with permission - sc.dashboardPermissionScenario(VIEWER, models.PERMISSION_ADMIN, NO_ACCESS) - sc.dashboardPermissionScenario(VIEWER, models.PERMISSION_EDIT, NO_ACCESS) - sc.dashboardPermissionScenario(VIEWER, models.PERMISSION_VIEW, NO_ACCESS) + sc.dashboardPermissionScenario(VIEWER, dashboards.PERMISSION_ADMIN, NO_ACCESS) + sc.dashboardPermissionScenario(VIEWER, dashboards.PERMISSION_EDIT, NO_ACCESS) + sc.dashboardPermissionScenario(VIEWER, dashboards.PERMISSION_VIEW, NO_ACCESS) // parent folder has user with permission - sc.parentFolderPermissionScenario(USER, models.PERMISSION_ADMIN, FULL_ACCESS) - sc.parentFolderPermissionScenario(USER, models.PERMISSION_EDIT, EDITOR_ACCESS) - sc.parentFolderPermissionScenario(USER, models.PERMISSION_VIEW, VIEWER_ACCESS) + sc.parentFolderPermissionScenario(USER, dashboards.PERMISSION_ADMIN, FULL_ACCESS) + sc.parentFolderPermissionScenario(USER, dashboards.PERMISSION_EDIT, EDITOR_ACCESS) + sc.parentFolderPermissionScenario(USER, dashboards.PERMISSION_VIEW, VIEWER_ACCESS) // parent folder has team with permission - sc.parentFolderPermissionScenario(TEAM, models.PERMISSION_ADMIN, FULL_ACCESS) - sc.parentFolderPermissionScenario(TEAM, models.PERMISSION_EDIT, EDITOR_ACCESS) - sc.parentFolderPermissionScenario(TEAM, models.PERMISSION_VIEW, VIEWER_ACCESS) + sc.parentFolderPermissionScenario(TEAM, dashboards.PERMISSION_ADMIN, FULL_ACCESS) + sc.parentFolderPermissionScenario(TEAM, dashboards.PERMISSION_EDIT, EDITOR_ACCESS) + sc.parentFolderPermissionScenario(TEAM, dashboards.PERMISSION_VIEW, VIEWER_ACCESS) // parent folder has editor role with permission - sc.parentFolderPermissionScenario(EDITOR, models.PERMISSION_ADMIN, FULL_ACCESS) - sc.parentFolderPermissionScenario(EDITOR, models.PERMISSION_EDIT, EDITOR_ACCESS) - sc.parentFolderPermissionScenario(EDITOR, models.PERMISSION_VIEW, VIEWER_ACCESS) + sc.parentFolderPermissionScenario(EDITOR, dashboards.PERMISSION_ADMIN, FULL_ACCESS) + sc.parentFolderPermissionScenario(EDITOR, dashboards.PERMISSION_EDIT, EDITOR_ACCESS) + sc.parentFolderPermissionScenario(EDITOR, dashboards.PERMISSION_VIEW, VIEWER_ACCESS) // parent folder has viewer role with permission - sc.parentFolderPermissionScenario(VIEWER, models.PERMISSION_ADMIN, NO_ACCESS) - sc.parentFolderPermissionScenario(VIEWER, models.PERMISSION_EDIT, NO_ACCESS) - sc.parentFolderPermissionScenario(VIEWER, models.PERMISSION_VIEW, NO_ACCESS) + sc.parentFolderPermissionScenario(VIEWER, dashboards.PERMISSION_ADMIN, NO_ACCESS) + sc.parentFolderPermissionScenario(VIEWER, dashboards.PERMISSION_EDIT, NO_ACCESS) + sc.parentFolderPermissionScenario(VIEWER, dashboards.PERMISSION_VIEW, NO_ACCESS) }) } @@ -138,44 +137,44 @@ func TestGuardianViewer(t *testing.T) { sc.defaultPermissionScenario(USER, VIEWER_ACCESS) // dashboard has user with permission - sc.dashboardPermissionScenario(USER, models.PERMISSION_ADMIN, FULL_ACCESS) - sc.dashboardPermissionScenario(USER, models.PERMISSION_EDIT, EDITOR_ACCESS) - sc.dashboardPermissionScenario(USER, models.PERMISSION_VIEW, VIEWER_ACCESS) + sc.dashboardPermissionScenario(USER, dashboards.PERMISSION_ADMIN, FULL_ACCESS) + sc.dashboardPermissionScenario(USER, dashboards.PERMISSION_EDIT, EDITOR_ACCESS) + sc.dashboardPermissionScenario(USER, dashboards.PERMISSION_VIEW, VIEWER_ACCESS) // dashboard has team with permission - sc.dashboardPermissionScenario(TEAM, models.PERMISSION_ADMIN, FULL_ACCESS) - sc.dashboardPermissionScenario(TEAM, models.PERMISSION_EDIT, EDITOR_ACCESS) - sc.dashboardPermissionScenario(TEAM, models.PERMISSION_VIEW, VIEWER_ACCESS) + sc.dashboardPermissionScenario(TEAM, dashboards.PERMISSION_ADMIN, FULL_ACCESS) + sc.dashboardPermissionScenario(TEAM, dashboards.PERMISSION_EDIT, EDITOR_ACCESS) + sc.dashboardPermissionScenario(TEAM, dashboards.PERMISSION_VIEW, VIEWER_ACCESS) // dashboard has editor role with permission - sc.dashboardPermissionScenario(EDITOR, models.PERMISSION_ADMIN, NO_ACCESS) - sc.dashboardPermissionScenario(EDITOR, models.PERMISSION_EDIT, NO_ACCESS) - sc.dashboardPermissionScenario(EDITOR, models.PERMISSION_VIEW, NO_ACCESS) + sc.dashboardPermissionScenario(EDITOR, dashboards.PERMISSION_ADMIN, NO_ACCESS) + sc.dashboardPermissionScenario(EDITOR, dashboards.PERMISSION_EDIT, NO_ACCESS) + sc.dashboardPermissionScenario(EDITOR, dashboards.PERMISSION_VIEW, NO_ACCESS) // dashboard has viewer role with permission - sc.dashboardPermissionScenario(VIEWER, models.PERMISSION_ADMIN, FULL_ACCESS) - sc.dashboardPermissionScenario(VIEWER, models.PERMISSION_EDIT, EDITOR_ACCESS) - sc.dashboardPermissionScenario(VIEWER, models.PERMISSION_VIEW, VIEWER_ACCESS) + sc.dashboardPermissionScenario(VIEWER, dashboards.PERMISSION_ADMIN, FULL_ACCESS) + sc.dashboardPermissionScenario(VIEWER, dashboards.PERMISSION_EDIT, EDITOR_ACCESS) + sc.dashboardPermissionScenario(VIEWER, dashboards.PERMISSION_VIEW, VIEWER_ACCESS) // parent folder has user with permission - sc.parentFolderPermissionScenario(USER, models.PERMISSION_ADMIN, FULL_ACCESS) - sc.parentFolderPermissionScenario(USER, models.PERMISSION_EDIT, EDITOR_ACCESS) - sc.parentFolderPermissionScenario(USER, models.PERMISSION_VIEW, VIEWER_ACCESS) + sc.parentFolderPermissionScenario(USER, dashboards.PERMISSION_ADMIN, FULL_ACCESS) + sc.parentFolderPermissionScenario(USER, dashboards.PERMISSION_EDIT, EDITOR_ACCESS) + sc.parentFolderPermissionScenario(USER, dashboards.PERMISSION_VIEW, VIEWER_ACCESS) // parent folder has team with permission - sc.parentFolderPermissionScenario(TEAM, models.PERMISSION_ADMIN, FULL_ACCESS) - sc.parentFolderPermissionScenario(TEAM, models.PERMISSION_EDIT, EDITOR_ACCESS) - sc.parentFolderPermissionScenario(TEAM, models.PERMISSION_VIEW, VIEWER_ACCESS) + sc.parentFolderPermissionScenario(TEAM, dashboards.PERMISSION_ADMIN, FULL_ACCESS) + sc.parentFolderPermissionScenario(TEAM, dashboards.PERMISSION_EDIT, EDITOR_ACCESS) + sc.parentFolderPermissionScenario(TEAM, dashboards.PERMISSION_VIEW, VIEWER_ACCESS) // parent folder has editor role with permission - sc.parentFolderPermissionScenario(EDITOR, models.PERMISSION_ADMIN, NO_ACCESS) - sc.parentFolderPermissionScenario(EDITOR, models.PERMISSION_EDIT, NO_ACCESS) - sc.parentFolderPermissionScenario(EDITOR, models.PERMISSION_VIEW, NO_ACCESS) + sc.parentFolderPermissionScenario(EDITOR, dashboards.PERMISSION_ADMIN, NO_ACCESS) + sc.parentFolderPermissionScenario(EDITOR, dashboards.PERMISSION_EDIT, NO_ACCESS) + sc.parentFolderPermissionScenario(EDITOR, dashboards.PERMISSION_VIEW, NO_ACCESS) // parent folder has viewer role with permission - sc.parentFolderPermissionScenario(VIEWER, models.PERMISSION_ADMIN, FULL_ACCESS) - sc.parentFolderPermissionScenario(VIEWER, models.PERMISSION_EDIT, EDITOR_ACCESS) - sc.parentFolderPermissionScenario(VIEWER, models.PERMISSION_VIEW, VIEWER_ACCESS) + sc.parentFolderPermissionScenario(VIEWER, dashboards.PERMISSION_ADMIN, FULL_ACCESS) + sc.parentFolderPermissionScenario(VIEWER, dashboards.PERMISSION_EDIT, EDITOR_ACCESS) + sc.parentFolderPermissionScenario(VIEWER, dashboards.PERMISSION_VIEW, VIEWER_ACCESS) }) apiKeyScenario("Given api key with viewer role", t, org.RoleViewer, func(sc *scenarioContext) { @@ -189,8 +188,8 @@ func (sc *scenarioContext) defaultPermissionScenario(pt permissionType, flag per sc.callerFile = callerFile sc.callerLine = callerLine existingPermissions := []*dashboards.DashboardACLInfoDTO{ - toDto(newEditorRolePermission(defaultDashboardID, models.PERMISSION_EDIT)), - toDto(newViewerRolePermission(defaultDashboardID, models.PERMISSION_VIEW)), + toDto(newEditorRolePermission(defaultDashboardID, dashboards.PERMISSION_EDIT)), + toDto(newViewerRolePermission(defaultDashboardID, dashboards.PERMISSION_VIEW)), } permissionScenario("and existing permissions are the default permissions (everyone with editor role can edit, everyone with viewer role can view)", @@ -203,7 +202,7 @@ func (sc *scenarioContext) defaultPermissionScenario(pt permissionType, flag per }) } -func (sc *scenarioContext) dashboardPermissionScenario(pt permissionType, permission models.PermissionType, flag permissionFlags) { +func (sc *scenarioContext) dashboardPermissionScenario(pt permissionType, permission dashboards.PermissionType, flag permissionFlags) { _, callerFile, callerLine, _ := runtime.Caller(1) sc.callerFile = callerFile sc.callerLine = callerLine @@ -230,7 +229,7 @@ func (sc *scenarioContext) dashboardPermissionScenario(pt permissionType, permis }) } -func (sc *scenarioContext) parentFolderPermissionScenario(pt permissionType, permission models.PermissionType, flag permissionFlags) { +func (sc *scenarioContext) parentFolderPermissionScenario(pt permissionType, permission dashboards.PermissionType, flag permissionFlags) { _, callerFile, callerLine, _ := runtime.Caller(1) sc.callerFile = callerFile sc.callerLine = callerLine @@ -313,11 +312,11 @@ func (sc *scenarioContext) verifyDuplicatePermissionsShouldNotBeAllowed() { tc := "When updating dashboard permissions with duplicate permission for user should not be allowed" sc.t.Run(tc, func(t *testing.T) { p := []*dashboards.DashboardACL{ - newDefaultUserPermission(dashboardID, models.PERMISSION_VIEW), - newDefaultUserPermission(dashboardID, models.PERMISSION_ADMIN), + newDefaultUserPermission(dashboardID, dashboards.PERMISSION_VIEW), + newDefaultUserPermission(dashboardID, dashboards.PERMISSION_ADMIN), } sc.updatePermissions = p - _, err := sc.g.CheckPermissionBeforeUpdate(models.PERMISSION_ADMIN, p) + _, err := sc.g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, p) if !errors.Is(err, ErrGuardianPermissionExists) { sc.reportFailure(tc, ErrGuardianPermissionExists, err) @@ -328,11 +327,11 @@ func (sc *scenarioContext) verifyDuplicatePermissionsShouldNotBeAllowed() { tc = "When updating dashboard permissions with duplicate permission for team should not be allowed" sc.t.Run(tc, func(t *testing.T) { p := []*dashboards.DashboardACL{ - newDefaultTeamPermission(dashboardID, models.PERMISSION_VIEW), - newDefaultTeamPermission(dashboardID, models.PERMISSION_ADMIN), + newDefaultTeamPermission(dashboardID, dashboards.PERMISSION_VIEW), + newDefaultTeamPermission(dashboardID, dashboards.PERMISSION_ADMIN), } sc.updatePermissions = p - _, err := sc.g.CheckPermissionBeforeUpdate(models.PERMISSION_ADMIN, p) + _, err := sc.g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, p) if !errors.Is(err, ErrGuardianPermissionExists) { sc.reportFailure(tc, ErrGuardianPermissionExists, err) } @@ -342,11 +341,11 @@ func (sc *scenarioContext) verifyDuplicatePermissionsShouldNotBeAllowed() { tc = "When updating dashboard permissions with duplicate permission for editor role should not be allowed" sc.t.Run(tc, func(t *testing.T) { p := []*dashboards.DashboardACL{ - newEditorRolePermission(dashboardID, models.PERMISSION_VIEW), - newEditorRolePermission(dashboardID, models.PERMISSION_ADMIN), + newEditorRolePermission(dashboardID, dashboards.PERMISSION_VIEW), + newEditorRolePermission(dashboardID, dashboards.PERMISSION_ADMIN), } sc.updatePermissions = p - _, err := sc.g.CheckPermissionBeforeUpdate(models.PERMISSION_ADMIN, p) + _, err := sc.g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, p) if !errors.Is(err, ErrGuardianPermissionExists) { sc.reportFailure(tc, ErrGuardianPermissionExists, err) @@ -357,11 +356,11 @@ func (sc *scenarioContext) verifyDuplicatePermissionsShouldNotBeAllowed() { tc = "When updating dashboard permissions with duplicate permission for viewer role should not be allowed" sc.t.Run(tc, func(t *testing.T) { p := []*dashboards.DashboardACL{ - newViewerRolePermission(dashboardID, models.PERMISSION_VIEW), - newViewerRolePermission(dashboardID, models.PERMISSION_ADMIN), + newViewerRolePermission(dashboardID, dashboards.PERMISSION_VIEW), + newViewerRolePermission(dashboardID, dashboards.PERMISSION_ADMIN), } sc.updatePermissions = p - _, err := sc.g.CheckPermissionBeforeUpdate(models.PERMISSION_ADMIN, p) + _, err := sc.g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, p) if !errors.Is(err, ErrGuardianPermissionExists) { sc.reportFailure(tc, ErrGuardianPermissionExists, err) } @@ -371,10 +370,10 @@ func (sc *scenarioContext) verifyDuplicatePermissionsShouldNotBeAllowed() { tc = "When updating dashboard permissions with duplicate permission for admin role should not be allowed" sc.t.Run(tc, func(t *testing.T) { p := []*dashboards.DashboardACL{ - newAdminRolePermission(dashboardID, models.PERMISSION_ADMIN), + newAdminRolePermission(dashboardID, dashboards.PERMISSION_ADMIN), } sc.updatePermissions = p - _, err := sc.g.CheckPermissionBeforeUpdate(models.PERMISSION_ADMIN, p) + _, err := sc.g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, p) if !errors.Is(err, ErrGuardianPermissionExists) { sc.reportFailure(tc, ErrGuardianPermissionExists, err) } @@ -387,7 +386,7 @@ func (sc *scenarioContext) verifyUpdateDashboardPermissionsShouldBeAllowed(pt pe return } - for _, p := range []models.PermissionType{models.PERMISSION_ADMIN, models.PERMISSION_EDIT, models.PERMISSION_VIEW} { + for _, p := range []dashboards.PermissionType{dashboards.PERMISSION_ADMIN, dashboards.PERMISSION_EDIT, dashboards.PERMISSION_VIEW} { tc := fmt.Sprintf("When updating dashboard permissions with %s permissions should be allowed", p.String()) sc.t.Run(tc, func(t *testing.T) { permissionList := []*dashboards.DashboardACL{} @@ -416,7 +415,7 @@ func (sc *scenarioContext) verifyUpdateDashboardPermissionsShouldBeAllowed(pt pe } sc.updatePermissions = permissionList - ok, err := sc.g.CheckPermissionBeforeUpdate(models.PERMISSION_ADMIN, permissionList) + ok, err := sc.g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, permissionList) if err != nil { sc.reportFailure(tc, nil, err) } @@ -433,7 +432,7 @@ func (sc *scenarioContext) verifyUpdateDashboardPermissionsShouldNotBeAllowed(pt return } - for _, p := range []models.PermissionType{models.PERMISSION_ADMIN, models.PERMISSION_EDIT, models.PERMISSION_VIEW} { + for _, p := range []dashboards.PermissionType{dashboards.PERMISSION_ADMIN, dashboards.PERMISSION_EDIT, dashboards.PERMISSION_VIEW} { tc := fmt.Sprintf("When updating dashboard permissions with %s permissions should NOT be allowed", p.String()) sc.t.Run(tc, func(t *testing.T) { permissionList := []*dashboards.DashboardACL{ @@ -456,7 +455,7 @@ func (sc *scenarioContext) verifyUpdateDashboardPermissionsShouldNotBeAllowed(pt } sc.updatePermissions = permissionList - ok, err := sc.g.CheckPermissionBeforeUpdate(models.PERMISSION_ADMIN, permissionList) + ok, err := sc.g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, permissionList) if err != nil { sc.reportFailure(tc, nil, err) } @@ -468,12 +467,12 @@ func (sc *scenarioContext) verifyUpdateDashboardPermissionsShouldNotBeAllowed(pt } } -func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsShouldBeAllowed(pt permissionType, parentFolderPermission models.PermissionType) { +func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsShouldBeAllowed(pt permissionType, parentFolderPermission dashboards.PermissionType) { if !sc.expectedFlags.canAdmin() { return } - for _, p := range []models.PermissionType{models.PERMISSION_ADMIN, models.PERMISSION_EDIT, models.PERMISSION_VIEW} { + for _, p := range []dashboards.PermissionType{dashboards.PERMISSION_ADMIN, dashboards.PERMISSION_EDIT, dashboards.PERMISSION_VIEW} { tc := fmt.Sprintf("When updating child dashboard permissions with %s permissions should be allowed", p.String()) sc.t.Run(tc, func(t *testing.T) { permissionList := []*dashboards.DashboardACL{} @@ -517,7 +516,7 @@ func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsShouldBeAllowed( } sc.updatePermissions = permissionList - ok, err := sc.g.CheckPermissionBeforeUpdate(models.PERMISSION_ADMIN, permissionList) + ok, err := sc.g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, permissionList) if err != nil { sc.reportFailure(tc, nil, err) } @@ -529,12 +528,12 @@ func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsShouldBeAllowed( } } -func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsShouldNotBeAllowed(pt permissionType, parentFolderPermission models.PermissionType) { +func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsShouldNotBeAllowed(pt permissionType, parentFolderPermission dashboards.PermissionType) { if sc.expectedFlags.canAdmin() { return } - for _, p := range []models.PermissionType{models.PERMISSION_ADMIN, models.PERMISSION_EDIT, models.PERMISSION_VIEW} { + for _, p := range []dashboards.PermissionType{dashboards.PERMISSION_ADMIN, dashboards.PERMISSION_EDIT, dashboards.PERMISSION_VIEW} { tc := fmt.Sprintf("When updating child dashboard permissions with %s permissions should NOT be allowed", p.String()) sc.t.Run(tc, func(t *testing.T) { permissionList := []*dashboards.DashboardACL{} @@ -578,7 +577,7 @@ func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsShouldNotBeAllow } sc.updatePermissions = permissionList - ok, err := sc.g.CheckPermissionBeforeUpdate(models.PERMISSION_ADMIN, permissionList) + ok, err := sc.g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, permissionList) if err != nil { sc.reportFailure(tc, nil, err) } @@ -590,12 +589,12 @@ func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsShouldNotBeAllow } } -func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsWithOverrideShouldBeAllowed(pt permissionType, parentFolderPermission models.PermissionType) { +func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsWithOverrideShouldBeAllowed(pt permissionType, parentFolderPermission dashboards.PermissionType) { if !sc.expectedFlags.canAdmin() { return } - for _, p := range []models.PermissionType{models.PERMISSION_ADMIN, models.PERMISSION_EDIT, models.PERMISSION_VIEW} { + for _, p := range []dashboards.PermissionType{dashboards.PERMISSION_ADMIN, dashboards.PERMISSION_EDIT, dashboards.PERMISSION_VIEW} { // permission to update is higher than parent folder permission if p > parentFolderPermission { continue @@ -624,7 +623,7 @@ func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsWithOverrideShou } sc.updatePermissions = permissionList - _, err := sc.g.CheckPermissionBeforeUpdate(models.PERMISSION_ADMIN, permissionList) + _, err := sc.g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, permissionList) if !errors.Is(err, ErrGuardianOverride) { sc.reportFailure(tc, ErrGuardianOverride, err) } @@ -633,12 +632,12 @@ func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsWithOverrideShou } } -func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsWithOverrideShouldNotBeAllowed(pt permissionType, parentFolderPermission models.PermissionType) { +func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsWithOverrideShouldNotBeAllowed(pt permissionType, parentFolderPermission dashboards.PermissionType) { if !sc.expectedFlags.canAdmin() { return } - for _, p := range []models.PermissionType{models.PERMISSION_ADMIN, models.PERMISSION_EDIT, models.PERMISSION_VIEW} { + for _, p := range []dashboards.PermissionType{dashboards.PERMISSION_ADMIN, dashboards.PERMISSION_EDIT, dashboards.PERMISSION_VIEW} { // permission to update is lower than or equal to parent folder permission if p <= parentFolderPermission { continue @@ -669,12 +668,12 @@ func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsWithOverrideShou } } - _, err := sc.g.CheckPermissionBeforeUpdate(models.PERMISSION_ADMIN, permissionList) + _, err := sc.g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, permissionList) if err != nil { sc.reportFailure(tc, nil, err) } sc.updatePermissions = permissionList - ok, err := sc.g.CheckPermissionBeforeUpdate(models.PERMISSION_ADMIN, permissionList) + ok, err := sc.g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, permissionList) if err != nil { sc.reportFailure(tc, nil, err) } @@ -691,9 +690,9 @@ func TestGuardianGetHiddenACL(t *testing.T) { store := dbtest.NewFakeDB() dashSvc := dashboards.NewFakeDashboardService(t) qResult := []*dashboards.DashboardACLInfoDTO{ - {Inherited: false, UserID: 1, UserLogin: "user1", Permission: models.PERMISSION_EDIT}, - {Inherited: false, UserID: 2, UserLogin: "user2", Permission: models.PERMISSION_ADMIN}, - {Inherited: true, UserID: 3, UserLogin: "user3", Permission: models.PERMISSION_VIEW}, + {Inherited: false, UserID: 1, UserLogin: "user1", Permission: dashboards.PERMISSION_EDIT}, + {Inherited: false, UserID: 2, UserLogin: "user2", Permission: dashboards.PERMISSION_ADMIN}, + {Inherited: true, UserID: 3, UserLogin: "user3", Permission: dashboards.PERMISSION_VIEW}, } dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult, nil) var qResultDash *dashboards.Dashboard @@ -752,14 +751,14 @@ func TestGuardianGetACLWithoutDuplicates(t *testing.T) { store := dbtest.NewFakeDB() dashSvc := dashboards.NewFakeDashboardService(t) qResult := []*dashboards.DashboardACLInfoDTO{ - {Inherited: true, UserID: 3, UserLogin: "user3", Permission: models.PERMISSION_EDIT}, - {Inherited: false, UserID: 3, UserLogin: "user3", Permission: models.PERMISSION_VIEW}, - {Inherited: false, UserID: 2, UserLogin: "user2", Permission: models.PERMISSION_ADMIN}, - {Inherited: true, UserID: 4, UserLogin: "user4", Permission: models.PERMISSION_ADMIN}, - {Inherited: false, UserID: 4, UserLogin: "user4", Permission: models.PERMISSION_ADMIN}, - {Inherited: false, UserID: 5, UserLogin: "user5", Permission: models.PERMISSION_EDIT}, - {Inherited: true, UserID: 6, UserLogin: "user6", Permission: models.PERMISSION_VIEW}, - {Inherited: false, UserID: 6, UserLogin: "user6", Permission: models.PERMISSION_EDIT}, + {Inherited: true, UserID: 3, UserLogin: "user3", Permission: dashboards.PERMISSION_EDIT}, + {Inherited: false, UserID: 3, UserLogin: "user3", Permission: dashboards.PERMISSION_VIEW}, + {Inherited: false, UserID: 2, UserLogin: "user2", Permission: dashboards.PERMISSION_ADMIN}, + {Inherited: true, UserID: 4, UserLogin: "user4", Permission: dashboards.PERMISSION_ADMIN}, + {Inherited: false, UserID: 4, UserLogin: "user4", Permission: dashboards.PERMISSION_ADMIN}, + {Inherited: false, UserID: 5, UserLogin: "user5", Permission: dashboards.PERMISSION_EDIT}, + {Inherited: true, UserID: 6, UserLogin: "user6", Permission: dashboards.PERMISSION_VIEW}, + {Inherited: false, UserID: 6, UserLogin: "user6", Permission: dashboards.PERMISSION_EDIT}, } dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult, nil) qResultDash := &dashboards.Dashboard{} @@ -786,12 +785,12 @@ func TestGuardianGetACLWithoutDuplicates(t *testing.T) { require.NotNil(t, acl) require.Len(t, acl, 6) require.ElementsMatch(t, []*dashboards.DashboardACLInfoDTO{ - {Inherited: true, UserID: 3, UserLogin: "user3", Permission: models.PERMISSION_EDIT}, - {Inherited: true, UserID: 4, UserLogin: "user4", Permission: models.PERMISSION_ADMIN}, - {Inherited: true, UserID: 6, UserLogin: "user6", Permission: models.PERMISSION_VIEW}, - {Inherited: false, UserID: 2, UserLogin: "user2", Permission: models.PERMISSION_ADMIN}, - {Inherited: false, UserID: 5, UserLogin: "user5", Permission: models.PERMISSION_EDIT}, - {Inherited: false, UserID: 6, UserLogin: "user6", Permission: models.PERMISSION_EDIT}, + {Inherited: true, UserID: 3, UserLogin: "user3", Permission: dashboards.PERMISSION_EDIT}, + {Inherited: true, UserID: 4, UserLogin: "user4", Permission: dashboards.PERMISSION_ADMIN}, + {Inherited: true, UserID: 6, UserLogin: "user6", Permission: dashboards.PERMISSION_VIEW}, + {Inherited: false, UserID: 2, UserLogin: "user2", Permission: dashboards.PERMISSION_ADMIN}, + {Inherited: false, UserID: 5, UserLogin: "user5", Permission: dashboards.PERMISSION_EDIT}, + {Inherited: false, UserID: 6, UserLogin: "user6", Permission: dashboards.PERMISSION_EDIT}, }, acl) }) }) diff --git a/pkg/services/guardian/guardian_util_test.go b/pkg/services/guardian/guardian_util_test.go index cba02cca47c..ba84307fe48 100644 --- a/pkg/services/guardian/guardian_util_test.go +++ b/pkg/services/guardian/guardian_util_test.go @@ -12,7 +12,6 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db/dbtest" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/team" @@ -262,31 +261,31 @@ func (sc *scenarioContext) reportFailure(desc string, expected interface{}, actu sc.t.Fatalf(buf.String()) } -func newCustomUserPermission(dashboardID int64, userID int64, permission models.PermissionType) *dashboards.DashboardACL { +func newCustomUserPermission(dashboardID int64, userID int64, permission dashboards.PermissionType) *dashboards.DashboardACL { return &dashboards.DashboardACL{OrgID: orgID, DashboardID: dashboardID, UserID: userID, Permission: permission} } -func newDefaultUserPermission(dashboardID int64, permission models.PermissionType) *dashboards.DashboardACL { +func newDefaultUserPermission(dashboardID int64, permission dashboards.PermissionType) *dashboards.DashboardACL { return newCustomUserPermission(dashboardID, userID, permission) } -func newCustomTeamPermission(dashboardID int64, teamID int64, permission models.PermissionType) *dashboards.DashboardACL { +func newCustomTeamPermission(dashboardID int64, teamID int64, permission dashboards.PermissionType) *dashboards.DashboardACL { return &dashboards.DashboardACL{OrgID: orgID, DashboardID: dashboardID, TeamID: teamID, Permission: permission} } -func newDefaultTeamPermission(dashboardID int64, permission models.PermissionType) *dashboards.DashboardACL { +func newDefaultTeamPermission(dashboardID int64, permission dashboards.PermissionType) *dashboards.DashboardACL { return newCustomTeamPermission(dashboardID, teamID, permission) } -func newAdminRolePermission(dashboardID int64, permission models.PermissionType) *dashboards.DashboardACL { +func newAdminRolePermission(dashboardID int64, permission dashboards.PermissionType) *dashboards.DashboardACL { return &dashboards.DashboardACL{OrgID: orgID, DashboardID: dashboardID, Role: &adminRole, Permission: permission} } -func newEditorRolePermission(dashboardID int64, permission models.PermissionType) *dashboards.DashboardACL { +func newEditorRolePermission(dashboardID int64, permission dashboards.PermissionType) *dashboards.DashboardACL { return &dashboards.DashboardACL{OrgID: orgID, DashboardID: dashboardID, Role: &editorRole, Permission: permission} } -func newViewerRolePermission(dashboardID int64, permission models.PermissionType) *dashboards.DashboardACL { +func newViewerRolePermission(dashboardID int64, permission dashboards.PermissionType) *dashboards.DashboardACL { return &dashboards.DashboardACL{OrgID: orgID, DashboardID: dashboardID, Role: &viewerRole, Permission: permission} } diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index 16eddaa0a00..99f6f1d3057 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -243,7 +243,7 @@ func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signed builder.Write(" INNER JOIN dashboard AS dashboard on le.folder_id = dashboard.id AND le.folder_id <> 0") writeParamSelectorSQL(&builder, params...) if signedInUser.OrgRole != org.RoleAdmin { - builder.WriteDashboardPermissionFilter(signedInUser, models.PERMISSION_VIEW) + builder.WriteDashboardPermissionFilter(signedInUser, dashboards.PERMISSION_VIEW) } builder.Write(` OR dashboard.id=0`) if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryElements); err != nil { @@ -360,7 +360,7 @@ func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedI return err } if signedInUser.OrgRole != org.RoleAdmin { - builder.WriteDashboardPermissionFilter(signedInUser, models.PERMISSION_VIEW) + builder.WriteDashboardPermissionFilter(signedInUser, dashboards.PERMISSION_VIEW) } if query.sortDirection == search.SortAlphaDesc.Name { builder.Write(" ORDER BY 1 DESC") @@ -575,7 +575,7 @@ func (l *LibraryElementService) getConnections(c context.Context, signedInUser * builder.Write(" INNER JOIN dashboard AS dashboard on lec.connection_id = dashboard.id") builder.Write(` WHERE lec.element_id=?`, element.ID) if signedInUser.OrgRole != org.RoleAdmin { - builder.WriteDashboardPermissionFilter(signedInUser, models.PERMISSION_VIEW) + builder.WriteDashboardPermissionFilter(signedInUser, dashboards.PERMISSION_VIEW) } if err := session.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&libraryElementConnections); err != nil { return err diff --git a/pkg/services/libraryelements/libraryelements_permissions_test.go b/pkg/services/libraryelements/libraryelements_permissions_test.go index cca13cb678c..3e3c1b4e6dc 100644 --- a/pkg/services/libraryelements/libraryelements_permissions_test.go +++ b/pkg/services/libraryelements/libraryelements_permissions_test.go @@ -6,20 +6,22 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/web" - "github.com/stretchr/testify/require" ) func TestLibraryElementPermissions(t *testing.T) { var defaultPermissions = []folderACLItem{} - var adminOnlyPermissions = []folderACLItem{{org.RoleAdmin, models.PERMISSION_EDIT}} - var editorOnlyPermissions = []folderACLItem{{org.RoleEditor, models.PERMISSION_EDIT}} - var editorAndViewerPermissions = []folderACLItem{{org.RoleEditor, models.PERMISSION_EDIT}, {org.RoleViewer, models.PERMISSION_EDIT}} - var viewerOnlyPermissions = []folderACLItem{{org.RoleViewer, models.PERMISSION_EDIT}} - var everyonePermissions = []folderACLItem{{org.RoleAdmin, models.PERMISSION_EDIT}, {org.RoleEditor, models.PERMISSION_EDIT}, {org.RoleViewer, models.PERMISSION_EDIT}} - var noPermissions = []folderACLItem{{org.RoleViewer, models.PERMISSION_VIEW}} + var adminOnlyPermissions = []folderACLItem{{org.RoleAdmin, dashboards.PERMISSION_EDIT}} + var editorOnlyPermissions = []folderACLItem{{org.RoleEditor, dashboards.PERMISSION_EDIT}} + var editorAndViewerPermissions = []folderACLItem{{org.RoleEditor, dashboards.PERMISSION_EDIT}, {org.RoleViewer, dashboards.PERMISSION_EDIT}} + var viewerOnlyPermissions = []folderACLItem{{org.RoleViewer, dashboards.PERMISSION_EDIT}} + var everyonePermissions = []folderACLItem{{org.RoleAdmin, dashboards.PERMISSION_EDIT}, {org.RoleEditor, dashboards.PERMISSION_EDIT}, {org.RoleViewer, dashboards.PERMISSION_EDIT}} + var noPermissions = []folderACLItem{{org.RoleViewer, dashboards.PERMISSION_VIEW}} var folderCases = [][]folderACLItem{ defaultPermissions, adminOnlyPermissions, diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 0ad7ae45523..b9ebdf9f2dc 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -267,7 +267,7 @@ type scenarioContext struct { type folderACLItem struct { roleType org.RoleType - permission models.PermissionType + permission dashboards.PermissionType } func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash *dashboards.Dashboard, folderID int64) *dashboards.Dashboard { diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 3671fa717bf..c0ae1ba624c 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -609,7 +609,7 @@ type scenarioContext struct { type folderACLItem struct { roleType org.RoleType - permission models.PermissionType + permission dashboards.PermissionType } func toLibraryElement(t *testing.T, res libraryelements.LibraryElementDTO) libraryElement { diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index 6151f9f239b..0992b375de0 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/guardian" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -326,12 +327,12 @@ func (st DBstore) GetRuleGroupInterval(ctx context.Context, orgID int64, namespa func (st DBstore) GetUserVisibleNamespaces(ctx context.Context, orgID int64, user *user.SignedInUser) (map[string]*folder.Folder, error) { namespaceMap := make(map[string]*folder.Folder) - searchQuery := models.FindPersistedDashboardsQuery{ + searchQuery := dashboards.FindPersistedDashboardsQuery{ OrgId: orgID, SignedInUser: user, Type: searchstore.TypeAlertFolder, Limit: -1, - Permission: models.PERMISSION_VIEW, + Permission: dashboards.PERMISSION_VIEW, Sort: models.SortOption{}, Filters: []interface{}{ searchstore.FolderWithAlertsFilter{}, diff --git a/pkg/services/search/service.go b/pkg/services/search/service.go index 0aff848cff5..8f45effe504 100644 --- a/pkg/services/search/service.go +++ b/pkg/services/search/service.go @@ -39,7 +39,7 @@ type Query struct { DashboardUIDs []string DashboardIds []int64 FolderIds []int64 - Permission models.PermissionType + Permission dashboards.PermissionType Sort string Result models.HitList @@ -59,7 +59,7 @@ type SearchService struct { } func (s *SearchService) SearchHandler(ctx context.Context, query *Query) error { - dashboardQuery := models.FindPersistedDashboardsQuery{ + dashboardQuery := dashboards.FindPersistedDashboardsQuery{ Title: query.Title, SignedInUser: query.SignedInUser, IsStarred: query.IsStarred, diff --git a/pkg/services/search/service_test.go b/pkg/services/search/service_test.go index 7694bbc356e..19b94758b68 100644 --- a/pkg/services/search/service_test.go +++ b/pkg/services/search/service_test.go @@ -22,8 +22,8 @@ func TestSearch_SortedResults(t *testing.T) { db := dbtest.NewFakeDB() us := usertest.NewUserServiceFake() ds := dashboards.NewFakeDashboardService(t) - ds.On("SearchDashboards", mock.Anything, mock.AnythingOfType("*models.FindPersistedDashboardsQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.FindPersistedDashboardsQuery) + ds.On("SearchDashboards", mock.Anything, mock.AnythingOfType("*dashboards.FindPersistedDashboardsQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*dashboards.FindPersistedDashboardsQuery) q.Result = models.HitList{ &models.Hit{ID: 16, Title: "CCAA", Type: "dash-db", Tags: []string{"BB", "AA"}}, &models.Hit{ID: 10, Title: "AABB", Type: "dash-db", Tags: []string{"CC", "AA"}}, diff --git a/pkg/services/searchV2/auth.go b/pkg/services/searchV2/auth.go index f22e35f0869..9fa87551ae2 100644 --- a/pkg/services/searchV2/auth.go +++ b/pkg/services/searchV2/auth.go @@ -4,8 +4,8 @@ import ( "context" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/sqlstore/permissions" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/services/user" @@ -37,11 +37,11 @@ func (a *simpleSQLAuthService) getDashboardTableAuthFilter(user *user.SignedInUs OrgId: user.OrgID, Dialect: a.sql.GetDialect(), UserId: user.UserID, - PermissionLevel: models.PERMISSION_VIEW, + PermissionLevel: dashboards.PERMISSION_VIEW, } } - return permissions.NewAccessControlDashboardPermissionFilter(user, models.PERMISSION_VIEW, "") + return permissions.NewAccessControlDashboardPermissionFilter(user, dashboards.PERMISSION_VIEW, "") } func (a *simpleSQLAuthService) GetDashboardReadFilter(user *user.SignedInUser) (ResourceFilter, error) { diff --git a/pkg/services/sqlstore/migrations/accesscontrol/dashboard_permissions.go b/pkg/services/sqlstore/migrations/accesscontrol/dashboard_permissions.go index 8f9d1cc9349..42420dd2e31 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/dashboard_permissions.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/dashboard_permissions.go @@ -8,22 +8,21 @@ import ( "xorm.io/xorm" - "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" ) -var dashboardPermissionTranslation = map[models.PermissionType][]string{ - models.PERMISSION_VIEW: { +var dashboardPermissionTranslation = map[dashboards.PermissionType][]string{ + dashboards.PERMISSION_VIEW: { dashboards.ActionDashboardsRead, }, - models.PERMISSION_EDIT: { + dashboards.PERMISSION_EDIT: { dashboards.ActionDashboardsRead, dashboards.ActionDashboardsWrite, dashboards.ActionDashboardsDelete, }, - models.PERMISSION_ADMIN: { + dashboards.PERMISSION_ADMIN: { dashboards.ActionDashboardsRead, dashboards.ActionDashboardsWrite, dashboards.ActionDashboardsCreate, @@ -33,17 +32,17 @@ var dashboardPermissionTranslation = map[models.PermissionType][]string{ }, } -var folderPermissionTranslation = map[models.PermissionType][]string{ - models.PERMISSION_VIEW: append(dashboardPermissionTranslation[models.PERMISSION_VIEW], []string{ +var folderPermissionTranslation = map[dashboards.PermissionType][]string{ + dashboards.PERMISSION_VIEW: append(dashboardPermissionTranslation[dashboards.PERMISSION_VIEW], []string{ dashboards.ActionFoldersRead, }...), - models.PERMISSION_EDIT: append(dashboardPermissionTranslation[models.PERMISSION_EDIT], []string{ + dashboards.PERMISSION_EDIT: append(dashboardPermissionTranslation[dashboards.PERMISSION_EDIT], []string{ dashboards.ActionDashboardsCreate, dashboards.ActionFoldersRead, dashboards.ActionFoldersWrite, dashboards.ActionFoldersDelete, }...), - models.PERMISSION_ADMIN: append(dashboardPermissionTranslation[models.PERMISSION_ADMIN], []string{ + dashboards.PERMISSION_ADMIN: append(dashboardPermissionTranslation[dashboards.PERMISSION_ADMIN], []string{ dashboards.ActionFoldersRead, dashboards.ActionFoldersWrite, dashboards.ActionFoldersDelete, @@ -98,9 +97,9 @@ func (m dashboardPermissionsMigrator) Exec(sess *xorm.Session, migrator *migrato return nil } -func (m dashboardPermissionsMigrator) migratePermissions(dashboards []dashboard, aclMap map[int64][]dashboards.DashboardACL, migrator *migrator.Migrator) error { +func (m dashboardPermissionsMigrator) migratePermissions(dashes []dashboard, aclMap map[int64][]dashboards.DashboardACL, migrator *migrator.Migrator) error { permissionMap := map[int64]map[string][]*ac.Permission{} - for _, d := range dashboards { + for _, d := range dashes { if d.ID == -1 { continue } @@ -112,11 +111,11 @@ func (m dashboardPermissionsMigrator) migratePermissions(dashboards []dashboard, if (d.IsFolder || d.FolderID == 0) && len(acls) == 0 && !d.HasAcl { permissionMap[d.OrgID]["managed:builtins:editor:permissions"] = append( permissionMap[d.OrgID]["managed:builtins:editor:permissions"], - m.mapPermission(d.ID, models.PERMISSION_EDIT, d.IsFolder)..., + m.mapPermission(d.ID, dashboards.PERMISSION_EDIT, d.IsFolder)..., ) permissionMap[d.OrgID]["managed:builtins:viewer:permissions"] = append( permissionMap[d.OrgID]["managed:builtins:viewer:permissions"], - m.mapPermission(d.ID, models.PERMISSION_VIEW, d.IsFolder)..., + m.mapPermission(d.ID, dashboards.PERMISSION_VIEW, d.IsFolder)..., ) } else { for _, a := range deduplicateAcl(acls) { @@ -195,7 +194,7 @@ func (m dashboardPermissionsMigrator) setPermissions(allRoles []*ac.Role, permis return nil } -func (m dashboardPermissionsMigrator) mapPermission(id int64, p models.PermissionType, isFolder bool) []*ac.Permission { +func (m dashboardPermissionsMigrator) mapPermission(id int64, p dashboards.PermissionType, isFolder bool) []*ac.Permission { if isFolder { actions := folderPermissionTranslation[p] scope := dashboards.ScopeFoldersProvider.GetResourceScope(strconv.FormatInt(id, 10)) @@ -559,15 +558,15 @@ func (m *managedFolderAlertActionsRepeatMigrator) Exec(sess *xorm.Session, mg *m } func hasFolderAdmin(permissions []ac.Permission) bool { - return hasActions(folderPermissionTranslation[models.PERMISSION_ADMIN], permissions) + return hasActions(folderPermissionTranslation[dashboards.PERMISSION_ADMIN], permissions) } func hasFolderEdit(permissions []ac.Permission) bool { - return hasActions(folderPermissionTranslation[models.PERMISSION_EDIT], permissions) + return hasActions(folderPermissionTranslation[dashboards.PERMISSION_EDIT], permissions) } func hasFolderView(permissions []ac.Permission) bool { - return hasActions(folderPermissionTranslation[models.PERMISSION_VIEW], permissions) + return hasActions(folderPermissionTranslation[dashboards.PERMISSION_VIEW], permissions) } func hasActions(actions []string, permissions []ac.Permission) bool { diff --git a/pkg/services/sqlstore/migrations/accesscontrol/team_membership.go b/pkg/services/sqlstore/migrations/accesscontrol/team_membership.go index c9e9f29c207..c2252b17860 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/team_membership.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/team_membership.go @@ -7,8 +7,8 @@ import ( "xorm.io/xorm" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/team" @@ -64,12 +64,12 @@ func (p *teamPermissionMigrator) setRolePermissions(roleID int64, permissions [] } // mapPermissionToRBAC translates the legacy membership (Member or Admin) into RBAC permissions -func (p *teamPermissionMigrator) mapPermissionToRBAC(permission models.PermissionType, teamID int64) []accesscontrol.Permission { +func (p *teamPermissionMigrator) mapPermissionToRBAC(permission dashboards.PermissionType, teamID int64) []accesscontrol.Permission { teamIDScope := accesscontrol.Scope("teams", "id", strconv.FormatInt(teamID, 10)) switch permission { case 0: return []accesscontrol.Permission{{Action: "teams:read", Scope: teamIDScope}} - case models.PERMISSION_ADMIN: + case dashboards.PERMISSION_ADMIN: return []accesscontrol.Permission{ {Action: "teams:delete", Scope: teamIDScope}, {Action: "teams:read", Scope: teamIDScope}, @@ -210,7 +210,7 @@ func (p *teamPermissionMigrator) generateAssociatedPermissions(teamMemberships [ // Downgrade team permissions if needed: // only admins or editors (when editorsCanAdmin option is enabled) // can access team administration endpoints - if m.Permission == models.PERMISSION_ADMIN { + if m.Permission == dashboards.PERMISSION_ADMIN { if userRolesByOrg[m.OrgID][m.UserID] == string(org.RoleViewer) || (userRolesByOrg[m.OrgID][m.UserID] == string(org.RoleEditor) && !p.editorsCanAdmin) { m.Permission = 0 diff --git a/pkg/services/sqlstore/migrations/accesscontrol/test/ac_test.go b/pkg/services/sqlstore/migrations/accesscontrol/test/ac_test.go index a1fb2e3d72a..0501f62580c 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/test/ac_test.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/test/ac_test.go @@ -9,8 +9,8 @@ import ( "xorm.io/xorm" "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/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/sqlstore/migrations" acmig "github.com/grafana/grafana/pkg/services/sqlstore/migrations/accesscontrol" @@ -353,7 +353,7 @@ func setupTeams(t *testing.T, x *xorm.Engine) { TeamID: 1, UserID: 2, External: false, - Permission: models.PERMISSION_ADMIN, + Permission: dashboards.PERMISSION_ADMIN, Created: now, Updated: now, }, @@ -363,7 +363,7 @@ func setupTeams(t *testing.T, x *xorm.Engine) { TeamID: 1, UserID: 3, External: false, - Permission: models.PERMISSION_ADMIN, + Permission: dashboards.PERMISSION_ADMIN, Created: now, Updated: now, }, @@ -373,7 +373,7 @@ func setupTeams(t *testing.T, x *xorm.Engine) { TeamID: 1, UserID: 4, External: false, - Permission: models.PERMISSION_ADMIN, + Permission: dashboards.PERMISSION_ADMIN, Created: now, Updated: now, }, diff --git a/pkg/services/sqlstore/migrations/ualert/permissions.go b/pkg/services/sqlstore/migrations/ualert/permissions.go index 08e153056b6..271a8b2f5fc 100644 --- a/pkg/services/sqlstore/migrations/ualert/permissions.go +++ b/pkg/services/sqlstore/migrations/ualert/permissions.go @@ -7,13 +7,11 @@ import ( "xorm.io/xorm" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/services/dashboards" dashver "github.com/grafana/grafana/pkg/services/dashboardversion" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/util" - - "github.com/grafana/grafana/pkg/infra/metrics" - "github.com/grafana/grafana/pkg/models" ) type roleType string @@ -206,7 +204,7 @@ func (m *folderHelper) setACL(orgID int64, dashboardID int64, items []*dashboard seen := make(map[keyType]struct{}, len(items)) for _, item := range items { if item.UserID == 0 && item.TeamID == 0 && (item.Role == nil || !item.Role.IsValid()) { - return models.ErrDashboardACLInfoMissing + return dashboards.ErrDashboardACLInfoMissing } // ignore duplicate user permissions diff --git a/pkg/services/sqlstore/permissions/dashboard.go b/pkg/services/sqlstore/permissions/dashboard.go index 9f8a1e3c834..bb50a034061 100644 --- a/pkg/services/sqlstore/permissions/dashboard.go +++ b/pkg/services/sqlstore/permissions/dashboard.go @@ -3,7 +3,6 @@ package permissions import ( "strings" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" @@ -17,7 +16,7 @@ type DashboardPermissionFilter struct { Dialect migrator.Dialect UserId int64 OrgId int64 - PermissionLevel models.PermissionType + PermissionLevel dashboards.PermissionType } func (d DashboardPermissionFilter) Where() (string, []interface{}) { @@ -85,9 +84,9 @@ type AccessControlDashboardPermissionFilter struct { folderActions []string } -// NewAccessControlDashboardPermissionFilter creates a new AccessControlDashboardPermissionFilter that is configured with specific actions calculated based on the models.PermissionType and query type -func NewAccessControlDashboardPermissionFilter(user *user.SignedInUser, permissionLevel models.PermissionType, queryType string) AccessControlDashboardPermissionFilter { - needEdit := permissionLevel > models.PERMISSION_VIEW +// NewAccessControlDashboardPermissionFilter creates a new AccessControlDashboardPermissionFilter that is configured with specific actions calculated based on the dashboards.PermissionType and query type +func NewAccessControlDashboardPermissionFilter(user *user.SignedInUser, permissionLevel dashboards.PermissionType, queryType string) AccessControlDashboardPermissionFilter { + needEdit := permissionLevel > dashboards.PERMISSION_VIEW var folderActions []string var dashboardActions []string diff --git a/pkg/services/sqlstore/permissions/dashboard_test.go b/pkg/services/sqlstore/permissions/dashboard_test.go index 52b36474066..d9552da351c 100644 --- a/pkg/services/sqlstore/permissions/dashboard_test.go +++ b/pkg/services/sqlstore/permissions/dashboard_test.go @@ -6,9 +6,11 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" @@ -16,8 +18,6 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore/permissions" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/services/user" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestIntegration_DashboardPermissionFilter(t *testing.T) { @@ -28,7 +28,7 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) { type testCase struct { desc string queryType string - permission models.PermissionType + permission dashboards.PermissionType permissions []accesscontrol.Permission expectedResult int } @@ -36,7 +36,7 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) { tests := []testCase{ { desc: "Should be able to view all dashboards with wildcard scope", - permission: models.PERMISSION_VIEW, + permission: dashboards.PERMISSION_VIEW, permissions: []accesscontrol.Permission{ {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeDashboardsAll}, }, @@ -44,7 +44,7 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) { }, { desc: "Should be able to view all dashboards with folder wildcard scope", - permission: models.PERMISSION_VIEW, + permission: dashboards.PERMISSION_VIEW, permissions: []accesscontrol.Permission{ {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersAll}, }, @@ -52,7 +52,7 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) { }, { desc: "Should be able to view a subset of dashboards with dashboard scopes", - permission: models.PERMISSION_VIEW, + permission: dashboards.PERMISSION_VIEW, permissions: []accesscontrol.Permission{ {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:110"}, {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:40"}, @@ -65,7 +65,7 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) { }, { desc: "Should be able to view a subset of dashboards with dashboard action and folder scope", - permission: models.PERMISSION_VIEW, + permission: dashboards.PERMISSION_VIEW, permissions: []accesscontrol.Permission{ {Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:8"}, {Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:10"}, @@ -74,7 +74,7 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) { }, { desc: "Should be able to view all folders with folder wildcard", - permission: models.PERMISSION_VIEW, + permission: dashboards.PERMISSION_VIEW, permissions: []accesscontrol.Permission{ {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:*"}, }, @@ -82,7 +82,7 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) { }, { desc: "Should be able to view a subset folders", - permission: models.PERMISSION_VIEW, + permission: dashboards.PERMISSION_VIEW, permissions: []accesscontrol.Permission{ {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3"}, {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:6"}, @@ -92,7 +92,7 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) { }, { desc: "Should return folders and dashboard with 'edit' permission", - permission: models.PERMISSION_EDIT, + permission: dashboards.PERMISSION_EDIT, permissions: []accesscontrol.Permission{ {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3"}, {Action: dashboards.ActionDashboardsCreate, Scope: "folders:uid:3"}, @@ -103,7 +103,7 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) { }, { desc: "Should return folders that users can read alerts from", - permission: models.PERMISSION_VIEW, + permission: dashboards.PERMISSION_VIEW, queryType: searchstore.TypeAlertFolder, permissions: []accesscontrol.Permission{ {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:3"}, @@ -115,7 +115,7 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) { }, { desc: "Should return folders that users can read alerts when user has read wildcard", - permission: models.PERMISSION_VIEW, + permission: dashboards.PERMISSION_VIEW, queryType: searchstore.TypeAlertFolder, permissions: []accesscontrol.Permission{ {Action: dashboards.ActionFoldersRead, Scope: "*"}, diff --git a/pkg/services/sqlstore/permissions/dashboards_bench_test.go b/pkg/services/sqlstore/permissions/dashboards_bench_test.go index b2e6af1d005..d213fee5ac8 100644 --- a/pkg/services/sqlstore/permissions/dashboards_bench_test.go +++ b/pkg/services/sqlstore/permissions/dashboards_bench_test.go @@ -7,17 +7,17 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/permissions" "github.com/grafana/grafana/pkg/services/user" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func benchmarkDashboardPermissionFilter(b *testing.B, numUsers, numDashboards int) { @@ -25,7 +25,7 @@ func benchmarkDashboardPermissionFilter(b *testing.B, numUsers, numDashboards in b.ResetTimer() for i := 0; i < b.N; i++ { usr := &user.SignedInUser{UserID: 1, OrgID: 1, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{1: {}}} - filter := permissions.NewAccessControlDashboardPermissionFilter(usr, models.PERMISSION_VIEW, "") + filter := permissions.NewAccessControlDashboardPermissionFilter(usr, dashboards.PERMISSION_VIEW, "") var result int err := store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { q, params := filter.Where() diff --git a/pkg/services/sqlstore/searchstore/search_test.go b/pkg/services/sqlstore/searchstore/search_test.go index 74ce0dcaeba..9d0945aac3d 100644 --- a/pkg/services/sqlstore/searchstore/search_test.go +++ b/pkg/services/sqlstore/searchstore/search_test.go @@ -10,7 +10,6 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/sqlstore/permissions" @@ -122,7 +121,7 @@ func TestBuilder_Permissions(t *testing.T) { store := setupTestEnvironment(t) createDashboards(t, store, 0, 1, user.OrgID) - level := models.PERMISSION_EDIT + level := dashboards.PERMISSION_EDIT builder := &searchstore.Builder{ Filters: []interface{}{ diff --git a/pkg/services/stats/statsimpl/stats.go b/pkg/services/stats/statsimpl/stats.go index d70929500e7..156c541900d 100644 --- a/pkg/services/stats/statsimpl/stats.go +++ b/pkg/services/stats/statsimpl/stats.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/stats" @@ -97,10 +98,10 @@ func (ss *sqlStatsService) GetSystemStats(ctx context.Context, query *stats.GetS WHERE d.is_folder = ? ) AS folder_permissions,`, dialect.BooleanStr(true)) - sb.Write(viewersPermissionsCounterSQL(ss.db, "dashboards_viewers_can_edit", false, models.PERMISSION_EDIT)) - sb.Write(viewersPermissionsCounterSQL(ss.db, "dashboards_viewers_can_admin", false, models.PERMISSION_ADMIN)) - sb.Write(viewersPermissionsCounterSQL(ss.db, "folders_viewers_can_edit", true, models.PERMISSION_EDIT)) - sb.Write(viewersPermissionsCounterSQL(ss.db, "folders_viewers_can_admin", true, models.PERMISSION_ADMIN)) + sb.Write(viewersPermissionsCounterSQL(ss.db, "dashboards_viewers_can_edit", false, dashboards.PERMISSION_EDIT)) + sb.Write(viewersPermissionsCounterSQL(ss.db, "dashboards_viewers_can_admin", false, dashboards.PERMISSION_ADMIN)) + sb.Write(viewersPermissionsCounterSQL(ss.db, "folders_viewers_can_edit", true, dashboards.PERMISSION_EDIT)) + sb.Write(viewersPermissionsCounterSQL(ss.db, "folders_viewers_can_admin", true, dashboards.PERMISSION_ADMIN)) sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("dashboard_provisioning") + `) AS provisioned_dashboards,`) sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("dashboard_snapshot") + `) AS snapshots,`) @@ -151,7 +152,7 @@ func (ss *sqlStatsService) roleCounterSQL(ctx context.Context) string { return sqlQuery } -func viewersPermissionsCounterSQL(db db.DB, statName string, isFolder bool, permission models.PermissionType) string { +func viewersPermissionsCounterSQL(db db.DB, statName string, isFolder bool, permission dashboards.PermissionType) string { dialect := db.GetDialect() return `( SELECT COUNT(*) diff --git a/pkg/services/team/model.go b/pkg/services/team/model.go index f7d3b347d74..c19333c8015 100644 --- a/pkg/services/team/model.go +++ b/pkg/services/team/model.go @@ -4,7 +4,7 @@ import ( "errors" "time" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/user" ) @@ -81,14 +81,14 @@ type SearchTeamsQuery struct { } type TeamDTO struct { - ID int64 `json:"id" xorm:"id"` - OrgID int64 `json:"orgId" xorm:"org_id"` - Name string `json:"name"` - Email string `json:"email"` - AvatarURL string `json:"avatarUrl"` - MemberCount int64 `json:"memberCount"` - Permission models.PermissionType `json:"permission"` - AccessControl map[string]bool `json:"accessControl"` + ID int64 `json:"id" xorm:"id"` + OrgID int64 `json:"orgId" xorm:"org_id"` + Name string `json:"name"` + Email string `json:"email"` + AvatarURL string `json:"avatarUrl"` + MemberCount int64 `json:"memberCount"` + Permission dashboards.PermissionType `json:"permission"` + AccessControl map[string]bool `json:"accessControl"` } type SearchTeamQueryResult struct { @@ -109,7 +109,7 @@ type TeamMember struct { TeamID int64 `xorm:"team_id"` UserID int64 `xorm:"user_id"` External bool // Signals that the membership has been created by an external systems, such as LDAP - Permission models.PermissionType + Permission dashboards.PermissionType Created time.Time Updated time.Time @@ -119,18 +119,18 @@ type TeamMember struct { // COMMANDS type AddTeamMemberCommand struct { - UserID int64 `json:"userId" binding:"Required"` - OrgID int64 `json:"-"` - TeamID int64 `json:"-"` - External bool `json:"-"` - Permission models.PermissionType `json:"-"` + UserID int64 `json:"userId" binding:"Required"` + OrgID int64 `json:"-"` + TeamID int64 `json:"-"` + External bool `json:"-"` + Permission dashboards.PermissionType `json:"-"` } type UpdateTeamMemberCommand struct { - UserID int64 `json:"-"` - OrgID int64 `json:"-"` - TeamID int64 `json:"-"` - Permission models.PermissionType `json:"permission"` + UserID int64 `json:"-"` + OrgID int64 `json:"-"` + TeamID int64 `json:"-"` + Permission dashboards.PermissionType `json:"permission"` } type RemoveTeamMemberCommand struct { @@ -154,15 +154,15 @@ type GetTeamMembersQuery struct { // Projections and DTOs type TeamMemberDTO struct { - OrgID int64 `json:"orgId" xorm:"org_id"` - TeamID int64 `json:"teamId" xorm:"team_id"` - UserID int64 `json:"userId" xorm:"user_id"` - External bool `json:"-"` - AuthModule string `json:"auth_module"` - Email string `json:"email"` - Name string `json:"name"` - Login string `json:"login"` - AvatarURL string `json:"avatarUrl" xorm:"avatar_url"` - Labels []string `json:"labels"` - Permission models.PermissionType `json:"permission"` + OrgID int64 `json:"orgId" xorm:"org_id"` + TeamID int64 `json:"teamId" xorm:"team_id"` + UserID int64 `json:"userId" xorm:"user_id"` + External bool `json:"-"` + AuthModule string `json:"auth_module"` + Email string `json:"email"` + Name string `json:"name"` + Login string `json:"login"` + AvatarURL string `json:"avatarUrl" xorm:"avatar_url"` + Labels []string `json:"labels"` + Permission dashboards.PermissionType `json:"permission"` } diff --git a/pkg/services/team/team.go b/pkg/services/team/team.go index 730a5cc0aa0..9b9643894ff 100644 --- a/pkg/services/team/team.go +++ b/pkg/services/team/team.go @@ -3,7 +3,7 @@ package team import ( "context" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" ) type Service interface { @@ -13,7 +13,7 @@ type Service interface { SearchTeams(ctx context.Context, query *SearchTeamsQuery) (SearchTeamQueryResult, error) GetTeamByID(ctx context.Context, query *GetTeamByIDQuery) (*TeamDTO, error) GetTeamsByUser(ctx context.Context, query *GetTeamsByUserQuery) ([]*TeamDTO, error) - AddTeamMember(userID, orgID, teamID int64, isExternal bool, permission models.PermissionType) error + AddTeamMember(userID, orgID, teamID int64, isExternal bool, permission dashboards.PermissionType) error UpdateTeamMember(ctx context.Context, cmd *UpdateTeamMemberCommand) error IsTeamMember(orgId int64, teamId int64, userId int64) (bool, error) RemoveTeamMember(ctx context.Context, cmd *RemoveTeamMemberCommand) error diff --git a/pkg/services/team/teamimpl/store.go b/pkg/services/team/teamimpl/store.go index c5eadcc36db..585b9c1b4b3 100644 --- a/pkg/services/team/teamimpl/store.go +++ b/pkg/services/team/teamimpl/store.go @@ -8,8 +8,8 @@ import ( "time" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -22,7 +22,7 @@ type store interface { Search(ctx context.Context, query *team.SearchTeamsQuery) (team.SearchTeamQueryResult, error) GetByID(ctx context.Context, query *team.GetTeamByIDQuery) (*team.TeamDTO, error) GetByUser(ctx context.Context, query *team.GetTeamsByUserQuery) ([]*team.TeamDTO, error) - AddMember(userID, orgID, teamID int64, isExternal bool, permission models.PermissionType) error + AddMember(userID, orgID, teamID int64, isExternal bool, permission dashboards.PermissionType) error UpdateMember(ctx context.Context, cmd *team.UpdateTeamMemberCommand) error IsMember(orgId int64, teamId int64, userId int64) (bool, error) RemoveMember(ctx context.Context, cmd *team.RemoveTeamMemberCommand) error @@ -358,7 +358,7 @@ func (ss *xormStore) GetByUser(ctx context.Context, query *team.GetTeamsByUserQu } // AddTeamMember adds a user to a team -func (ss *xormStore) AddMember(userID, orgID, teamID int64, isExternal bool, permission models.PermissionType) error { +func (ss *xormStore) AddMember(userID, orgID, teamID int64, isExternal bool, permission dashboards.PermissionType) error { return ss.db.WithTransactionalDbSession(context.Background(), func(sess *db.Session) error { if isMember, err := isTeamMember(sess, orgID, teamID, userID); err != nil { return err @@ -416,7 +416,7 @@ func isTeamMember(sess *db.Session, orgId int64, teamId int64, userId int64) (bo // AddOrUpdateTeamMemberHook is called from team resource permission service // it adds user to a team or updates user permissions in a team within the given transaction session -func AddOrUpdateTeamMemberHook(sess *db.Session, userID, orgID, teamID int64, isExternal bool, permission models.PermissionType) error { +func AddOrUpdateTeamMemberHook(sess *db.Session, userID, orgID, teamID int64, isExternal bool, permission dashboards.PermissionType) error { isMember, err := isTeamMember(sess, orgID, teamID, userID) if err != nil { return err @@ -431,7 +431,7 @@ func AddOrUpdateTeamMemberHook(sess *db.Session, userID, orgID, teamID int64, is return err } -func addTeamMember(sess *db.Session, orgID, teamID, userID int64, isExternal bool, permission models.PermissionType) error { +func addTeamMember(sess *db.Session, orgID, teamID, userID int64, isExternal bool, permission dashboards.PermissionType) error { if _, err := teamExists(orgID, teamID, sess); err != nil { return err } @@ -450,13 +450,13 @@ func addTeamMember(sess *db.Session, orgID, teamID, userID int64, isExternal boo return err } -func updateTeamMember(sess *db.Session, orgID, teamID, userID int64, permission models.PermissionType) error { +func updateTeamMember(sess *db.Session, orgID, teamID, userID int64, permission dashboards.PermissionType) error { member, err := getTeamMember(sess, orgID, teamID, userID) if err != nil { return err } - if permission != models.PERMISSION_ADMIN { + if permission != dashboards.PERMISSION_ADMIN { permission = 0 // make sure we don't get invalid permission levels in store } @@ -590,7 +590,7 @@ func (ss *xormStore) IsAdmin(ctx context.Context, query *team.IsAdminOfTeamsQuer var queryResult bool err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { sql := "SELECT COUNT(team.id) AS count FROM team INNER JOIN team_member ON team_member.team_id = team.id WHERE team.org_id = ? AND team_member.user_id = ? AND team_member.permission = ?" - params := []interface{}{query.SignedInUser.OrgID, query.SignedInUser.UserID, models.PERMISSION_ADMIN} + params := []interface{}{query.SignedInUser.OrgID, query.SignedInUser.UserID, dashboards.PERMISSION_ADMIN} type teamCount struct { Count int64 diff --git a/pkg/services/team/teamimpl/store_test.go b/pkg/services/team/teamimpl/store_test.go index a759671115b..73cdb964e69 100644 --- a/pkg/services/team/teamimpl/store_test.go +++ b/pkg/services/team/teamimpl/store_test.go @@ -11,7 +11,6 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org/orgimpl" @@ -165,7 +164,7 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { UserID: userId, OrgID: testOrgID, TeamID: team1.ID, - Permission: models.PERMISSION_ADMIN, + Permission: dashboards.PERMISSION_ADMIN, }) require.NoError(t, err) @@ -173,7 +172,7 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { qAfterUpdate := &team.GetTeamMembersQuery{OrgID: testOrgID, TeamID: team1.ID, SignedInUser: testUser} qAfterUpdateResult, err := teamSvc.GetTeamMembers(context.Background(), qAfterUpdate) require.NoError(t, err) - require.Equal(t, qAfterUpdateResult[0].Permission, models.PERMISSION_ADMIN) + require.Equal(t, qAfterUpdateResult[0].Permission, dashboards.PERMISSION_ADMIN) }) t.Run("Should default to member permission level when updating a user with invalid permission level", func(t *testing.T) { @@ -188,7 +187,7 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { require.NoError(t, err) require.EqualValues(t, qBeforeUpdateResult[0].Permission, 0) - invalidPermissionLevel := models.PERMISSION_EDIT + invalidPermissionLevel := dashboards.PERMISSION_EDIT err = teamSvc.UpdateTeamMember(context.Background(), &team.UpdateTeamMemberCommand{ UserID: userID, OrgID: testOrgID, @@ -211,7 +210,7 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { UserID: 1, OrgID: testOrgID, TeamID: team1.ID, - Permission: models.PERMISSION_ADMIN, + Permission: dashboards.PERMISSION_ADMIN, }) require.Error(t, err, team.ErrTeamMemberNotFound) @@ -267,7 +266,7 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { }) t.Run("Should have empty teams", func(t *testing.T) { - err = teamSvc.AddTeamMember(userIds[0], testOrgID, team1.ID, false, models.PERMISSION_ADMIN) + err = teamSvc.AddTeamMember(userIds[0], testOrgID, team1.ID, false, dashboards.PERMISSION_ADMIN) require.NoError(t, err) t.Run("A user should be able to remove the admin permission for the last admin", func(t *testing.T) { @@ -284,10 +283,10 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { sqlStore = db.InitTestDB(t) setup() - err = teamSvc.AddTeamMember(userIds[0], testOrgID, team1.ID, false, models.PERMISSION_ADMIN) + err = teamSvc.AddTeamMember(userIds[0], testOrgID, team1.ID, false, dashboards.PERMISSION_ADMIN) require.NoError(t, err) - err = teamSvc.AddTeamMember(userIds[1], testOrgID, team1.ID, false, models.PERMISSION_ADMIN) + err = teamSvc.AddTeamMember(userIds[1], testOrgID, team1.ID, false, dashboards.PERMISSION_ADMIN) require.NoError(t, err) err = teamSvc.UpdateTeamMember(context.Background(), &team.UpdateTeamMemberCommand{OrgID: testOrgID, TeamID: team1.ID, UserID: userIds[0], Permission: 0}) require.NoError(t, err) @@ -301,7 +300,7 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { err = teamSvc.AddTeamMember(userIds[2], testOrgID, groupID, false, 0) require.NoError(t, err) err = updateDashboardACL(t, sqlStore, 1, &dashboards.DashboardACL{ - DashboardID: 1, OrgID: testOrgID, Permission: models.PERMISSION_EDIT, TeamID: groupID, + DashboardID: 1, OrgID: testOrgID, Permission: dashboards.PERMISSION_EDIT, TeamID: groupID, }) require.NoError(t, err) err = teamSvc.DeleteTeam(context.Background(), &team.DeleteTeamCommand{OrgID: testOrgID, ID: groupID}) @@ -324,7 +323,7 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { groupId := team2.ID err := teamSvc.AddTeamMember(userIds[0], testOrgID, groupId, false, 0) require.NoError(t, err) - err = teamSvc.AddTeamMember(userIds[1], testOrgID, groupId, false, models.PERMISSION_ADMIN) + err = teamSvc.AddTeamMember(userIds[1], testOrgID, groupId, false, dashboards.PERMISSION_ADMIN) require.NoError(t, err) query := &team.IsAdminOfTeamsQuery{SignedInUser: &user.SignedInUser{OrgID: testOrgID, UserID: userIds[0]}} @@ -630,11 +629,11 @@ func updateDashboardACL(t *testing.T, sqlStore *sqlstore.SQLStore, dashboardID i item.Created = time.Now() item.Updated = time.Now() if item.UserID == 0 && item.TeamID == 0 && (item.Role == nil || !item.Role.IsValid()) { - return models.ErrDashboardACLInfoMissing + return dashboards.ErrDashboardACLInfoMissing } if item.DashboardID == 0 { - return models.ErrDashboardPermissionDashboardEmpty + return dashboards.ErrDashboardPermissionDashboardEmpty } sess.Nullable("user_id", "team_id") diff --git a/pkg/services/team/teamimpl/team.go b/pkg/services/team/teamimpl/team.go index f9a33d0715f..31ce4295cf5 100644 --- a/pkg/services/team/teamimpl/team.go +++ b/pkg/services/team/teamimpl/team.go @@ -4,7 +4,7 @@ import ( "context" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/setting" ) @@ -41,7 +41,7 @@ func (s *Service) GetTeamsByUser(ctx context.Context, query *team.GetTeamsByUser return s.store.GetByUser(ctx, query) } -func (s *Service) AddTeamMember(userID, orgID, teamID int64, isExternal bool, permission models.PermissionType) error { +func (s *Service) AddTeamMember(userID, orgID, teamID int64, isExternal bool, permission dashboards.PermissionType) error { return s.store.AddMember(userID, orgID, teamID, isExternal, permission) } diff --git a/pkg/services/team/teamtest/team.go b/pkg/services/team/teamtest/team.go index 5baf567c5e1..332e9bce0af 100644 --- a/pkg/services/team/teamtest/team.go +++ b/pkg/services/team/teamtest/team.go @@ -3,7 +3,7 @@ package teamtest import ( "context" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/team" ) @@ -44,7 +44,7 @@ func (s *FakeService) GetTeamsByUser(ctx context.Context, query *team.GetTeamsBy return s.ExpectedTeamsByUser, s.ExpectedError } -func (s *FakeService) AddTeamMember(userID, orgID, teamID int64, isExternal bool, permission models.PermissionType) error { +func (s *FakeService) AddTeamMember(userID, orgID, teamID int64, isExternal bool, permission dashboards.PermissionType) error { return s.ExpectedError } diff --git a/pkg/services/teamguardian/manager/service.go b/pkg/services/teamguardian/manager/service.go index 9c60116c639..a21018c8302 100644 --- a/pkg/services/teamguardian/manager/service.go +++ b/pkg/services/teamguardian/manager/service.go @@ -3,7 +3,7 @@ package manager import ( "context" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/services/teamguardian" @@ -40,7 +40,7 @@ func (s *Service) CanAdmin(ctx context.Context, orgId int64, teamId int64, user } for _, member := range results { - if member.UserID == user.UserID && member.Permission == models.PERMISSION_ADMIN { + if member.UserID == user.UserID && member.Permission == dashboards.PERMISSION_ADMIN { return nil } } diff --git a/pkg/services/teamguardian/manager/service_test.go b/pkg/services/teamguardian/manager/service_test.go index abde850dadb..d4da1f1c5f5 100644 --- a/pkg/services/teamguardian/manager/service_test.go +++ b/pkg/services/teamguardian/manager/service_test.go @@ -4,13 +4,14 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/models" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/services/teamguardian/database" "github.com/grafana/grafana/pkg/services/user" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" ) func TestUpdateTeam(t *testing.T) { @@ -50,7 +51,7 @@ func TestUpdateTeam(t *testing.T) { OrgID: testTeam.OrgID, TeamID: testTeam.ID, UserID: editor.UserID, - Permission: models.PERMISSION_ADMIN, + Permission: dashboards.PERMISSION_ADMIN, }} store.On("GetTeamMembers", ctx, mock.Anything).Return(result, nil).Once() @@ -72,7 +73,7 @@ func TestUpdateTeam(t *testing.T) { OrgID: testTeamOtherOrg.OrgID, TeamID: testTeamOtherOrg.ID, UserID: editor.UserID, - Permission: models.PERMISSION_ADMIN, + Permission: dashboards.PERMISSION_ADMIN, }} store.On("GetTeamMembers", ctx, mock.Anything).Return(result, nil).Once() diff --git a/pkg/services/user/userimpl/store_test.go b/pkg/services/user/userimpl/store_test.go index 57561580190..ec86278bde9 100644 --- a/pkg/services/user/userimpl/store_test.go +++ b/pkg/services/user/userimpl/store_test.go @@ -10,7 +10,6 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" @@ -284,7 +283,7 @@ func TestIntegrationUserDataAccess(t *testing.T) { err = updateDashboardACL(t, ss, 1, &dashboards.DashboardACL{ DashboardID: 1, OrgID: users[0].OrgID, UserID: users[1].ID, - Permission: models.PERMISSION_EDIT, + Permission: dashboards.PERMISSION_EDIT, }) require.Nil(t, err) @@ -423,7 +422,7 @@ func TestIntegrationUserDataAccess(t *testing.T) { err = updateDashboardACL(t, ss, 1, &dashboards.DashboardACL{ DashboardID: 1, OrgID: users[0].OrgID, UserID: users[1].ID, - Permission: models.PERMISSION_EDIT, + Permission: dashboards.PERMISSION_EDIT, }) require.Nil(t, err) @@ -457,7 +456,7 @@ func TestIntegrationUserDataAccess(t *testing.T) { err = updateDashboardACL(t, ss, 1, &dashboards.DashboardACL{ DashboardID: 1, OrgID: users[0].OrgID, UserID: users[1].ID, - Permission: models.PERMISSION_EDIT, + Permission: dashboards.PERMISSION_EDIT, }) require.Nil(t, err) @@ -831,11 +830,11 @@ func updateDashboardACL(t *testing.T, sqlStore db.DB, dashboardID int64, items . item.Created = time.Now() item.Updated = time.Now() if item.UserID == 0 && item.TeamID == 0 && (item.Role == nil || !item.Role.IsValid()) { - return models.ErrDashboardACLInfoMissing + return dashboards.ErrDashboardACLInfoMissing } if item.DashboardID == 0 { - return models.ErrDashboardPermissionDashboardEmpty + return dashboards.ErrDashboardPermissionDashboardEmpty } sess.Nullable("user_id", "team_id") From 354f6d9e23eaacde73515418508ecd71174b7e04 Mon Sep 17 00:00:00 2001 From: Ezequiel Victorero Date: Thu, 26 Jan 2023 10:51:00 -0300 Subject: [PATCH 083/172] Schema: Create PublicDashboard core kind (#62155) --- .../core/publicdashboard/schema-reference.md | 25 ++++ .../publicdashboard/public_dashboard_kind.cue | 26 ++++ packages/grafana-schema/src/index.gen.ts | 3 + .../x/publicdashboard_types.gen.ts | 36 ++++++ .../publicdashboard_kind_gen.go | 113 ++++++++++++++++++ .../publicdashboard_types_gen.go | 32 +++++ pkg/kindsys/report.json | 32 ++++- pkg/registry/corekind/base_gen.go | 26 +++- 8 files changed, 285 insertions(+), 8 deletions(-) create mode 100644 docs/sources/developers/kinds/core/publicdashboard/schema-reference.md create mode 100644 kinds/publicdashboard/public_dashboard_kind.cue create mode 100644 packages/grafana-schema/src/raw/publicdashboard/x/publicdashboard_types.gen.ts create mode 100644 pkg/kinds/publicdashboard/publicdashboard_kind_gen.go create mode 100644 pkg/kinds/publicdashboard/publicdashboard_types_gen.go diff --git a/docs/sources/developers/kinds/core/publicdashboard/schema-reference.md b/docs/sources/developers/kinds/core/publicdashboard/schema-reference.md new file mode 100644 index 00000000000..afc5180618b --- /dev/null +++ b/docs/sources/developers/kinds/core/publicdashboard/schema-reference.md @@ -0,0 +1,25 @@ +--- +keywords: + - grafana + - schema +title: PublicDashboard kind +--- +> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. + +# PublicDashboard kind + +## Maturity: merged +## Version: 0.0 + +## Properties + +| Property | Type | Required | Description | +|------------------------|---------|----------|-----------------------------------------------------------------| +| `annotationsEnabled` | boolean | **Yes** | Flag that indicates if annotations are enabled | +| `dashboardUid` | string | **Yes** | Dashboard unique identifier referenced by this public dashboard | +| `isEnabled` | boolean | **Yes** | Flag that indicates if the public dashboard is enabled | +| `timeSelectionEnabled` | boolean | **Yes** | Flag that indicates if the time range picker is enabled | +| `uid` | string | **Yes** | Unique public dashboard identifier | +| `accessToken` | string | No | Unique public access token | + + diff --git a/kinds/publicdashboard/public_dashboard_kind.cue b/kinds/publicdashboard/public_dashboard_kind.cue new file mode 100644 index 00000000000..1e960fa7cb4 --- /dev/null +++ b/kinds/publicdashboard/public_dashboard_kind.cue @@ -0,0 +1,26 @@ +package kind + +name: "PublicDashboard" +maturity: "merged" + +lineage: seqs: [ + { + schemas: [ + // 0.0 + { + // Unique public dashboard identifier + uid: string + // Dashboard unique identifier referenced by this public dashboard + dashboardUid: string + // Unique public access token + accessToken?: string + // Flag that indicates if the public dashboard is enabled + isEnabled: bool + // Flag that indicates if annotations are enabled + annotationsEnabled: bool + // Flag that indicates if the time range picker is enabled + timeSelectionEnabled: bool + }, + ] + }, +] diff --git a/packages/grafana-schema/src/index.gen.ts b/packages/grafana-schema/src/index.gen.ts index f0b02135a78..a7a76a0c3df 100644 --- a/packages/grafana-schema/src/index.gen.ts +++ b/packages/grafana-schema/src/index.gen.ts @@ -101,6 +101,9 @@ export type { QueryHistoryPreference } from './raw/preferences/x/preferences_types.gen'; +// Raw generated types from PublicDashboard kind. +export type { PublicDashboard } from './raw/publicdashboard/x/publicdashboard_types.gen'; + // Raw generated types from ServiceAccount kind. export type { ServiceAccount, diff --git a/packages/grafana-schema/src/raw/publicdashboard/x/publicdashboard_types.gen.ts b/packages/grafana-schema/src/raw/publicdashboard/x/publicdashboard_types.gen.ts new file mode 100644 index 00000000000..b94e4baefe3 --- /dev/null +++ b/packages/grafana-schema/src/raw/publicdashboard/x/publicdashboard_types.gen.ts @@ -0,0 +1,36 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. +// +// Generated by: +// kinds/gen.go +// Using jennies: +// TSTypesJenny +// LatestMajorsOrXJenny +// +// Run 'make gen-cue' from repository root to regenerate. + +export interface PublicDashboard { + /** + * Unique public access token + */ + accessToken?: string; + /** + * Flag that indicates if annotations are enabled + */ + annotationsEnabled: boolean; + /** + * Dashboard unique identifier referenced by this public dashboard + */ + dashboardUid: string; + /** + * Flag that indicates if the public dashboard is enabled + */ + isEnabled: boolean; + /** + * Flag that indicates if the time range picker is enabled + */ + timeSelectionEnabled: boolean; + /** + * Unique public dashboard identifier + */ + uid: string; +} diff --git a/pkg/kinds/publicdashboard/publicdashboard_kind_gen.go b/pkg/kinds/publicdashboard/publicdashboard_kind_gen.go new file mode 100644 index 00000000000..70add7f3adb --- /dev/null +++ b/pkg/kinds/publicdashboard/publicdashboard_kind_gen.go @@ -0,0 +1,113 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. +// +// Generated by: +// kinds/gen.go +// Using jennies: +// CoreKindJenny +// +// Run 'make gen-cue' from repository root to regenerate. + +package publicdashboard + +import ( + "github.com/grafana/grafana/pkg/kindsys" + "github.com/grafana/thema" + "github.com/grafana/thema/vmux" +) + +// rootrel is the relative path from the grafana repository root to the +// directory containing the .cue files in which this kind is declared. Necessary +// for runtime errors related to the declaration and/or lineage to provide +// a real path to the correct .cue file. +const rootrel string = "kinds/publicdashboard" + +// TODO standard generated docs +type Kind struct { + lin thema.ConvergentLineage[*PublicDashboard] + jcodec vmux.Codec + valmux vmux.ValueMux[*PublicDashboard] + decl kindsys.Decl[kindsys.CoreProperties] +} + +// type guard +var _ kindsys.Core = &Kind{} + +// TODO standard generated docs +func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) { + decl, err := kindsys.LoadCoreKind(rootrel, rt.Context(), nil) + if err != nil { + return nil, err + } + k := &Kind{ + decl: decl, + } + + lin, err := decl.Some().BindKindLineage(rt, opts...) + if err != nil { + return nil, err + } + + // Get the thema.Schema that the meta says is in the current version (which + // codegen ensures is always the latest) + cursch := thema.SchemaP(lin, k.decl.Properties.CurrentVersion) + tsch, err := thema.BindType[*PublicDashboard](cursch, &PublicDashboard{}) + if err != nil { + // Should be unreachable, modulo bugs in the Thema->Go code generator + return nil, err + } + + k.jcodec = vmux.NewJSONCodec("publicdashboard.json") + k.lin = tsch.ConvergentLineage() + k.valmux = vmux.NewValueMux(k.lin.TypedSchema(), k.jcodec) + return k, nil +} + +// TODO standard generated docs +func (k *Kind) Name() string { + return "publicdashboard" +} + +// TODO standard generated docs +func (k *Kind) MachineName() string { + return "publicdashboard" +} + +// TODO standard generated docs +func (k *Kind) Lineage() thema.Lineage { + return k.lin +} + +// TODO standard generated docs +func (k *Kind) ConvergentLineage() thema.ConvergentLineage[*PublicDashboard] { + return k.lin +} + +// JSONValueMux is a version multiplexer that maps a []byte containing JSON data +// at any schematized dashboard version to an instance of PublicDashboard. +// +// Validation and translation errors emitted from this func will identify the +// input bytes as "dashboard.json". +// +// This is a thin wrapper around Thema's [vmux.ValueMux]. +func (k *Kind) JSONValueMux(b []byte) (*PublicDashboard, thema.TranslationLacunas, error) { + return k.valmux(b) +} + +// TODO standard generated docs +func (k *Kind) Maturity() kindsys.Maturity { + return k.decl.Properties.Maturity +} + +// Decl returns the [kindsys.Decl] containing both CUE and Go representations of the +// publicdashboard declaration in .cue files. +func (k *Kind) Decl() kindsys.Decl[kindsys.CoreProperties] { + return k.decl +} + +// Props returns a [kindsys.SomeKindProps], with underlying type [kindsys.CoreProperties], +// representing the static properties declared in the publicdashboard kind. +// +// This method is identical to calling Decl().Props. It is provided to satisfy [kindsys.Interface]. +func (k *Kind) Props() kindsys.SomeKindProperties { + return k.decl.Properties +} diff --git a/pkg/kinds/publicdashboard/publicdashboard_types_gen.go b/pkg/kinds/publicdashboard/publicdashboard_types_gen.go new file mode 100644 index 00000000000..80da0c962c1 --- /dev/null +++ b/pkg/kinds/publicdashboard/publicdashboard_types_gen.go @@ -0,0 +1,32 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. +// +// Generated by: +// kinds/gen.go +// Using jennies: +// GoTypesJenny +// LatestJenny +// +// Run 'make gen-cue' from repository root to regenerate. + +package publicdashboard + +// PublicDashboard defines model for PublicDashboard. +type PublicDashboard struct { + // Unique public access token + AccessToken *string `json:"accessToken,omitempty"` + + // Flag that indicates if annotations are enabled + AnnotationsEnabled bool `json:"annotationsEnabled"` + + // Dashboard unique identifier referenced by this public dashboard + DashboardUid string `json:"dashboardUid"` + + // Flag that indicates if the public dashboard is enabled + IsEnabled bool `json:"isEnabled"` + + // Flag that indicates if the time range picker is enabled + TimeSelectionEnabled bool `json:"timeSelectionEnabled"` + + // Unique public dashboard identifier + Uid string `json:"uid"` +} diff --git a/pkg/kindsys/report.json b/pkg/kindsys/report.json index a2db2a60086..bd30c27facf 100644 --- a/pkg/kindsys/report.json +++ b/pkg/kindsys/report.json @@ -1281,6 +1281,32 @@ "pluralName": "PrometheusDataSourceCfgs", "schemaInterface": "DataSourceCfg" }, + "publicdashboard": { + "category": "core", + "codeowners": [ + "grafana/grafana-as-code", + "grafana/grafana-bi-squad", + "grafana/plugins-platform-frontend", + "grafana/user-essentials" + ], + "currentVersion": [ + 0, + 0 + ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "https://grafana.com/docs/grafana/next/developers/kinds/core/publicdashboard/schema-reference", + "go": "https://github.com/grafana/grafana/tree/main/pkg/kinds/publicdashboard", + "schema": "https://github.com/grafana/grafana/tree/main/kinds/publicdashboard/publicdashboard_kind.cue", + "ts": "https://github.com/grafana/grafana/tree/main/packages/grafana-schema/src/raw/publicdashboard/x/publicdashboard_types.gen.ts" + }, + "machineName": "publicdashboard", + "maturity": "merged", + "name": "PublicDashboard", + "pluralMachineName": "publicdashboards", + "pluralName": "PublicDashboards" + }, "query": { "category": "core", "codeowners": [], @@ -1818,6 +1844,7 @@ "folder", "playlist", "preferences", + "publicdashboard", "query", "queryhistory", "serviceaccount", @@ -1825,7 +1852,7 @@ "thumb", "user" ], - "count": 12 + "count": 13 } }, "maturity": { @@ -1860,10 +1887,11 @@ "alertgroupspanelcfg", "playlist", "preferences", + "publicdashboard", "serviceaccount", "team" ], - "count": 5 + "count": 6 }, "planned": { "name": "planned", diff --git a/pkg/registry/corekind/base_gen.go b/pkg/registry/corekind/base_gen.go index c9488dc66a7..8a180db1ca0 100644 --- a/pkg/registry/corekind/base_gen.go +++ b/pkg/registry/corekind/base_gen.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/kinds/dashboard" "github.com/grafana/grafana/pkg/kinds/playlist" "github.com/grafana/grafana/pkg/kinds/preferences" + "github.com/grafana/grafana/pkg/kinds/publicdashboard" "github.com/grafana/grafana/pkg/kinds/serviceaccount" "github.com/grafana/grafana/pkg/kinds/team" "github.com/grafana/grafana/pkg/kindsys" @@ -32,12 +33,13 @@ import ( // Prefer All*() methods when performing operations generically across all kinds. // For example, a validation HTTP middleware for any kind-schematized object type. type Base struct { - all []kindsys.Core - dashboard *dashboard.Kind - playlist *playlist.Kind - preferences *preferences.Kind - serviceaccount *serviceaccount.Kind - team *team.Kind + all []kindsys.Core + dashboard *dashboard.Kind + playlist *playlist.Kind + preferences *preferences.Kind + publicdashboard *publicdashboard.Kind + serviceaccount *serviceaccount.Kind + team *team.Kind } // type guards @@ -45,6 +47,7 @@ var ( _ kindsys.Core = &dashboard.Kind{} _ kindsys.Core = &playlist.Kind{} _ kindsys.Core = &preferences.Kind{} + _ kindsys.Core = &publicdashboard.Kind{} _ kindsys.Core = &serviceaccount.Kind{} _ kindsys.Core = &team.Kind{} ) @@ -64,6 +67,11 @@ func (b *Base) Preferences() *preferences.Kind { return b.preferences } +// PublicDashboard returns the [kindsys.Interface] implementation for the publicdashboard kind. +func (b *Base) PublicDashboard() *publicdashboard.Kind { + return b.publicdashboard +} + // ServiceAccount returns the [kindsys.Interface] implementation for the serviceaccount kind. func (b *Base) ServiceAccount() *serviceaccount.Kind { return b.serviceaccount @@ -96,6 +104,12 @@ func doNewBase(rt *thema.Runtime) *Base { } reg.all = append(reg.all, reg.preferences) + reg.publicdashboard, err = publicdashboard.NewKind(rt) + if err != nil { + panic(fmt.Sprintf("error while initializing the publicdashboard Kind: %s", err)) + } + reg.all = append(reg.all, reg.publicdashboard) + reg.serviceaccount, err = serviceaccount.NewKind(rt) if err != nil { panic(fmt.Sprintf("error while initializing the serviceaccount Kind: %s", err)) From 75ffbe422b444312556ab95fb6ac88e69fd3f82c Mon Sep 17 00:00:00 2001 From: "lean.dev" <34773040+leandro-deveikis@users.noreply.github.com> Date: Thu, 26 Jan 2023 11:06:35 -0300 Subject: [PATCH 084/172] Snapshots: Add new snapshot configuration to documentation (#62110) * Add new snapshot configuration to documentation Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> --- docs/sources/setup-grafana/configure-grafana/_index.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 8c9c85661d0..8e3415c9af5 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -675,6 +675,10 @@ List of allowed headers to be set by the user. Suggested to use for if authentic ## [snapshots] +### enabled + +Set to `false` to disable the snapshot feature (default `true`). + ### external_enabled Set to `false` to disable external snapshot publish endpoint (default `true`). From 995e2715adb352f72a14b76dd6903adf63ca96f4 Mon Sep 17 00:00:00 2001 From: Levente Balogh Date: Thu, 26 Jan 2023 15:10:09 +0100 Subject: [PATCH 085/172] Navigation: Fix finding the active nav item for plugins (#62123) fix: look for the active page under children first --- public/app/features/plugins/utils.test.ts | 7 +++++++ public/app/features/plugins/utils.ts | 9 ++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/public/app/features/plugins/utils.test.ts b/public/app/features/plugins/utils.test.ts index bca1665ffb6..b79a5840279 100644 --- a/public/app/features/plugins/utils.test.ts +++ b/public/app/features/plugins/utils.test.ts @@ -69,6 +69,13 @@ describe('buildPluginSectionNav', () => { expect(result?.node.text).toBe('page2'); }); + it('Should only set the most specific match as active (not the parents)', () => { + config.featureToggles.topnav = true; + const result = buildPluginSectionNav(appsSection, null, '/a/plugin1/page2'); + expect(result?.main.children![0].children![1].active).toBe(true); + expect(result?.main.children![0].active).not.toBe(true); // Parent should not be active + }); + it('Should set app section to active', () => { config.featureToggles.topnav = true; const result = buildPluginSectionNav(appsSection, null, '/a/plugin1'); diff --git a/public/app/features/plugins/utils.ts b/public/app/features/plugins/utils.ts index c28a58e70df..145431cc039 100644 --- a/public/app/features/plugins/utils.ts +++ b/public/app/features/plugins/utils.ts @@ -49,6 +49,8 @@ export function buildPluginSectionNav( return page; } + // Check if there is already an active page found with with a more specific url (possibly a child of the current page) + // (In this case we bail out early and don't mark the parent as active) if (activePage && (activePage.url?.length ?? 0) > (page.url?.length ?? 0)) { return page; } @@ -58,15 +60,20 @@ export function buildPluginSectionNav( } activePage = { ...page, active: true }; + return activePage; } // Find and set active page copiedPluginNavSection.children = (copiedPluginNavSection?.children ?? []).map((child) => { if (child.children) { + // Doing this here to make sure that first we check if any of the children is active + // (In case yes, then the check for the parent will not mark it as active) + const children = child.children.map((pluginPage) => setPageToActive(pluginPage, currentUrl)); + return { ...setPageToActive(child, currentUrl), - children: child.children.map((pluginPage) => setPageToActive(pluginPage, currentUrl)), + children, }; } From 2eaa3fb4d2ee063cb8c009ca86f82bdb4b8ba62e Mon Sep 17 00:00:00 2001 From: ying-jeanne <74549700+ying-jeanne@users.noreply.github.com> Date: Thu, 26 Jan 2023 22:24:11 +0800 Subject: [PATCH 086/172] [Xorm] remove oracle driver + unused function (#62125) [Xorm] Some more clean up --- pkg/util/xorm/dialect_oracle.go | 902 -------------------------------- pkg/util/xorm/engine.go | 42 -- pkg/util/xorm/xorm.go | 2 - 3 files changed, 946 deletions(-) delete mode 100644 pkg/util/xorm/dialect_oracle.go diff --git a/pkg/util/xorm/dialect_oracle.go b/pkg/util/xorm/dialect_oracle.go deleted file mode 100644 index d23ab1678af..00000000000 --- a/pkg/util/xorm/dialect_oracle.go +++ /dev/null @@ -1,902 +0,0 @@ -// Copyright 2015 The Xorm Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package xorm - -import ( - "errors" - "fmt" - "regexp" - "strconv" - "strings" - - "xorm.io/core" -) - -var ( - oracleReservedWords = map[string]bool{ - "ACCESS": true, - "ACCOUNT": true, - "ACTIVATE": true, - "ADD": true, - "ADMIN": true, - "ADVISE": true, - "AFTER": true, - "ALL": true, - "ALL_ROWS": true, - "ALLOCATE": true, - "ALTER": true, - "ANALYZE": true, - "AND": true, - "ANY": true, - "ARCHIVE": true, - "ARCHIVELOG": true, - "ARRAY": true, - "AS": true, - "ASC": true, - "AT": true, - "AUDIT": true, - "AUTHENTICATED": true, - "AUTHORIZATION": true, - "AUTOEXTEND": true, - "AUTOMATIC": true, - "BACKUP": true, - "BECOME": true, - "BEFORE": true, - "BEGIN": true, - "BETWEEN": true, - "BFILE": true, - "BITMAP": true, - "BLOB": true, - "BLOCK": true, - "BODY": true, - "BY": true, - "CACHE": true, - "CACHE_INSTANCES": true, - "CANCEL": true, - "CASCADE": true, - "CAST": true, - "CFILE": true, - "CHAINED": true, - "CHANGE": true, - "CHAR": true, - "CHAR_CS": true, - "CHARACTER": true, - "CHECK": true, - "CHECKPOINT": true, - "CHOOSE": true, - "CHUNK": true, - "CLEAR": true, - "CLOB": true, - "CLONE": true, - "CLOSE": true, - "CLOSE_CACHED_OPEN_CURSORS": true, - "CLUSTER": true, - "COALESCE": true, - "COLUMN": true, - "COLUMNS": true, - "COMMENT": true, - "COMMIT": true, - "COMMITTED": true, - "COMPATIBILITY": true, - "COMPILE": true, - "COMPLETE": true, - "COMPOSITE_LIMIT": true, - "COMPRESS": true, - "COMPUTE": true, - "CONNECT": true, - "CONNECT_TIME": true, - "CONSTRAINT": true, - "CONSTRAINTS": true, - "CONTENTS": true, - "CONTINUE": true, - "CONTROLFILE": true, - "CONVERT": true, - "COST": true, - "CPU_PER_CALL": true, - "CPU_PER_SESSION": true, - "CREATE": true, - "CURRENT": true, - "CURRENT_SCHEMA": true, - "CURREN_USER": true, - "CURSOR": true, - "CYCLE": true, - "DANGLING": true, - "DATABASE": true, - "DATAFILE": true, - "DATAFILES": true, - "DATAOBJNO": true, - "DATE": true, - "DBA": true, - "DBHIGH": true, - "DBLOW": true, - "DBMAC": true, - "DEALLOCATE": true, - "DEBUG": true, - "DEC": true, - "DECIMAL": true, - "DECLARE": true, - "DEFAULT": true, - "DEFERRABLE": true, - "DEFERRED": true, - "DEGREE": true, - "DELETE": true, - "DEREF": true, - "DESC": true, - "DIRECTORY": true, - "DISABLE": true, - "DISCONNECT": true, - "DISMOUNT": true, - "DISTINCT": true, - "DISTRIBUTED": true, - "DML": true, - "DOUBLE": true, - "DROP": true, - "DUMP": true, - "EACH": true, - "ELSE": true, - "ENABLE": true, - "END": true, - "ENFORCE": true, - "ENTRY": true, - "ESCAPE": true, - "EXCEPT": true, - "EXCEPTIONS": true, - "EXCHANGE": true, - "EXCLUDING": true, - "EXCLUSIVE": true, - "EXECUTE": true, - "EXISTS": true, - "EXPIRE": true, - "EXPLAIN": true, - "EXTENT": true, - "EXTENTS": true, - "EXTERNALLY": true, - "FAILED_LOGIN_ATTEMPTS": true, - "FALSE": true, - "FAST": true, - "FILE": true, - "FIRST_ROWS": true, - "FLAGGER": true, - "FLOAT": true, - "FLOB": true, - "FLUSH": true, - "FOR": true, - "FORCE": true, - "FOREIGN": true, - "FREELIST": true, - "FREELISTS": true, - "FROM": true, - "FULL": true, - "FUNCTION": true, - "GLOBAL": true, - "GLOBALLY": true, - "GLOBAL_NAME": true, - "GRANT": true, - "GROUP": true, - "GROUPS": true, - "HASH": true, - "HASHKEYS": true, - "HAVING": true, - "HEADER": true, - "HEAP": true, - "IDENTIFIED": true, - "IDGENERATORS": true, - "IDLE_TIME": true, - "IF": true, - "IMMEDIATE": true, - "IN": true, - "INCLUDING": true, - "INCREMENT": true, - "INDEX": true, - "INDEXED": true, - "INDEXES": true, - "INDICATOR": true, - "IND_PARTITION": true, - "INITIAL": true, - "INITIALLY": true, - "INITRANS": true, - "INSERT": true, - "INSTANCE": true, - "INSTANCES": true, - "INSTEAD": true, - "INT": true, - "INTEGER": true, - "INTERMEDIATE": true, - "INTERSECT": true, - "INTO": true, - "IS": true, - "ISOLATION": true, - "ISOLATION_LEVEL": true, - "KEEP": true, - "KEY": true, - "KILL": true, - "LABEL": true, - "LAYER": true, - "LESS": true, - "LEVEL": true, - "LIBRARY": true, - "LIKE": true, - "LIMIT": true, - "LINK": true, - "LIST": true, - "LOB": true, - "LOCAL": true, - "LOCK": true, - "LOCKED": true, - "LOG": true, - "LOGFILE": true, - "LOGGING": true, - "LOGICAL_READS_PER_CALL": true, - "LOGICAL_READS_PER_SESSION": true, - "LONG": true, - "MANAGE": true, - "MASTER": true, - "MAX": true, - "MAXARCHLOGS": true, - "MAXDATAFILES": true, - "MAXEXTENTS": true, - "MAXINSTANCES": true, - "MAXLOGFILES": true, - "MAXLOGHISTORY": true, - "MAXLOGMEMBERS": true, - "MAXSIZE": true, - "MAXTRANS": true, - "MAXVALUE": true, - "MIN": true, - "MEMBER": true, - "MINIMUM": true, - "MINEXTENTS": true, - "MINUS": true, - "MINVALUE": true, - "MLSLABEL": true, - "MLS_LABEL_FORMAT": true, - "MODE": true, - "MODIFY": true, - "MOUNT": true, - "MOVE": true, - "MTS_DISPATCHERS": true, - "MULTISET": true, - "NATIONAL": true, - "NCHAR": true, - "NCHAR_CS": true, - "NCLOB": true, - "NEEDED": true, - "NESTED": true, - "NETWORK": true, - "NEW": true, - "NEXT": true, - "NOARCHIVELOG": true, - "NOAUDIT": true, - "NOCACHE": true, - "NOCOMPRESS": true, - "NOCYCLE": true, - "NOFORCE": true, - "NOLOGGING": true, - "NOMAXVALUE": true, - "NOMINVALUE": true, - "NONE": true, - "NOORDER": true, - "NOOVERRIDE": true, - "NOPARALLEL": true, - "NOREVERSE": true, - "NORMAL": true, - "NOSORT": true, - "NOT": true, - "NOTHING": true, - "NOWAIT": true, - "NULL": true, - "NUMBER": true, - "NUMERIC": true, - "NVARCHAR2": true, - "OBJECT": true, - "OBJNO": true, - "OBJNO_REUSE": true, - "OF": true, - "OFF": true, - "OFFLINE": true, - "OID": true, - "OIDINDEX": true, - "OLD": true, - "ON": true, - "ONLINE": true, - "ONLY": true, - "OPCODE": true, - "OPEN": true, - "OPTIMAL": true, - "OPTIMIZER_GOAL": true, - "OPTION": true, - "OR": true, - "ORDER": true, - "ORGANIZATION": true, - "OSLABEL": true, - "OVERFLOW": true, - "OWN": true, - "PACKAGE": true, - "PARALLEL": true, - "PARTITION": true, - "PASSWORD": true, - "PASSWORD_GRACE_TIME": true, - "PASSWORD_LIFE_TIME": true, - "PASSWORD_LOCK_TIME": true, - "PASSWORD_REUSE_MAX": true, - "PASSWORD_REUSE_TIME": true, - "PASSWORD_VERIFY_FUNCTION": true, - "PCTFREE": true, - "PCTINCREASE": true, - "PCTTHRESHOLD": true, - "PCTUSED": true, - "PCTVERSION": true, - "PERCENT": true, - "PERMANENT": true, - "PLAN": true, - "PLSQL_DEBUG": true, - "POST_TRANSACTION": true, - "PRECISION": true, - "PRESERVE": true, - "PRIMARY": true, - "PRIOR": true, - "PRIVATE": true, - "PRIVATE_SGA": true, - "PRIVILEGE": true, - "PRIVILEGES": true, - "PROCEDURE": true, - "PROFILE": true, - "PUBLIC": true, - "PURGE": true, - "QUEUE": true, - "QUOTA": true, - "RANGE": true, - "RAW": true, - "RBA": true, - "READ": true, - "READUP": true, - "REAL": true, - "REBUILD": true, - "RECOVER": true, - "RECOVERABLE": true, - "RECOVERY": true, - "REF": true, - "REFERENCES": true, - "REFERENCING": true, - "REFRESH": true, - "RENAME": true, - "REPLACE": true, - "RESET": true, - "RESETLOGS": true, - "RESIZE": true, - "RESOURCE": true, - "RESTRICTED": true, - "RETURN": true, - "RETURNING": true, - "REUSE": true, - "REVERSE": true, - "REVOKE": true, - "ROLE": true, - "ROLES": true, - "ROLLBACK": true, - "ROW": true, - "ROWID": true, - "ROWNUM": true, - "ROWS": true, - "RULE": true, - "SAMPLE": true, - "SAVEPOINT": true, - "SB4": true, - "SCAN_INSTANCES": true, - "SCHEMA": true, - "SCN": true, - "SCOPE": true, - "SD_ALL": true, - "SD_INHIBIT": true, - "SD_SHOW": true, - "SEGMENT": true, - "SEG_BLOCK": true, - "SEG_FILE": true, - "SELECT": true, - "SEQUENCE": true, - "SERIALIZABLE": true, - "SESSION": true, - "SESSION_CACHED_CURSORS": true, - "SESSIONS_PER_USER": true, - "SET": true, - "SHARE": true, - "SHARED": true, - "SHARED_POOL": true, - "SHRINK": true, - "SIZE": true, - "SKIP": true, - "SKIP_UNUSABLE_INDEXES": true, - "SMALLINT": true, - "SNAPSHOT": true, - "SOME": true, - "SORT": true, - "SPECIFICATION": true, - "SPLIT": true, - "SQL_TRACE": true, - "STANDBY": true, - "START": true, - "STATEMENT_ID": true, - "STATISTICS": true, - "STOP": true, - "STORAGE": true, - "STORE": true, - "STRUCTURE": true, - "SUCCESSFUL": true, - "SWITCH": true, - "SYS_OP_ENFORCE_NOT_NULL$": true, - "SYS_OP_NTCIMG$": true, - "SYNONYM": true, - "SYSDATE": true, - "SYSDBA": true, - "SYSOPER": true, - "SYSTEM": true, - "TABLE": true, - "TABLES": true, - "TABLESPACE": true, - "TABLESPACE_NO": true, - "TABNO": true, - "TEMPORARY": true, - "THAN": true, - "THE": true, - "THEN": true, - "THREAD": true, - "TIMESTAMP": true, - "TIME": true, - "TO": true, - "TOPLEVEL": true, - "TRACE": true, - "TRACING": true, - "TRANSACTION": true, - "TRANSITIONAL": true, - "TRIGGER": true, - "TRIGGERS": true, - "TRUE": true, - "TRUNCATE": true, - "TX": true, - "TYPE": true, - "UB2": true, - "UBA": true, - "UID": true, - "UNARCHIVED": true, - "UNDO": true, - "UNION": true, - "UNIQUE": true, - "UNLIMITED": true, - "UNLOCK": true, - "UNRECOVERABLE": true, - "UNTIL": true, - "UNUSABLE": true, - "UNUSED": true, - "UPDATABLE": true, - "UPDATE": true, - "USAGE": true, - "USE": true, - "USER": true, - "USING": true, - "VALIDATE": true, - "VALIDATION": true, - "VALUE": true, - "VALUES": true, - "VARCHAR": true, - "VARCHAR2": true, - "VARYING": true, - "VIEW": true, - "WHEN": true, - "WHENEVER": true, - "WHERE": true, - "WITH": true, - "WITHOUT": true, - "WORK": true, - "WRITE": true, - "WRITEDOWN": true, - "WRITEUP": true, - "XID": true, - "YEAR": true, - "ZONE": true, - } -) - -type oracle struct { - core.Base -} - -func (db *oracle) Init(d *core.DB, uri *core.Uri, drivername, dataSourceName string) error { - return db.Base.Init(d, db, uri, drivername, dataSourceName) -} - -func (db *oracle) SqlType(c *core.Column) string { - var res string - switch t := c.SQLType.Name; t { - case core.Bit, core.TinyInt, core.SmallInt, core.MediumInt, core.Int, core.Integer, core.BigInt, core.Bool, core.Serial, core.BigSerial: - res = "NUMBER" - case core.Binary, core.VarBinary, core.Blob, core.TinyBlob, core.MediumBlob, core.LongBlob, core.Bytea: - return core.Blob - case core.Time, core.DateTime, core.TimeStamp: - res = core.TimeStamp - case core.TimeStampz: - res = "TIMESTAMP WITH TIME ZONE" - case core.Float, core.Double, core.Numeric, core.Decimal: - res = "NUMBER" - case core.Text, core.MediumText, core.LongText, core.Json: - res = "CLOB" - case core.Char, core.Varchar, core.TinyText: - res = "VARCHAR2" - default: - res = t - } - - hasLen1 := (c.Length > 0) - hasLen2 := (c.Length2 > 0) - - if hasLen2 { - res += "(" + strconv.Itoa(c.Length) + "," + strconv.Itoa(c.Length2) + ")" - } else if hasLen1 { - res += "(" + strconv.Itoa(c.Length) + ")" - } - return res -} - -func (db *oracle) AutoIncrStr() string { - return "AUTO_INCREMENT" -} - -func (db *oracle) SupportInsertMany() bool { - return true -} - -func (db *oracle) IsReserved(name string) bool { - _, ok := oracleReservedWords[name] - return ok -} - -func (db *oracle) Quote(name string) string { - return "[" + name + "]" -} - -func (db *oracle) SupportEngine() bool { - return false -} - -func (db *oracle) SupportCharset() bool { - return false -} - -func (db *oracle) SupportDropIfExists() bool { - return false -} - -func (db *oracle) IndexOnTable() bool { - return false -} - -func (db *oracle) DropTableSql(tableName string) string { - return fmt.Sprintf("DROP TABLE `%s`", tableName) -} - -func (db *oracle) CreateTableSql(table *core.Table, tableName, storeEngine, charset string) string { - var sql string - sql = "CREATE TABLE " - if tableName == "" { - tableName = table.Name - } - - sql += db.Quote(tableName) + " (" - - pkList := table.PrimaryKeys - - for _, colName := range table.ColumnsSeq() { - col := table.GetColumn(colName) - /*if col.IsPrimaryKey && len(pkList) == 1 { - sql += col.String(b.dialect) - } else {*/ - sql += col.StringNoPk(db) - // } - sql = strings.TrimSpace(sql) - sql += ", " - } - - if len(pkList) > 0 { - sql += "PRIMARY KEY ( " - sql += db.Quote(strings.Join(pkList, db.Quote(","))) - sql += " ), " - } - - sql = sql[:len(sql)-2] + ")" - if db.SupportEngine() && storeEngine != "" { - sql += " ENGINE=" + storeEngine - } - if db.SupportCharset() { - if len(charset) == 0 { - charset = db.URI().Charset - } - if len(charset) > 0 { - sql += " DEFAULT CHARSET " + charset - } - } - return sql -} - -func (db *oracle) IndexCheckSql(tableName, idxName string) (string, []interface{}) { - args := []interface{}{tableName, idxName} - return `SELECT INDEX_NAME FROM USER_INDEXES ` + - `WHERE TABLE_NAME = :1 AND INDEX_NAME = :2`, args -} - -func (db *oracle) TableCheckSql(tableName string) (string, []interface{}) { - args := []interface{}{tableName} - return `SELECT table_name FROM user_tables WHERE table_name = :1`, args -} - -func (db *oracle) MustDropTable(tableName string) error { - sql, args := db.TableCheckSql(tableName) - db.LogSQL(sql, args) - - rows, err := db.DB().Query(sql, args...) - if err != nil { - return err - } - defer rows.Close() - - if !rows.Next() { - return nil - } - - sql = "Drop Table \"" + tableName + "\"" - db.LogSQL(sql, args) - - _, err = db.DB().Exec(sql) - return err -} - -/*func (db *oracle) ColumnCheckSql(tableName, colName string) (string, []interface{}) { - args := []interface{}{strings.ToUpper(tableName), strings.ToUpper(colName)} - return "SELECT column_name FROM USER_TAB_COLUMNS WHERE table_name = ?" + - " AND column_name = ?", args -}*/ - -func (db *oracle) IsColumnExist(tableName, colName string) (bool, error) { - args := []interface{}{tableName, colName} - query := "SELECT column_name FROM USER_TAB_COLUMNS WHERE table_name = :1" + - " AND column_name = :2" - db.LogSQL(query, args) - - rows, err := db.DB().Query(query, args...) - if err != nil { - return false, err - } - defer rows.Close() - - if rows.Next() { - return true, nil - } - return false, nil -} - -func (db *oracle) GetColumns(tableName string) ([]string, map[string]*core.Column, error) { - args := []interface{}{tableName} - s := "SELECT column_name,data_default,data_type,data_length,data_precision,data_scale," + - "nullable FROM USER_TAB_COLUMNS WHERE table_name = :1" - db.LogSQL(s, args) - - rows, err := db.DB().Query(s, args...) - if err != nil { - return nil, nil, err - } - defer rows.Close() - - cols := make(map[string]*core.Column) - colSeq := make([]string, 0) - for rows.Next() { - col := new(core.Column) - col.Indexes = make(map[string]int) - - var colName, colDefault, nullable, dataType, dataPrecision, dataScale *string - var dataLen int - - err = rows.Scan(&colName, &colDefault, &dataType, &dataLen, &dataPrecision, - &dataScale, &nullable) - if err != nil { - return nil, nil, err - } - - col.Name = strings.Trim(*colName, `" `) - if colDefault != nil { - col.Default = *colDefault - col.DefaultIsEmpty = false - } - - if *nullable == "Y" { - col.Nullable = true - } else { - col.Nullable = false - } - - var ignore bool - - var dt string - var len1, len2 int - dts := strings.Split(*dataType, "(") - dt = dts[0] - if len(dts) > 1 { - lens := strings.Split(dts[1][:len(dts[1])-1], ",") - if len(lens) > 1 { - len1, _ = strconv.Atoi(lens[0]) - len2, _ = strconv.Atoi(lens[1]) - } else { - len1, _ = strconv.Atoi(lens[0]) - } - } - - switch dt { - case "VARCHAR2": - col.SQLType = core.SQLType{Name: core.Varchar, DefaultLength: len1, DefaultLength2: len2} - case "NVARCHAR2": - col.SQLType = core.SQLType{Name: core.NVarchar, DefaultLength: len1, DefaultLength2: len2} - case "TIMESTAMP WITH TIME ZONE": - col.SQLType = core.SQLType{Name: core.TimeStampz, DefaultLength: 0, DefaultLength2: 0} - case "NUMBER": - col.SQLType = core.SQLType{Name: core.Double, DefaultLength: len1, DefaultLength2: len2} - case "LONG", "LONG RAW": - col.SQLType = core.SQLType{Name: core.Text, DefaultLength: 0, DefaultLength2: 0} - case "RAW": - col.SQLType = core.SQLType{Name: core.Binary, DefaultLength: 0, DefaultLength2: 0} - case "ROWID": - col.SQLType = core.SQLType{Name: core.Varchar, DefaultLength: 18, DefaultLength2: 0} - case "AQ$_SUBSCRIBERS": - ignore = true - default: - col.SQLType = core.SQLType{Name: strings.ToUpper(dt), DefaultLength: len1, DefaultLength2: len2} - } - - if ignore { - continue - } - - if _, ok := core.SqlTypes[col.SQLType.Name]; !ok { - return nil, nil, fmt.Errorf("unknown colType %v %v", *dataType, col.SQLType) - } - - col.Length = dataLen - - if col.SQLType.IsText() || col.SQLType.IsTime() { - if !col.DefaultIsEmpty { - col.Default = "'" + col.Default + "'" - } - } - cols[col.Name] = col - colSeq = append(colSeq, col.Name) - } - - return colSeq, cols, nil -} - -func (db *oracle) GetTables() ([]*core.Table, error) { - args := []interface{}{} - s := "SELECT table_name FROM user_tables" - db.LogSQL(s, args) - - rows, err := db.DB().Query(s, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - tables := make([]*core.Table, 0) - for rows.Next() { - table := core.NewEmptyTable() - err = rows.Scan(&table.Name) - if err != nil { - return nil, err - } - - tables = append(tables, table) - } - return tables, nil -} - -func (db *oracle) GetIndexes(tableName string) (map[string]*core.Index, error) { - args := []interface{}{tableName} - s := "SELECT t.column_name,i.uniqueness,i.index_name FROM user_ind_columns t,user_indexes i " + - "WHERE t.index_name = i.index_name and t.table_name = i.table_name and t.table_name =:1" - db.LogSQL(s, args) - - rows, err := db.DB().Query(s, args...) - if err != nil { - return nil, err - } - defer rows.Close() - - indexes := make(map[string]*core.Index, 0) - for rows.Next() { - var indexType int - var indexName, colName, uniqueness string - - err = rows.Scan(&colName, &uniqueness, &indexName) - if err != nil { - return nil, err - } - - indexName = strings.Trim(indexName, `" `) - - var isRegular bool - if strings.HasPrefix(indexName, "IDX_"+tableName) || strings.HasPrefix(indexName, "UQE_"+tableName) { - indexName = indexName[5+len(tableName):] - isRegular = true - } - - if uniqueness == "UNIQUE" { - indexType = core.UniqueType - } else { - indexType = core.IndexType - } - - var index *core.Index - var ok bool - if index, ok = indexes[indexName]; !ok { - index = new(core.Index) - index.Type = indexType - index.Name = indexName - index.IsRegular = isRegular - indexes[indexName] = index - } - index.AddColumn(colName) - } - return indexes, nil -} - -func (db *oracle) Filters() []core.Filter { - return []core.Filter{&core.QuoteFilter{}, &core.SeqFilter{Prefix: ":", Start: 1}, &core.IdFilter{}} -} - -type goracleDriver struct { -} - -func (cfg *goracleDriver) Parse(driverName, dataSourceName string) (*core.Uri, error) { - db := &core.Uri{DbType: core.ORACLE} - dsnPattern := regexp.MustCompile( - `^(?:(?P.*?)(?::(?P.*))?@)?` + // [user[:password]@] - `(?:(?P[^\(]*)(?:\((?P[^\)]*)\))?)?` + // [net[(addr)]] - `\/(?P.*?)` + // /dbname - `(?:\?(?P[^\?]*))?$`) // [?param1=value1¶mN=valueN] - matches := dsnPattern.FindStringSubmatch(dataSourceName) - // tlsConfigRegister := make(map[string]*tls.Config) - names := dsnPattern.SubexpNames() - - for i, match := range matches { - switch names[i] { - case "dbname": - db.DbName = match - } - } - if db.DbName == "" { - return nil, errors.New("dbname is empty") - } - return db, nil -} - -type oci8Driver struct { -} - -// dataSourceName=user/password@ipv4:port/dbname -// dataSourceName=user/password@[ipv6]:port/dbname -func (p *oci8Driver) Parse(driverName, dataSourceName string) (*core.Uri, error) { - db := &core.Uri{DbType: core.ORACLE} - dsnPattern := regexp.MustCompile( - `^(?P.*)\/(?P.*)@` + // user:password@ - `(?P.*)` + // ip:port - `\/(?P.*)`) // dbname - matches := dsnPattern.FindStringSubmatch(dataSourceName) - names := dsnPattern.SubexpNames() - for i, match := range matches { - switch names[i] { - case "dbname": - db.DbName = match - } - } - if db.DbName == "" { - return nil, errors.New("dbname is empty") - } - return db, nil -} diff --git a/pkg/util/xorm/engine.go b/pkg/util/xorm/engine.go index 3c1267ae194..47608749d0d 100644 --- a/pkg/util/xorm/engine.go +++ b/pkg/util/xorm/engine.go @@ -46,13 +46,6 @@ type Engine struct { defaultContext context.Context } -// BufferSize sets buffer size for iterate -func (engine *Engine) BufferSize(size int) *Session { - session := engine.NewSession() - session.isAutoClose = true - return session.BufferSize(size) -} - // CondDeleted returns the conditions whether a record is soft deleted. func (engine *Engine) CondDeleted(col *core.Column) builder.Cond { var cond = builder.NewCond() @@ -238,14 +231,6 @@ func (engine *Engine) SetMaxIdleConns(conns int) { engine.db.SetMaxIdleConns(conns) } -// NoCache If you has set default cacher, and you want temporilly stop use cache, -// you can use NoCache() -func (engine *Engine) NoCache() *Session { - session := engine.NewSession() - session.isAutoClose = true - return session.NoCache() -} - // NewDB provides an interface to operate database directly func (engine *Engine) NewDB() (*core.DB, error) { return core.OpenDialect(engine.dialect) @@ -280,17 +265,6 @@ func (engine *Engine) Ping() error { return session.Ping() } -// logSQL save sql -func (engine *Engine) logSQL(sqlStr string, sqlArgs ...interface{}) { - if engine.showSQL && !engine.showExecTime { - if len(sqlArgs) > 0 { - engine.logger.Infof("[SQL] %v %#v", sqlStr, sqlArgs) - } else { - engine.logger.Infof("[SQL] %v", sqlStr) - } - } -} - // Sql provides raw sql input parameter. When you have a complex SQL statement // and cannot use Where, Id, In and etc. Methods to describe, you can use SQL. // @@ -311,22 +285,6 @@ func (engine *Engine) SQL(query interface{}, args ...interface{}) *Session { return session.SQL(query, args...) } -// NoAutoTime Default if your struct has "created" or "updated" filed tag, the fields -// will automatically be filled with current time when Insert or Update -// invoked. Call NoAutoTime if you dont' want to fill automatically. -func (engine *Engine) NoAutoTime() *Session { - session := engine.NewSession() - session.isAutoClose = true - return session.NoAutoTime() -} - -// NoAutoCondition disable auto generate Where condition from bean or not -func (engine *Engine) NoAutoCondition(no ...bool) *Session { - session := engine.NewSession() - session.isAutoClose = true - return session.NoAutoCondition(no...) -} - func (engine *Engine) loadTableInfo(table *core.Table) error { colSeq, cols, err := engine.dialect.GetColumns(table.Name) if err != nil { diff --git a/pkg/util/xorm/xorm.go b/pkg/util/xorm/xorm.go index c3f60c56d66..b3178773518 100644 --- a/pkg/util/xorm/xorm.go +++ b/pkg/util/xorm/xorm.go @@ -35,8 +35,6 @@ func regDrvsNDialects() bool { "postgres": {"postgres", func() core.Driver { return &pqDriver{} }, func() core.Dialect { return &postgres{} }}, "pgx": {"postgres", func() core.Driver { return &pqDriverPgx{} }, func() core.Dialect { return &postgres{} }}, "sqlite3": {"sqlite3", func() core.Driver { return &sqlite3Driver{} }, func() core.Dialect { return &sqlite3{} }}, - "oci8": {"oracle", func() core.Driver { return &oci8Driver{} }, func() core.Dialect { return &oracle{} }}, - "goracle": {"oracle", func() core.Driver { return &goracleDriver{} }, func() core.Dialect { return &oracle{} }}, } for driverName, v := range providedDrvsNDialects { From f4be855e3055f231cf6a9f1a5040e6d78d802d3c Mon Sep 17 00:00:00 2001 From: Jack Baldry Date: Thu, 26 Jan 2023 14:29:26 +0000 Subject: [PATCH 087/172] Update doc-validator to latest release (#62170) * Update doc-validator to latest release Signed-off-by: Jack Baldry * Skip image validation Signed-off-by: Jack Baldry Signed-off-by: Jack Baldry --- .github/workflows/doc-validator.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/doc-validator.yml b/.github/workflows/doc-validator.yml index 695b07ef338..5444eef33a0 100644 --- a/.github/workflows/doc-validator.yml +++ b/.github/workflows/doc-validator.yml @@ -7,10 +7,10 @@ jobs: doc-validator: runs-on: "ubuntu-latest" container: - image: "grafana/doc-validator:v1.5.0" + image: "grafana/doc-validator:v1.9.0" steps: - name: "Checkout code" uses: "actions/checkout@v3" - name: "Run doc-validator tool" # Ensure that the CI always passes until all errors are resolved. - run: "doc-validator ./docs/sources || true" + run: "doc-validator --skip-image-validation ./docs/sources /docs/grafana/latest || true" From 5e1dc22f8892de142d0eb68c329329aacf200d82 Mon Sep 17 00:00:00 2001 From: Ieva Date: Thu, 26 Jan 2023 14:39:52 +0000 Subject: [PATCH 088/172] Chore: fix builds on main (#62218) fix builds --- pkg/services/store/k8saccess/dashboard_service.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/services/store/k8saccess/dashboard_service.go b/pkg/services/store/k8saccess/dashboard_service.go index ba8347a0077..2aedde47aca 100644 --- a/pkg/services/store/k8saccess/dashboard_service.go +++ b/pkg/services/store/k8saccess/dashboard_service.go @@ -4,7 +4,6 @@ import ( "context" "fmt" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/store/entity" @@ -32,7 +31,7 @@ func (s *k8sDashboardService) DeleteDashboard(ctx context.Context, dashboardId i return s.orig.DeleteDashboard(ctx, dashboardId, orgId) } -func (s *k8sDashboardService) FindDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) { +func (s *k8sDashboardService) FindDashboards(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) { return s.orig.FindDashboards(ctx, query) } @@ -77,7 +76,7 @@ func (s *k8sDashboardService) SaveDashboard(ctx context.Context, dto *dashboards return s.orig.SaveDashboard(ctx, dto, allowUiUpdate) } -func (s *k8sDashboardService) SearchDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) error { +func (s *k8sDashboardService) SearchDashboards(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) error { return s.orig.SearchDashboards(ctx, query) } From ea1fcbb8667fe46ac99dc3bde2247ccda6dd5a40 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Thu, 26 Jan 2023 16:06:10 +0100 Subject: [PATCH 089/172] Explore: Add feature to open log sample in split view (#62097) * Add tests * Implement split open to see logs functionality * Fix imports in test * Update packages/grafana-data/src/types/logs.ts Co-authored-by: Matias Chomicki * Update packages/grafana-data/src/types/logs.ts Co-authored-by: Matias Chomicki * Update default scneario to throw error * Exit early in getSupplementaryQuery * Update public/app/features/explore/LogsSamplePanel.tsx Co-authored-by: Matias Chomicki --- packages/grafana-data/src/types/logs.ts | 13 +++ public/app/features/explore/Explore.tsx | 6 +- .../app/features/explore/LogsSample.test.tsx | 40 ++++++- .../app/features/explore/LogsSamplePanel.tsx | 78 ++++++++++--- public/app/features/explore/state/helpers.ts | 1 + .../app/features/explore/state/query.test.ts | 1 + .../elasticsearch/datasource.test.ts | 51 ++++++++ .../datasource/elasticsearch/datasource.ts | 108 ++++++++++------- .../datasource/loki/datasource.test.ts | 93 ++++++++++++++- .../app/plugins/datasource/loki/datasource.ts | 110 ++++++++++-------- 10 files changed, 392 insertions(+), 109 deletions(-) diff --git a/packages/grafana-data/src/types/logs.ts b/packages/grafana-data/src/types/logs.ts index 714a56acf9d..6469be8404a 100644 --- a/packages/grafana-data/src/types/logs.ts +++ b/packages/grafana-data/src/types/logs.ts @@ -195,11 +195,23 @@ export enum SupplementaryQueryType { * @internal */ export interface DataSourceWithSupplementaryQueriesSupport { + /** + * Returns an observable that will be used to fetch supplementary data based on the provided + * supplementary query type and original request. + */ getDataProvider( type: SupplementaryQueryType, request: DataQueryRequest ): Observable | undefined; + /** + * Returns supplementary query types that data source supports. + */ getSupportedSupplementaryQueryTypes(): SupplementaryQueryType[]; + /** + * Returns a supplementary query to be used to fetch supplementary data based on the provided type and original query. + * If provided query is not suitable for provided supplementary query type, undefined should be returned. + */ + getSupplementaryQuery(type: SupplementaryQueryType, query: TQuery): TQuery | undefined; } export const hasSupplementaryQuerySupport = ( @@ -214,6 +226,7 @@ export const hasSupplementaryQuerySupport = ( return ( withSupplementaryQueriesSupport.getDataProvider !== undefined && + withSupplementaryQueriesSupport.getSupplementaryQuery !== undefined && withSupplementaryQueriesSupport.getSupportedSupplementaryQueryTypes().includes(type) ); }; diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 0a0f2443d9e..b99eba23dc6 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -367,15 +367,17 @@ export class Explore extends React.PureComponent { } renderLogsSamplePanel() { - const { logsSample, timeZone, setSupplementaryQueryEnabled, exploreId, datasourceInstance } = this.props; + const { logsSample, timeZone, setSupplementaryQueryEnabled, exploreId, datasourceInstance, queries } = this.props; return ( + splitOpen={this.onSplitOpen('logsSample')} + setLogsSampleEnabled={(enabled: boolean) => setSupplementaryQueryEnabled(exploreId, enabled, SupplementaryQueryType.LogsSample) } /> diff --git a/public/app/features/explore/LogsSample.test.tsx b/public/app/features/explore/LogsSample.test.tsx index 967457fdb86..158a062f2da 100644 --- a/public/app/features/explore/LogsSample.test.tsx +++ b/public/app/features/explore/LogsSample.test.tsx @@ -2,7 +2,15 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React, { ComponentProps } from 'react'; -import { ArrayVector, FieldType, LoadingState, MutableDataFrame } from '@grafana/data'; +import { + ArrayVector, + FieldType, + LoadingState, + MutableDataFrame, + SupplementaryQueryType, + DataSourceApi, +} from '@grafana/data'; +import { DataQuery } from '@grafana/schema'; import { LogsSamplePanel } from './LogsSamplePanel'; @@ -20,6 +28,8 @@ const createProps = (propOverrides?: Partial { expect(screen.getByText('Failed to load logs sample for this query')).toBeInTheDocument(); expect(screen.getByText('Test error message')).toBeInTheDocument(); }); + it('has split open button functionality', async () => { + const datasourceInstance = { + uid: 'test_uid', + getDataProvider: jest.fn(), + getSupportedSupplementaryQueryTypes: jest.fn().mockImplementation(() => [SupplementaryQueryType.LogsSample]), + getSupplementaryQuery: jest.fn().mockImplementation(() => { + return { + refId: 'test_refid', + } as DataQuery; + }), + } as unknown as DataSourceApi; + const splitOpen = jest.fn(); + render( + + ); + const splitButton = screen.getByText('Open logs in split view'); + expect(splitButton).toBeInTheDocument(); + + await userEvent.click(splitButton); + expect(splitOpen).toHaveBeenCalledWith({ datasourceUid: 'test_uid', query: { refId: 'test_refid' } }); + }); }); diff --git a/public/app/features/explore/LogsSamplePanel.tsx b/public/app/features/explore/LogsSamplePanel.tsx index 72dbc4d8bc0..8d9d0cf949c 100644 --- a/public/app/features/explore/LogsSamplePanel.tsx +++ b/public/app/features/explore/LogsSamplePanel.tsx @@ -1,9 +1,19 @@ +import { css } from '@emotion/css'; import React from 'react'; -import { DataQueryResponse, DataSourceApi, LoadingState, LogsDedupStrategy } from '@grafana/data'; +import { + DataQueryResponse, + DataSourceApi, + GrafanaTheme2, + hasSupplementaryQuerySupport, + LoadingState, + LogsDedupStrategy, + SplitOpen, + SupplementaryQueryType, +} from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; -import { TimeZone } from '@grafana/schema'; -import { Collapse } from '@grafana/ui'; +import { TimeZone, DataQuery } from '@grafana/schema'; +import { Button, Collapse, useStyles2 } from '@grafana/ui'; import { dataFrameToLogsModel } from 'app/core/logsModel'; import store from 'app/core/store'; @@ -16,13 +26,16 @@ type Props = { queryResponse: DataQueryResponse | undefined; enabled: boolean; timeZone: TimeZone; + queries: DataQuery[]; datasourceInstance: DataSourceApi | null | undefined; + splitOpen: SplitOpen; setLogsSampleEnabled: (enabled: boolean) => void; }; export function LogsSamplePanel(props: Props) { - const { queryResponse, timeZone, enabled, setLogsSampleEnabled, datasourceInstance } = props; + const { queryResponse, timeZone, enabled, setLogsSampleEnabled, datasourceInstance, queries, splitOpen } = props; + const styles = useStyles2(getStyles); const onToggleLogsSampleCollapse = (isOpen: boolean) => { setLogsSampleEnabled(isOpen); reportInteraction('grafana_explore_logs_sample_toggle_clicked', { @@ -31,6 +44,32 @@ export function LogsSamplePanel(props: Props) { }); }; + const OpenInSplitViewButton = () => { + if (!hasSupplementaryQuerySupport(datasourceInstance, SupplementaryQueryType.LogsSample)) { + return null; + } + + const logSampleQueries = queries + .map((query) => datasourceInstance.getSupplementaryQuery(SupplementaryQueryType.LogsSample, query)) + .filter((query): query is DataQuery => !!query); + + if (!logSampleQueries.length) { + return null; + } + + return ( + + ); + }; + let LogsSamplePanelContent: JSX.Element | null; if (queryResponse === undefined) { @@ -46,16 +85,19 @@ export function LogsSamplePanel(props: Props) { } else { const logs = dataFrameToLogsModel(queryResponse.data); LogsSamplePanelContent = ( - + <> + + + ); } @@ -65,3 +107,11 @@ export function LogsSamplePanel(props: Props) { ); } + +const getStyles = (theme: GrafanaTheme2) => ({ + logSamplesButton: css` + position: absolute; + top: ${theme.spacing(1)}; + right: ${theme.spacing(1)}; ; + `, +}); diff --git a/public/app/features/explore/state/helpers.ts b/public/app/features/explore/state/helpers.ts index fe3fdba14c6..511e24acc81 100644 --- a/public/app/features/explore/state/helpers.ts +++ b/public/app/features/explore/state/helpers.ts @@ -27,6 +27,7 @@ export const createDefaultInitialState = () => { getSupportedSupplementaryQueryTypes: jest .fn() .mockImplementation(() => [SupplementaryQueryType.LogsVolume, SupplementaryQueryType.LogsSample]), + getSupplementaryQuery: jest.fn(), meta: { id: 'something', }, diff --git a/public/app/features/explore/state/query.test.ts b/public/app/features/explore/state/query.test.ts index a75697284da..4c461a0fd78 100644 --- a/public/app/features/explore/state/query.test.ts +++ b/public/app/features/explore/state/query.test.ts @@ -413,6 +413,7 @@ describe('reducer', () => { SupplementaryQueryType.LogsVolume, SupplementaryQueryType.LogsSample, ], + getSupplementaryQuery: jest.fn(), }, }, }, diff --git a/public/app/plugins/datasource/elasticsearch/datasource.test.ts b/public/app/plugins/datasource/elasticsearch/datasource.test.ts index f5b349c0157..44be841ff29 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.test.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.test.ts @@ -15,6 +15,7 @@ import { FieldType, MutableDataFrame, RawTimeRange, + SupplementaryQueryType, TimeRange, toUtc, } from '@grafana/data'; @@ -924,6 +925,56 @@ describe('ElasticDatasource', () => { expect((interpolatedQuery.bucketAggs![0] as Filters).settings!.filters![0].query).toBe('*'); }); + + describe('getSupplementaryQuery', () => { + let ds: ElasticDatasource; + beforeEach(() => { + ds = getTestContext().ds; + }); + + it('does not return logs volume query for metric query', () => { + expect( + ds.getSupplementaryQuery(SupplementaryQueryType.LogsVolume, { + refId: 'A', + metrics: [{ type: 'count', id: '1' }], + bucketAggs: [{ type: 'filters', settings: { filters: [{ query: 'foo', label: '' }] }, id: '1' }], + query: 'foo="bar"', + }) + ).toEqual(undefined); + }); + + it('returns logs volume query for log query', () => { + expect( + ds.getSupplementaryQuery(SupplementaryQueryType.LogsVolume, { + refId: 'A', + metrics: [{ type: 'logs', id: '1' }], + query: 'foo="bar"', + }) + ).toEqual({ + bucketAggs: [ + { + field: '', + id: '3', + settings: { + interval: 'auto', + min_doc_count: '0', + trimEdges: '0', + }, + type: 'date_histogram', + }, + ], + metrics: [ + { + id: '1', + type: 'count', + }, + ], + query: 'foo="bar"', + refId: 'log-volume-A', + timeField: '', + }); + }); + }); }); describe('getMultiSearchUrl', () => { diff --git a/public/app/plugins/datasource/elasticsearch/datasource.ts b/public/app/plugins/datasource/elasticsearch/datasource.ts index dfa3f5eeecb..dcdd8e15a8f 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.ts @@ -600,58 +600,80 @@ export class ElasticDatasource return [SupplementaryQueryType.LogsVolume]; } - getLogsVolumeDataProvider(request: DataQueryRequest): Observable | undefined { - const isLogsVolumeAvailable = request.targets.some((target) => { - return target.metrics?.length === 1 && target.metrics[0].type === 'logs'; - }); - if (!isLogsVolumeAvailable) { + getSupplementaryQuery(type: SupplementaryQueryType, query: ElasticsearchQuery): ElasticsearchQuery | undefined { + if (!this.getSupportedSupplementaryQueryTypes().includes(type)) { return undefined; } - const logsVolumeRequest = cloneDeep(request); - logsVolumeRequest.targets = logsVolumeRequest.targets.map((target) => { - const bucketAggs: BucketAggregation[] = []; - const timeField = this.timeField ?? '@timestamp'; - if (this.logLevelField) { + let isQuerySuitable = false; + + switch (type) { + case SupplementaryQueryType.LogsVolume: + // it has to be a logs-producing range-query + isQuerySuitable = !!(query.metrics?.length === 1 && query.metrics[0].type === 'logs'); + if (!isQuerySuitable) { + return undefined; + } + const bucketAggs: BucketAggregation[] = []; + const timeField = this.timeField ?? '@timestamp'; + + if (this.logLevelField) { + bucketAggs.push({ + id: '2', + type: 'terms', + settings: { + min_doc_count: '0', + size: '0', + order: 'desc', + orderBy: '_count', + missing: LogLevel.unknown, + }, + field: this.logLevelField, + }); + } bucketAggs.push({ - id: '2', - type: 'terms', + id: '3', + type: 'date_histogram', settings: { + interval: 'auto', min_doc_count: '0', - size: '0', - order: 'desc', - orderBy: '_count', - missing: LogLevel.unknown, + trimEdges: '0', }, - field: this.logLevelField, + field: timeField, }); + + return { + refId: `${REF_ID_STARTER_LOG_VOLUME}${query.refId}`, + query: query.query, + metrics: [{ type: 'count', id: '1' }], + timeField, + bucketAggs, + }; + + default: + return undefined; + } + } + + getLogsVolumeDataProvider(request: DataQueryRequest): Observable | undefined { + const logsVolumeRequest = cloneDeep(request); + const targets = logsVolumeRequest.targets + .map((target) => this.getSupplementaryQuery(SupplementaryQueryType.LogsVolume, target)) + .filter((query): query is ElasticsearchQuery => !!query); + + if (!targets.length) { + return undefined; + } + + return queryLogsVolume( + this, + { ...logsVolumeRequest, targets }, + { + range: request.range, + targets: request.targets, + extractLevel: (dataFrame) => getLogLevelFromKey(dataFrame.name || ''), } - bucketAggs.push({ - id: '3', - type: 'date_histogram', - settings: { - interval: 'auto', - min_doc_count: '0', - trimEdges: '0', - }, - field: timeField, - }); - - const logsVolumeQuery: ElasticsearchQuery = { - refId: `${REF_ID_STARTER_LOG_VOLUME}${target.refId}`, - query: target.query, - metrics: [{ type: 'count', id: '1' }], - timeField, - bucketAggs, - }; - return logsVolumeQuery; - }); - - return queryLogsVolume(this, logsVolumeRequest, { - range: request.range, - targets: request.targets, - extractLevel: (dataFrame) => getLogLevelFromKey(dataFrame.name || ''), - }); + ); } query(request: DataQueryRequest): Observable { diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index c902aaca82f..5e183049033 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -894,7 +894,7 @@ describe('LokiDatasource', () => { it('creates provider for logs query', () => { const options = getQueryOptions({ - targets: [{ expr: '{label=value}', refId: 'A' }], + targets: [{ expr: '{label=value}', refId: 'A', queryType: LokiQueryType.Range }], }); expect(ds.getDataProvider(SupplementaryQueryType.LogsVolume, options)).toBeDefined(); @@ -911,8 +911,8 @@ describe('LokiDatasource', () => { it('creates provider if at least one query is a logs query', () => { const options = getQueryOptions({ targets: [ - { expr: 'rate({label=value}[1m])', refId: 'A' }, - { expr: '{label=value}', refId: 'B' }, + { expr: 'rate({label=value}[1m])', queryType: LokiQueryType.Range, refId: 'A' }, + { expr: '{label=value}', queryType: LokiQueryType.Range, refId: 'B' }, ], }); @@ -962,6 +962,93 @@ describe('LokiDatasource', () => { }); }); + describe('getSupplementaryQuery', () => { + let ds: LokiDatasource; + beforeEach(() => { + ds = createLokiDatasource(templateSrvStub); + }); + + describe('logs volume', () => { + it('returns logs volume query for range log query', () => { + expect( + ds.getSupplementaryQuery(SupplementaryQueryType.LogsVolume, { + expr: '{label=value}', + queryType: LokiQueryType.Range, + refId: 'A', + }) + ).toEqual({ + expr: 'sum by (level) (count_over_time({label=value}[$__interval]))', + instant: false, + queryType: 'range', + refId: 'log-volume-A', + volumeQuery: true, + }); + }); + + it('does not return logs volume query for instant log query', () => { + expect( + ds.getSupplementaryQuery(SupplementaryQueryType.LogsVolume, { + expr: '{label=value}', + queryType: LokiQueryType.Instant, + refId: 'A', + }) + ).toEqual(undefined); + }); + + it('does not return logs volume query for metric query', () => { + expect( + ds.getSupplementaryQuery(SupplementaryQueryType.LogsVolume, { + expr: 'rate({label=value}[5m]', + queryType: LokiQueryType.Range, + refId: 'A', + }) + ).toEqual(undefined); + }); + }); + + describe('logs sample', () => { + it('returns logs sample query for range metric query', () => { + expect( + ds.getSupplementaryQuery(SupplementaryQueryType.LogsSample, { + expr: 'rate({label=value}[5m]', + queryType: LokiQueryType.Range, + refId: 'A', + }) + ).toEqual({ + expr: '{label=value}', + queryType: 'range', + refId: 'log-sample-A', + maxLines: 100, + }); + }); + + it('returns logs sample query for instant metric query', () => { + expect( + ds.getSupplementaryQuery(SupplementaryQueryType.LogsSample, { + expr: 'rate({label=value}[5m]', + queryType: LokiQueryType.Instant, + refId: 'A', + }) + ).toEqual({ + expr: '{label=value}', + queryType: 'instant', + refId: 'log-sample-A', + maxLines: 100, + }); + }); + + it('does not return logs sample query for log query query', () => { + expect( + ds.getSupplementaryQuery(SupplementaryQueryType.LogsSample, { + expr: '{label=value}', + queryType: LokiQueryType.Range, + refId: 'A', + }) + ).toEqual(undefined); + }); + }); + }); + describe('importing queries', () => { let ds: LokiDatasource; beforeEach(() => { diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index a88ef2a63d1..4d28ff4e299 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -161,62 +161,80 @@ export class LokiDatasource return [SupplementaryQueryType.LogsVolume, SupplementaryQueryType.LogsSample]; } - getLogsVolumeDataProvider(request: DataQueryRequest): Observable | undefined { - const isQuerySuitable = (query: LokiQuery) => { - const normalized = getNormalizedLokiQuery(query); - const { expr } = normalized; - // it has to be a logs-producing range-query - return expr && isLogsQuery(expr) && normalized.queryType === LokiQueryType.Range; - }; - - const isLogsVolumeAvailable = request.targets.some(isQuerySuitable); - - if (!isLogsVolumeAvailable) { + getSupplementaryQuery(type: SupplementaryQueryType, query: LokiQuery): LokiQuery | undefined { + if (!this.getSupportedSupplementaryQueryTypes().includes(type)) { return undefined; } - const logsVolumeRequest = cloneDeep(request); - logsVolumeRequest.targets = logsVolumeRequest.targets.filter(isQuerySuitable).map((target) => { - const query = removeCommentsFromQuery(target.expr); - return { - ...target, - refId: `${REF_ID_STARTER_LOG_VOLUME}${target.refId}`, - instant: false, - volumeQuery: true, - expr: `sum by (level) (count_over_time(${query}[$__interval]))`, - }; - }); + const normalizedQuery = getNormalizedLokiQuery(query); + const expr = removeCommentsFromQuery(normalizedQuery.expr); + let isQuerySuitable = false; - return queryLogsVolume(this, logsVolumeRequest, { - extractLevel, - range: request.range, - targets: request.targets, - }); + switch (type) { + case SupplementaryQueryType.LogsVolume: + // it has to be a logs-producing range-query + isQuerySuitable = !!(query.expr && isLogsQuery(query.expr) && query.queryType === LokiQueryType.Range); + if (!isQuerySuitable) { + return undefined; + } + + return { + ...normalizedQuery, + refId: `${REF_ID_STARTER_LOG_VOLUME}${normalizedQuery.refId}`, + instant: false, + volumeQuery: true, + expr: `sum by (level) (count_over_time(${expr}[$__interval]))`, + }; + + case SupplementaryQueryType.LogsSample: + // it has to be a metric query + isQuerySuitable = !!(query.expr && !isLogsQuery(query.expr)); + if (!isQuerySuitable) { + return undefined; + } + return { + ...normalizedQuery, + refId: `${REF_ID_STARTER_LOG_SAMPLE}${normalizedQuery.refId}`, + expr: getLogQueryFromMetricsQuery(expr), + maxLines: 100, + }; + + default: + return undefined; + } + } + + getLogsVolumeDataProvider(request: DataQueryRequest): Observable | undefined { + const logsVolumeRequest = cloneDeep(request); + const targets = logsVolumeRequest.targets + .map((query) => this.getSupplementaryQuery(SupplementaryQueryType.LogsVolume, query)) + .filter((query): query is LokiQuery => !!query); + + if (!targets.length) { + return undefined; + } + + return queryLogsVolume( + this, + { ...logsVolumeRequest, targets }, + { + extractLevel, + range: request.range, + targets: request.targets, + } + ); } getLogsSampleDataProvider(request: DataQueryRequest): Observable | undefined { - const isQuerySuitable = (query: LokiQuery) => { - return query.expr && !isLogsQuery(query.expr); - }; + const logsSampleRequest = cloneDeep(request); + const targets = logsSampleRequest.targets + .map((query) => this.getSupplementaryQuery(SupplementaryQueryType.LogsSample, query)) + .filter((query): query is LokiQuery => !!query); - const isLogsSampleAvailable = request.targets.some(isQuerySuitable); - - if (!isLogsSampleAvailable) { + if (!targets.length) { return undefined; } - - const logsSampleRequest = cloneDeep(request); - logsSampleRequest.targets = logsSampleRequest.targets.filter(isQuerySuitable).map((target) => { - const query = removeCommentsFromQuery(target.expr); - return { - ...target, - refId: `${REF_ID_STARTER_LOG_SAMPLE}${target.refId}`, - expr: getLogQueryFromMetricsQuery(query), - maxLines: 100, - }; - }); - - return queryLogsSample(this, logsSampleRequest); + return queryLogsSample(this, { ...logsSampleRequest, targets }); } query(request: DataQueryRequest): Observable { From 8bbef70363b6690f93b64ec775c8e827ce013bb1 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Thu, 26 Jan 2023 16:11:29 +0100 Subject: [PATCH 090/172] Alerting: Fix recording rules being shown in the rules table on edit group modal (#62171) * Fix recording rules being shown in the rules table on edit group modal * Show no alerts message when there is not some non-recording rules and add tests * Move specific mock constants to the test file --- .../rules/EditRuleGroupModal.test.tsx | 151 ++++++++++++++++++ .../components/rules/EditRuleGroupModal.tsx | 38 ++--- public/app/features/alerting/unified/mocks.ts | 13 ++ 3 files changed, 179 insertions(+), 23 deletions(-) create mode 100644 public/app/features/alerting/unified/components/rules/EditRuleGroupModal.test.tsx diff --git a/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.test.tsx b/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.test.tsx new file mode 100644 index 00000000000..f46fdb629a2 --- /dev/null +++ b/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.test.tsx @@ -0,0 +1,151 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { Provider } from 'react-redux'; + +import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting'; +import { RulerRulesConfigDTO } from 'app/types/unified-alerting-dto'; + +import { + mockCombinedRule, + mockDataSource, + mockPromAlertingRule, + mockRulerAlertingRule, + mockRulerRecordingRule, + mockRulerRuleGroup, + mockStore, + someRulerRules, +} from '../../mocks'; +import { GRAFANA_DATASOURCE_NAME } from '../../utils/datasource'; + +import { CombinedGroupAndNameSpace, EditCloudGroupModal, ModalProps } from './EditRuleGroupModal'; + +const dsSettings = mockDataSource({ + name: 'Prometheus-1', + uid: 'Prometheus-1', +}); + +export const someCloudRulerRules: RulerRulesConfigDTO = { + namespace1: [ + mockRulerRuleGroup({ + name: 'group1', + rules: [ + mockRulerRecordingRule({ + record: 'instance:node_num_cpu:sum', + expr: 'count without (cpu) (count without (mode) (node_cpu_seconds_total{job="integrations/node_exporter"}))', + labels: { type: 'cpu' }, + }), + mockRulerAlertingRule({ alert: 'nonRecordingRule' }), + ], + }), + ], +}; + +export const onlyRecordingRulerRules: RulerRulesConfigDTO = { + namespace1: [ + mockRulerRuleGroup({ + name: 'group1', + rules: [ + mockRulerRecordingRule({ + record: 'instance:node_num_cpu:sum', + expr: 'count without (cpu) (count without (mode) (node_cpu_seconds_total{job="integrations/node_exporter"}))', + labels: { type: 'cpu' }, + }), + ], + }), + ], +}; + +const grafanaNamespace: CombinedRuleNamespace = { + name: 'namespace1', + rulesSource: dsSettings, + groups: [ + { + name: 'group1', + rules: [ + mockCombinedRule({ + namespace: { + groups: [], + name: 'namespace1', + rulesSource: mockDataSource(), + }, + promRule: mockPromAlertingRule(), + rulerRule: mockRulerAlertingRule(), + }), + ], + }, + ], +}; + +const group1: CombinedRuleGroup = { + name: 'group1', + rules: [ + mockCombinedRule({ + namespace: { + groups: [], + name: 'namespace1', + rulesSource: mockDataSource({ name: 'Prometheus-1' }), + }, + promRule: mockPromAlertingRule({ name: 'nonRecordingRule' }), + rulerRule: mockRulerAlertingRule({ alert: 'recordingRule' }), + }), + ], +}; + +const nameSpaceAndGroup: CombinedGroupAndNameSpace = { + namespace: grafanaNamespace, + group: group1, +}; +const defaultProps: ModalProps = { + nameSpaceAndGroup: nameSpaceAndGroup, + sourceName: 'Prometheus-1', + groupInterval: '1m', + onClose: jest.fn(), +}; + +jest.mock('app/types', () => ({ + ...jest.requireActual('app/types'), + useDispatch: () => jest.fn(), +})); + +function getProvidersWrapper(cloudRules?: RulerRulesConfigDTO) { + return function Wrapper({ children }: React.PropsWithChildren<{}>) { + const store = mockStore((store) => { + store.unifiedAlerting.rulerRules[GRAFANA_DATASOURCE_NAME] = { + loading: false, + dispatched: true, + result: someRulerRules, + }; + store.unifiedAlerting.rulerRules['Prometheus-1'] = { + loading: false, + dispatched: true, + result: cloudRules ?? someCloudRulerRules, + }; + }); + + return {children}; + }; +} + +describe('EditGroupModal component on cloud alert rules', () => { + it('Should show alert table in case of having some non-recording rules in the group', () => { + render(, { + wrapper: getProvidersWrapper(), + }); + expect(screen.getByText(/nonRecordingRule/i)).toBeInTheDocument(); + }); + it('Should not show alert table in case of not having some non-recording rules in the group', () => { + render(, { + wrapper: getProvidersWrapper(onlyRecordingRulerRules), + }); + expect(screen.queryByText(/nonRecordingRule/i)).not.toBeInTheDocument(); + expect(screen.getByText(/this group does not contain alert rules\./i)); + }); +}); +describe('EditGroupModal component on grafana-managed alert rules', () => { + it('Should show alert table', () => { + render(, { + wrapper: getProvidersWrapper(), + }); + expect(screen.getByText(/alert1/i)).toBeInTheDocument(); + }); +}); diff --git a/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx b/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx index 3b02777b07a..d7fd81e8e40 100644 --- a/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx +++ b/public/app/features/alerting/unified/components/rules/EditRuleGroupModal.tsx @@ -4,19 +4,19 @@ import { FormProvider, RegisterOptions, useForm, useFormContext } from 'react-ho import { GrafanaTheme2 } from '@grafana/data'; import { Stack } from '@grafana/experimental'; -import { Modal, Button, Field, Input, useStyles2, Label, Badge } from '@grafana/ui'; +import { Badge, Button, Field, Input, Label, Modal, useStyles2 } from '@grafana/ui'; import { useAppNotification } from 'app/core/copy/appNotification'; import { useCleanup } from 'app/core/hooks/useCleanup'; import { useDispatch } from 'app/types'; import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting'; -import { RulerRulesConfigDTO, RulerRuleGroupDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto'; +import { RulerRuleDTO, RulerRuleGroupDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto'; import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; import { rulesInSameGroupHaveInvalidFor, updateLotexNamespaceAndGroupAction } from '../../state/actions'; import { checkEvaluationIntervalGlobalLimit } from '../../utils/config'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; import { initialAsyncRequestState } from '../../utils/redux'; -import { isAlertingRulerRule, isGrafanaRulerRule } from '../../utils/rules'; +import { isAlertingRulerRule, isGrafanaRulerRule, isRecordingRulerRule } from '../../utils/rules'; import { parsePrometheusDuration } from '../../utils/time'; import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable'; import { InfoIcon } from '../InfoIcon'; @@ -122,24 +122,14 @@ export const safeParseDurationstr = (duration: string): number => { type AlertsWithForTableColumnProps = DynamicTableColumnProps; type AlertsWithForTableProps = DynamicTableItemProps; -export const RulesForGroupTable = ({ - rulerRules, - groupName, - folderName, -}: { - rulerRules: RulerRulesConfigDTO | null | undefined; - groupName: string; - folderName: string; -}) => { +export const RulesForGroupTable = ({ rulesWithoutRecordingRules }: { rulesWithoutRecordingRules: RulerRuleDTO[] }) => { const styles = useStyles2(getStyles); - const group = getGroupFromRuler(rulerRules, groupName, folderName); - const rules: RulerRuleDTO[] = group?.rules ?? []; const { watch } = useFormContext(); const currentInterval = watch('groupInterval'); const unknownCurrentInterval = !Boolean(currentInterval); - const rows: AlertsWithForTableProps[] = rules + const rows: AlertsWithForTableProps[] = rulesWithoutRecordingRules .slice() .map((rule: RulerRuleDTO, index) => ({ id: index, @@ -198,7 +188,7 @@ export const RulesForGroupTable = ({ ); }; -interface CombinedGroupAndNameSpace { +export interface CombinedGroupAndNameSpace { namespace: CombinedRuleNamespace; group: CombinedRuleGroup; } @@ -206,7 +196,7 @@ interface GroupAndNameSpaceNames { namespace: string; group: string; } -interface ModalProps { +export interface ModalProps { nameSpaceAndGroup: CombinedGroupAndNameSpace | GroupAndNameSpaceNames; sourceName: string; groupInterval: string; @@ -328,6 +318,11 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement { const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules); const groupfoldersForSource = rulerRuleRequests[sourceName]; + const groupWithRules = getGroupFromRuler(groupfoldersForSource?.result, groupName, nameSpaceName); + const rulesWithoutRecordingRules: RulerRuleDTO[] = + groupWithRules?.rules.filter((rule: RulerRuleDTO) => !isRecordingRulerRule(rule)) ?? []; + const hasSomeNoRecordingRules = rulesWithoutRecordingRules.length > 0; + return (
- {rulerRuleRequests && ( + {rulerRuleRequests && !hasSomeNoRecordingRules &&
This group does not contain alert rules.
} + {rulerRuleRequests && hasSomeNoRecordingRules && ( <>
List of rules that belong to this group
#Evaluations column represents the number of evaluations needed before alert starts firing.
- + )} diff --git a/public/app/features/alerting/unified/mocks.ts b/public/app/features/alerting/unified/mocks.ts index 2148209f3f7..517f4142be5 100644 --- a/public/app/features/alerting/unified/mocks.ts +++ b/public/app/features/alerting/unified/mocks.ts @@ -41,6 +41,7 @@ import { PromRuleType, RulerAlertingRuleDTO, RulerGrafanaRuleDTO, + RulerRecordingRuleDTO, RulerRuleGroupDTO, RulerRulesConfigDTO, } from 'app/types/unified-alerting-dto'; @@ -134,6 +135,18 @@ export const mockRulerAlertingRule = (partial: Partial = { ...partial, }); +export const mockRulerRecordingRule = (partial: Partial = {}): RulerAlertingRuleDTO => ({ + alert: 'alert1', + expr: 'up = 1', + labels: { + severity: 'warning', + }, + annotations: { + summary: 'test alert', + }, + ...partial, +}); + export const mockRulerRuleGroup = (partial: Partial = {}): RulerRuleGroupDTO => ({ name: 'group1', rules: [mockRulerAlertingRule()], From 4bcd3b41ec1edcacd30ad97b8129f6cb373b3abf Mon Sep 17 00:00:00 2001 From: Jo Date: Thu, 26 Jan 2023 16:13:22 +0100 Subject: [PATCH 091/172] Server: Remove unused services (#62015) remove unused entries Co-authored-by: Gabriel MABILLE Co-authored-by: Gabriel MABILLE --- pkg/login/social/social.go | 5 ++++- pkg/server/server.go | 16 ++-------------- pkg/server/server_test.go | 3 +-- pkg/services/supportbundles/interface.go | 20 ++++++++++++++------ 4 files changed, 21 insertions(+), 23 deletions(-) diff --git a/pkg/login/social/social.go b/pkg/login/social/social.go index 5cc6d9f0969..af56b006c4b 100644 --- a/pkg/login/social/social.go +++ b/pkg/login/social/social.go @@ -60,7 +60,9 @@ type OAuthInfo struct { AutoLogin bool } -func ProvideService(cfg *setting.Cfg, features *featuremgmt.FeatureManager) *SocialService { +func ProvideService(cfg *setting.Cfg, + features *featuremgmt.FeatureManager, +) *SocialService { ss := SocialService{ cfg: cfg, oAuthProvider: make(map[string]*OAuthInfo), @@ -227,6 +229,7 @@ func ProvideService(cfg *setting.Cfg, features *featuremgmt.FeatureManager) *Soc } } } + return &ss } diff --git a/pkg/server/server.go b/pkg/server/server.go index 321cbeb8e10..c59a37e1c6f 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -13,17 +13,13 @@ import ( "github.com/grafana/grafana/pkg/infra/usagestats/statscollector" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/loginattempt" "github.com/grafana/grafana/pkg/api" _ "github.com/grafana/grafana/pkg/extensions" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" - "github.com/grafana/grafana/pkg/login" - "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/provisioning" - "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "golang.org/x/sync/errgroup" @@ -43,10 +39,9 @@ type Options struct { func New(opts Options, cfg *setting.Cfg, httpServer *api.HTTPServer, roleRegistry accesscontrol.RoleRegistry, provisioningService provisioning.ProvisioningService, backgroundServiceProvider registry.BackgroundServiceRegistry, usageStatsProvidersRegistry registry.UsageStatsProvidersRegistry, statsCollectorService *statscollector.Service, - userService user.Service, loginAttemptService loginattempt.Service, ) (*Server, error) { statsCollectorService.RegisterProviders(usageStatsProvidersRegistry.GetServices()) - s, err := newServer(opts, cfg, httpServer, roleRegistry, provisioningService, backgroundServiceProvider, userService, loginAttemptService) + s, err := newServer(opts, cfg, httpServer, roleRegistry, provisioningService, backgroundServiceProvider) if err != nil { return nil, err } @@ -59,7 +54,7 @@ func New(opts Options, cfg *setting.Cfg, httpServer *api.HTTPServer, roleRegistr } func newServer(opts Options, cfg *setting.Cfg, httpServer *api.HTTPServer, roleRegistry accesscontrol.RoleRegistry, - provisioningService provisioning.ProvisioningService, backgroundServiceProvider registry.BackgroundServiceRegistry, userService user.Service, loginAttemptService loginattempt.Service, + provisioningService provisioning.ProvisioningService, backgroundServiceProvider registry.BackgroundServiceRegistry, ) (*Server, error) { rootCtx, shutdownFn := context.WithCancel(context.Background()) childRoutines, childCtx := errgroup.WithContext(rootCtx) @@ -79,8 +74,6 @@ func newServer(opts Options, cfg *setting.Cfg, httpServer *api.HTTPServer, roleR commit: opts.Commit, buildBranch: opts.BuildBranch, backgroundServices: backgroundServiceProvider.GetServices(), - userService: userService, - loginAttemptService: loginAttemptService, } return s, nil @@ -107,8 +100,6 @@ type Server struct { HTTPServer *api.HTTPServer roleRegistry accesscontrol.RoleRegistry provisioningService provisioning.ProvisioningService - userService user.Service - loginAttemptService loginattempt.Service } // init initializes the server and its services. @@ -129,9 +120,6 @@ func (s *Server) init() error { return err } - login.ProvideService(s.HTTPServer.SQLStore, s.HTTPServer.Login, s.loginAttemptService, s.userService) - social.ProvideService(s.cfg, s.HTTPServer.Features) - if err := s.roleRegistry.RegisterFixedRoles(s.context); err != nil { return err } diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index 324f772d5ef..be4aca1585e 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -10,7 +10,6 @@ import ( "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/server/backgroundsvcs" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" - "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/require" ) @@ -48,7 +47,7 @@ func (s *testService) IsDisabled() bool { func testServer(t *testing.T, services ...registry.BackgroundService) *Server { t.Helper() - s, err := newServer(Options{}, setting.NewCfg(), nil, &acimpl.Service{}, nil, backgroundsvcs.NewBackgroundServiceRegistry(services...), usertest.NewUserServiceFake(), nil) + s, err := newServer(Options{}, setting.NewCfg(), nil, &acimpl.Service{}, nil, backgroundsvcs.NewBackgroundServiceRegistry(services...)) require.NoError(t, err) // Required to skip configuration initialization that causes // DI errors in this test. diff --git a/pkg/services/supportbundles/interface.go b/pkg/services/supportbundles/interface.go index 66ed4a1e867..e876412087b 100644 --- a/pkg/services/supportbundles/interface.go +++ b/pkg/services/supportbundles/interface.go @@ -32,12 +32,20 @@ type Bundle struct { type CollectorFunc func(context.Context) (*SupportItem, error) type Collector struct { - UID string `json:"uid"` - DisplayName string `json:"displayName"` - Description string `json:"description"` - IncludedByDefault bool `json:"includedByDefault"` - Default bool `json:"default"` - Fn CollectorFunc `json:"-"` + // UID is a unique identifier for the collector. + UID string `json:"uid"` + // DisplayName is the name of the collector. User facing. + DisplayName string `json:"displayName"` + // Description is a description of the collector. User facing. + Description string `json:"description"` + // IncludedByDefault determines if the collector is included by default. + // User cannot override this. + IncludedByDefault bool `json:"includedByDefault"` + // Default determines if the collector is included by default. + // User can override this. + Default bool `json:"default"` + // Fn is the function that collects the support item. + Fn CollectorFunc `json:"-"` } type Service interface { From 8246fc64fad7ce2989fd4fe5de5b7c8a1bcad182 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Thu, 26 Jan 2023 16:23:13 +0100 Subject: [PATCH 092/172] Explore: Trigger logs sample only when user click to see it (#62226) LogSamples: Trigger log samples on user action --- public/app/features/explore/utils/supplementaryQueries.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/public/app/features/explore/utils/supplementaryQueries.ts b/public/app/features/explore/utils/supplementaryQueries.ts index cef393d2401..bb00172523f 100644 --- a/public/app/features/explore/utils/supplementaryQueries.ts +++ b/public/app/features/explore/utils/supplementaryQueries.ts @@ -17,7 +17,7 @@ export const loadSupplementaryQueries = (): SupplementaryQueries => { // We default to true for all supp queries let supplementaryQueries: SupplementaryQueries = { [SupplementaryQueryType.LogsVolume]: { enabled: true }, - [SupplementaryQueryType.LogsSample]: { enabled: true }, + [SupplementaryQueryType.LogsSample]: { enabled: false }, }; for (const type of supplementaryQueryTypes) { @@ -36,6 +36,11 @@ export const loadSupplementaryQueries = (): SupplementaryQueries => { } } + // We want to skip LogsSample and default it to false for now to trigger it only on user action + if (type === SupplementaryQueryType.LogsSample) { + continue; + } + // Only if "false" value in local storage, we disable it const shouldBeEnabled = store.get(getSupplementaryQuerySettingKey(type)); if (shouldBeEnabled === 'false') { From 0cd14091c436cd2f39abedbada2755767b8fa487 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 26 Jan 2023 15:25:21 +0000 Subject: [PATCH 093/172] Changelog: Updated changelog for 9.3.6 (#62231) --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf51c8a8de5..97e79f113de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ + + +# 9.3.6 (2023-01-26) + +### Bug fixes + +- **QueryEditorRow:** Fixes issue loading query editor when data source variable selected. [#61927](https://github.com/grafana/grafana/pull/61927), [@torkelo](https://github.com/torkelo) + + # 9.3.4 (2023-01-25) From 42732539ed9bf83f4c610529a084c8f450ce5d48 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Thu, 26 Jan 2023 16:31:34 +0100 Subject: [PATCH 094/172] TestData: Introduce schema types (#62130) * schematize data query * add the stuff you dingus * feat(testdatasource): add scenario to generated types * use generated testdata query in frontend * update code owners * Add path exception for testdata datasource * use specific numeric data types * fix test * fix e2e smoketest * add test data query type * use test data query type * fix betterer * Fix typo * move to experimental Co-authored-by: Alex Khomenko Co-authored-by: Jack Westbrook Co-authored-by: Marcus Efraimsson Co-authored-by: sam boyer Co-authored-by: Alex Khomenko --- .betterer.results | 17 +- .github/CODEOWNERS | 4 +- .../testdatadataquery/schema-reference.md | 121 +++++++ .../grafana-e2e/src/flows/addDataSource.ts | 2 +- pkg/kindsys/report.json | 46 +-- pkg/plugins/codegen/jenny_plugingotypes.go | 5 + .../api/plugins/data/expectedListResp.json | 2 +- .../kinds/dataquery/types_dataquery_gen.go | 338 ++++++++++++++++++ public/app/features/scenes/scenes/demo.tsx | 3 +- .../scenes/scenes/gridMultiTimeRange.tsx | 3 +- .../scenes/scenes/gridWithMultipleData.tsx | 3 +- public/app/features/scenes/scenes/queries.ts | 4 +- .../datasource/testdata/QueryEditor.test.tsx | 14 +- .../datasource/testdata/QueryEditor.tsx | 76 ++-- .../testdata/__mocks__/scenarios.ts | 42 +-- .../testdata/components/CSVWaveEditor.tsx | 2 +- .../testdata/components/NodeGraphEditor.tsx | 4 +- .../components/PredictablePulseEditor.tsx | 2 +- .../testdata/components/RandomWalkEditor.tsx | 4 +- .../components/SimulationQueryEditor.tsx | 2 +- .../components/StreamingClientEditor.tsx | 2 +- .../testdata/components/USAQueryEditor.tsx | 2 +- .../plugins/datasource/testdata/constants.ts | 6 +- .../plugins/datasource/testdata/dataquery.cue | 117 ++++++ .../datasource/testdata/dataquery.gen.ts | 137 +++++++ .../plugins/datasource/testdata/datasource.ts | 42 +-- .../plugins/datasource/testdata/plugin.json | 2 +- .../plugins/datasource/testdata/runStreams.ts | 16 +- .../app/plugins/datasource/testdata/types.ts | 77 ---- .../plugins/datasource/testdata/variables.ts | 6 +- 30 files changed, 876 insertions(+), 225 deletions(-) create mode 100644 docs/sources/developers/kinds/composable/testdatadataquery/schema-reference.md create mode 100644 pkg/tsdb/testdatasource/kinds/dataquery/types_dataquery_gen.go create mode 100644 public/app/plugins/datasource/testdata/dataquery.cue create mode 100644 public/app/plugins/datasource/testdata/dataquery.gen.ts delete mode 100644 public/app/plugins/datasource/testdata/types.ts diff --git a/.betterer.results b/.betterer.results index e0b69715f9d..36836d9b169 100644 --- a/.betterer.results +++ b/.betterer.results @@ -6710,12 +6710,9 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Do not use any type assertions.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"], - [0, 0, 0, "Do not use any type assertions.", "7"], - [0, 0, 0, "Unexpected any. Specify a different type.", "8"] + [0, 0, 0, "Unexpected any. Specify a different type.", "3"], + [0, 0, 0, "Do not use any type assertions.", "4"], + [0, 0, 0, "Unexpected any. Specify a different type.", "5"] ], "public/app/plugins/datasource/testdata/components/PredictablePulseEditor.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] @@ -6750,10 +6747,7 @@ exports[`better eslint`] = { "public/app/plugins/datasource/testdata/datasource.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"] + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], "public/app/plugins/datasource/testdata/module.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] @@ -6773,9 +6767,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] ], - "public/app/plugins/datasource/testdata/types.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/plugins/datasource/zipkin/QueryField.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 252f7feac62..650212fa67d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -259,7 +259,6 @@ WORKFLOW.md @torkelo /pkg/services/querylibrary/ @grafana/multitenancy-squad /pkg/services/export/ @grafana/multitenancy-squad /pkg/infra/filestorage/ @grafana/multitenancy-squad -/pkg/tsdb/testdatasource/ @grafana/multitenancy-squad /pkg/util/converter/ @grafana/multitenancy-squad # Alerting @@ -281,6 +280,7 @@ WORKFLOW.md @torkelo /pkg/services/datasources/ @grafana/plugins-platform-backend /pkg/services/pluginsintegration/ @grafana/plugins-platform-backend /pkg/plugins/pfs/ @grafana/plugins-platform-backend @grafana/grafana-as-code +/pkg/tsdb/testdatasource/ @grafana/plugins-platform-backend # Dashboard previews / crawler (behind feature flag) /pkg/services/thumbs/ @grafana/multitenancy-squad @@ -486,7 +486,7 @@ lerna.json @grafana/frontend-ops /public/app/plugins/datasource/cloudwatch/ @grafana/aws-plugins /public/app/plugins/datasource/elasticsearch/ @grafana/observability-logs /public/app/plugins/datasource/grafana/ @grafana/user-essentials -/public/app/plugins/datasource/testdata/ @grafana/backend-platform +/public/app/plugins/datasource/testdata/ @grafana/plugins-platform-frontend /public/app/plugins/datasource/grafana-azure-monitor-datasource/ @grafana/partner-plugins /public/app/plugins/datasource/graphite/ @grafana/observability-metrics /public/app/plugins/datasource/influxdb/ @grafana/observability-metrics diff --git a/docs/sources/developers/kinds/composable/testdatadataquery/schema-reference.md b/docs/sources/developers/kinds/composable/testdatadataquery/schema-reference.md new file mode 100644 index 00000000000..561d2037195 --- /dev/null +++ b/docs/sources/developers/kinds/composable/testdatadataquery/schema-reference.md @@ -0,0 +1,121 @@ +--- +keywords: + - grafana + - schema +title: TestDataDataQuery kind +--- +> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. + +# TestDataDataQuery kind + +## Maturity: experimental +## Version: 0.0 + +## Properties + +| Property | Type | Required | Description | +|-------------------|-------------------------------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `alias` | string | No | | +| `channel` | string | No | | +| `csvContent` | string | No | | +| `csvFileName` | string | No | | +| `csvWave` | [CSVWave](#csvwave)[] | No | | +| `errorType` | string | No | Possible values are: `server_panic`, `frontend_exception`, `frontend_observable`. | +| `labels` | string | No | | +| `levelColumn` | boolean | No | | +| `lines` | integer | No | | +| `nodes` | [NodesQuery](#nodesquery) | No | | +| `points` | array[] | No | | +| `pulseWave` | [PulseWaveQuery](#pulsewavequery) | No | | +| `rawFrameContent` | string | No | | +| `scenarioId` | string | No | Possible values are: `random_walk`, `slow_query`, `random_walk_with_error`, `random_walk_table`, `exponential_heatmap_bucket_data`, `linear_heatmap_bucket_data`, `no_data_points`, `datapoints_outside_range`, `csv_metric_values`, `predictable_pulse`, `predictable_csv_wave`, `streaming_client`, `simulation`, `usa`, `live`, `grafana_api`, `arrow`, `annotations`, `table_static`, `server_error_500`, `logs`, `node_graph`, `flame_graph`, `raw_frame`, `csv_file`, `csv_content`, `trace`, `manual_entry`, `variables-query`. | +| `seriesCount` | integer | No | | +| `sim` | [SimulationQuery](#simulationquery) | No | | +| `spanCount` | integer | No | | +| `stream` | [StreamingQuery](#streamingquery) | No | | +| `stringInput` | string | No | | +| `usa` | [USAQuery](#usaquery) | No | | + +## CSVWave + +### Properties + +| Property | Type | Required | Description | +|-------------|---------|----------|-------------| +| `labels` | string | No | | +| `name` | string | No | | +| `timeStep` | integer | No | | +| `valuesCSV` | string | No | | + +## NodesQuery + +### Properties + +| Property | Type | Required | Description | +|----------|---------|----------|------------------------------------------------------------| +| `count` | integer | No | | +| `type` | string | No | Possible values are: `random`, `response`, `random edges`. | + +## PulseWaveQuery + +### Properties + +| Property | Type | Required | Description | +|------------|---------|----------|-------------| +| `offCount` | integer | No | | +| `offValue` | number | No | | +| `onCount` | integer | No | | +| `onValue` | number | No | | +| `timeStep` | integer | No | | + +## SimulationQuery + +### Properties + +| Property | Type | Required | Description | +|----------|-------------------|----------|-------------| +| `key` | [object](#key) | **Yes** | | +| `config` | [object](#config) | No | | +| `last` | boolean | No | | +| `stream` | boolean | No | | + +### config + +| Property | Type | Required | Description | +|----------|------|----------|-------------| + +### key + +#### Properties + +| Property | Type | Required | Description | +|----------|--------|----------|-------------| +| `tick` | number | **Yes** | | +| `type` | string | **Yes** | | +| `uid` | string | No | | + +## StreamingQuery + +### Properties + +| Property | Type | Required | Description | +|----------|---------|----------|-------------------------------------------------| +| `noise` | integer | **Yes** | | +| `speed` | integer | **Yes** | | +| `spread` | integer | **Yes** | | +| `type` | string | **Yes** | Possible values are: `signal`, `logs`, `fetch`. | +| `bands` | integer | No | | +| `url` | string | No | | + +## USAQuery + +### Properties + +| Property | Type | Required | Description | +|----------|----------|----------|-------------| +| `fields` | string[] | No | | +| `mode` | string | No | | +| `period` | string | No | | +| `states` | string[] | No | | + + diff --git a/packages/grafana-e2e/src/flows/addDataSource.ts b/packages/grafana-e2e/src/flows/addDataSource.ts index c38947bba44..4737d1e726a 100644 --- a/packages/grafana-e2e/src/flows/addDataSource.ts +++ b/packages/grafana-e2e/src/flows/addDataSource.ts @@ -26,7 +26,7 @@ export const addDataSource = (config?: Partial) => { form: () => {}, name: `e2e-${uuidv4()}`, skipTlsVerify: false, - type: 'TestData DB', + type: 'TestData', ...config, }; diff --git a/pkg/kindsys/report.json b/pkg/kindsys/report.json index bd30c27facf..6ca2c368e7f 100644 --- a/pkg/kindsys/report.json +++ b/pkg/kindsys/report.json @@ -1539,9 +1539,11 @@ "pluralName": "TempoDataSourceCfgs", "schemaInterface": "DataSourceCfg" }, - "testdatadbdataquery": { + "testdatadataquery": { "category": "composable", - "codeowners": [], + "codeowners": [ + "grafana/plugins-platform-frontend" + ], "currentVersion": [ 0, 0 @@ -1549,19 +1551,19 @@ "grafanaMaturityCount": 0, "lineageIsGroup": false, "links": { - "docs": "n/a", - "go": "n/a", - "schema": "n/a", - "ts": "n/a" + "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/testdatadataquery/schema-reference", + "go": "https://github.com/grafana/grafana/tree/main/pkg/tsdb/testdata/kinds/dataquery/types_dataquery_gen.go", + "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/datasource/testdata/dataquery.cue", + "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/datasource/testdata/dataquery.gen.ts" }, - "machineName": "testdatadbdataquery", - "maturity": "planned", - "name": "TestDataDBDataQuery", - "pluralMachineName": "testdatadbdataquerys", - "pluralName": "TestDataDBDataQuerys", + "machineName": "testdatadataquery", + "maturity": "experimental", + "name": "TestDataDataQuery", + "pluralMachineName": "testdatadataquerys", + "pluralName": "TestDataDataQuerys", "schemaInterface": "DataQuery" }, - "testdatadbdatasourcecfg": { + "testdatadatasourcecfg": { "category": "composable", "codeowners": [], "currentVersion": [ @@ -1576,11 +1578,11 @@ "schema": "n/a", "ts": "n/a" }, - "machineName": "testdatadbdatasourcecfg", + "machineName": "testdatadatasourcecfg", "maturity": "planned", - "name": "TestDataDBDataSourceCfg", - "pluralMachineName": "testdatadbdatasourcecfgs", - "pluralName": "TestDataDBDataSourceCfgs", + "name": "TestDataDataSourceCfg", + "pluralMachineName": "testdatadatasourcecfgs", + "pluralName": "TestDataDataSourceCfgs", "schemaInterface": "DataSourceCfg" }, "textpanelcfg": { @@ -1824,8 +1826,8 @@ "tableoldpanelcfg", "tempodataquery", "tempodatasourcecfg", - "testdatadbdataquery", - "testdatadbdatasourcecfg", + "testdatadataquery", + "testdatadatasourcecfg", "textpanelcfg", "tracespanelcfg", "welcomepanelcfg", @@ -1871,10 +1873,11 @@ "statetimelinepanelcfg", "statpanelcfg", "statushistorypanelcfg", + "testdatadataquery", "textpanelcfg", "xychartpanelcfg" ], - "count": 14 + "count": 15 }, "mature": { "name": "mature", @@ -1946,8 +1949,7 @@ "tableoldpanelcfg", "tempodataquery", "tempodatasourcecfg", - "testdatadbdataquery", - "testdatadbdatasourcecfg", + "testdatadatasourcecfg", "thumb", "tracespanelcfg", "user", @@ -1955,7 +1957,7 @@ "zipkindataquery", "zipkindatasourcecfg" ], - "count": 58 + "count": 57 }, "stable": { "name": "stable", diff --git a/pkg/plugins/codegen/jenny_plugingotypes.go b/pkg/plugins/codegen/jenny_plugingotypes.go index 59103eed880..cc34c610352 100644 --- a/pkg/plugins/codegen/jenny_plugingotypes.go +++ b/pkg/plugins/codegen/jenny_plugingotypes.go @@ -51,6 +51,11 @@ func (j *pgoJenny) Generate(decl *pfs.PluginDecl) (*codejen.File, error) { } pluginfolder := filepath.Base(decl.PluginPath) + // hardcoded exception for testdata datasource, ONLY because "testdata" is basically a + // language-reserved keyword for Go + if pluginfolder == "testdata" { + pluginfolder = "testdatasource" + } filename := fmt.Sprintf("types_%s_gen.go", slotname) return codejen.NewFile(filepath.Join(j.root, pluginfolder, "kinds", slotname, filename), byt, j), nil } diff --git a/pkg/tests/api/plugins/data/expectedListResp.json b/pkg/tests/api/plugins/data/expectedListResp.json index e8337cebf39..b3c930afb2d 100644 --- a/pkg/tests/api/plugins/data/expectedListResp.json +++ b/pkg/tests/api/plugins/data/expectedListResp.json @@ -1437,7 +1437,7 @@ "signatureOrg": "" }, { - "name": "TestData DB", + "name": "TestData", "type": "datasource", "id": "testdata", "enabled": true, diff --git a/pkg/tsdb/testdatasource/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/testdatasource/kinds/dataquery/types_dataquery_gen.go new file mode 100644 index 00000000000..92f9ba53b1e --- /dev/null +++ b/pkg/tsdb/testdatasource/kinds/dataquery/types_dataquery_gen.go @@ -0,0 +1,338 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. +// +// Generated by: +// public/app/plugins/gen.go +// Using jennies: +// PluginGoTypesJenny +// +// Run 'make gen-cue' from repository root to regenerate. + +package dataquery + +// Defines values for NodesQueryType. +const ( + NodesQueryTypeRandom NodesQueryType = "random" + + NodesQueryTypeRandomEdges NodesQueryType = "random edges" + + NodesQueryTypeResponse NodesQueryType = "response" +) + +// Defines values for StreamingQueryType. +const ( + StreamingQueryTypeFetch StreamingQueryType = "fetch" + + StreamingQueryTypeLogs StreamingQueryType = "logs" + + StreamingQueryTypeSignal StreamingQueryType = "signal" +) + +// Defines values for ErrorType. +const ( + ErrorTypeFrontendException ErrorType = "frontend_exception" + + ErrorTypeFrontendObservable ErrorType = "frontend_observable" + + ErrorTypeServerPanic ErrorType = "server_panic" +) + +// Defines values for NodesType. +const ( + NodesTypeRandom NodesType = "random" + + NodesTypeRandomEdges NodesType = "random edges" + + NodesTypeResponse NodesType = "response" +) + +// Defines values for ScenarioId. +const ( + ScenarioIdAnnotations ScenarioId = "annotations" + + ScenarioIdArrow ScenarioId = "arrow" + + ScenarioIdCsvContent ScenarioId = "csv_content" + + ScenarioIdCsvFile ScenarioId = "csv_file" + + ScenarioIdCsvMetricValues ScenarioId = "csv_metric_values" + + ScenarioIdDatapointsOutsideRange ScenarioId = "datapoints_outside_range" + + ScenarioIdExponentialHeatmapBucketData ScenarioId = "exponential_heatmap_bucket_data" + + ScenarioIdFlameGraph ScenarioId = "flame_graph" + + ScenarioIdGrafanaApi ScenarioId = "grafana_api" + + ScenarioIdLinearHeatmapBucketData ScenarioId = "linear_heatmap_bucket_data" + + ScenarioIdLive ScenarioId = "live" + + ScenarioIdLogs ScenarioId = "logs" + + ScenarioIdManualEntry ScenarioId = "manual_entry" + + ScenarioIdNoDataPoints ScenarioId = "no_data_points" + + ScenarioIdNodeGraph ScenarioId = "node_graph" + + ScenarioIdPredictableCsvWave ScenarioId = "predictable_csv_wave" + + ScenarioIdPredictablePulse ScenarioId = "predictable_pulse" + + ScenarioIdRandomWalk ScenarioId = "random_walk" + + ScenarioIdRandomWalkTable ScenarioId = "random_walk_table" + + ScenarioIdRandomWalkWithError ScenarioId = "random_walk_with_error" + + ScenarioIdRawFrame ScenarioId = "raw_frame" + + ScenarioIdServerError500 ScenarioId = "server_error_500" + + ScenarioIdSimulation ScenarioId = "simulation" + + ScenarioIdSlowQuery ScenarioId = "slow_query" + + ScenarioIdStreamingClient ScenarioId = "streaming_client" + + ScenarioIdTableStatic ScenarioId = "table_static" + + ScenarioIdTrace ScenarioId = "trace" + + ScenarioIdUsa ScenarioId = "usa" + + ScenarioIdVariablesQuery ScenarioId = "variables-query" +) + +// Defines values for StreamType. +const ( + StreamTypeFetch StreamType = "fetch" + + StreamTypeLogs StreamType = "logs" + + StreamTypeSignal StreamType = "signal" +) + +// Defines values for TestDataQueryType. +const ( + TestDataQueryTypeAnnotations TestDataQueryType = "annotations" + + TestDataQueryTypeArrow TestDataQueryType = "arrow" + + TestDataQueryTypeCsvContent TestDataQueryType = "csv_content" + + TestDataQueryTypeCsvFile TestDataQueryType = "csv_file" + + TestDataQueryTypeCsvMetricValues TestDataQueryType = "csv_metric_values" + + TestDataQueryTypeDatapointsOutsideRange TestDataQueryType = "datapoints_outside_range" + + TestDataQueryTypeExponentialHeatmapBucketData TestDataQueryType = "exponential_heatmap_bucket_data" + + TestDataQueryTypeFlameGraph TestDataQueryType = "flame_graph" + + TestDataQueryTypeGrafanaApi TestDataQueryType = "grafana_api" + + TestDataQueryTypeLinearHeatmapBucketData TestDataQueryType = "linear_heatmap_bucket_data" + + TestDataQueryTypeLive TestDataQueryType = "live" + + TestDataQueryTypeLogs TestDataQueryType = "logs" + + TestDataQueryTypeManualEntry TestDataQueryType = "manual_entry" + + TestDataQueryTypeNoDataPoints TestDataQueryType = "no_data_points" + + TestDataQueryTypeNodeGraph TestDataQueryType = "node_graph" + + TestDataQueryTypePredictableCsvWave TestDataQueryType = "predictable_csv_wave" + + TestDataQueryTypePredictablePulse TestDataQueryType = "predictable_pulse" + + TestDataQueryTypeRandomWalk TestDataQueryType = "random_walk" + + TestDataQueryTypeRandomWalkTable TestDataQueryType = "random_walk_table" + + TestDataQueryTypeRandomWalkWithError TestDataQueryType = "random_walk_with_error" + + TestDataQueryTypeRawFrame TestDataQueryType = "raw_frame" + + TestDataQueryTypeServerError500 TestDataQueryType = "server_error_500" + + TestDataQueryTypeSimulation TestDataQueryType = "simulation" + + TestDataQueryTypeSlowQuery TestDataQueryType = "slow_query" + + TestDataQueryTypeStreamingClient TestDataQueryType = "streaming_client" + + TestDataQueryTypeTableStatic TestDataQueryType = "table_static" + + TestDataQueryTypeTrace TestDataQueryType = "trace" + + TestDataQueryTypeUsa TestDataQueryType = "usa" + + TestDataQueryTypeVariablesQuery TestDataQueryType = "variables-query" +) + +// CSVWave defines model for CSVWave. +type CSVWave struct { + Labels *string `json:"labels,omitempty"` + Name *string `json:"name,omitempty"` + TimeStep *int64 `json:"timeStep,omitempty"` + ValuesCSV *string `json:"valuesCSV,omitempty"` +} + +// NodesQuery defines model for NodesQuery. +type NodesQuery struct { + Count *int64 `json:"count,omitempty"` + Type *NodesQueryType `json:"type,omitempty"` +} + +// NodesQueryType defines model for NodesQuery.Type. +type NodesQueryType string + +// PulseWaveQuery defines model for PulseWaveQuery. +type PulseWaveQuery struct { + OffCount *int64 `json:"offCount,omitempty"` + OffValue *float64 `json:"offValue,omitempty"` + OnCount *int64 `json:"onCount,omitempty"` + OnValue *float64 `json:"onValue,omitempty"` + TimeStep *int64 `json:"timeStep,omitempty"` +} + +// TODO: Should this live here given it's not used in the dataquery? +type Scenario struct { + Description *string `json:"description,omitempty"` + HideAliasField *bool `json:"hideAliasField,omitempty"` + Id string `json:"id"` + Name string `json:"name"` + StringInput string `json:"stringInput"` +} + +// SimulationQuery defines model for SimulationQuery. +type SimulationQuery struct { + Config map[string]interface{} `json:"config,omitempty"` + Key struct { + Tick float64 `json:"tick"` + Type string `json:"type"` + Uid *string `json:"uid,omitempty"` + } `json:"key"` + Last *bool `json:"last,omitempty"` + Stream *bool `json:"stream,omitempty"` +} + +// StreamingQuery defines model for StreamingQuery. +type StreamingQuery struct { + Bands *int32 `json:"bands,omitempty"` + Noise int32 `json:"noise"` + Speed int32 `json:"speed"` + Spread int32 `json:"spread"` + Type StreamingQueryType `json:"type"` + Url *string `json:"url,omitempty"` +} + +// StreamingQueryType defines model for StreamingQuery.Type. +type StreamingQueryType string + +// TestDataDataQuery defines model for TestDataDataQuery. +type TestDataDataQuery struct { + Alias *string `json:"alias,omitempty"` + Channel *string `json:"channel,omitempty"` + CsvContent *string `json:"csvContent,omitempty"` + CsvFileName *string `json:"csvFileName,omitempty"` + CsvWave *[]struct { + Labels *string `json:"labels,omitempty"` + Name *string `json:"name,omitempty"` + TimeStep *int64 `json:"timeStep,omitempty"` + ValuesCSV *string `json:"valuesCSV,omitempty"` + } `json:"csvWave,omitempty"` + + // For mixed data sources the selected datasource is on the query level. + // For non mixed scenarios this is undefined. + // TODO find a better way to do this ^ that's friendly to schema + // TODO this shouldn't be unknown but DataSourceRef | null + Datasource *interface{} `json:"datasource,omitempty"` + ErrorType *ErrorType `json:"errorType,omitempty"` + + // true if query is disabled (ie should not be returned to the dashboard) + Hide *bool `json:"hide,omitempty"` + + // Unique, guid like, string used in explore mode + Key *string `json:"key,omitempty"` + Labels *string `json:"labels,omitempty"` + LevelColumn *bool `json:"levelColumn,omitempty"` + Lines *int64 `json:"lines,omitempty"` + Nodes *struct { + Count *int64 `json:"count,omitempty"` + Type *NodesType `json:"type,omitempty"` + } `json:"nodes,omitempty"` + Points *[][]interface{} `json:"points,omitempty"` + PulseWave *struct { + OffCount *int64 `json:"offCount,omitempty"` + OffValue *float64 `json:"offValue,omitempty"` + OnCount *int64 `json:"onCount,omitempty"` + OnValue *float64 `json:"onValue,omitempty"` + TimeStep *int64 `json:"timeStep,omitempty"` + } `json:"pulseWave,omitempty"` + + // Specify the query flavor + // TODO make this required and give it a default + QueryType *string `json:"queryType,omitempty"` + RawFrameContent *string `json:"rawFrameContent,omitempty"` + + // A - Z + RefId string `json:"refId"` + ScenarioId *ScenarioId `json:"scenarioId,omitempty"` + SeriesCount *int32 `json:"seriesCount,omitempty"` + Sim *struct { + Config map[string]interface{} `json:"config,omitempty"` + Key struct { + Tick float64 `json:"tick"` + Type string `json:"type"` + Uid *string `json:"uid,omitempty"` + } `json:"key"` + Last *bool `json:"last,omitempty"` + Stream *bool `json:"stream,omitempty"` + } `json:"sim,omitempty"` + SpanCount *int32 `json:"spanCount,omitempty"` + Stream *struct { + Bands *int32 `json:"bands,omitempty"` + Noise int32 `json:"noise"` + Speed int32 `json:"speed"` + Spread int32 `json:"spread"` + Type StreamType `json:"type"` + Url *string `json:"url,omitempty"` + } `json:"stream,omitempty"` + StringInput *string `json:"stringInput,omitempty"` + Usa *struct { + Fields *[]string `json:"fields,omitempty"` + Mode *string `json:"mode,omitempty"` + Period *string `json:"period,omitempty"` + States *[]string `json:"states,omitempty"` + } `json:"usa,omitempty"` +} + +// ErrorType defines model for TestDataDataQuery.ErrorType. +type ErrorType string + +// NodesType defines model for TestDataDataQuery.Nodes.Type. +type NodesType string + +// ScenarioId defines model for TestDataDataQuery.ScenarioId. +type ScenarioId string + +// StreamType defines model for TestDataDataQuery.Stream.Type. +type StreamType string + +// TestDataQueryType defines model for TestDataQueryType. +type TestDataQueryType string + +// USAQuery defines model for USAQuery. +type USAQuery struct { + Fields *[]string `json:"fields,omitempty"` + Mode *string `json:"mode,omitempty"` + Period *string `json:"period,omitempty"` + States *[]string `json:"states,omitempty"` +} diff --git a/public/app/features/scenes/scenes/demo.tsx b/public/app/features/scenes/scenes/demo.tsx index 489e3088a23..d7c50662682 100644 --- a/public/app/features/scenes/scenes/demo.tsx +++ b/public/app/features/scenes/scenes/demo.tsx @@ -8,6 +8,7 @@ import { SceneToolbarInput, SceneDataNode, } from '@grafana/scenes'; +import { TestDataQueryType } from 'app/plugins/datasource/testdata/dataquery.gen'; import { panelBuilders } from '../builders/panelBuilders'; import { DashboardScene } from '../dashboard/DashboardScene'; @@ -69,7 +70,7 @@ export function getScenePanelRepeaterTest(): DashboardScene { const queryRunner = getQueryRunnerWithRandomWalkQuery({ seriesCount: 2, alias: '__server_names', - scenarioId: 'random_walk', + scenarioId: TestDataQueryType.RandomWalk, }); return new DashboardScene({ diff --git a/public/app/features/scenes/scenes/gridMultiTimeRange.tsx b/public/app/features/scenes/scenes/gridMultiTimeRange.tsx index 052f5ee9903..75080b77a45 100644 --- a/public/app/features/scenes/scenes/gridMultiTimeRange.tsx +++ b/public/app/features/scenes/scenes/gridMultiTimeRange.tsx @@ -1,4 +1,5 @@ import { VizPanel, SceneGridRow, SceneTimePicker, SceneGridLayout, SceneTimeRange } from '@grafana/scenes'; +import { TestDataQueryType } from 'app/plugins/datasource/testdata/dataquery.gen'; import { DashboardScene } from '../dashboard/DashboardScene'; import { SceneEditManager } from '../editor/SceneEditManager'; @@ -18,7 +19,7 @@ export function getGridWithMultipleTimeRanges(): DashboardScene { children: [ new SceneGridRow({ $timeRange: row1TimeRange, - $data: getQueryRunnerWithRandomWalkQuery({ scenarioId: 'random_walk_table' }), + $data: getQueryRunnerWithRandomWalkQuery({ scenarioId: TestDataQueryType.RandomWalkTable }), title: 'Row A - has its own query, last year time range', key: 'Row A', isCollapsed: true, diff --git a/public/app/features/scenes/scenes/gridWithMultipleData.tsx b/public/app/features/scenes/scenes/gridWithMultipleData.tsx index 051c539c61a..dfd8d69494a 100644 --- a/public/app/features/scenes/scenes/gridWithMultipleData.tsx +++ b/public/app/features/scenes/scenes/gridWithMultipleData.tsx @@ -1,4 +1,5 @@ import { VizPanel, SceneGridRow, SceneTimePicker, SceneGridLayout, SceneTimeRange } from '@grafana/scenes'; +import { TestDataQueryType } from 'app/plugins/datasource/testdata/dataquery.gen'; import { DashboardScene } from '../dashboard/DashboardScene'; import { SceneEditManager } from '../editor/SceneEditManager'; @@ -12,7 +13,7 @@ export function getGridWithMultipleData(): DashboardScene { children: [ new SceneGridRow({ $timeRange: new SceneTimeRange(), - $data: getQueryRunnerWithRandomWalkQuery({ scenarioId: 'random_walk_table' }), + $data: getQueryRunnerWithRandomWalkQuery({ scenarioId: TestDataQueryType.RandomWalkTable }), title: 'Row A - has its own query', key: 'Row A', isCollapsed: true, diff --git a/public/app/features/scenes/scenes/queries.ts b/public/app/features/scenes/scenes/queries.ts index cce0057e0a7..684013e24d2 100644 --- a/public/app/features/scenes/scenes/queries.ts +++ b/public/app/features/scenes/scenes/queries.ts @@ -1,8 +1,8 @@ import { QueryRunnerState, SceneQueryRunner } from '@grafana/scenes'; -import { TestDataQuery } from 'app/plugins/datasource/testdata/types'; +import { TestData } from 'app/plugins/datasource/testdata/dataquery.gen'; export function getQueryRunnerWithRandomWalkQuery( - overrides?: Partial, + overrides?: Partial, queryRunnerOverrides?: Partial ) { return new SceneQueryRunner({ diff --git a/public/app/plugins/datasource/testdata/QueryEditor.test.tsx b/public/app/plugins/datasource/testdata/QueryEditor.test.tsx index 4713d9752b2..c4eeee9a3ad 100644 --- a/public/app/plugins/datasource/testdata/QueryEditor.test.tsx +++ b/public/app/plugins/datasource/testdata/QueryEditor.test.tsx @@ -5,6 +5,7 @@ import React from 'react'; import { QueryEditor, Props } from './QueryEditor'; import { scenarios } from './__mocks__/scenarios'; import { defaultQuery } from './constants'; +import { TestDataQueryType } from './dataquery.gen'; import { defaultStreamQuery } from './runStreams'; beforeEach(() => { @@ -45,11 +46,13 @@ describe('Test Datasource Query Editor', () => { expect(scs).toHaveLength(scenarios.length); await userEvent.click(screen.getByText('CSV Metric Values')); - expect(mockOnChange).toHaveBeenCalledWith(expect.objectContaining({ scenarioId: 'csv_metric_values' })); + expect(mockOnChange).toHaveBeenCalledWith( + expect.objectContaining({ scenarioId: TestDataQueryType.CSVMetricValues }) + ); await rerender( ); expect(await screen.findByRole('textbox', { name: /string input/i })).toBeInTheDocument(); @@ -61,7 +64,10 @@ describe('Test Datasource Query Editor', () => { expect.objectContaining({ scenarioId: 'grafana_api', stringInput: 'datasources' }) ); rerender( - + ); expect(await screen.findByText('Grafana API')).toBeInTheDocument(); expect(screen.getByText('Data Sources')).toBeInTheDocument(); @@ -72,7 +78,7 @@ describe('Test Datasource Query Editor', () => { expect.objectContaining({ scenarioId: 'streaming_client', stream: defaultStreamQuery }) ); - const streamQuery = { ...defaultQuery, stream: defaultStreamQuery, scenarioId: 'streaming_client' }; + const streamQuery = { ...defaultQuery, stream: defaultStreamQuery, scenarioId: TestDataQueryType.StreamingClient }; rerender(); diff --git a/public/app/plugins/datasource/testdata/QueryEditor.tsx b/public/app/plugins/datasource/testdata/QueryEditor.tsx index b3c7d996b97..699fcd2da1d 100644 --- a/public/app/plugins/datasource/testdata/QueryEditor.tsx +++ b/public/app/plugins/datasource/testdata/QueryEditor.tsx @@ -17,9 +17,9 @@ import { RawFrameEditor } from './components/RawFrameEditor'; import { SimulationQueryEditor } from './components/SimulationQueryEditor'; import { USAQueryEditor, usaQueryModes } from './components/USAQueryEditor'; import { defaultCSVWaveQuery, defaultPulseQuery, defaultQuery } from './constants'; +import { CSVWave, NodesQuery, TestData, TestDataQueryType, USAQuery } from './dataquery.gen'; import { TestDataDataSource } from './datasource'; import { defaultStreamQuery } from './runStreams'; -import { CSVWave, NodesQuery, TestDataQuery, USAQuery } from './types'; const showLabelsFor = ['random_walk', 'predictable_pulse']; const endpoints = [ @@ -32,26 +32,26 @@ const selectors = editorSelectors.components.DataSource.TestData.QueryTab; export interface EditorProps { onChange: (value: any) => void; - query: TestDataQuery; + query: TestData; ds: TestDataDataSource; } -export type Props = QueryEditorProps; +export type Props = QueryEditorProps; export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props) => { query = { ...defaultQuery, ...query }; const { loading, value: scenarioList } = useAsync(async () => { // migrate manual_entry (unusable since 7, removed in 8) - if (query.scenarioId === 'manual_entry' && (query as any).points) { + if (query.scenarioId === TestDataQueryType.ManualEntry && query.points) { let csvContent = 'Time,Value\n'; - for (const point of (query as any).points) { + for (const point of query.points) { csvContent += `${point[1]},${point[0]}\n`; } onChange({ refId: query.refId, datasource: query.datasource, - scenarioId: 'csv_content', + scenarioId: TestDataQueryType.CSVContent, csvContent, }); } @@ -64,7 +64,7 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props) })); }, []); - const onUpdate = (query: TestDataQuery) => { + const onUpdate = (query: TestData) => { onChange(query); onRunQuery(); }; @@ -84,8 +84,8 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props) } // Clear model from existing props that belong to other scenarios - const update: TestDataQuery = { - scenarioId: item.value!, + const update: TestData = { + scenarioId: item.value! as TestDataQueryType, refId: query.refId, alias: query.alias, datasource: query.datasource, @@ -96,25 +96,25 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props) } switch (scenario.id) { - case 'grafana_api': + case TestDataQueryType.GrafanaAPI: update.stringInput = 'datasources'; break; - case 'streaming_client': + case TestDataQueryType.StreamingClient: update.stream = defaultStreamQuery; break; - case 'live': + case TestDataQueryType.Live: update.channel = 'random-2s-stream'; // default stream break; - case 'simulation': + case TestDataQueryType.Simulation: update.sim = { key: { type: 'flight', tick: 10 } }; // default stream break; - case 'predictable_pulse': + case TestDataQueryType.PredictablePulse: update.pulseWave = defaultPulseQuery; break; - case 'predictable_csv_wave': + case TestDataQueryType.PredictableCSVWave: update.csvWave = defaultCSVWaveQuery; break; - case 'usa': + case TestDataQueryType.USA: update.usa = { mode: usaQueryModes[0].value, }; @@ -243,16 +243,24 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props) )} - {scenarioId === 'random_walk' && } - {scenarioId === 'streaming_client' && ( + {scenarioId === TestDataQueryType.RandomWalk && ( + + )} + {scenarioId === TestDataQueryType.StreamingClient && ( )} - {scenarioId === 'live' && } - {scenarioId === 'simulation' && } - {scenarioId === 'raw_frame' && } - {scenarioId === 'csv_file' && } - {scenarioId === 'csv_content' && } - {scenarioId === 'logs' && ( + {scenarioId === TestDataQueryType.Live && } + {scenarioId === TestDataQueryType.Simulation && ( + + )} + {scenarioId === TestDataQueryType.RawFrame && ( + + )} + {scenarioId === TestDataQueryType.CSVFile && } + {scenarioId === TestDataQueryType.CSVContent && ( + + )} + {scenarioId === TestDataQueryType.Logs && ( )} - {scenarioId === 'usa' && } - {scenarioId === 'grafana_api' && ( + {scenarioId === TestDataQueryType.USA && } + {scenarioId === TestDataQueryType.GrafanaAPI && (