From 8f37295b02f4ceab9ba4d293e3599e1788c8246b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 14 Jan 2023 09:30:43 +0100 Subject: [PATCH 01/65] Scenes: Panel repeater change (#61302) * Update scene panel repeater by change in scenes package * Use latest scenes package * Fix package.json * lock update * Fix lock Co-authored-by: Dominik Prokop --- package.json | 9 +--- public/app/features/scenes/scenes/demo.tsx | 61 +++++++++++++--------- yarn.lock | 33 ++++++++---- 3 files changed, 61 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index 1f25f7c4400..d1c7d835bde 100644 --- a/package.json +++ b/package.json @@ -253,7 +253,7 @@ "@grafana/lezer-logql": "0.1.1", "@grafana/monaco-logql": "^0.0.6", "@grafana/runtime": "workspace:*", - "@grafana/scenes": "0.0.4", + "@grafana/scenes": "latest", "@grafana/schema": "workspace:*", "@grafana/ui": "workspace:*", "@jaegertracing/jaeger-ui-components": "workspace:*", @@ -429,10 +429,5 @@ "engines": { "node": ">= 16" }, - "packageManager": "yarn@3.3.0", - "dependenciesMeta": { - "@grafana/scenes@0.0.3": { - "unplugged": true - } - } + "packageManager": "yarn@3.3.0" } diff --git a/public/app/features/scenes/scenes/demo.tsx b/public/app/features/scenes/scenes/demo.tsx index a8c3c861987..bf63f63c019 100644 --- a/public/app/features/scenes/scenes/demo.tsx +++ b/public/app/features/scenes/scenes/demo.tsx @@ -2,11 +2,12 @@ import { SceneFlexLayout, SceneTimeRange, SceneTimePicker, - ScenePanelRepeater, + SceneByFrameRepeater, VizPanel, SceneCanvasText, SceneToolbarInput, EmbeddedScene, + SceneDataNode, } from '@grafana/scenes'; import { panelBuilders } from '../builders/panelBuilders'; @@ -76,33 +77,41 @@ export function getScenePanelRepeaterTest(standalone: boolean): Scene | Embedded const state = { title: 'Panel repeater test', - body: new ScenePanelRepeater({ - layout: new SceneFlexLayout({ + body: new SceneByFrameRepeater({ + body: new SceneFlexLayout({ direction: 'column', - children: [ - new SceneFlexLayout({ - direction: 'row', - placement: { minHeight: 200 }, - children: [ - new VizPanel({ - pluginId: 'timeseries', - title: 'Title', - options: { - legend: { displayMode: 'hidden' }, - }, - }), - new VizPanel({ - placement: { width: 300 }, - pluginId: 'stat', - fieldConfig: { defaults: { displayName: 'Last' }, overrides: [] }, - options: { - graphMode: 'none', - }, - }), - ], - }), - ], + children: [], }), + getLayoutChild: (data, frame, frameIndex) => { + return new SceneFlexLayout({ + key: `panel-${frameIndex}`, + $data: new SceneDataNode({ + data: { + ...data, + series: [frame], + }, + }), + direction: 'row', + placement: { minHeight: 200 }, + children: [ + new VizPanel({ + pluginId: 'timeseries', + title: 'Title', + options: { + legend: { displayMode: 'hidden' }, + }, + }), + new VizPanel({ + placement: { width: 300 }, + pluginId: 'stat', + fieldConfig: { defaults: { displayName: 'Last' }, overrides: [] }, + options: { + graphMode: 'none', + }, + }), + ], + }); + }, }), $editor: new SceneEditManager({}), $timeRange: new SceneTimeRange(), diff --git a/yarn.lock b/yarn.lock index 794a19e5195..45fa0fb762d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4922,6 +4922,17 @@ __metadata: languageName: unknown linkType: soft +"@grafana/e2e-selectors@npm:canary": + version: 9.4.0-96193pre + resolution: "@grafana/e2e-selectors@npm:9.4.0-96193pre" + dependencies: + "@grafana/tsconfig": ^1.2.0-rc1 + tslib: 2.4.1 + typescript: 4.8.4 + checksum: 1ce663607c0c816ec02cad33d80c38731c4dfafdc7d1693baa989b8eff6588e3cc1d8a031c4f30f888bbd8aa8768cce7c5dd4cb2455306040f39dcac933ceae4 + languageName: node + linkType: hard + "@grafana/e2e@workspace:*, @grafana/e2e@workspace:packages/grafana-e2e": version: 0.0.0-use.local resolution: "@grafana/e2e@workspace:packages/grafana-e2e" @@ -5109,10 +5120,17 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes@npm:0.0.4": - version: 0.0.4 - resolution: "@grafana/scenes@npm:0.0.4" - checksum: adbc69e4033bdd40a5c2fe13ee7af2b787c236e548c389b6820b057f76ae3727fc4f85d2205d6658e008f69f75bbf84c04453fedb29f9979a76517267f1f91ad +"@grafana/scenes@npm:latest": + version: 0.0.5 + resolution: "@grafana/scenes@npm:0.0.5" + dependencies: + "@grafana/e2e-selectors": canary + "@grafana/experimental": 1.0.1 + react-grid-layout: 1.3.4 + react-use: 17.4.0 + react-virtualized-auto-sizer: 1.0.7 + uuid: ^9.0.0 + checksum: 7388aaccb0788801f94734126d1abab71df3f81131eec41c967b0c462223a13fcfbc5a65ce7a45bf6761032d8efd1330cb85ac8e86136cba9e4dd244f41b1054 languageName: node linkType: hard @@ -22015,7 +22033,7 @@ __metadata: "@grafana/lezer-logql": 0.1.1 "@grafana/monaco-logql": ^0.0.6 "@grafana/runtime": "workspace:*" - "@grafana/scenes": 0.0.4 + "@grafana/scenes": latest "@grafana/schema": "workspace:*" "@grafana/toolkit": "workspace:*" "@grafana/tsconfig": ^1.2.0-rc1 @@ -22304,9 +22322,6 @@ __metadata: webpack-manifest-plugin: 5.0.0 webpack-merge: 5.8.0 whatwg-fetch: 3.6.2 - dependenciesMeta: - "@grafana/scenes@0.0.3": - unplugged: true languageName: unknown linkType: soft @@ -38417,7 +38432,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:9.0.0": +"uuid@npm:9.0.0, uuid@npm:^9.0.0": version: 9.0.0 resolution: "uuid@npm:9.0.0" bin: From e481673b776b464df3fac624e001b3ab60c8326c Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Mon, 16 Jan 2023 09:49:35 +0100 Subject: [PATCH 02/65] AzureMonitor: Use list of resources for metrics (#61485) --- .../__mocks__/query.ts | 3 +- .../azure_monitor_datasource.test.ts | 11 ++-- .../azure_monitor/azure_monitor_datasource.ts | 15 +++--- .../MetricsQueryEditor.test.tsx | 11 ++-- .../MetricsQueryEditor/MetricsQueryEditor.tsx | 4 +- .../MetricsQueryEditor/dataHooks.test.ts | 54 ++++++++++++++----- .../MetricsQueryEditor/dataHooks.ts | 22 ++++++-- .../components/ResourcePicker/utils.test.ts | 8 ++- .../components/ResourcePicker/utils.ts | 3 +- .../components/SubscriptionField.tsx | 3 +- .../datasource.test.ts | 6 ++- .../types/query.ts | 13 ++++- .../utils/migrateQuery.test.ts | 11 ++-- .../utils/migrateQuery.ts | 22 +++++++- 14 files changed, 132 insertions(+), 54 deletions(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query.ts index 2d08a83eb61..12d14c1f449 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query.ts @@ -36,8 +36,7 @@ export default function createMockQuery(overrides?: Partial): metricName: 'Metric A', metricNamespace: 'Microsoft.Compute/virtualMachines', customNamespace: '', - resourceGroup: 'grafanastaging', - resourceName: 'grafana', + resources: [{ resourceGroup: 'grafanastaging', resourceName: 'grafana' }], timeGrain: 'auto', alias: '', // timeGrains: [], diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts index 406e9d39891..622bdfc88ea 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts @@ -43,12 +43,12 @@ describe('AzureMonitorDatasource', () => { }, { description: 'filter query with no resourceGroup', - query: createMockQuery({ azureMonitor: { resourceGroup: undefined } }), + query: createMockQuery({ azureMonitor: { resources: [{ resourceGroup: undefined }] } }), filtered: false, }, { description: 'filter query with no resourceName', - query: createMockQuery({ azureMonitor: { resourceName: undefined } }), + query: createMockQuery({ azureMonitor: { resources: [{ resourceName: undefined }] } }), filtered: false, }, { @@ -117,9 +117,8 @@ describe('AzureMonitorDatasource', () => { expect(templatedQuery).toMatchObject({ subscription, azureMonitor: { - resourceGroup, metricNamespace, - resourceName, + resources: [{ resourceGroup, resourceName }], }, }); }); @@ -346,8 +345,8 @@ describe('AzureMonitorDatasource', () => { it('should return a query with any template variables replaced', () => { const templateableProps = [ - 'resourceGroup', - 'resourceName', + 'resources[0].resourceGroup', + 'resources[0].resourceName', 'metricNamespace', 'timeGrain', 'aggregation', diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts index ad4633dd32c..16c37b1b2e5 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts @@ -68,8 +68,9 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend 0 && + item.azureMonitor.resources.every((r) => hasValue(r.resourceGroup) && hasValue(r.resourceName)) && hasValue(item?.azureMonitor?.metricDefinition || item?.azureMonitor?.metricNamespace); const hasResourceUri = hasValue(item.azureMonitor?.resourceUri); @@ -97,8 +98,10 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend ({ + resourceGroup: templateSrv.replace(r.resourceGroup, scopedVars), + resourceName: templateSrv.replace(r.resourceName, scopedVars), + })); const metricNamespace = templateSrv.replace(item.metricNamespace, scopedVars); const customNamespace = templateSrv.replace(item.customNamespace, scopedVars); const timeGrain = templateSrv.replace((item.timeGrain || '').toString(), scopedVars); @@ -117,10 +120,10 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend { const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() }); const query = createMockQuery(); delete query?.subscription; - delete query?.azureMonitor?.resourceGroup; - delete query?.azureMonitor?.resourceName; + delete query?.azureMonitor?.resources; delete query?.azureMonitor?.metricNamespace; const onChange = jest.fn(); @@ -125,8 +124,12 @@ describe('MetricsQueryEditor', () => { subscription: 'def-456', azureMonitor: expect.objectContaining({ metricNamespace: 'microsoft.compute/virtualmachines', - resourceGroup: 'dev-3', - resourceName: 'web-server', + resources: [ + { + resourceGroup: 'dev-3', + resourceName: 'web-server', + }, + ], }), }) ); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.tsx index b9e8251f358..39b39a76555 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.tsx @@ -39,9 +39,9 @@ const MetricsQueryEditor: React.FC = ({ const metricNames = useMetricNames(query, datasource, onChange, setError); const resource: AzureMetricResource = { subscription: query.subscription, - resourceGroup: query.azureMonitor?.resourceGroup, + resourceGroup: query.azureMonitor?.resources?.[0]?.resourceGroup, metricNamespace: query.azureMonitor?.metricNamespace, - resourceName: query.azureMonitor?.resourceName, + resourceName: query.azureMonitor?.resources?.[0]?.resourceName, }; return ( diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.test.ts index 3ca999e8e25..20f7ab9cc88 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.test.ts @@ -45,13 +45,21 @@ describe('AzureMonitor: metrics dataHooks', () => { hook: useMetricNames, emptyQueryPartial: { metricNamespace: 'azure/vm', - resourceGroup: 'rg', - resourceName: 'rn', + resources: [ + { + resourceGroup: 'rg', + resourceName: 'rn', + }, + ], }, customProperties: { metricNamespace: 'azure/vm', - resourceGroup: 'rg', - resourceName: 'rn', + resources: [ + { + resourceGroup: 'rg', + resourceName: 'rn', + }, + ], metricName: 'metric-$ENVIRONMENT', }, expectedOptions: [ @@ -75,13 +83,21 @@ describe('AzureMonitor: metrics dataHooks', () => { hook: useMetricNamespaces, emptyQueryPartial: { metricNamespace: 'azure/vm', - resourceGroup: 'rg', - resourceName: 'rn', + resources: [ + { + resourceGroup: 'rg', + resourceName: 'rn', + }, + ], }, customProperties: { metricNamespace: 'azure/vm-$ENVIRONMENT', - resourceGroup: 'rg', - resourceName: 'rn', + resources: [ + { + resourceGroup: 'rg', + resourceName: 'rn', + }, + ], metricName: 'metric-name', }, expectedOptions: [ @@ -188,8 +204,12 @@ describe('AzureMonitor: metrics dataHooks', () => { name: 'useMetricMetadata', hook: useMetricMetadata, emptyQueryPartial: { - resourceGroup: 'rg', - resourceName: 'rn', + resources: [ + { + resourceGroup: 'rg', + resourceName: 'rn', + }, + ], metricNamespace: 'azure/vm', metricName: 'Average CPU', }, @@ -240,8 +260,12 @@ describe('AzureMonitor: metrics dataHooks', () => { name: 'useMetricNamespaces', hook: useMetricNamespaces, emptyQueryPartial: { - resourceGroup: 'rg', - resourceName: 'rn', + resources: [ + { + resourceGroup: 'rg', + resourceName: 'rn', + }, + ], metricNamespace: 'azure/vm', }, customProperties: {}, @@ -264,7 +288,11 @@ describe('AzureMonitor: metrics dataHooks', () => { expect(result.current).toEqual(metricNamespacesConfig.expectedOptions); expect(datasource.azureMonitorDatasource.getMetricNamespaces).toHaveBeenCalledWith( - expect.objectContaining(metricNamespacesConfig.emptyQueryPartial), + expect.objectContaining({ + resourceGroup: 'rg', + resourceName: 'rn', + metricNamespace: 'azure/vm', + }), // Here, "global" should be false false ); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.ts index 19185023676..b6edf825d7c 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/dataHooks.ts @@ -4,7 +4,7 @@ import { rangeUtil } from '@grafana/data'; import Datasource from '../../datasource'; import TimegrainConverter from '../../time_grain_converter'; -import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery } from '../../types'; +import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery, AzureMonitorResource } from '../../types'; import { toOption } from '../../utils/common'; import { useAsyncState } from '../../utils/useAsyncState'; @@ -38,9 +38,20 @@ export interface MetricMetadata { type OnChangeFn = (newQuery: AzureMonitorQuery) => void; +const getResourceGroupAndName = (resources?: AzureMonitorResource[]) => { + if (!resources || !resources.length) { + return { resourceGroup: '', resourceName: '' }; + } + return { + resourceGroup: resources[0].resourceGroup ?? '', + resourceName: resources[0].resourceName ?? '', + }; +}; + export const useMetricNamespaces: DataHook = (query, datasource, onChange, setError) => { const { subscription } = query; - const { metricNamespace, resourceGroup, resourceName } = query.azureMonitor ?? {}; + const { metricNamespace, resources } = query.azureMonitor ?? {}; + const { resourceGroup, resourceName } = getResourceGroupAndName(resources); const metricNamespaces = useAsyncState( async () => { @@ -75,7 +86,8 @@ export const useMetricNamespaces: DataHook = (query, datasource, onChange, setEr export const useMetricNames: DataHook = (query, datasource, onChange, setError) => { const { subscription } = query; - const { metricNamespace, metricName, resourceGroup, resourceName, customNamespace } = query.azureMonitor ?? {}; + const { metricNamespace, metricName, resources, customNamespace } = query.azureMonitor ?? {}; + const { resourceGroup, resourceName } = getResourceGroupAndName(resources); return useAsyncState( async () => { @@ -110,8 +122,8 @@ const defaultMetricMetadata: MetricMetadata = { export const useMetricMetadata = (query: AzureMonitorQuery, datasource: Datasource, onChange: OnChangeFn) => { const [metricMetadata, setMetricMetadata] = useState(defaultMetricMetadata); const { subscription } = query; - const { resourceGroup, resourceName, metricNamespace, metricName, aggregation, timeGrain, customNamespace } = - query.azureMonitor ?? {}; + const { resources, metricNamespace, metricName, aggregation, timeGrain, customNamespace } = query.azureMonitor ?? {}; + const { resourceGroup, resourceName } = getResourceGroupAndName(resources); // Fetch new metric metadata when the fields change useEffect(() => { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.test.ts index 57083d316fd..c0101c4b63b 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.test.ts @@ -184,8 +184,12 @@ describe('AzureMonitor ResourcePicker utils', () => { aggregation: undefined, metricName: undefined, metricNamespace: 'microsoft.storage/storageaccounts', - resourceGroup: 'rg', - resourceName: 'testacct', + resources: [ + { + resourceGroup: 'rg', + resourceName: 'testacct', + }, + ], }, }); }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.ts index a5a8778eb76..16ae043f0d1 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.ts @@ -145,9 +145,8 @@ export function setResource(query: AzureMonitorQuery, resource?: string | AzureM subscription: resource?.subscription, azureMonitor: { ...query.azureMonitor, - resourceGroup: resource?.resourceGroup, metricNamespace: resource?.metricNamespace?.toLocaleLowerCase(), - resourceName: resource?.resourceName, + resources: [{ resourceGroup: resource?.resourceGroup, resourceName: resource?.resourceName }], metricName: undefined, aggregation: undefined, timeGrain: '', diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/SubscriptionField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/SubscriptionField.tsx index cb349249883..72e4843d35a 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/SubscriptionField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/SubscriptionField.tsx @@ -36,9 +36,8 @@ const SubscriptionField: React.FC = ({ if (query.queryType === AzureQueryType.AzureMonitor) { newQuery.azureMonitor = { ...newQuery.azureMonitor, - resourceGroup: undefined, + resources: undefined, metricNamespace: undefined, - resourceName: undefined, metricName: undefined, aggregation: undefined, timeGrain: '', diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.test.ts index ff3e0b329df..6c4713cd651 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.test.ts @@ -6,7 +6,7 @@ describe('Azure Monitor Datasource', () => { describe('interpolateVariablesInQueries()', () => { it('should interpolate variables in the queries', () => { const ds = new Datasource(createMockInstanceSetttings()); - const queries = [createMockQuery({ azureMonitor: { resourceGroup: '$resourceGroup' } })]; + const queries = [createMockQuery({ azureMonitor: { resources: [{ resourceGroup: '$resourceGroup' }] } })]; const interpolatedQueries = ds.interpolateVariablesInQueries(queries, { resourceGroup: { text: 'the-resource-group', value: 'the-resource-group' }, @@ -14,7 +14,9 @@ describe('Azure Monitor Datasource', () => { expect(interpolatedQueries).toContainEqual( expect.objectContaining({ - azureMonitor: expect.objectContaining({ resourceGroup: 'the-resource-group' }), + azureMonitor: expect.objectContaining({ + resources: [expect.objectContaining({ resourceGroup: 'the-resource-group' })], + }), }) ); }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/query.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/query.ts index bd24cdaa1d1..844a56d6f2b 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/query.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/query.ts @@ -39,12 +39,16 @@ export interface AzureMonitorQuery extends DataQuery { resource?: string; } +export interface AzureMonitorResource { + resourceGroup?: string; + resourceName?: string; +} + /** * Azure Monitor Metrics sub-query properties */ export interface AzureMetricQuery { - resourceGroup?: string; - resourceName?: string; + resources?: AzureMetricResource[]; /** metricNamespace is used as the resource type (or resource namespace). * It's usually equal to the target metric namespace. * Kept the name of the variable as metricNamespace to avoid backward incompatibility issues. @@ -74,6 +78,11 @@ export interface AzureMetricQuery { /** @deprecated Use resourceGroup, resourceName and metricNamespace instead */ resourceUri?: string; + + /** @deprecated Use resources instead */ + resourceGroup?: string; + /** @deprecated Use resources instead */ + resourceName?: string; } /** diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/migrateQuery.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/migrateQuery.test.ts index 7fe16431aec..9f3437bb6d7 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/migrateQuery.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/migrateQuery.test.ts @@ -53,8 +53,12 @@ const modernMetricsQuery: AzureMonitorQuery = { dimensionFilters: [{ dimension: 'dependency/success', filters: ['*'], operator: 'eq' }], metricName: 'dependencies/duration', metricNamespace: 'microsoft.insights/components', - resourceGroup: 'cloud-datasources', - resourceName: 'AppInsightsTestData', + resources: [ + { + resourceGroup: 'cloud-datasources', + resourceName: 'AppInsightsTestData', + }, + ], timeGrain: 'PT5M', top: '10', }, @@ -173,8 +177,7 @@ describe('AzureMonitor: migrateQuery', () => { subscription: modernMetricsQuery.subscription, azureMonitor: expect.objectContaining({ metricNamespace: modernMetricsQuery.azureMonitor!.metricNamespace, - resourceGroup: modernMetricsQuery.azureMonitor!.resourceGroup, - resourceName: modernMetricsQuery.azureMonitor!.resourceName, + resources: modernMetricsQuery.azureMonitor!.resources, resourceUri: undefined, }), }) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/migrateQuery.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/migrateQuery.ts index 1b44cb3be91..bf8a895e69b 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/migrateQuery.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/migrateQuery.ts @@ -28,6 +28,10 @@ export default function migrateQuery(query: AzureMonitorQuery): AzureMonitorQuer workingQuery = migrateDimensionToResourceObj(workingQuery); } + if (workingQuery.azureMonitor?.resourceGroup || workingQuery.azureMonitor?.resourceName) { + workingQuery = migrateResourceGroupAndName(workingQuery); + } + return workingQuery; } @@ -144,9 +148,8 @@ function migrateDimensionToResourceObj(query: AzureMonitorQuery): AzureMonitorQu subscription: details?.subscription, azureMonitor: { ...query.azureMonitor, - resourceGroup: details?.resourceGroup, + resources: [{ resourceGroup: details?.resourceGroup, resourceName: details?.resourceName }], metricNamespace: details?.metricNamespace, - resourceName: details?.resourceName, resourceUri: isWellFormedUri ? undefined : query.azureMonitor.resourceUri, }, }; @@ -154,3 +157,18 @@ function migrateDimensionToResourceObj(query: AzureMonitorQuery): AzureMonitorQu return query; } + +function migrateResourceGroupAndName(query: AzureMonitorQuery): AzureMonitorQuery { + let workingQuery = query; + + if (workingQuery.azureMonitor) { + workingQuery.azureMonitor.resources = [ + { resourceGroup: workingQuery.azureMonitor.resourceGroup, resourceName: workingQuery.azureMonitor.resourceName }, + ]; + + delete workingQuery.azureMonitor.resourceGroup; + delete workingQuery.azureMonitor.resourceName; + } + + return workingQuery; +} From b8b08ea292c0b1f85aba894f5ba8d6e34c4da7c6 Mon Sep 17 00:00:00 2001 From: Misi Date: Mon, 16 Jan 2023 10:50:34 +0100 Subject: [PATCH 03/65] Auth: Add sub claim check to JWT Auth pre-checks (#61417) * Auth: Add sub claim check to JWT Auth pre-checks * Add #nosec annotation to the test tokens --- pkg/middleware/middleware_jwt_auth_test.go | 5 ++- pkg/middleware/middleware_test.go | 46 +++++++++++++++++++++- pkg/services/auth/jwt/auth.go | 19 ++++++++- pkg/services/authn/clients/jwt.go | 12 ++---- pkg/services/authn/clients/jwt_test.go | 26 ++++++++++-- pkg/services/contexthandler/auth_jwt.go | 15 +++---- 6 files changed, 97 insertions(+), 26 deletions(-) diff --git a/pkg/middleware/middleware_jwt_auth_test.go b/pkg/middleware/middleware_jwt_auth_test.go index abb35ae7469..ab41f4d4abe 100644 --- a/pkg/middleware/middleware_jwt_auth_test.go +++ b/pkg/middleware/middleware_jwt_auth_test.go @@ -55,7 +55,8 @@ func TestMiddlewareJWTAuth(t *testing.T) { cfg.JWTAuthAllowAssignGrafanaAdmin = true } - token := "some-token" + // #nosec G101 -- This is dummy/test token + token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2bGFkaW1pckBleGFtcGxlLmNvbSIsImlhdCI6MTUxNjIzOTAyMiwiZm9vLXVzZXJuYW1lIjoidmxhZGltaXIiLCJuYW1lIjoiVmxhZGltaXIgRXhhbXBsZSIsImZvby1lbWFpbCI6InZsYWRpbWlyQGV4YW1wbGUuY29tIn0.MeNU1pCzRHGdQuu5ppeftxT31_2Le2kM1wd1GK2jExs" middlewareScenario(t, "Valid token with valid login claim", func(t *testing.T, sc *scenarioContext) { myUsername := "vladimir" @@ -85,7 +86,7 @@ func TestMiddlewareJWTAuth(t *testing.T) { myUsername := "vladimir" // We can ignore gosec G101 since this does not contain any credentials. // nolint:gosec - myToken := "some.jwt.token" + myToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2bGFkaW1pckBleGFtcGxlLmNvbSIsImlhdCI6MTUxNjIzOTAyMiwiZm9vLXVzZXJuYW1lIjoidmxhZGltaXIiLCJuYW1lIjoiVmxhZGltaXIgRXhhbXBsZSIsImZvby1lbWFpbCI6InZsYWRpbWlyQGV4YW1wbGUuY29tIn0.MeNU1pCzRHGdQuu5ppeftxT31_2Le2kM1wd1GK2jExs" var verifiedToken string sc.jwtAuthService.VerifyProvider = func(ctx context.Context, token string) (models.JWTClaims, error) { verifiedToken = myToken diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 173c295b45d..60d59fa12a1 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -234,6 +234,50 @@ func TestMiddlewareContext(t *testing.T) { assert.Equal(t, org.RoleEditor, sc.context.OrgRole) }, configureJWTAuthHeader) + middlewareScenario(t, "Valid Basic Auth header with JWT enabled and empty 'sub' claim", func(t *testing.T, sc *scenarioContext) { + const password = "MyPass" + const orgID int64 = 2 + const userID int64 = 12 + // #nosec G101 -- This is dummy/test token + const emptySubToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJzdWIiOiIiLCJpYXQiOjE1MTYyMzkwMjJ9.tnwtOHK58d47dO4DHW4b9MzeToxa1kGiko5Oo887Rqc" + + sc.userService.ExpectedSignedInUser = &user.SignedInUser{OrgID: orgID, UserID: userID} + authHeader := util.GetBasicAuthHeader("myuser", password) + sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).withJWTAuthHeader(emptySubToken).exec() + + require.Equal(t, 200, sc.resp.Code) + + assert.True(t, sc.context.IsSignedIn) + assert.Equal(t, orgID, sc.context.OrgID) + assert.Equal(t, userID, sc.context.UserID) + }, func(cfg *setting.Cfg) { + cfg.JWTAuthEnabled = true + cfg.JWTAuthHeaderName = "X-JWT-Token" + cfg.BasicAuthEnabled = true + }) + + middlewareScenario(t, "Valid Basic Auth header with JWT enabled and missing 'sub' claim", func(t *testing.T, sc *scenarioContext) { + const password = "MyPass" + const orgID int64 = 2 + const userID int64 = 12 + // #nosec G101 -- This is dummy/test token + const missingSubToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.8nYFUX869Y1mnDDDU4yL11aANgVRuifoxrE8BHZY1iE" + + sc.userService.ExpectedSignedInUser = &user.SignedInUser{OrgID: orgID, UserID: userID} + authHeader := util.GetBasicAuthHeader("myuser", password) + sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).withJWTAuthHeader(missingSubToken).exec() + + require.Equal(t, 200, sc.resp.Code) + + assert.True(t, sc.context.IsSignedIn) + assert.Equal(t, orgID, sc.context.OrgID) + assert.Equal(t, userID, sc.context.UserID) + }, func(cfg *setting.Cfg) { + cfg.JWTAuthEnabled = true + cfg.JWTAuthHeaderName = "X-JWT-Token" + cfg.BasicAuthEnabled = true + }) + middlewareScenario(t, "Valid API key, but does not match DB hash", func(t *testing.T, sc *scenarioContext) { const keyhash = "Something_not_matching" sc.apiKeyService.ExpectedAPIKey = &apikey.APIKey{OrgId: 12, Role: org.RoleEditor, Key: keyhash} @@ -696,7 +740,7 @@ func TestMiddlewareContext(t *testing.T) { }) middlewareScenario(t, "Request body should not be read in default context handler, but query should be altered - jwt", func(t *testing.T, sc *scenarioContext) { - sc.fakeReq("POST", "/?targetOrgId=123&auth_token=token") + sc.fakeReq("POST", "/?targetOrgId=123&auth_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NSIsImlhdCI6MTUxNjIzOTAyMn0.1E9qmtctlHAeJzNLPgGFfxdA8WfbEl_vwYO91ffQGxs") body := "key=value" sc.req.Body = io.NopCloser(strings.NewReader(body)) diff --git a/pkg/services/auth/jwt/auth.go b/pkg/services/auth/jwt/auth.go index 469eb6ce129..28adad3a2ff 100644 --- a/pkg/services/auth/jwt/auth.go +++ b/pkg/services/auth/jwt/auth.go @@ -6,11 +6,12 @@ import ( "errors" "strings" + "gopkg.in/square/go-jose.v2/jwt" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" - "gopkg.in/square/go-jose.v2/jwt" ) const ServiceName = "AuthService" @@ -102,3 +103,19 @@ func (s *AuthService) Verify(ctx context.Context, strToken string) (models.JWTCl return claims, nil } + +// HasSubClaim checks if the provided JWT token contains a non-empty "sub" claim. +// Returns true if it contains, otherwise returns false. +func HasSubClaim(jwtToken string) bool { + parsed, err := jwt.ParseSigned(sanitizeJWT(jwtToken)) + if err != nil { + return false + } + + var claims jwt.Claims + if err := parsed.UnsafeClaimsWithoutVerification(&claims); err != nil { + return false + } + + return claims.Subject != "" +} diff --git a/pkg/services/authn/clients/jwt.go b/pkg/services/authn/clients/jwt.go index 4d4f5256918..5c3a23e523f 100644 --- a/pkg/services/authn/clients/jwt.go +++ b/pkg/services/authn/clients/jwt.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/auth" + authJWT "github.com/grafana/grafana/pkg/services/auth/jwt" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" @@ -143,21 +144,14 @@ func (s *JWT) Test(ctx context.Context, r *authn.Request) bool { return false } - // The header is Authorization and the token does not look like a JWT, - // this is likely an API key. Pass it on. - if s.cfg.JWTAuthHeaderName == "Authorization" && !looksLikeJWT(jwtToken) { + // If the "sub" claim is missing or empty then pass the control to the next handler + if !authJWT.HasSubClaim(jwtToken) { return false } return true } -func looksLikeJWT(token string) bool { - // A JWT must have 3 parts separated by `.`. - parts := strings.Split(token, ".") - return len(parts) == 3 -} - const roleGrafanaAdmin = "GrafanaAdmin" func (s *JWT) extractRoleAndAdmin(claims map[string]interface{}) (org.RoleType, bool) { diff --git a/pkg/services/authn/clients/jwt_test.go b/pkg/services/authn/clients/jwt_test.go index 7b8cea2e86f..c511ac2fe4d 100644 --- a/pkg/services/authn/clients/jwt_test.go +++ b/pkg/services/authn/clients/jwt_test.go @@ -7,12 +7,13 @@ import ( "net/url" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func stringPtr(s string) *string { @@ -87,8 +88,13 @@ func TestAuthenticateJWT(t *testing.T) { func TestJWTTest(t *testing.T) { jwtService := &models.FakeJWTService{} jwtHeaderName := "X-Forwarded-User" - validFormatToken := "sample.token.valid" + // #nosec G101 -- This is dummy/test token + validFormatToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o" invalidFormatToken := "sampletokeninvalid" + // #nosec G101 -- This is dummy/test token + missingSubToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.8nYFUX869Y1mnDDDU4yL11aANgVRuifoxrE8BHZY1iE" + // #nosec G101 -- This is dummy/test token + emptySubToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJzdWIiOiIiLCJpYXQiOjE1MTYyMzkwMjJ9.tnwtOHK58d47dO4DHW4b9MzeToxa1kGiko5Oo887Rqc" type testCase struct { desc string @@ -144,6 +150,20 @@ func TestJWTTest(t *testing.T) { token: validFormatToken, want: false, }, + { + desc: "token without a sub claim", + reqHeaderName: "Authorization", + cfgHeaderName: "Authorization", + token: missingSubToken, + want: false, + }, + { + desc: "token with an empty sub claim", + reqHeaderName: "Authorization", + cfgHeaderName: "Authorization", + token: emptySubToken, + want: false, + }, } for _, tc := range testCases { diff --git a/pkg/services/contexthandler/auth_jwt.go b/pkg/services/contexthandler/auth_jwt.go index 25ed124412b..79b51216eff 100644 --- a/pkg/services/contexthandler/auth_jwt.go +++ b/pkg/services/contexthandler/auth_jwt.go @@ -6,13 +6,15 @@ import ( "net/http" "strings" + "github.com/jmespath/go-jmespath" + "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/models" + authJWT "github.com/grafana/grafana/pkg/services/auth/jwt" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" - "github.com/jmespath/go-jmespath" ) const ( @@ -59,9 +61,8 @@ func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64) // Strip the 'Bearer' prefix if it exists. jwtToken = strings.TrimPrefix(jwtToken, "Bearer ") - // The header is Authorization and the token does not look like a JWT, - // this is likely an API key. Pass it on. - if h.Cfg.JWTAuthHeaderName == "Authorization" && !looksLikeJWT(jwtToken) { + // If the "sub" claim is missing or empty then pass the control to the next handler + if !authJWT.HasSubClaim(jwtToken) { return false } @@ -222,9 +223,3 @@ func searchClaimsForStringAttr(attributePath string, claims map[string]interface return "", nil } - -func looksLikeJWT(token string) bool { - // A JWT must have 3 parts separated by `.`. - parts := strings.Split(token, ".") - return len(parts) == 3 -} From 43ca4e0bc7baecd4069ced99789fe72beeb6bad7 Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Mon, 16 Jan 2023 12:09:20 +0200 Subject: [PATCH 04/65] Fix Barchart legend aligning right when orientation is horizontal (#61451) --- packages/grafana-ui/src/components/uPlot/PlotLegend.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/uPlot/PlotLegend.tsx b/packages/grafana-ui/src/components/uPlot/PlotLegend.tsx index 12dbefd7d04..5e498152762 100644 --- a/packages/grafana-ui/src/components/uPlot/PlotLegend.tsx +++ b/packages/grafana-ui/src/components/uPlot/PlotLegend.tsx @@ -56,7 +56,7 @@ export const PlotLegend: React.FC = React.memo( fieldIndex, color: seriesColor, label, - yAxis: axisPlacement === AxisPlacement.Left ? 1 : 2, + yAxis: axisPlacement === AxisPlacement.Left || axisPlacement === AxisPlacement.Bottom ? 1 : 2, getDisplayValues: () => { if (!calcs?.length) { return []; From 1b6c0d9752f05f0d8850a749f4600b4bb7d34a76 Mon Sep 17 00:00:00 2001 From: Hamas Shafiq Date: Mon, 16 Jan 2023 10:25:47 +0000 Subject: [PATCH 05/65] Chore: Convert `ViewingLayer.test.tsx` to RTL (#61358) --- .betterer.results | 3 - .../SpanGraph/ViewingLayer.test.tsx | 352 +++--------------- .../SpanGraph/ViewingLayer.tsx | 11 +- 3 files changed, 69 insertions(+), 297 deletions(-) diff --git a/.betterer.results b/.betterer.results index 17a6ea61e05..86ada2c5135 100644 --- a/.betterer.results +++ b/.betterer.results @@ -8,9 +8,6 @@ exports[`no enzyme tests`] = { "packages/grafana-ui/src/components/QueryField/QueryField.test.tsx:2976628669": [ [0, 26, 13, "RegExp match", "2409514259"] ], - "packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.test.tsx:793800575": [ - [14, 35, 13, "RegExp match", "2409514259"] - ], "packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/index.test.tsx:3266788928": [ [14, 56, 13, "RegExp match", "2409514259"] ], diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.test.tsx b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.test.tsx index e83e65f778c..bbe8f896ffb 100644 --- a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.test.tsx +++ b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.test.tsx @@ -12,18 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { shallow, ShallowWrapper } from 'enzyme'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import React from 'react'; -import { createTheme } from '@grafana/data'; - -import { ViewRangeTime } from '../../TraceTimelineViewer/types'; -import DraggableManager, { DraggingUpdate, EUpdateTypes } from '../../utils/DraggableManager'; import { polyfill as polyfillAnimationFrame } from '../../utils/test/requestAnimationFrame'; -import GraphTicks from './GraphTicks'; -import Scrubber, { ScrubberProps } from './Scrubber'; -import ViewingLayer, { dragTypes, getStyles, ViewingLayerProps, UnthemedViewingLayer } from './ViewingLayer'; +import ViewingLayer, { ViewingLayerProps } from './ViewingLayer'; function getViewRange(viewStart: number, viewEnd: number) { return { @@ -33,11 +28,10 @@ function getViewRange(viewStart: number, viewEnd: number) { }; } -describe('', () => { +describe('', () => { polyfillAnimationFrame(window); let props: ViewingLayerProps; - let wrapper: ShallowWrapper; beforeEach(() => { props = { @@ -47,302 +41,74 @@ describe('', () => { updateViewRangeTime: jest.fn(), viewRange: getViewRange(0, 1), } as unknown as ViewingLayerProps; - - wrapper = shallow() - .dive() - .dive(); }); - describe('_getDraggingBounds()', () => { - beforeEach(() => { - props = { ...props, viewRange: getViewRange(0.1, 0.9) }; - wrapper = shallow() - .dive() - .dive(); - - wrapper.instance()._setRoot({ - getBoundingClientRect() { - return { left: 10, width: 100 }; - }, - } as SVGElement); - }); - - it('throws if _root is not set', () => { - const instance = wrapper.instance(); - instance._root = null; - expect(() => instance._getDraggingBounds(dragTypes.REFRAME)).toThrow(); - }); - - it('returns the correct bounds for reframe', () => { - const bounds = wrapper.instance()._getDraggingBounds(dragTypes.REFRAME); - expect(bounds).toEqual({ - clientXLeft: 10, - width: 100, - maxValue: 1, - minValue: 0, - }); - }); - - it('returns the correct bounds for shiftStart', () => { - const bounds = wrapper.instance()._getDraggingBounds(dragTypes.SHIFT_START); - expect(bounds).toEqual({ - clientXLeft: 10, - width: 100, - maxValue: 0.9, - minValue: 0, - }); - }); - - it('returns the correct bounds for shiftEnd', () => { - const bounds = wrapper.instance()._getDraggingBounds(dragTypes.SHIFT_END); - expect(bounds).toEqual({ - clientXLeft: 10, - width: 100, - maxValue: 1, - minValue: 0.1, - }); - }); + it('does not render ViewingLayerCursorGuide if the cursor position is not defined', () => { + render(); + expect(screen.queryByTestId('ViewingLayerCursorGuide')).toBeNull(); }); - describe('DraggableManager callbacks', () => { - describe('reframe', () => { - it('handles mousemove', () => { - const value = 0.5; - wrapper.instance()._handleReframeMouseMove({ value } as DraggingUpdate); - const calls = jest.mocked(props.updateNextViewRangeTime).mock.calls; - expect(calls).toEqual([[{ cursor: value }]]); - }); - - it('handles mouseleave', () => { - wrapper.instance()._handleReframeMouseLeave(); - const calls = jest.mocked(props.updateNextViewRangeTime).mock.calls; - expect(calls).toEqual([[{ cursor: null }]]); - }); - - describe('drag update', () => { - it('handles sans anchor', () => { - const value = 0.5; - wrapper.instance()._handleReframeDragUpdate({ value } as DraggingUpdate); - const calls = jest.mocked(props.updateNextViewRangeTime).mock.calls; - expect(calls).toEqual([[{ reframe: { anchor: value, shift: value } }]]); - }); - - it('handles the existing anchor', () => { - const value = 0.5; - const anchor = 0.1; - const time = { ...props.viewRange.time, reframe: { anchor } } as ViewRangeTime; - props = { ...props, viewRange: { time } }; - wrapper = shallow() - .dive() - .dive(); - wrapper.instance()._handleReframeDragUpdate({ value } as DraggingUpdate); - const calls = jest.mocked(props.updateNextViewRangeTime).mock.calls; - expect(calls).toEqual([[{ reframe: { anchor, shift: value } }]]); - }); - }); - - describe('drag end', () => { - let manager: DraggableManager; - - beforeEach(() => { - manager = { resetBounds: jest.fn() } as unknown as DraggableManager; - }); - - it('handles sans anchor', () => { - const value = 0.5; - wrapper.instance()._handleReframeDragEnd({ manager, value } as DraggingUpdate); - expect((manager.resetBounds as jest.Mock).mock.calls).toEqual([[]]); - const calls = (props.updateViewRangeTime as jest.Mock).mock.calls; - expect(calls).toEqual([[value, value, 'minimap']]); - }); - - it('handles dragged left (anchor is greater)', () => { - const value = 0.5; - const anchor = 0.6; - const time = { ...props.viewRange.time, reframe: { anchor } } as ViewRangeTime; - props = { ...props, viewRange: { time } }; - wrapper = shallow() - .dive() - .dive(); - wrapper.instance()._handleReframeDragEnd({ manager, value } as DraggingUpdate); - - expect((manager.resetBounds as jest.Mock).mock.calls).toEqual([[]]); - const calls = (props.updateViewRangeTime as jest.Mock).mock.calls; - expect(calls).toEqual([[value, anchor, 'minimap']]); - }); - - it('handles dragged right (anchor is less)', () => { - const value = 0.5; - const anchor = 0.4; - const time = { ...props.viewRange.time, reframe: { anchor } } as ViewRangeTime; - props = { ...props, viewRange: { time } }; - wrapper = shallow() - .dive() - .dive(); - wrapper.instance()._handleReframeDragEnd({ manager, value } as DraggingUpdate); - - expect((manager.resetBounds as jest.Mock).mock.calls).toEqual([[]]); - const calls = (props.updateViewRangeTime as jest.Mock).mock.calls; - expect(calls).toEqual([[anchor, value, 'minimap']]); - }); - }); - }); - - describe('scrubber', () => { - it('prevents the cursor from being drawn on scrubber mouseover', () => { - wrapper.instance()._handleScrubberEnterLeave({ type: EUpdateTypes.MouseEnter } as DraggingUpdate); - expect(wrapper.state('preventCursorLine')).toBe(true); - }); - - it('prevents the cursor from being drawn on scrubber mouseleave', () => { - wrapper.instance()._handleScrubberEnterLeave({ type: EUpdateTypes.MouseLeave } as DraggingUpdate); - expect(wrapper.state('preventCursorLine')).toBe(false); - }); - - describe('drag start and update', () => { - it('stops propagation on drag start', () => { - const stopPropagation = jest.fn(); - const update = { - event: { stopPropagation }, - type: EUpdateTypes.DragStart, - }; - wrapper.instance()._handleScrubberDragUpdate(update as unknown as DraggingUpdate); - expect(stopPropagation.mock.calls).toEqual([[]]); - }); - - it('updates the viewRange for shiftStart and shiftEnd', () => { - const instance = wrapper.instance(); - const value = 0.5; - const cases = [ - { - dragUpdate: { - value, - tag: dragTypes.SHIFT_START, - type: EUpdateTypes.DragMove, - }, - viewRangeUpdate: { shiftStart: value }, - }, - { - dragUpdate: { - value, - tag: dragTypes.SHIFT_END, - type: EUpdateTypes.DragMove, - }, - viewRangeUpdate: { shiftEnd: value }, - }, - ]; - cases.forEach((_case) => { - instance._handleScrubberDragUpdate(_case.dragUpdate as DraggingUpdate); - expect(props.updateNextViewRangeTime).lastCalledWith(_case.viewRangeUpdate); - }); - }); - }); - - it('updates the view on drag end', () => { - const instance = wrapper.instance(); - const [viewStart, viewEnd] = props.viewRange.time.current; - const value = 0.5; - const cases = [ - { - dragUpdate: { - value, - manager: { resetBounds: jest.fn() }, - tag: dragTypes.SHIFT_START, - }, - viewRangeUpdate: [value, viewEnd], - }, - { - dragUpdate: { - value, - manager: { resetBounds: jest.fn() }, - tag: dragTypes.SHIFT_END, - }, - viewRangeUpdate: [viewStart, value], - }, - ]; - cases.forEach((_case) => { - const { manager } = _case.dragUpdate; - wrapper.setState({ preventCursorLine: true }); - expect(wrapper.state('preventCursorLine')).toBe(true); - instance._handleScrubberDragEnd(_case.dragUpdate as unknown as DraggingUpdate); - expect(wrapper.state('preventCursorLine')).toBe(false); - expect((manager.resetBounds as jest.Mock).mock.calls).toEqual([[]]); - expect(props.updateViewRangeTime).lastCalledWith(..._case.viewRangeUpdate, 'minimap'); - }); - }); - }); - - describe('.ViewingLayer--resetZoom', () => { - it('should not render .ViewingLayer--resetZoom if props.viewRange.time.current = [0,1]', () => { - expect(wrapper.find(`.${getStyles(createTheme()).ViewingLayerResetZoom}`).length).toBe(0); - wrapper.setProps({ viewRange: { time: { current: [0, 1] } } }); - expect(wrapper.find(`.${getStyles(createTheme()).ViewingLayerResetZoom}`).length).toBe(0); - }); - - it('should render ViewingLayer--resetZoom if props.viewRange.time.current[0] !== 0', () => { - // If the test fails on the following expect statement, this may be a false negative - expect(wrapper.find(`.${getStyles(createTheme()).ViewingLayerResetZoom}`).length).toBe(0); - wrapper.setProps({ viewRange: { time: { current: [0.1, 1] } } }); - expect(wrapper.find(`.${getStyles(createTheme()).ViewingLayerResetZoom}`).length).toBe(1); - }); - - it('should render ViewingLayer--resetZoom if props.viewRange.time.current[1] !== 1', () => { - // If the test fails on the following expect statement, this may be a false negative - expect(wrapper.find(`.${getStyles(createTheme()).ViewingLayerResetZoom}`).length).toBe(0); - wrapper.setProps({ viewRange: { time: { current: [0, 0.9] } } }); - expect(wrapper.find(`.${getStyles(createTheme()).ViewingLayerResetZoom}`).length).toBe(1); - }); - - it('should call props.updateViewRangeTime when clicked', () => { - wrapper.setProps({ viewRange: { time: { current: [0.1, 0.9] } } }); - const resetZoomButton = wrapper.find(`.${getStyles(createTheme()).ViewingLayerResetZoom}`); - // If the test fails on the following expect statement, this may be a false negative caused - // by a regression to rendering. - expect(resetZoomButton.length).toBe(1); - - resetZoomButton.simulate('click'); - expect(props.updateViewRangeTime).lastCalledWith(0, 1); - }); - }); + it('renders ViewingLayerCursorGuide when the cursor position is defined', () => { + props = { ...props, viewRange: { time: { current: [0.1, 1], cursor: 0.5 } } }; + render(); + expect(screen.getByTestId('ViewingLayerCursorGuide')).toBeTruthy(); }); - it('renders a ', () => { - expect(wrapper.find(GraphTicks).length).toBe(1); + it('renders ', () => { + render(); + expect(screen.getByTestId('ticks')).toBeTruthy(); + }); + + it('renders the scrubber component lines in the correct locations when an area of the minimap is selected', () => { + props = { ...props, viewRange: { time: { current: [0.3, 0.7] } } }; + render(); + expect(screen.getAllByTestId('scrubber-component-line')[0]).toHaveAttribute('x1', '30%'); + expect(screen.getAllByTestId('scrubber-component-line')[1]).toHaveAttribute('x1', '70%'); + }); + + it('renders the scrubbers', () => { + render(); + expect(screen.getAllByTestId('scrubber-component')).toBeTruthy(); }); it('renders a filtering box if leftBound exists', () => { - const _props = { ...props, viewRange: getViewRange(0.2, 1) }; - wrapper = shallow() - .dive() - .dive(); - - const leftBox = wrapper.find(`.${getStyles(createTheme()).ViewingLayerInactive}`); - expect(leftBox.length).toBe(1); - const width = Number(leftBox.prop('width')?.toString().slice(0, -1)); - const x = leftBox.prop('x'); - expect(Math.round(width)).toBe(20); - expect(x).toBe(0); + props = { ...props, viewRange: { time: { current: [0.1, 0.9] } } }; + render(); + expect(screen.getByTestId('left-ViewingLayerInactive')).toHaveAttribute('width', '10%'); + expect(screen.getByTestId('left-ViewingLayerInactive')).toHaveAttribute('x', '0'); }); it('renders a filtering box if rightBound exists', () => { - const _props = { ...props, viewRange: getViewRange(0, 0.8) }; - wrapper = shallow() - .dive() - .dive(); - - const rightBox = wrapper.find(`.${getStyles(createTheme()).ViewingLayerInactive}`); - expect(rightBox.length).toBe(1); - const width = Number(rightBox.prop('width')?.toString().slice(0, -1)); - const x = Number(rightBox.prop('x')?.toString().slice(0, -1)); - expect(Math.round(width)).toBe(20); - expect(x).toBe(80); + props = { ...props, viewRange: { time: { current: [0, 0.8] } } }; + render(); + expect(screen.getByTestId('right-ViewingLayerInactive')).toHaveAttribute('width', '20%'); + expect(screen.getByTestId('right-ViewingLayerInactive')).toHaveAttribute('x', '80%'); }); - it('renders handles for the timeRangeFilter', () => { - const [viewStart, viewEnd] = props.viewRange.time.current; - let scrubber = ; - expect(wrapper.containsMatchingElement(scrubber)).toBeTruthy(); - scrubber = ; - expect(wrapper.containsMatchingElement(scrubber)).toBeTruthy(); + describe('reset selection button', () => { + it('should not render the reset selection button if props.viewRange.time.current = [0,1]', () => { + render(); + expect(screen.queryByRole('button', { hidden: true })).toBeNull(); + }); + + it('should render the reset selection button if props.viewRange.time.current[0] !== 0', () => { + props = { ...props, viewRange: { time: { current: [0.1, 1] } } }; + render(); + expect(screen.queryByRole('button', { hidden: true })).toBeInTheDocument(); + }); + + it('should render the reset selection button if props.viewRange.time.current[1] !== 1', () => { + props = { ...props, viewRange: { time: { current: [0, 0.9] } } }; + render(); + expect(screen.queryByRole('button', { hidden: true })).toBeInTheDocument(); + }); + + it('should call props.updateViewRangeTime when clicked', async () => { + props = { ...props, viewRange: { time: { current: [0.1, 0.9] } } }; + render(); + const button = screen.queryByRole('button', { hidden: true })!; + await userEvent.click(button); + expect(props.updateViewRangeTime).toHaveBeenCalledWith(0, 1); + }); }); }); diff --git a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.tsx b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.tsx index 17025ba4a65..eb2277c1b3d 100644 --- a/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.tsx +++ b/packages/jaeger-ui-components/src/TracePageHeader/SpanGraph/ViewingLayer.tsx @@ -364,7 +364,14 @@ export class UnthemedViewingLayer extends React.PureComponent {leftInactive > 0 && ( - + )} {rightInactive > 0 && ( )} @@ -384,6 +392,7 @@ export class UnthemedViewingLayer extends React.PureComponent )} {shiftStart != null && this._getMarkers(viewStart, shiftStart)} From a4c2237d16162b1680ca99d5e27012a3b708cdc2 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Mon, 16 Jan 2023 11:26:56 +0100 Subject: [PATCH 06/65] AzureMonitor: Add region to the resource info (#61504) --- .../MetricsQueryEditor/MetricsQueryEditor.tsx | 1 + .../components/ResourcePicker/NestedRow.tsx | 2 +- .../components/ResourcePicker/ResourcePicker.tsx | 2 +- .../components/ResourcePicker/types.ts | 1 + .../components/ResourcePicker/utils.test.ts | 2 ++ .../components/ResourcePicker/utils.ts | 11 ++++++++--- .../resourcePicker/resourcePickerData.test.ts | 3 ++- .../resourcePicker/resourcePickerData.ts | 3 ++- .../grafana-azure-monitor-datasource/types/query.ts | 2 ++ 9 files changed, 20 insertions(+), 7 deletions(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.tsx index 39b39a76555..1d1c8c89f4d 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.tsx @@ -42,6 +42,7 @@ const MetricsQueryEditor: React.FC = ({ resourceGroup: query.azureMonitor?.resources?.[0]?.resourceGroup, metricNamespace: query.azureMonitor?.metricNamespace, resourceName: query.azureMonitor?.resources?.[0]?.resourceName, + region: query.azureMonitor?.region, }; return ( diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/NestedRow.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/NestedRow.tsx index 15d11998a84..eea2a9cfbb3 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/NestedRow.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/NestedRow.tsx @@ -76,7 +76,7 @@ const NestedRow: React.FC = ({ {row.typeLabel} - {row.location ?? '-'} + {row.locationDisplayName ?? '-'} {isOpen && diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.tsx index 4b9b1fe4f19..5a45f394632 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.tsx @@ -113,7 +113,7 @@ const ResourcePicker = ({ const handleSelectionChanged = useCallback( (row: ResourceRow, isSelected: boolean) => { isSelected - ? setInternalSelected(resourceIsString ? row.uri : parseResourceDetails(row.uri)) + ? setInternalSelected(resourceIsString ? row.uri : parseResourceDetails(row.uri, row.location)) : setInternalSelected(resourceIsString ? '' : {}); }, [resourceIsString] diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/types.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/types.ts index 7183f427bf9..d6022dd8eaa 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/types.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/types.ts @@ -12,6 +12,7 @@ export interface ResourceRow { name: string; type: ResourceRowType; typeLabel: string; + locationDisplayName?: string; location?: string; children?: ResourceRowGroup; } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.test.ts index c0101c4b63b..dbe224d5900 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.test.ts @@ -177,6 +177,7 @@ describe('AzureMonitor ResourcePicker utils', () => { resourceGroup: 'rg', metricNamespace: 'Microsoft.Storage/storageAccounts', resourceName: 'testacct', + region: 'westus', }) ).toMatchObject({ subscription: 'sub', @@ -184,6 +185,7 @@ describe('AzureMonitor ResourcePicker utils', () => { aggregation: undefined, metricName: undefined, metricNamespace: 'microsoft.storage/storageaccounts', + region: 'westus', resources: [ { resourceGroup: 'rg', diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.ts index 16ae043f0d1..37900a6d151 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.ts @@ -34,7 +34,7 @@ function parseNamespaceAndName(metricNamespaceAndName?: string) { return { metricNamespace: namespaceArray.join('/'), resourceName: resourceNameArray.join('/') }; } -export function parseResourceURI(resourceURI: string) { +export function parseResourceURI(resourceURI: string): AzureMetricResource { const matches = RESOURCE_URI_REGEX.exec(resourceURI); const groups: RegexGroups = matches?.groups ?? {}; const { subscription, resourceGroup, metricNamespaceAndResource } = groups; @@ -43,9 +43,13 @@ export function parseResourceURI(resourceURI: string) { return { subscription, resourceGroup, metricNamespace, resourceName }; } -export function parseResourceDetails(resource: string | AzureMetricResource) { +export function parseResourceDetails(resource: string | AzureMetricResource, location?: string) { if (typeof resource === 'string') { - return parseResourceURI(resource); + const res = parseResourceURI(resource); + if (location) { + res.region = location; + } + return res; } return resource; } @@ -146,6 +150,7 @@ export function setResource(query: AzureMonitorQuery, resource?: string | AzureM azureMonitor: { ...query.azureMonitor, metricNamespace: resource?.metricNamespace?.toLocaleLowerCase(), + region: resource?.region, resources: [{ resourceGroup: resource?.resourceGroup, resourceName: resource?.resourceName }], metricName: undefined, aggregation: undefined, diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.test.ts index 5e379f8fd93..c5295fee21b 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.test.ts @@ -241,7 +241,8 @@ describe('AzureMonitor resourcePickerData', () => { id: 'web-server', name: 'web-server', type: 'Resource', - location: 'North Europe', + location: 'northeurope', + locationDisplayName: 'North Europe', resourceGroupName: 'dev', typeLabel: 'Microsoft.Compute/virtualMachines', uri: '/subscriptions/def-456/resourceGroups/dev/providers/Microsoft.Compute/virtualMachines/web-server', diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.ts index 0dd259b7143..db23d7f4d40 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.ts @@ -247,7 +247,8 @@ export default class ResourcePickerData extends DataSourceWithBackend Date: Mon, 16 Jan 2023 10:54:15 +0000 Subject: [PATCH 07/65] AuthN: User Quota (#61540) * remove reqContext from quota checks in login * add guards for nil ScopeParams --- pkg/services/apikey/apikeyimpl/sqlx_store.go | 2 +- pkg/services/apikey/apikeyimpl/xorm_store.go | 2 +- pkg/services/authn/authnimpl/usersync/usersync.go | 15 +++++++++++++++ pkg/services/dashboards/database/database.go | 2 +- pkg/services/datasources/service/store.go | 2 +- pkg/services/login/loginservice/loginservice.go | 3 ++- pkg/services/ngalert/api/api.go | 8 +++++++- pkg/services/org/orgimpl/store.go | 4 ++-- 8 files changed, 30 insertions(+), 8 deletions(-) diff --git a/pkg/services/apikey/apikeyimpl/sqlx_store.go b/pkg/services/apikey/apikeyimpl/sqlx_store.go index b9935a58123..7476976f496 100644 --- a/pkg/services/apikey/apikeyimpl/sqlx_store.go +++ b/pkg/services/apikey/apikeyimpl/sqlx_store.go @@ -161,7 +161,7 @@ func (ss *sqlxStore) Count(ctx context.Context, scopeParams *quota.ScopeParamete u.Set(tag, r.Count) } - if scopeParams.OrgID != 0 { + if scopeParams != nil && scopeParams.OrgID != 0 { if err := ss.sess.Get(ctx, &r, `SELECT COUNT(*) AS count FROM api_key WHERE org_id = ?`, scopeParams.OrgID); err != nil { return u, err } else { diff --git a/pkg/services/apikey/apikeyimpl/xorm_store.go b/pkg/services/apikey/apikeyimpl/xorm_store.go index bf2ba4ce6d4..c777b659346 100644 --- a/pkg/services/apikey/apikeyimpl/xorm_store.go +++ b/pkg/services/apikey/apikeyimpl/xorm_store.go @@ -200,7 +200,7 @@ func (ss *sqlStore) Count(ctx context.Context, scopeParams *quota.ScopeParameter u.Set(tag, r.Count) } - if scopeParams.OrgID != 0 { + if scopeParams != nil && scopeParams.OrgID != 0 { if err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { rawSQL := "SELECT COUNT(*) AS count FROM api_key WHERE org_id = ?" if _, err := sess.SQL(rawSQL, scopeParams.OrgID).Get(&r); err != nil { diff --git a/pkg/services/authn/authnimpl/usersync/usersync.go b/pkg/services/authn/authnimpl/usersync/usersync.go index 2fd6803440a..e200e0d1ef3 100644 --- a/pkg/services/authn/authnimpl/usersync/usersync.go +++ b/pkg/services/authn/authnimpl/usersync/usersync.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/user" ) @@ -44,6 +45,20 @@ func (s *UserSync) SyncUser(ctx context.Context, id *authn.Identity, _ *authn.Re return login.ErrSignupNotAllowed } + // quota check (FIXME: (jguer) this should be done in the user service) + // we may insert in both user and org_user tables + // therefore we need to query check quota for both user and org services + for _, srv := range []string{user.QuotaTargetSrv, org.QuotaTargetSrv} { + limitReached, errLimit := s.quotaService.CheckQuotaReached(ctx, quota.TargetSrv(srv), nil) + if errLimit != nil { + s.log.Warn("error getting user quota.", "error", errLimit) + return login.ErrGettingUserQuota + } + if limitReached { + return login.ErrUsersQuotaReached + } + } + // create user var errCreate error usr, errCreate = s.createUser(ctx, id) diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index 46ef267d094..b63830bcea5 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -333,7 +333,7 @@ func (d *DashboardStore) Count(ctx context.Context, scopeParams *quota.ScopePara u.Set(tag, r.Count) } - if scopeParams.OrgID != 0 { + if scopeParams != nil && scopeParams.OrgID != 0 { if err := d.store.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { rawSQL := fmt.Sprintf("SELECT COUNT(*) AS count FROM dashboard WHERE org_id=? AND is_folder=%s", d.store.GetDialect().BooleanStr(false)) if _, err := sess.SQL(rawSQL, scopeParams.OrgID).Get(&r); err != nil { diff --git a/pkg/services/datasources/service/store.go b/pkg/services/datasources/service/store.go index 9cc07b3a2b8..306f43d2c35 100644 --- a/pkg/services/datasources/service/store.go +++ b/pkg/services/datasources/service/store.go @@ -198,7 +198,7 @@ func (ss *SqlStore) Count(ctx context.Context, scopeParams *quota.ScopeParameter u.Set(tag, r.Count) } - if scopeParams.OrgID != 0 { + if scopeParams != nil && scopeParams.OrgID != 0 { if err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { rawSQL := "SELECT COUNT(*) AS count FROM data_source WHERE org_id=?" if _, err := sess.SQL(rawSQL, scopeParams.OrgID).Get(&r); err != nil { diff --git a/pkg/services/login/loginservice/loginservice.go b/pkg/services/login/loginservice/loginservice.go index e9231ee6995..37f38af1373 100644 --- a/pkg/services/login/loginservice/loginservice.go +++ b/pkg/services/login/loginservice/loginservice.go @@ -62,10 +62,11 @@ func (ls *Implementation) UpsertUser(ctx context.Context, cmd *models.UpsertUser return login.ErrSignupNotAllowed } + // quota check (FIXME: (jguer) this should be done in the user service) // we may insert in both user and org_user tables // therefore we need to query check quota for both user and org services for _, srv := range []string{user.QuotaTargetSrv, org.QuotaTargetSrv} { - limitReached, errLimit := ls.QuotaService.QuotaReached(cmd.ReqContext, quota.TargetSrv(srv)) + limitReached, errLimit := ls.QuotaService.CheckQuotaReached(ctx, quota.TargetSrv(srv), nil) if errLimit != nil { cmd.ReqContext.Logger.Warn("Error getting user quota.", "error", errLimit) return login.ErrGettingUserQuota diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index 3defbc4c995..045fc3e0e06 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -156,7 +156,13 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) { func (api *API) Usage(ctx context.Context, scopeParams *quota.ScopeParameters) (*quota.Map, error) { u := "a.Map{} - if orgUsage, err := api.RuleStore.Count(ctx, scopeParams.OrgID); err != nil { + + var orgID int64 = 0 + if scopeParams != nil { + orgID = scopeParams.OrgID + } + + if orgUsage, err := api.RuleStore.Count(ctx, orgID); err != nil { return u, err } else { tag, err := quota.NewTag(models.QuotaTargetSrv, models.QuotaTarget, quota.OrgScope) diff --git a/pkg/services/org/orgimpl/store.go b/pkg/services/org/orgimpl/store.go index f281d8ccb70..aafa97ac96d 100644 --- a/pkg/services/org/orgimpl/store.go +++ b/pkg/services/org/orgimpl/store.go @@ -420,7 +420,7 @@ func (ss *sqlStore) Count(ctx context.Context, scopeParams *quota.ScopeParameter u.Set(tag, r.Count) } - if scopeParams.OrgID != 0 { + if scopeParams != nil && scopeParams.OrgID != 0 { if err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { rawSQL := fmt.Sprintf("SELECT COUNT(*) AS count FROM (SELECT user_id FROM org_user WHERE org_id=? AND user_id IN (SELECT id AS user_id FROM %s WHERE is_service_account=%s)) as subq", ss.db.GetDialect().Quote("user"), @@ -441,7 +441,7 @@ func (ss *sqlStore) Count(ctx context.Context, scopeParams *quota.ScopeParameter } } - if scopeParams.UserID != 0 { + if scopeParams != nil && scopeParams.UserID != 0 { if err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { // should we exclude service accounts? rawSQL := "SELECT COUNT(*) AS count FROM org_user WHERE user_id=?" From be3b81fecdd5b804679db65fdefa17953ae01a8d Mon Sep 17 00:00:00 2001 From: Jo Date: Mon, 16 Jan 2023 11:15:14 +0000 Subject: [PATCH 08/65] AuthN: Readd user protection service to user sync (#61534) * add user protection service to user sync * fix tests --- pkg/services/authn/authnimpl/service.go | 3 +- .../authn/authnimpl/usersync/usersync.go | 32 +++++++++++++++---- .../authn/authnimpl/usersync/usersync_test.go | 20 +++--------- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index 37dcc4b324d..9a874c33c41 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -40,6 +40,7 @@ func ProvideService( accessControlService accesscontrol.Service, apikeyService apikey.Service, userService user.Service, jwtService auth.JWTVerifierService, + userProtectionService login.UserProtectionService, loginAttempts loginattempt.Service, quotaService quota.Service, authInfoService login.AuthInfoService, renderService rendering.Service, ) *Service { @@ -83,7 +84,7 @@ func ProvideService( } // FIXME (jguer): move to User package - userSyncService := sync.ProvideUserSync(userService, authInfoService, quotaService) + userSyncService := sync.ProvideUserSync(userService, userProtectionService, authInfoService, quotaService) orgUserSyncService := sync.ProvideOrgSync(userService, orgService, accessControlService) s.RegisterPostAuthHook(userSyncService.SyncUser) s.RegisterPostAuthHook(orgUserSyncService.SyncOrgUser) diff --git a/pkg/services/authn/authnimpl/usersync/usersync.go b/pkg/services/authn/authnimpl/usersync/usersync.go index e200e0d1ef3..0736cc2891a 100644 --- a/pkg/services/authn/authnimpl/usersync/usersync.go +++ b/pkg/services/authn/authnimpl/usersync/usersync.go @@ -12,23 +12,37 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/util/errutil" ) -func ProvideUserSync(userService user.Service, authInfoService login.AuthInfoService, quotaService quota.Service) *UserSync { - return &UserSync{userService, authInfoService, quotaService, log.New("user.sync")} +var ( + errUserProtection = errutil.NewBase(errutil.StatusForbidden, + "user.sync.protected role", errutil.WithPublicMessage("Unable to sync due to protected role")) +) + +func ProvideUserSync(userService user.Service, + userProtectionService login.UserProtectionService, + authInfoService login.AuthInfoService, quotaService quota.Service) *UserSync { + return &UserSync{ + userService: userService, + authInfoService: authInfoService, + userProtectionService: userProtectionService, + quotaService: quotaService, + log: log.New("user.sync"), + } } type UserSync struct { - userService user.Service - authInfoService login.AuthInfoService - quotaService quota.Service - log log.Logger + userService user.Service + authInfoService login.AuthInfoService + userProtectionService login.UserProtectionService + quotaService quota.Service + log log.Logger } // SyncUser syncs a user with the database func (s *UserSync) SyncUser(ctx context.Context, id *authn.Identity, _ *authn.Request) error { if !id.ClientParams.SyncUser { - s.log.Debug("Not syncing user", "auth_module", id.AuthModule, "auth_id", id.AuthID) return nil } @@ -67,6 +81,10 @@ func (s *UserSync) SyncUser(ctx context.Context, id *authn.Identity, _ *authn.Re } } + if errProtection := s.userProtectionService.AllowUserMapping(usr, id.AuthModule); errProtection != nil { + return errUserProtection.Errorf("user mapping not allowed: %w", errProtection) + } + // update user if errUpdate := s.updateUserAttributes(ctx, usr, id); errUpdate != nil { return errUpdate diff --git a/pkg/services/authn/authnimpl/usersync/usersync_test.go b/pkg/services/authn/authnimpl/usersync/usersync_test.go index 634f6b3c93d..f836bd01071 100644 --- a/pkg/services/authn/authnimpl/usersync/usersync_test.go +++ b/pkg/services/authn/authnimpl/usersync/usersync_test.go @@ -4,10 +4,10 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/login/authinfoservice" "github.com/grafana/grafana/pkg/services/login/logintest" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/quota/quotatest" @@ -29,6 +29,8 @@ func ptrInt64(i int64) *int64 { } func TestUserSync_SyncUser(t *testing.T) { + userProtection := &authinfoservice.OSSUserProtectionImpl{} + authFakeNil := &logintest.AuthInfoServiceFake{ ExpectedUser: nil, ExpectedError: user.ErrUserNotFound, @@ -82,7 +84,6 @@ func TestUserSync_SyncUser(t *testing.T) { userService user.Service authInfoService login.AuthInfoService quotaService quota.Service - log log.Logger } type args struct { ctx context.Context @@ -101,7 +102,6 @@ func TestUserSync_SyncUser(t *testing.T) { userService: userService, authInfoService: authFakeNil, quotaService: "atest.FakeQuotaService{}, - log: log.NewNopLogger(), }, args: args{ ctx: context.Background(), @@ -140,7 +140,6 @@ func TestUserSync_SyncUser(t *testing.T) { userService: userService, authInfoService: authFakeNil, quotaService: "atest.FakeQuotaService{}, - log: log.NewNopLogger(), }, args: args{ ctx: context.Background(), @@ -182,7 +181,6 @@ func TestUserSync_SyncUser(t *testing.T) { userService: userService, authInfoService: authFakeNil, quotaService: "atest.FakeQuotaService{}, - log: log.NewNopLogger(), }, args: args{ ctx: context.Background(), @@ -224,7 +222,6 @@ func TestUserSync_SyncUser(t *testing.T) { userService: userService, authInfoService: authFakeNil, quotaService: "atest.FakeQuotaService{}, - log: log.NewNopLogger(), }, args: args{ ctx: context.Background(), @@ -266,7 +263,6 @@ func TestUserSync_SyncUser(t *testing.T) { userService: userService, authInfoService: authFakeUserID, quotaService: "atest.FakeQuotaService{}, - log: log.NewNopLogger(), }, args: args{ ctx: context.Background(), @@ -309,7 +305,6 @@ func TestUserSync_SyncUser(t *testing.T) { userService: userService, authInfoService: authFakeNil, quotaService: "atest.FakeQuotaService{}, - log: log.NewNopLogger(), }, args: args{ ctx: context.Background(), @@ -338,7 +333,6 @@ func TestUserSync_SyncUser(t *testing.T) { userService: userServiceNil, authInfoService: authFakeNil, quotaService: "atest.FakeQuotaService{}, - log: log.NewNopLogger(), }, args: args{ ctx: context.Background(), @@ -389,7 +383,6 @@ func TestUserSync_SyncUser(t *testing.T) { userService: userServiceMod, authInfoService: authFakeNil, quotaService: "atest.FakeQuotaService{}, - log: log.NewNopLogger(), }, args: args{ ctx: context.Background(), @@ -433,12 +426,7 @@ func TestUserSync_SyncUser(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - s := &UserSync{ - userService: tt.fields.userService, - authInfoService: tt.fields.authInfoService, - quotaService: tt.fields.quotaService, - log: tt.fields.log, - } + s := ProvideUserSync(tt.fields.userService, userProtection, tt.fields.authInfoService, tt.fields.quotaService) err := s.SyncUser(tt.args.ctx, tt.args.id, nil) if tt.wantErr { require.Error(t, err) From 0d513224af410d3bc4bc1e762365791270b218c0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Jan 2023 12:46:42 +0100 Subject: [PATCH 09/65] Update dependency @lezer/lr to v1.3.1 (#61341) 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 d1c7d835bde..0938ff5d28a 100644 --- a/package.json +++ b/package.json @@ -261,7 +261,7 @@ "@leeoniya/ufuzzy": "0.9.1", "@lezer/common": "1.0.1", "@lezer/highlight": "1.1.2", - "@lezer/lr": "1.2.3", + "@lezer/lr": "1.3.1", "@opentelemetry/api": "1.3.0", "@opentelemetry/exporter-collector": "0.25.0", "@opentelemetry/semantic-conventions": "1.8.0", diff --git a/yarn.lock b/yarn.lock index 45fa0fb762d..0fcfb12c7a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7107,12 +7107,12 @@ __metadata: languageName: node linkType: hard -"@lezer/lr@npm:1.2.3": - version: 1.2.3 - resolution: "@lezer/lr@npm:1.2.3" +"@lezer/lr@npm:1.3.1": + version: 1.3.1 + resolution: "@lezer/lr@npm:1.3.1" dependencies: "@lezer/common": ^1.0.0 - checksum: 118db077d32f6eb9d6b219c766d7e5715c6c004d89d76ed0137ea2a364aeca67f21dc5924ea67a649b5de1d13bfbb85571923b38536e6995820eb0a5c8844594 + checksum: 01b421b9724d43a00a97639344cd89c539e9740b7bedc8270882192e4205f68679e5b7c6837dde7756970a418bd4e710e4a778ab7c6713a2331bea3049232ca8 languageName: node linkType: hard @@ -22043,7 +22043,7 @@ __metadata: "@leeoniya/ufuzzy": 0.9.1 "@lezer/common": 1.0.1 "@lezer/highlight": 1.1.2 - "@lezer/lr": 1.2.3 + "@lezer/lr": 1.3.1 "@opentelemetry/api": 1.3.0 "@opentelemetry/exporter-collector": 0.25.0 "@opentelemetry/semantic-conventions": 1.8.0 From 7e51eac7405f66b03dabb7ca71dfc9302a6bbc68 Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Mon, 16 Jan 2023 12:52:55 +0100 Subject: [PATCH 10/65] Chore: Remove unused models (#61533) * Chore: Remove unused models * put AuthModuleConversion back --- pkg/models/address.go | 10 ---------- pkg/models/dashboards.go | 16 ---------------- pkg/models/helpflags.go | 8 -------- pkg/models/plugin_settings.go | 8 -------- 4 files changed, 42 deletions(-) delete mode 100644 pkg/models/address.go delete mode 100644 pkg/models/helpflags.go diff --git a/pkg/models/address.go b/pkg/models/address.go deleted file mode 100644 index 0cf8aaf7ddb..00000000000 --- a/pkg/models/address.go +++ /dev/null @@ -1,10 +0,0 @@ -package models - -type Address struct { - Address1 string `json:"address1"` - Address2 string `json:"address2"` - City string `json:"city"` - ZipCode string `json:"zipCode"` - State string `json:"state"` - Country string `json:"country"` -} diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index d4aabc68e6c..85e628d0fd0 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -172,10 +172,6 @@ func GetFolderUrl(folderUid string, slug string) string { return fmt.Sprintf("%s/dashboards/f/%s/%s", setting.AppSubUrl, folderUid, slug) } -type ValidateDashboardBeforeSaveResult struct { - IsParentFolderChanged bool -} - // // COMMANDS // @@ -261,18 +257,6 @@ type GetDashboardsByPluginIdQuery struct { Result []*Dashboard } -type GetDashboardSlugByIdQuery struct { - Id int64 - Result string -} - -type GetDashboardsBySlugQuery struct { - OrgId int64 - Slug string - - Result []*Dashboard -} - type DashboardRef struct { Uid string Slug string diff --git a/pkg/models/helpflags.go b/pkg/models/helpflags.go deleted file mode 100644 index 48b35954aa0..00000000000 --- a/pkg/models/helpflags.go +++ /dev/null @@ -1,8 +0,0 @@ -package models - -import "github.com/grafana/grafana/pkg/services/user" - -type SetUserHelpFlagCommand struct { - HelpFlags1 user.HelpFlags1 - UserId int64 -} diff --git a/pkg/models/plugin_settings.go b/pkg/models/plugin_settings.go index 2052976917c..e9e46191f03 100644 --- a/pkg/models/plugin_settings.go +++ b/pkg/models/plugin_settings.go @@ -57,14 +57,6 @@ type UpdatePluginSettingVersionCmd struct { // --------------------- // QUERIES -type PluginSettingInfoDTO struct { - OrgId int64 - PluginId string - Enabled bool - Pinned bool - PluginVersion string -} - type GetPluginSettingByIdQuery struct { PluginId string OrgId int64 From 7e505ea49cc4a2914b7245dcb4f4a03846abf6c8 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Mon, 16 Jan 2023 13:07:02 +0100 Subject: [PATCH 11/65] Loki Autocomplete: Suggest only possible labels for unwrap (#61411) * feat(loki-autocomplete): filter valid labels for unwrap autocomplete suggestions * refactor(unwrap): reuse new function in builder --- .../datasource/loki/LanguageProvider.test.ts | 11 ++++++- .../datasource/loki/LanguageProvider.ts | 17 ++++++++--- .../CompletionDataProvider.test.ts | 1 + .../completions.test.ts | 19 +++++++++++- .../monaco-completion-provider/completions.ts | 4 +-- .../components/UnwrapParamEditor.tsx | 30 ++----------------- .../datasource/loki/responseUtils.test.ts | 14 +++++++++ .../plugins/datasource/loki/responseUtils.ts | 26 +++++++++++++++- 8 files changed, 86 insertions(+), 36 deletions(-) diff --git a/public/app/plugins/datasource/loki/LanguageProvider.test.ts b/public/app/plugins/datasource/loki/LanguageProvider.test.ts index a2429ffeb96..e57116ae9ff 100644 --- a/public/app/plugins/datasource/loki/LanguageProvider.test.ts +++ b/public/app/plugins/datasource/loki/LanguageProvider.test.ts @@ -6,7 +6,11 @@ import { TypeaheadInput } from '@grafana/ui'; import LanguageProvider, { LokiHistoryItem } from './LanguageProvider'; import { LokiDatasource } from './datasource'; import { createLokiDatasource, createMetadataRequest } from './mocks'; -import { extractLogParserFromDataFrame, extractLabelKeysFromDataFrame } from './responseUtils'; +import { + extractLogParserFromDataFrame, + extractLabelKeysFromDataFrame, + extractUnwrapLabelKeysFromDataFrame, +} from './responseUtils'; import { LokiQueryType } from './types'; jest.mock('./responseUtils'); @@ -304,11 +308,13 @@ describe('Query imports', () => { let datasource: LokiDatasource, languageProvider: LanguageProvider; const extractLogParserFromDataFrameMock = jest.mocked(extractLogParserFromDataFrame); const extractedLabelKeys = ['extracted', 'label']; + const unwrapLabelKeys = ['unwrap', 'labels']; beforeEach(() => { datasource = createLokiDatasource(); languageProvider = new LanguageProvider(datasource); jest.mocked(extractLabelKeysFromDataFrame).mockReturnValue(extractedLabelKeys); + jest.mocked(extractUnwrapLabelKeysFromDataFrame).mockReturnValue(unwrapLabelKeys); }); it('identifies selectors with JSON parser data', async () => { @@ -317,6 +323,7 @@ describe('Query imports', () => { expect(await languageProvider.getParserAndLabelKeys('{place="luna"}')).toEqual({ extractedLabelKeys, + unwrapLabelKeys, hasJSON: true, hasLogfmt: false, }); @@ -328,6 +335,7 @@ describe('Query imports', () => { expect(await languageProvider.getParserAndLabelKeys('{place="luna"}')).toEqual({ extractedLabelKeys, + unwrapLabelKeys, hasJSON: false, hasLogfmt: true, }); @@ -339,6 +347,7 @@ describe('Query imports', () => { expect(await languageProvider.getParserAndLabelKeys('{place="luna"}')).toEqual({ extractedLabelKeys: [], + unwrapLabelKeys: [], hasJSON: false, hasLogfmt: false, }); diff --git a/public/app/plugins/datasource/loki/LanguageProvider.ts b/public/app/plugins/datasource/loki/LanguageProvider.ts index ee98b9690fe..47936a29d4b 100644 --- a/public/app/plugins/datasource/loki/LanguageProvider.ts +++ b/public/app/plugins/datasource/loki/LanguageProvider.ts @@ -12,7 +12,11 @@ import { } from 'app/plugins/datasource/prometheus/language_utils'; import { LokiDatasource } from './datasource'; -import { extractLabelKeysFromDataFrame, extractLogParserFromDataFrame } from './responseUtils'; +import { + extractLabelKeysFromDataFrame, + extractLogParserFromDataFrame, + extractUnwrapLabelKeysFromDataFrame, +} from './responseUtils'; import syntax, { FUNCTIONS, PIPE_PARSERS, PIPE_OPERATORS } from './syntax'; import { LokiQuery, LokiQueryType } from './types'; @@ -465,15 +469,20 @@ export default class LokiLanguageProvider extends LanguageProvider { async getParserAndLabelKeys( selector: string - ): Promise<{ extractedLabelKeys: string[]; hasJSON: boolean; hasLogfmt: boolean }> { + ): Promise<{ extractedLabelKeys: string[]; hasJSON: boolean; hasLogfmt: boolean; unwrapLabelKeys: string[] }> { const series = await this.datasource.getDataSamples({ expr: selector, refId: 'data-samples' }); if (!series.length) { - return { extractedLabelKeys: [], hasJSON: false, hasLogfmt: false }; + return { extractedLabelKeys: [], unwrapLabelKeys: [], hasJSON: false, hasLogfmt: false }; } const { hasLogfmt, hasJSON } = extractLogParserFromDataFrame(series[0]); - return { extractedLabelKeys: extractLabelKeysFromDataFrame(series[0]), hasJSON, hasLogfmt }; + return { + extractedLabelKeys: extractLabelKeysFromDataFrame(series[0]), + unwrapLabelKeys: extractUnwrapLabelKeysFromDataFrame(series[0]), + hasJSON, + hasLogfmt, + }; } } diff --git a/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/CompletionDataProvider.test.ts b/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/CompletionDataProvider.test.ts index bc9270f62e6..6ecf0c0871c 100644 --- a/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/CompletionDataProvider.test.ts +++ b/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/CompletionDataProvider.test.ts @@ -50,6 +50,7 @@ const otherLabels: Label[] = [ const seriesLabels = { place: ['series', 'labels'], source: [], other: [] }; const parserAndLabelKeys = { extractedLabelKeys: ['extracted', 'label', 'keys'], + unwrapLabelKeys: ['unwrap', 'labels'], hasJSON: true, hasLogfmt: false, }; diff --git a/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/completions.test.ts b/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/completions.test.ts index d38edd3ba59..d914996f2f4 100644 --- a/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/completions.test.ts +++ b/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/completions.test.ts @@ -31,6 +31,7 @@ const labelNames = ['place', 'source']; const labelValues = ['moon', 'luna', 'server\\1']; // Source is duplicated to test handling duplicated labels const extractedLabelKeys = ['extracted', 'place', 'source']; +const unwrapLabelKeys = ['unwrap', 'labels']; const otherLabels: Label[] = [ { name: 'place', @@ -195,6 +196,7 @@ describe('getCompletions', () => { jest.spyOn(completionProvider, 'getLabelValues').mockResolvedValue(labelValues); jest.spyOn(completionProvider, 'getParserAndLabelKeys').mockResolvedValue({ extractedLabelKeys, + unwrapLabelKeys, hasJSON: false, hasLogfmt: false, }); @@ -327,6 +329,7 @@ describe('getCompletions', () => { async (afterPipe: boolean, hasSpace: boolean) => { jest.spyOn(completionProvider, 'getParserAndLabelKeys').mockResolvedValue({ extractedLabelKeys, + unwrapLabelKeys, hasJSON: true, hasLogfmt: false, }); @@ -343,6 +346,7 @@ describe('getCompletions', () => { async (afterPipe: boolean) => { jest.spyOn(completionProvider, 'getParserAndLabelKeys').mockResolvedValue({ extractedLabelKeys, + unwrapLabelKeys, hasJSON: false, hasLogfmt: true, }); @@ -368,7 +372,20 @@ describe('getCompletions', () => { const extractedCompletions = completions.filter((completion) => completion.type === 'LABEL_NAME'); const functionCompletions = completions.filter((completion) => completion.type === 'FUNCTION'); - expect(extractedCompletions).toHaveLength(3); + expect(extractedCompletions).toEqual([ + { + insertText: 'unwrap', + label: 'unwrap', + triggerOnInsert: false, + type: 'LABEL_NAME', + }, + { + insertText: 'labels', + label: 'labels', + triggerOnInsert: false, + type: 'LABEL_NAME', + }, + ]); expect(functionCompletions).toHaveLength(3); }); }); diff --git a/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/completions.ts b/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/completions.ts index 4e90f2a0bae..003118cc5ab 100644 --- a/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/completions.ts +++ b/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/completions.ts @@ -273,9 +273,9 @@ async function getAfterUnwrapCompletions( logQuery: string, dataProvider: CompletionDataProvider ): Promise { - const { extractedLabelKeys } = await dataProvider.getParserAndLabelKeys(logQuery); + const { unwrapLabelKeys } = await dataProvider.getParserAndLabelKeys(logQuery); - const labelCompletions: Completion[] = extractedLabelKeys.map((label) => ({ + const labelCompletions: Completion[] = unwrapLabelKeys.map((label) => ({ type: 'LABEL_NAME', label, insertText: label, diff --git a/public/app/plugins/datasource/loki/querybuilder/components/UnwrapParamEditor.tsx b/public/app/plugins/datasource/loki/querybuilder/components/UnwrapParamEditor.tsx index aa59e9093f9..d9400d30590 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/UnwrapParamEditor.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/UnwrapParamEditor.tsx @@ -1,14 +1,13 @@ -import { isNaN } from 'lodash'; import React, { useState } from 'react'; -import { isValidGoDuration, SelectableValue, toOption } from '@grafana/data'; +import { SelectableValue, toOption } from '@grafana/data'; import { Select } from '@grafana/ui'; import { getOperationParamId } from '../../../prometheus/querybuilder/shared/operationUtils'; import { QueryBuilderOperationParamEditorProps } from '../../../prometheus/querybuilder/shared/types'; import { LokiDatasource } from '../../datasource'; -import { isBytesString } from '../../languageUtils'; import { getLogQueryFromMetricsQuery, isValidQuery } from '../../queryUtils'; +import { extractUnwrapLabelKeysFromDataFrame } from '../../responseUtils'; import { lokiQueryModeller } from '../LokiQueryModeller'; import { LokiVisualQuery } from '../types'; @@ -62,30 +61,7 @@ async function loadUnwrapOptions( } const samples = await datasource.getDataSamples({ expr: logExpr, refId: 'unwrap_samples' }); - const labelsArray: Array<{ [key: string]: string }> | undefined = - samples[0]?.fields?.find((field) => field.name === 'labels')?.values.toArray() ?? []; - - if (!labelsArray || labelsArray.length === 0) { - return []; - } - - // We do this only for first label object, because we want to consider only labels that are present in all log lines - // possibleUnwrapLabels are labels with 1. number value OR 2. value that is valid go duration OR 3. bytes string value - const possibleUnwrapLabels = Object.keys(labelsArray[0]).filter((key) => { - const value = labelsArray[0][key]; - if (!value) { - return false; - } - return !isNaN(Number(value)) || isValidGoDuration(value) || isBytesString(value); - }); - - const unwrapLabels: string[] = []; - for (const label of possibleUnwrapLabels) { - // Add only labels that are present in every line to unwrapLabels - if (labelsArray.every((obj) => obj[label])) { - unwrapLabels.push(label); - } - } + const unwrapLabels = extractUnwrapLabelKeysFromDataFrame(samples[0]); const labelOptions = unwrapLabels.map((label) => ({ label, diff --git a/public/app/plugins/datasource/loki/responseUtils.test.ts b/public/app/plugins/datasource/loki/responseUtils.test.ts index aeb1e4f1814..b0721d875fa 100644 --- a/public/app/plugins/datasource/loki/responseUtils.test.ts +++ b/public/app/plugins/datasource/loki/responseUtils.test.ts @@ -8,6 +8,7 @@ import { extractLevelLikeLabelFromDataFrame, extractLogParserFromDataFrame, extractLabelKeysFromDataFrame, + extractUnwrapLabelKeysFromDataFrame, } from './responseUtils'; const frame: DataFrame = { @@ -105,3 +106,16 @@ describe('extractLabelKeysFromDataFrame', () => { expect(extractLabelKeysFromDataFrame(input)).toEqual(['level']); }); }); + +describe('extractUnwrapLabelKeysFromDataFrame', () => { + it('returns empty by default', () => { + const input = cloneDeep(frame); + input.fields[1].values = new ArrayVector([]); + expect(extractUnwrapLabelKeysFromDataFrame(input)).toEqual([]); + }); + it('extracts possible unwrap label keys', () => { + const input = cloneDeep(frame); + input.fields[1].values = new ArrayVector([{ number: 13 }]); + expect(extractUnwrapLabelKeysFromDataFrame(input)).toEqual(['number']); + }); +}); diff --git a/public/app/plugins/datasource/loki/responseUtils.ts b/public/app/plugins/datasource/loki/responseUtils.ts index 3a8c26281ec..29f462c0d9f 100644 --- a/public/app/plugins/datasource/loki/responseUtils.ts +++ b/public/app/plugins/datasource/loki/responseUtils.ts @@ -1,7 +1,9 @@ -import { DataFrame, FieldType, Labels } from '@grafana/data'; +import { DataFrame, FieldType, isValidGoDuration, Labels } from '@grafana/data'; import { isLogLineJSON, isLogLineLogfmt } from './lineParser'; +import { isBytesString } from './languageUtils'; + export function dataFrameHasLokiError(frame: DataFrame): boolean { const labelSets: Labels[] = frame.fields.find((f) => f.name === 'labels')?.values.toArray() ?? []; return labelSets.some((labels) => labels.__error__ !== undefined); @@ -46,6 +48,28 @@ export function extractLabelKeysFromDataFrame(frame: DataFrame): string[] { return Object.keys(labelsArray[0]); } +export function extractUnwrapLabelKeysFromDataFrame(frame: DataFrame): string[] { + const labelsArray: Array<{ [key: string]: string }> | undefined = + frame?.fields?.find((field) => field.name === 'labels')?.values.toArray() ?? []; + + if (!labelsArray?.length) { + return []; + } + + // We do this only for first label object, because we want to consider only labels that are present in all log lines + // possibleUnwrapLabels are labels with 1. number value OR 2. value that is valid go duration OR 3. bytes string value + const possibleUnwrapLabels = Object.keys(labelsArray[0]).filter((key) => { + const value = labelsArray[0][key]; + if (!value) { + return false; + } + return !isNaN(Number(value)) || isValidGoDuration(value) || isBytesString(value); + }); + + // Add only labels that are present in every line to unwrapLabels + return possibleUnwrapLabels.filter((label) => labelsArray.every((obj) => obj[label])); +} + export function extractHasErrorLabelFromDataFrame(frame: DataFrame): boolean { const labelField = frame.fields.find((field) => field.name === 'labels' && field.type === FieldType.other); if (labelField == null) { From c5e74ee607ebd07f27dbe10444a296be94be2485 Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Mon, 16 Jan 2023 13:16:01 +0100 Subject: [PATCH 12/65] Auth: Add skip_org_role_sync for AzureAD OAuth (#60322) * [WIP] Auth: add backend skipOrgRoleSync to AzureAD OAuth - add: skipOrgRoleSync - rename: skipOrgRoleSync to skipOrgRoleSyncBase (to make it clear that it is the base version of SocialBase) - add: tests for skipOrgRoleSync in AzureAD TODO: - [ ] frontend changes * add: docs * refactor: remove role from basicinfo * add: settings for grafanacom * add: settigns for frontend * add: logic for azureAD user skip org role * add: docs for skip_org_role_sync * refactor: docs a bit * add: tests for userinfo * refactor: to only extract if skiporgrolesync false * refactor: based on review comments * Update docs/sources/setup-grafana/configure-grafana/_index.md 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> Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> --- conf/sample.ini | 2 + .../setup-grafana/configure-grafana/_index.md | 20 ++- .../configure-authentication/azuread/index.md | 44 ++++-- packages/grafana-data/src/types/config.ts | 1 + pkg/api/frontendsettings.go | 1 + pkg/login/social/azuread_oauth.go | 11 +- pkg/login/social/azuread_oauth_test.go | 144 ++++++++++++++++++ pkg/login/social/social.go | 1 + pkg/setting/setting.go | 9 ++ public/app/features/admin/UserAdminPage.tsx | 11 +- 10 files changed, 217 insertions(+), 27 deletions(-) diff --git a/conf/sample.ini b/conf/sample.ini index 9fef1b9c5a4..284c8304f1c 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -582,6 +582,8 @@ ;allowed_groups = ;role_attribute_strict = false ;allow_assign_grafana_admin = false +# prevent synchronizing users organization roles +;skip_org_role_sync = false #################################### Okta OAuth ####################### [auth.okta] diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 7c8883d7c9d..696c0a112f1 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -849,7 +849,7 @@ Administrators can increase this if they experience OAuth login state mismatch e ### oauth_skip_org_role_update_sync -> **Note**: This option will soon be a legacy option in favor of OAuth provider specific `skip_org_role_sync` settings. +> **Note**: This option will soon be a legacy option in favor of OAuth provider specific `skip_org_role_sync` settings. The following sections explain settings for each provider. Skip forced assignment of OrgID `1` or `auto_assign_org_id` for external logins. Default is `false`. Use this setting to allow users with external login to be manually assigned to multiple organizations. @@ -860,13 +860,13 @@ By default, the users' organization and role is reset on every new login. > With Grafana 10, if `oauth_skip_org_role_update_sync` option is set to `false`, users with no mapping will be > reset to the default organization role on every login. [See `auto_assign_org_role` option]({{< relref ".#auto_assign_org_role" >}}). -### [auth.grafana_com] skip_org_role_update_sync +### [auth.grafana_com] skip_org_role_sync To prevent synchronization of organization roles for a specific OAuth integration, you can set the `skip_org_role_sync` option to `true`. Please note that there is also 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 setting `oauth_skip_org_role_update_sync` will be deprecated in favor of provider-specific settings. -The table below shows the available OAuth providers and their setting with the default value and the skip org role sync setting. +The table below show the OAuth provider and their 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 | | --- | --- | --- | --- | | Grafana.com | false | false | will sync with Grafana.com roles | @@ -874,6 +874,20 @@ The table below shows the available OAuth providers and their setting with the d | Grafana.com | false | true | skip org role sync for grafana.com users | | Grafana.com | true | true | skip org role sync for Grafana.com users and all other OAuth providers | +### [auth.azuread] skip_org_role_sync + +To prevent synchronization of organization roles for a specific OAuth integration, you can set the `skip_org_role_sync` option to `true`. Please note that there is also 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 setting `oauth_skip_org_role_update_sync` will be deprecated in favor of provider-specific settings. + +The following table shows the OAuth providers, the default value setting, and the skip org role sync setting. +| OAuth Provider | `oauth_skip_org_role_sync_update` | `skip_org_role_sync` | Behavior | +| --- | --- | --- | --- | +| AzureAD | false | false | will sync with AzureAD roles | +| AzureAD | true | false | skip org role sync for OAuth providers including AzureAD users | +| AzureAD | false | true | skip org role sync for AzureAD users | +| AzureAD | true | true | skip org role sync for AzureAD users and all other OAuth providers | + ### 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/azuread/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md index 321056dc7e4..c3243a11409 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md @@ -99,22 +99,6 @@ To enable the Azure AD OAuth2, register your application with Azure AD. 1. Click on **Users and Groups** and add Users/Groups to the Grafana roles by using **Add User**. -### Map roles - -By default, Azure AD authentication will map users to organization roles based on the most privileged application role assigned to the user in AzureAD. - -If no application role is found, the user is assigned the role specified by -[the `auto_assign_org_role` option]({{< relref "../../../configure-grafana#auto_assign_org_role" >}}). -You can disable this default role assignment by setting `role_attribute_strict = true`. -It denies user access if no role or an invalid role is returned. - -**On every login** the user organization role will be reset to match AzureAD's application role and -their organization membership will be reset to the default organization. - -If Azure AD authentication is not intended to sync user roles and organization membership, -`oauth_skip_org_role_update_sync` should be enabled. -See [configure-grafana]({{< relref "../../../configure-grafana#oauth_skip_org_role_update_sync" >}}) for more details. - ### Assign server administrator privileges > Available in Grafana v9.2 and later versions. @@ -157,6 +141,7 @@ allowed_domains = allowed_groups = role_attribute_strict = false allow_assign_grafana_admin = false +skip_org_role_sync = false ``` You can also use these environment variables to configure **client_id** and **client_secret**: @@ -244,3 +229,30 @@ To force fetching groups from Microsoft Graph API instead of the `id_token`. You ``` force_use_graph_api = true ``` + +### Map roles + +By default, Azure AD authentication will map users to organization roles based on the most privileged application role assigned to the user in AzureAD. + +If no application role is found, the user is assigned the role specified by +[the `auto_assign_org_role` option]({{< relref "../../../configure-grafana#auto_assign_org_role" >}}). +You can disable this default role assignment by setting `role_attribute_strict = true`. +It denies user access if no role or an invalid role is returned. + +**On every login** the user organization role will be reset to match AzureAD's application role and +their organization membership will be reset to the default organization. + +## Skip organization role sync + +If Azure AD authentication is not intended to sync user roles and organization membership, +`oauth_skip_org_role_update_sync` should be enabled, this is not recommended to use in favor of setting provider specific `skip_org_role_sync` option. +See [configure-grafana]({{< relref "../../../configure-grafana#oauth_skip_org_role_update_sync" >}}) for more details. + +To prevent the sync of org roles from Grafana.com, set `skip_org_role_sync` to `true`. This is useful if you want to manage the organization roles for your users from within Grafana. + +```ini +[auth.azuread] +# .. +# prevents the sync of org roles from Grafana.com +skip_org_role_sync = true +``` diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index 739bde84869..d3919b65ce2 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -224,5 +224,6 @@ export interface AuthSettings { SAMLSkipOrgRoleSync?: boolean; LDAPSkipOrgRoleSync?: boolean; GrafanaComSkipOrgRoleSync?: boolean; + AzureADSkipOrgRoleSync?: boolean; DisableSyncLock?: boolean; } diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 6e49a583422..23f355753fd 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -149,6 +149,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i "SAMLSkipOrgRoleSync": hs.Cfg.SectionWithEnvOverrides("auth.saml").Key("skip_org_role_sync").MustBool(false), "LDAPSkipOrgRoleSync": hs.Cfg.LDAPSkipOrgRoleSync, "GrafanaComSkipOrgRoleSync": hs.Cfg.GrafanaComSkipOrgRoleSync, + "AzureADSkipOrgRoleSync": hs.Cfg.AzureADSkipOrgRoleSync, "DisableSyncLock": hs.Cfg.DisableSyncLock, }, "buildInfo": map[string]interface{}{ diff --git a/pkg/login/social/azuread_oauth.go b/pkg/login/social/azuread_oauth.go index df6c94ca4eb..8f10f8e812c 100644 --- a/pkg/login/social/azuread_oauth.go +++ b/pkg/login/social/azuread_oauth.go @@ -8,6 +8,7 @@ import ( "net/http" "strings" + "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/services/org" "golang.org/x/oauth2" @@ -18,6 +19,7 @@ type SocialAzureAD struct { *SocialBase allowedGroups []string forceUseGraphAPI bool + skipOrgRoleSync bool } type azureClaims struct { @@ -70,18 +72,21 @@ func (s *SocialAzureAD) UserInfo(client *http.Client, token *oauth2.Token) (*Bas return nil, ErrEmailNotFound } - role, grafanaAdmin := s.extractRoleAndAdmin(&claims) + // setting the role, grafanaAdmin to empty to reflect that we are not syncronizing with the external provider + var role roletype.RoleType + var grafanaAdmin bool + if !s.skipOrgRoleSync { + role, grafanaAdmin = s.extractRoleAndAdmin(&claims) + } if s.roleAttributeStrict && !role.IsValid() { return nil, &InvalidBasicRoleError{idP: "Azure", assignedRole: string(role)} } - logger.Debug("AzureAD OAuth: extracted role", "email", email, "role", role) groups, err := s.extractGroups(client, claims, token) if err != nil { return nil, fmt.Errorf("failed to extract groups: %w", err) } - logger.Debug("AzureAD OAuth: extracted groups", "email", email, "groups", fmt.Sprintf("%v", groups)) if !s.IsGroupMember(groups) { return nil, errMissingGroupMembership diff --git a/pkg/login/social/azuread_oauth_test.go b/pkg/login/social/azuread_oauth_test.go index 4a03dc12002..26a005ddb22 100644 --- a/pkg/login/social/azuread_oauth_test.go +++ b/pkg/login/social/azuread_oauth_test.go @@ -483,3 +483,147 @@ func TestSocialAzureAD_UserInfo(t *testing.T) { }) } } + +func TestSocialAzureAD_SkipOrgRole(t *testing.T) { + type fields struct { + SocialBase *SocialBase + allowedGroups []string + forceUseGraphAPI bool + skipOrgRoleSync bool + } + type args struct { + client *http.Client + } + + tests := []struct { + name string + fields fields + claims *azureClaims + args args + settingAutoAssignOrgRole string + want *BasicUserInfo + wantErr bool + }{ + { + name: "Grafana Admin and Editor roles in claim, skipOrgRoleSync disabled should get roles, skipOrgRoleSyncBase disabled", + fields: fields{ + SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: true}, "", false, *featuremgmt.WithFeatures()), + skipOrgRoleSync: false, + }, + claims: &azureClaims{ + Email: "me@example.com", + PreferredUsername: "", + Roles: []string{"GrafanaAdmin", "Editor"}, + Name: "My Name", + ID: "1234", + }, + want: &BasicUserInfo{ + Id: "1234", + Name: "My Name", + Email: "me@example.com", + Login: "me@example.com", + Role: "Admin", + IsGrafanaAdmin: trueBoolPtr(), + Groups: []string{}, + }, + }, + { + name: "Grafana Admin and Editor roles in claim, skipOrgRoleSync disabled should not get roles", + fields: fields{ + SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: true}, "", false, *featuremgmt.WithFeatures()), + skipOrgRoleSync: false, + }, + claims: &azureClaims{ + Email: "me@example.com", + PreferredUsername: "", + Roles: []string{"GrafanaAdmin", "Editor"}, + Name: "My Name", + ID: "1234", + }, + want: &BasicUserInfo{ + Id: "1234", + Name: "My Name", + Email: "me@example.com", + Login: "me@example.com", + Role: "Admin", + IsGrafanaAdmin: trueBoolPtr(), + Groups: []string{}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &SocialAzureAD{ + SocialBase: tt.fields.SocialBase, + allowedGroups: tt.fields.allowedGroups, + forceUseGraphAPI: tt.fields.forceUseGraphAPI, + skipOrgRoleSync: tt.fields.skipOrgRoleSync, + } + + if tt.fields.SocialBase == nil { + s.SocialBase = newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "", false, *featuremgmt.WithFeatures()) + } + + key := []byte("secret") + sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: key}, (&jose.SignerOptions{}).WithType("JWT")) + if err != nil { + panic(err) + } + + cl := jwt.Claims{ + Subject: "subject", + Issuer: "issuer", + NotBefore: jwt.NewNumericDate(time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC)), + Audience: jwt.Audience{"leela", "fry"}, + } + + var raw string + if tt.claims != nil { + if tt.claims.ClaimNames.Groups != "" { + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + tokenParts := strings.Split(request.Header.Get("Authorization"), " ") + require.Len(t, tokenParts, 2) + require.Equal(t, "fake_token", tokenParts[1]) + + writer.WriteHeader(http.StatusOK) + + type response struct { + Value []string + } + res := response{Value: []string{"from_server"}} + require.NoError(t, json.NewEncoder(writer).Encode(&res)) + })) + // need to set the fake servers url as endpoint to capture request + tt.claims.ClaimSources = map[string]claimSource{ + tt.claims.ClaimNames.Groups: {Endpoint: server.URL}, + } + } + raw, err = jwt.Signed(sig).Claims(cl).Claims(tt.claims).CompactSerialize() + require.NoError(t, err) + } else { + raw, err = jwt.Signed(sig).Claims(cl).CompactSerialize() + require.NoError(t, err) + } + + token := &oauth2.Token{ + AccessToken: "fake_token", + } + if tt.claims != nil { + token = token.WithExtra(map[string]interface{}{"id_token": raw}) + } + + if tt.fields.SocialBase != nil { + tt.args.client = s.Client(context.Background(), token) + } + + got, err := s.UserInfo(tt.args.client, token) + if (err != nil) != tt.wantErr { + t.Errorf("UserInfo() error = %v, wantErr %v", err, tt.wantErr) + return + } + + require.EqualValues(t, tt.want, got) + }) + } +} diff --git a/pkg/login/social/social.go b/pkg/login/social/social.go index 19ac4b311de..056457a8238 100644 --- a/pkg/login/social/social.go +++ b/pkg/login/social/social.go @@ -171,6 +171,7 @@ func ProvideService(cfg *setting.Cfg, features *featuremgmt.FeatureManager) *Soc SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features), allowedGroups: util.SplitString(sec.Key("allowed_groups").String()), forceUseGraphAPI: sec.Key("force_use_graph_api").MustBool(false), + skipOrgRoleSync: cfg.AzureADSkipOrgRoleSync, } } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 921fdadadaf..441adea7d0f 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -303,6 +303,7 @@ type Cfg struct { SigV4AuthEnabled bool SigV4VerboseLogging bool AzureAuthEnabled bool + AzureSkipOrgRoleSync bool BasicAuthEnabled bool AdminUser string AdminPassword string @@ -422,6 +423,9 @@ type Cfg struct { ApplicationInsightsEndpointUrl string FeedbackLinksEnabled bool + // AzureAD + AzureADSkipOrgRoleSync bool + // LDAP LDAPEnabled bool LDAPSkipOrgRoleSync bool @@ -1354,6 +1358,10 @@ func readSecuritySettings(iniFile *ini.File, cfg *Cfg) error { return nil } +func readAuthAzureADSettings(iniFile *ini.File, cfg *Cfg) { + sec := iniFile.Section("auth.azuread") + cfg.AzureADSkipOrgRoleSync = sec.Key("skip_org_role_sync").MustBool(false) +} func readAuthGrafanaComSettings(iniFile *ini.File, cfg *Cfg) { sec := iniFile.Section("auth.grafana_com") @@ -1406,6 +1414,7 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { // Azure Auth AzureAuthEnabled = auth.Key("azure_auth_enabled").MustBool(false) cfg.AzureAuthEnabled = AzureAuthEnabled + readAuthAzureADSettings(iniFile, cfg) // anonymous access AnonymousEnabled = iniFile.Section("auth.anonymous").Key("enabled").MustBool(false) diff --git a/public/app/features/admin/UserAdminPage.tsx b/public/app/features/admin/UserAdminPage.tsx index 155adb3abd5..71456012b4b 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', 'AzureAD', 'OAuth']; +const SyncedOAuthLabels: string[] = ['GitHub', 'GitLab', 'OAuth']; export class UserAdminPage extends PureComponent { async componentDidMount() { @@ -113,9 +113,8 @@ export class UserAdminPage extends PureComponent { const isSAMLUser = user?.isExternal && user?.authLabels?.includes('SAML'); const isGoogleUser = user?.isExternal && user?.authLabels?.includes('Google'); 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'); - // isGrafanaComUser true - // isOAuthUserWithSkippableSync true const isUserSynced = !config.auth.DisableSyncLock && ((user?.isExternal && @@ -125,13 +124,15 @@ export class UserAdminPage extends PureComponent { isOAuthUserWithSkippableSync || isSAMLUser || isLDAPUser || + isAzureADUser || isGrafanaComUser )) || (!config.auth.OAuthSkipOrgRoleUpdateSync && isOAuthUserWithSkippableSync) || (!config.auth.SAMLSkipOrgRoleSync && isSAMLUser) || (!config.auth.LDAPSkipOrgRoleSync && isLDAPUser) || - // both OAuthSkipOrgRoleUpdateSync and GrafanaComSkipOrgRoleSync needs to be false for a GrafanaComUser to be synced - (!config.auth.OAuthSkipOrgRoleUpdateSync && !config.auth.GrafanaComSkipOrgRoleSync && isGrafanaComUser)); + // 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)); const pageNav: NavModelItem = { text: user?.login ?? '', From 59df3610874b034e271b7f452d9ca2b65166da7c Mon Sep 17 00:00:00 2001 From: bobemoe Date: Mon, 16 Jan 2023 12:26:05 +0000 Subject: [PATCH 13/65] Public Dashboards: Docs: add ENV method to enable (#60412) Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> --- docs/sources/dashboards/dashboard-public/index.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/sources/dashboards/dashboard-public/index.md b/docs/sources/dashboards/dashboard-public/index.md index 45e2080d6a6..bd40a262b55 100644 --- a/docs/sources/dashboards/dashboard-public/index.md +++ b/docs/sources/dashboards/dashboard-public/index.md @@ -32,6 +32,12 @@ Add the `publicDashboards` feature toggle to your `custom.ini` file. publicDashboards = true ``` +If you are using Docker, use an environment variable to enable public dashboards: + +``` +--env GF_FEATURE_TOGGLES_ENABLE=publicDashboards +``` + > **Note:** For Grafana Cloud, you will need to contact support to have the feature enabled. #### Make a dashboard public From 045a12047f71a45e3c21ad11875f9b1a0d02c846 Mon Sep 17 00:00:00 2001 From: Tania Date: Mon, 16 Jan 2023 13:55:40 +0100 Subject: [PATCH 14/65] Codegen: Generate per-kind reference docs (#60416) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add docs generator * Add json-to-md conversion * Fix lint issues * Remove check for kind type * Disable prettier for generated docs * Use schema ref names as identifiers for links & headers * Display the default value (if so) in the description * Undo 'draft:false' introduced by mistake * Update pkg/codegen/jenny_docs.go Co-authored-by: Jack Baldry * Undraft and unlist kinds documentation (#61476) * Support running containers without root daemon Signed-off-by: Jack Baldry * Use section shortcode to automatically list child pages Signed-off-by: Jack Baldry * Undraft and unlist kinds documentation This page and child pages are directly accessible but are not listed in the table of contents. Signed-off-by: Jack Baldry * Add docs-preview to browse drafted pages Signed-off-by: Jack Baldry Signed-off-by: Jack Baldry * Replace end of line and pipe characters in table codegen * Remove draft status from generated docs Signed-off-by: Jack Baldry Co-authored-by: Joan López de la Franca Beltran Co-authored-by: Jack Baldry Co-authored-by: Robert Horvath --- .prettierignore | 3 + docs/Makefile | 16 +- docs/sources/developers/kinds/_index.md | 10 + docs/sources/developers/kinds/core/_index.md | 10 + .../kinds/core/dashboard/schema-reference.md | 219 ++++++++ .../kinds/core/playlist/schema-reference.md | 32 ++ .../kinds/core/team/schema-reference.md | 34 ++ go.mod | 1 + go.sum | 3 + kinds/gen.go | 1 + pkg/codegen/generators.go | 2 +- pkg/codegen/jenny_docs.go | 490 ++++++++++++++++++ pkg/codegen/tmpl/docs.tmpl | 13 + 13 files changed, 827 insertions(+), 7 deletions(-) create mode 100644 docs/sources/developers/kinds/_index.md create mode 100644 docs/sources/developers/kinds/core/_index.md create mode 100644 docs/sources/developers/kinds/core/dashboard/schema-reference.md create mode 100644 docs/sources/developers/kinds/core/playlist/schema-reference.md create mode 100644 docs/sources/developers/kinds/core/team/schema-reference.md create mode 100644 pkg/codegen/jenny_docs.go create mode 100644 pkg/codegen/tmpl/docs.tmpl diff --git a/.prettierignore b/.prettierignore index cc9c39938ad..62700e93ea4 100644 --- a/.prettierignore +++ b/.prettierignore @@ -33,3 +33,6 @@ public/openapi3.json # Generated Kinds report kinds/report.json + +# Generated schema docs +docs/sources/developers/kinds/ diff --git a/docs/Makefile b/docs/Makefile index 4f553a5e878..96dc60c463f 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,24 +1,28 @@ .PHONY: pull docs docs-quick docs-no-pull docs-test docs-local-static +PODMAN = $(shell if command -v podman &>/dev/null; then echo podman; else echo docker; fi) IMAGE = grafana/docs-base:latest CONTENT_PATH = /hugo/content/docs/grafana/next LOCAL_STATIC_PATH = ../../website/static PORT = 3002:3002 pull: - docker pull $(IMAGE) + $(PODMAN) pull $(IMAGE) docs: pull - docker run -v $(shell pwd)/sources:$(CONTENT_PATH):Z -p $(PORT) --rm -it $(IMAGE) /bin/bash -c "make server" - + $(PODMAN) run -v $(shell pwd)/sources:$(CONTENT_PATH):Z -p $(PORT) --rm -it $(IMAGE) /bin/bash -c "make server" + +docs-preview: pull + $(PODMAN) run -v $(shell pwd)/sources:$(CONTENT_PATH):Z -p $(PORT) --rm -it $(IMAGE) /bin/bash -c "make server BUILD_DRAFTS=true" + docs-no-pull: - docker run -v $(shell pwd)/sources:$(CONTENT_PATH):Z -p $(PORT) --rm -it $(IMAGE) /bin/bash -c "make server" + $(PODMAN) run -v $(shell pwd)/sources:$(CONTENT_PATH):Z -p $(PORT) --rm -it $(IMAGE) /bin/bash -c "make server" docs-test: pull - docker run -v $(shell pwd)/sources:$(CONTENT_PATH):Z --rm -it $(IMAGE) /bin/bash -c 'make prod' + $(PODMAN) run -v $(shell pwd)/sources:$(CONTENT_PATH):Z --rm -it $(IMAGE) /bin/bash -c 'make prod' # expects that you have grafana/website checked out in same path as the grafana repo. docs-local-static: pull if [ ! -d "$(LOCAL_STATIC_PATH)" ]; then echo "local path (website project) $(LOCAL_STATIC_PATH) not found"]; exit 1; fi - docker run -v $(shell pwd)/sources:$(CONTENT_PATH):Z \ + $(PODMAN) run -v $(shell pwd)/sources:$(CONTENT_PATH):Z \ -v $(shell pwd)/$(LOCAL_STATIC_PATH):/hugo/static:Z -p $(PORT) --rm -it $(IMAGE) diff --git a/docs/sources/developers/kinds/_index.md b/docs/sources/developers/kinds/_index.md new file mode 100644 index 00000000000..cdb07012a0c --- /dev/null +++ b/docs/sources/developers/kinds/_index.md @@ -0,0 +1,10 @@ +--- +title: Grafana schema +weight: 200 +_build: + list: false +--- + +# Grafana schema + +{{< section >}} diff --git a/docs/sources/developers/kinds/core/_index.md b/docs/sources/developers/kinds/core/_index.md new file mode 100644 index 00000000000..9e1eedc72aa --- /dev/null +++ b/docs/sources/developers/kinds/core/_index.md @@ -0,0 +1,10 @@ +--- +title: Core kinds +weight: 200 +--- + +# Grafana core kinds + +Kinds that define Grafana’s core schematized object types - dashboards, datasources, users, etc. + +{{< section >}} diff --git a/docs/sources/developers/kinds/core/dashboard/schema-reference.md b/docs/sources/developers/kinds/core/dashboard/schema-reference.md new file mode 100644 index 00000000000..5fc5f9be873 --- /dev/null +++ b/docs/sources/developers/kinds/core/dashboard/schema-reference.md @@ -0,0 +1,219 @@ +--- +keywords: + - grafana + - schema +title: Dashboard kind +--- + +# Dashboard kind + +### Maturity: merged +### Version: 0.0 + +## Properties + +| Property | Type | Required | Description | +|------------------------|-----------------------------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `editable` | boolean | **Yes** | Whether a dashboard is editable or not. Default: `true`. | +| `graphTooltip` | integer | **Yes** | 0 for no shared crosshair or tooltip (default).
1 for shared crosshair.
2 for shared crosshair AND shared tooltip. Possible values are: `0`, `1`, `2`. Default: `0`. | +| `revision` | integer | **Yes** | Version of the current dashboard data Default: `-1`. | +| `schemaVersion` | integer | **Yes** | Version of the JSON schema, incremented each time a Grafana update brings
changes to said schema.
TODO this is the existing schema numbering system. It will be replaced by Thema's themaVersion Default: `36`. | +| `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 | +| `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 | +| `liveNow` | boolean | No | TODO docs | +| `panels` | [object](#panels)[] | No | | +| `refresh` | | No | TODO docs | +| `snapshot` | [Snapshot](#snapshot) | No | TODO docs | +| `tags` | string[] | No | Tags associated with dashboard. | +| `templating` | [object](#templating) | No | TODO docs | +| `time` | [object](#time) | No | Time range for dashboard, e.g. last 6 hours, last 7 days, etc | +| `timepicker` | [object](#timepicker) | No | TODO docs
TODO this appears to be spread all over in the frontend. Concepts will likely need tidying in tandem with schema changes | +| `timezone` | string | No | Timezone of dashboard, Possible values are: `browser`, `utc`, ``. Default: `browser`. | +| `title` | string | No | Title of dashboard. | +| `uid` | string | No | Unique dashboard identifier that can be generated by anyone. string (8-40) | +| `version` | integer | No | Version of the dashboard, incremented each time the dashboard is updated. | +| `weekStart` | string | No | TODO docs | + +## DashboardLink + +FROM public/app/features/dashboard/state/DashboardModels.ts - ish +TODO docs + +### Properties + +| Property | Type | Required | Description | +|---------------|----------|----------|------------------------------------------------------| +| `asDropdown` | boolean | **Yes** | Default: `false`. | +| `icon` | string | **Yes** | | +| `includeVars` | boolean | **Yes** | Default: `false`. | +| `keepTime` | boolean | **Yes** | Default: `false`. | +| `tags` | string[] | **Yes** | | +| `targetBlank` | boolean | **Yes** | Default: `false`. | +| `title` | string | **Yes** | | +| `tooltip` | string | **Yes** | | +| `type` | string | **Yes** | TODO docs Possible values are: `link`, `dashboards`. | +| `url` | string | **Yes** | | + +## Snapshot + +TODO docs + +### Properties + +| Property | Type | Required | Description | +|---------------|---------|----------|-------------| +| `created` | string | **Yes** | TODO docs | +| `expires` | string | **Yes** | TODO docs | +| `externalUrl` | string | **Yes** | TODO docs | +| `external` | boolean | **Yes** | TODO docs | +| `id` | integer | **Yes** | TODO docs | +| `key` | string | **Yes** | TODO docs | +| `name` | string | **Yes** | TODO docs | +| `orgId` | integer | **Yes** | TODO docs | +| `updated` | string | **Yes** | TODO docs | +| `userId` | integer | **Yes** | TODO docs | +| `url` | string | No | TODO docs | + +## annotations + +TODO docs + +### Properties + +| Property | Type | Required | Description | +|----------|---------------------------------------|----------|-------------| +| `list` | [AnnotationQuery](#annotationquery)[] | No | | + +### AnnotationQuery + +TODO docs +FROM: AnnotationQuery in grafana-data/src/types/annotations.ts + +#### Properties + +| Property | Type | Required | Description | +|--------------|---------------------------------------|----------|-------------------------------------------------| +| `builtIn` | integer | **Yes** | Default: `0`. | +| `datasource` | [object](#datasource) | **Yes** | Datasource to use for annotation. | +| `enable` | boolean | **Yes** | Whether annotation is enabled. Default: `true`. | +| `showIn` | integer | **Yes** | Default: `0`. | +| `type` | string | **Yes** | Default: `dashboard`. | +| `hide` | boolean | No | Whether to hide annotation. Default: `false`. | +| `iconColor` | string | No | Annotation icon color. | +| `name` | string | No | Name of annotation. | +| `rawQuery` | string | No | Query for annotation data. | +| `target` | [AnnotationTarget](#annotationtarget) | No | TODO docs | + +#### AnnotationTarget + +TODO docs + +##### Properties + +| Property | Type | Required | Description | +|------------|----------|----------|-------------| +| `limit` | integer | **Yes** | | +| `matchAny` | boolean | **Yes** | | +| `tags` | string[] | **Yes** | | +| `type` | string | **Yes** | | + +#### datasource + +Datasource to use for annotation. + +##### Properties + +| Property | Type | Required | Description | +|----------|--------|----------|-------------| +| `type` | string | No | | +| `uid` | string | No | | + +## panels + +| Property | Type | Required | Description | +|----------|------|----------|-------------| + +## templating + +TODO docs + +### Properties + +| Property | Type | Required | Description | +|----------|-----------------------------------|----------|-------------| +| `list` | [VariableModel](#variablemodel)[] | No | | + +### VariableModel + +FROM: packages/grafana-data/src/types/templateVars.ts +TODO docs +TODO what about what's in public/app/features/types.ts? +TODO there appear to be a lot of different kinds of [template] vars here? if so need a disjunction + +#### Properties + +| Property | Type | Required | Description | +|----------------|---------------------------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `global` | boolean | **Yes** | Default: `false`. | +| `hide` | integer | **Yes** | Possible values are: `0`, `1`, `2`. | +| `id` | string | **Yes** | Default: `00000000-0000-0000-0000-000000000000`. | +| `index` | integer | **Yes** | Default: `-1`. | +| `name` | string | **Yes** | | +| `skipUrlSync` | boolean | **Yes** | Default: `false`. | +| `state` | string | **Yes** | Possible values are: `NotStarted`, `Loading`, `Streaming`, `Done`, `Error`. | +| `type` | string | **Yes** | FROM: packages/grafana-data/src/types/templateVars.ts
TODO docs
TODO this implies some wider pattern/discriminated union, probably? Possible values are: `query`, `adhoc`, `constant`, `datasource`, `interval`, `textbox`, `custom`, `system`. | +| `datasource` | [DataSourceRef](#datasourceref) | No | Ref to a DataSource instance | +| `description` | string | No | | +| `error` | [object](#error) | No | | +| `label` | string | No | | +| `query` | | No | TODO: Move this into a separated QueryVariableModel type | +| `rootStateKey` | string | No | | + +#### DataSourceRef + +Ref to a DataSource instance + +##### Properties + +| Property | Type | Required | Description | +|----------|--------|----------|------------------------------| +| `type` | string | No | The plugin type-id | +| `uid` | string | No | Specific datasource instance | + +#### error + +| Property | Type | Required | Description | +|----------|------|----------|-------------| + +## time + +Time range for dashboard, e.g. last 6 hours, last 7 days, etc + +### Properties + +| Property | Type | Required | Description | +|----------|--------|----------|--------------------| +| `from` | string | **Yes** | Default: `now-6h`. | +| `to` | string | **Yes** | Default: `now`. | + +## timepicker + +TODO docs +TODO this appears to be spread all over in the frontend. Concepts will likely need tidying in tandem with schema changes + +### Properties + +| Property | Type | Required | Description | +|---------------------|----------|----------|----------------------------------------------------------------------------------------| +| `collapse` | boolean | **Yes** | Whether timepicker is collapsed or not. Default: `false`. | +| `enable` | boolean | **Yes** | Whether timepicker is enabled or not. Default: `true`. | +| `hidden` | boolean | **Yes** | Whether timepicker is visible or not. Default: `false`. | +| `refresh_intervals` | string[] | **Yes** | Selectable intervals for auto-refresh. Default: `[5s 10s 30s 1m 5m 15m 30m 1h 2h 1d]`. | +| `time_options` | string[] | **Yes** | TODO docs Default: `[5m 15m 1h 6h 12h 24h 2d 7d 30d]`. | + + diff --git a/docs/sources/developers/kinds/core/playlist/schema-reference.md b/docs/sources/developers/kinds/core/playlist/schema-reference.md new file mode 100644 index 00000000000..918c9cb05ca --- /dev/null +++ b/docs/sources/developers/kinds/core/playlist/schema-reference.md @@ -0,0 +1,32 @@ +--- +keywords: + - grafana + - schema +title: Playlist kind +--- + +# Playlist kind + +### Maturity: merged +### Version: 0.0 + +## Properties + +| Property | Type | Required | Description | +|------------|---------------------------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `interval` | string | **Yes** | Interval sets the time between switching views in a playlist.
FIXME: Is this based on a standardized format or what options are available? Can datemath be used? Default: `5m`. | +| `name` | string | **Yes** | Name of the playlist. | +| `uid` | string | **Yes** | Unique playlist identifier. Generated on creation, either by the
creator of the playlist of by the application. | +| `items` | [PlaylistItem](#playlistitem)[] | No | The ordered list of items that the playlist will iterate over.
FIXME! This should not be optional, but changing it makes the godegen awkward | + +## PlaylistItem + +### Properties + +| Property | Type | Required | Description | +|----------|--------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `type` | string | **Yes** | Type of the item. Possible values are: `dashboard_by_uid`, `dashboard_by_id`, `dashboard_by_tag`. | +| `value` | string | **Yes** | Value depends on type and describes the playlist item.

- dashboard_by_id: The value is an internal numerical identifier set by Grafana. This
is not portable as the numerical identifier is non-deterministic between different instances.
Will be replaced by dashboard_by_uid in the future. (deprecated)
- dashboard_by_tag: The value is a tag which is set on any number of dashboards. All
dashboards behind the tag will be added to the playlist.
- dashboard_by_uid: The value is the dashboard UID | +| `title` | string | No | Title is an unused property -- it will be removed in the future | + + diff --git a/docs/sources/developers/kinds/core/team/schema-reference.md b/docs/sources/developers/kinds/core/team/schema-reference.md new file mode 100644 index 00000000000..6da29527078 --- /dev/null +++ b/docs/sources/developers/kinds/core/team/schema-reference.md @@ -0,0 +1,34 @@ +--- +keywords: + - grafana + - schema +title: Team kind +--- + +# Team kind + +### Maturity: merged +### Version: 0.0 + +## Properties + +| Property | Type | Required | Description | +|-----------------|--------------------------|----------|----------------------------------------------------------| +| `created` | integer | **Yes** | Created indicates when the team was created. | +| `memberCount` | integer | **Yes** | MemberCount is the number of the team members. | +| `name` | string | **Yes** | Name of the team. | +| `orgId` | integer | **Yes** | OrgId is the ID of an organisation the team belongs to. | +| `permission` | integer | **Yes** | Possible values are: `0`, `1`, `2`, `4`. | +| `updated` | integer | **Yes** | Updated indicates when the team was updated. | +| `accessControl` | [object](#accesscontrol) | No | AccessControl metadata associated with a given resource. | +| `avatarUrl` | string | No | AvatarUrl is the team's avatar URL. | +| `email` | string | No | Email of the team. | + +## accessControl + +AccessControl metadata associated with a given resource. + +| Property | Type | Required | Description | +|----------|------|----------|-------------| + + diff --git a/go.mod b/go.mod index d420673b13d..12483f6bfa8 100644 --- a/go.mod +++ b/go.mod @@ -274,6 +274,7 @@ require ( require ( github.com/dave/dst v0.27.2 + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f github.com/parca-dev/parca v0.12.1 k8s.io/apimachinery v0.25.3 ) diff --git a/go.sum b/go.sum index b921e920314..10e99e1a63a 100644 --- a/go.sum +++ b/go.sum @@ -2560,9 +2560,12 @@ github.com/xdg/scram v1.0.3/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49 github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= github.com/xdg/stringprep v1.0.3/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= github.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk= diff --git a/kinds/gen.go b/kinds/gen.go index 9a06c592c39..3c437c7049d 100644 --- a/kinds/gen.go +++ b/kinds/gen.go @@ -40,6 +40,7 @@ func main() { codegen.BaseCoreRegistryJenny(filepath.Join("pkg", "registry", "corekind"), kindsys.GoCoreKindParentPath), codegen.LatestMajorsOrXJenny(kindsys.TSCoreKindParentPath, codegen.TSTypesJenny{}), codegen.TSVeneerIndexJenny(filepath.Join("packages", "grafana-schema", "src")), + codegen.DocsJenny(filepath.Join("docs", "sources", "developers", "kinds", "core")), ) coreKindsGen.AddPostprocessors(codegen.SlashHeaderMapper("kinds/gen.go")) diff --git a/pkg/codegen/generators.go b/pkg/codegen/generators.go index 037766d13d7..1c6d0743f9f 100644 --- a/pkg/codegen/generators.go +++ b/pkg/codegen/generators.go @@ -60,7 +60,7 @@ func SlashHeaderMapper(maingen string) codejen.FileMapper { return func(f codejen.File) (codejen.File, error) { // Never inject on certain filetypes, it's never valid switch filepath.Ext(f.RelativePath) { - case ".json", ".yml", ".yaml": + case ".json", ".yml", ".yaml", ".md": return f, nil default: buf := new(bytes.Buffer) diff --git a/pkg/codegen/jenny_docs.go b/pkg/codegen/jenny_docs.go new file mode 100644 index 00000000000..a2cb7667ad9 --- /dev/null +++ b/pkg/codegen/jenny_docs.go @@ -0,0 +1,490 @@ +package codegen + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "path" + "path/filepath" + "sort" + "strings" + "text/template" + + "cuelang.org/go/cue/cuecontext" + "github.com/grafana/codejen" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/thema/encoding/jsonschema" + "github.com/olekukonko/tablewriter" + "github.com/xeipuuv/gojsonpointer" +) + +func DocsJenny(docsPath string) OneToOne { + return docsJenny{ + docsPath: docsPath, + } +} + +type docsJenny struct { + docsPath string +} + +func (j docsJenny) JennyName() string { + return "DocsJenny" +} + +func (j docsJenny) Generate(decl *DeclForGen) (*codejen.File, error) { + f, err := jsonschema.GenerateSchema(decl.Lineage().Latest()) + if err != nil { + return nil, fmt.Errorf("failed to generate json representation for the schema: %v", err) + } + b, err := cuecontext.New().BuildFile(f).MarshalJSON() + if err != nil { + return nil, fmt.Errorf("failed to marshal schema value to json: %v", err) + } + + // We don't need entire json obj, only the value of components.schemas path + var obj struct { + Components struct { + Schemas json.RawMessage + } + } + err = json.Unmarshal(b, &obj) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal schema json: %v", err) + } + + // fixes the references between the types within a json after making components.schema. the root of the json + kindJsonStr := strings.Replace(string(obj.Components.Schemas), "#/components/schemas/", "#/", -1) + + kindProps := decl.Properties.Common() + kindName := strings.ToLower(kindProps.Name) + data := templateData{ + KindName: kindProps.Name, + KindVersion: decl.Lineage().Latest().Version().String(), + KindMaturity: string(kindProps.Maturity), + Markdown: "{{ .Markdown 1 }}", + } + + tmpl, err := makeTemplate(data, "docs.tmpl") + if err != nil { + return nil, err + } + + doc, err := jsonToMarkdown([]byte(kindJsonStr), string(tmpl), kindName) + if err != nil { + return nil, fmt.Errorf("failed to build markdown for kind %s: %v", kindName, err) + } + + return codejen.NewFile(filepath.Join(j.docsPath, kindName, "schema-reference.md"), doc, j), nil +} + +// makeTemplate pre-populates the template with the kind metadata +func makeTemplate(data templateData, tmpl string) ([]byte, error) { + buf := new(bytes.Buffer) + if err := tmpls.Lookup(tmpl).Execute(buf, data); err != nil { + return []byte{}, fmt.Errorf("failed to populate docs template with the kind metadata") + } + return buf.Bytes(), nil +} + +type templateData struct { + KindName string + KindVersion string + KindMaturity string + Markdown string +} + +// -------------------- JSON to Markdown conversion -------------------- +// Copied from https://github.com/marcusolsson/json-schema-docs and slightly changed to fit the DocsJenny + +type schema struct { + ID string `json:"$id,omitempty"` + Ref string `json:"$ref,omitempty"` + Schema string `json:"$schema,omitempty"` + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` + Required []string `json:"required,omitempty"` + Type PropertyTypes `json:"type,omitempty"` + Properties map[string]*schema `json:"properties,omitempty"` + Items *schema `json:"items,omitempty"` + Definitions map[string]*schema `json:"definitions,omitempty"` + Enum []Any `json:"enum"` + Default any `json:"default"` +} + +func jsonToMarkdown(jsonData []byte, tpl string, kindName string) ([]byte, error) { + sch, err := newSchema(jsonData, kindName) + if err != nil { + return []byte{}, err + } + + t, err := template.New("markdown").Parse(tpl) + if err != nil { + return []byte{}, err + } + + buf := new(bytes.Buffer) + err = t.Execute(buf, sch) + if err != nil { + return []byte{}, err + } + + return buf.Bytes(), nil +} + +func newSchema(b []byte, kindName string) (*schema, error) { + var data map[string]*schema + if err := json.Unmarshal(b, &data); err != nil { + return nil, err + } + + // Needed for resolving in-schema references. + root, err := simplejson.NewJson(b) + if err != nil { + return nil, err + } + + return resolveSchema(data[kindName], root) +} + +// resolveSchema recursively resolves schemas. +func resolveSchema(schem *schema, root *simplejson.Json) (*schema, error) { + for _, prop := range schem.Properties { + if prop.Ref != "" { + tmp, err := resolveReference(prop.Ref, root) + if err != nil { + return nil, err + } + *prop = *tmp + } + foo, err := resolveSchema(prop, root) + if err != nil { + return nil, err + } + *prop = *foo + } + + if schem.Items != nil { + if schem.Items.Ref != "" { + tmp, err := resolveReference(schem.Items.Ref, root) + if err != nil { + return nil, err + } + *schem.Items = *tmp + } + foo, err := resolveSchema(schem.Items, root) + if err != nil { + return nil, err + } + *schem.Items = *foo + } + + return schem, nil +} + +// resolveReference loads a schema from a $ref. +// If ref contains a hashtag (#), the part after represents a in-schema reference. +func resolveReference(ref string, root *simplejson.Json) (*schema, error) { + i := strings.Index(ref, "#") + + if i != 0 { + return nil, fmt.Errorf("not in-schema reference: %s", ref) + } + return resolveInSchemaReference(ref[i+1:], root) +} + +func resolveInSchemaReference(ref string, root *simplejson.Json) (*schema, error) { + // in-schema reference + pointer, err := gojsonpointer.NewJsonPointer(ref) + if err != nil { + return nil, err + } + + v, _, err := pointer.Get(root.MustMap()) + if err != nil { + return nil, err + } + + var sch schema + b, err := json.Marshal(v) + if err != nil { + return nil, err + } + + if err := json.Unmarshal(b, &sch); err != nil { + return nil, err + } + + // Set the ref name as title + sch.Title = path.Base(ref) + + return &sch, nil +} + +// Markdown returns the Markdown representation of the schema. +// +// The level argument can be used to offset the heading levels. This can be +// useful if you want to add the schema under a subheading. +func (s schema) Markdown(level int) string { + if level < 1 { + level = 1 + } + + var buf bytes.Buffer + + if s.Title != "" { + fmt.Fprintln(&buf, makeHeading(s.Title, level)) + fmt.Fprintln(&buf) + } + + if s.Description != "" { + fmt.Fprintln(&buf, s.Description) + if s.Default != nil { + fmt.Fprintf(&buf, "The default value is: `%v`.", s.Default) + } + fmt.Fprintln(&buf) + } + + if len(s.Properties) > 0 { + fmt.Fprintln(&buf, makeHeading("Properties", level+1)) + fmt.Fprintln(&buf) + } + + printProperties(&buf, &s) + + // Add padding. + fmt.Fprintln(&buf) + + for _, obj := range findDefinitions(&s) { + fmt.Fprint(&buf, obj.Markdown(level+1)) + } + + return buf.String() +} + +func makeHeading(heading string, level int) string { + if level < 0 { + return heading + } + + if level <= 6 { + return strings.Repeat("#", level) + " " + heading + } + + return fmt.Sprintf("**%s**", heading) +} + +func findDefinitions(s *schema) []*schema { + // Gather all properties of object type so that we can generate the + // properties for them recursively. + var objs []*schema + + for k, p := range s.Properties { + // Use the identifier as the title. + if p.Type.HasType(PropertyTypeObject) { + if len(p.Title) == 0 { + p.Title = k + } + objs = append(objs, p) + } + + // If the property is an array of objects, use the name of the array + // property as the title. + if p.Type.HasType(PropertyTypeArray) { + if p.Items != nil { + if p.Items.Type.HasType(PropertyTypeObject) { + if len(p.Items.Title) == 0 { + p.Items.Title = k + } + objs = append(objs, p.Items) + } + } + } + } + + // Sort the object schemas. + sort.Slice(objs, func(i, j int) bool { + return objs[i].Title < objs[j].Title + }) + + return objs +} + +func printProperties(w io.Writer, s *schema) { + table := tablewriter.NewWriter(w) + table.SetHeader([]string{"Property", "Type", "Required", "Description"}) + table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) + table.SetCenterSeparator("|") + table.SetAutoFormatHeaders(false) + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + table.SetAutoWrapText(false) + + // Buffer all property rows so that we can sort them before printing them. + var rows [][]string + + for k, p := range s.Properties { + // Generate relative links for objects and arrays of objects. + var propType []string + for _, pt := range p.Type { + switch pt { + case PropertyTypeObject: + name, anchor := propNameAndAnchor(k, p.Title) + propType = append(propType, fmt.Sprintf("[%s](#%s)", name, anchor)) + case PropertyTypeArray: + if p.Items != nil { + for _, pi := range p.Items.Type { + if pi == PropertyTypeObject { + name, anchor := propNameAndAnchor(k, p.Items.Title) + propType = append(propType, fmt.Sprintf("[%s](#%s)[]", name, anchor)) + } else { + propType = append(propType, fmt.Sprintf("%s[]", pi)) + } + } + } else { + propType = append(propType, string(pt)) + } + default: + propType = append(propType, string(pt)) + } + } + + var propTypeStr string + if len(propType) == 1 { + propTypeStr = propType[0] + } else if len(propType) == 2 { + propTypeStr = strings.Join(propType, " or ") + } else if len(propType) > 2 { + propTypeStr = fmt.Sprintf("%s, or %s", strings.Join(propType[:len(propType)-1], ", "), propType[len(propType)-1]) + } + + // Emphasize required properties. + var required string + if in(s.Required, k) { + required = "**Yes**" + } else { + required = "No" + } + + desc := p.Description + + if len(p.Enum) > 0 { + var vals []string + for _, e := range p.Enum { + vals = append(vals, e.String()) + } + desc += " Possible values are: `" + strings.Join(vals, "`, `") + "`." + } + + if p.Default != nil { + desc += fmt.Sprintf(" Default: `%v`.", p.Default) + } + + rows = append(rows, []string{fmt.Sprintf("`%s`", k), propTypeStr, required, formatForTable(desc)}) + } + + // Sort by the required column, then by the name column. + sort.Slice(rows, func(i, j int) bool { + if rows[i][2] < rows[j][2] { + return true + } + if rows[i][2] > rows[j][2] { + return false + } + return rows[i][0] < rows[j][0] + }) + + table.AppendBulk(rows) + table.Render() +} + +func propNameAndAnchor(prop, title string) (string, string) { + if len(title) > 0 { + return title, strings.ToLower(title) + } + return string(PropertyTypeObject), strings.ToLower(prop) +} + +// in returns true if a string slice contains a specific string. +func in(strs []string, str string) bool { + for _, s := range strs { + if s == str { + return true + } + } + return false +} + +// formatForTable returns string usable in a Markdown table. +// It trims white spaces, replaces new lines and pipe characters. +func formatForTable(in string) string { + s := strings.TrimSpace(in) + s = strings.ReplaceAll(s, "\n", "
") + s = strings.ReplaceAll(s, "|", "|") + return s +} + +type PropertyTypes []PropertyType + +func (pts *PropertyTypes) HasType(pt PropertyType) bool { + for _, t := range *pts { + if t == pt { + return true + } + } + return false +} + +func (pts *PropertyTypes) UnmarshalJSON(data []byte) error { + var value interface{} + if err := json.Unmarshal(data, &value); err != nil { + return err + } + + switch val := value.(type) { + case string: + *pts = []PropertyType{PropertyType(val)} + return nil + case []interface{}: + var pt []PropertyType + for _, t := range val { + s, ok := t.(string) + if !ok { + return errors.New("unsupported property type") + } + pt = append(pt, PropertyType(s)) + } + *pts = pt + default: + return errors.New("unsupported property type") + } + + return nil +} + +type PropertyType string + +const ( + PropertyTypeString PropertyType = "string" + PropertyTypeNumber PropertyType = "number" + PropertyTypeBoolean PropertyType = "boolean" + PropertyTypeObject PropertyType = "object" + PropertyTypeArray PropertyType = "array" + PropertyTypeNull PropertyType = "null" +) + +type Any struct { + value interface{} +} + +func (u *Any) UnmarshalJSON(data []byte) error { + if err := json.Unmarshal(data, &u.value); err != nil { + return err + } + return nil +} + +func (u *Any) String() string { + return fmt.Sprintf("%v", u.value) +} diff --git a/pkg/codegen/tmpl/docs.tmpl b/pkg/codegen/tmpl/docs.tmpl new file mode 100644 index 00000000000..7f135c97cea --- /dev/null +++ b/pkg/codegen/tmpl/docs.tmpl @@ -0,0 +1,13 @@ +--- +keywords: + - grafana + - schema +title: {{ .KindName }} kind +--- + +# {{ .KindName }} kind + +### Maturity: {{ .KindMaturity }} +### Version: {{ .KindVersion }} + +{{ .Markdown }} From 997105c20dcaaa6abbfcc06584251789c69195d9 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 16 Jan 2023 14:03:20 +0100 Subject: [PATCH 15/65] Config: Support JSON list syntax (#61288) * Config: Separate lists either by spaces or by commas. * Simplify space separation * use separate function for the config strings * Change behavior only if string contains quotes * add test for invalid string * Use JSON list syntax * ignore leading spaces when process list * Add notes about using JSON lists into the docs * Fix typo * Apply suggestions from code review Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> --- .../configure-authentication/gitlab/index.md | 6 ++++++ .../configure-authentication/okta/index.md | 6 ++++++ .../configure-authentication/saml/index.md | 6 ++++++ pkg/util/strings.go | 11 +++++++++++ pkg/util/strings_test.go | 17 +++++++++++------ 5 files changed, 40 insertions(+), 6 deletions(-) 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 63f38d3cd4d..c15151b064f 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 @@ -110,6 +110,12 @@ the group `foo`, set allowed_groups = example, foo/bar ``` +To put values containing spaces in the list, use the following JSON syntax: + +```ini +allowed_groups = ["Admins", "Software Engineers"] +``` + Note that in GitLab, the group or subgroup name doesn't always match its display name, especially if the display name contains spaces or special characters. Make sure you always use the group or subgroup name as it appears diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md index ae587b56cb4..4b3834cf72e 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md @@ -81,6 +81,12 @@ The `allowed_domains` option limits access to the users belonging to the specifi allowed_domains = mycompany.com mycompany.org ``` +To put values containing spaces in the list, use the following JSON syntax: + +```ini +allowed_groups = ["Admins", "Software Engineers"] +``` + ### Map roles Grafana can attempt to do role mapping through Okta OAuth. In order to achieve this, Grafana checks for the presence of a role using the [JMESPath](http://jmespath.org/examples.html) specified via the `role_attribute_path` configuration option. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md index df1e6340556..82aab5f70cd 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md @@ -390,6 +390,12 @@ You can use `*` as the Grafana organization in the mapping if you want all users With the [`allowed_organizations`]({{< relref "../../../configure-grafana/enterprise-configuration/#allowed-organizations" >}}) option you can specify a list of organizations where the user must be a member of at least one of them to be able to log in to Grafana. +To put values containing spaces in the list, use the following JSON syntax: + +```ini +allowed_organizations = ["org 1", "second org"] +``` + ### Example SAML configuration ```bash diff --git a/pkg/util/strings.go b/pkg/util/strings.go index 1cacce669ef..2b469833e5c 100644 --- a/pkg/util/strings.go +++ b/pkg/util/strings.go @@ -1,6 +1,7 @@ package util import ( + "encoding/json" "fmt" "math" "strings" @@ -33,6 +34,16 @@ func SplitString(str string) []string { return []string{} } + // JSON list syntax support + if strings.Index(strings.TrimSpace(str), "[") == 0 { + var res []string + err := json.Unmarshal([]byte(str), &res) + if err != nil { + return []string{} + } + return res + } + return strings.Fields(strings.ReplaceAll(str, ",", " ")) } diff --git a/pkg/util/strings_test.go b/pkg/util/strings_test.go index efb3dbbb773..876cb705974 100644 --- a/pkg/util/strings_test.go +++ b/pkg/util/strings_test.go @@ -46,12 +46,17 @@ func TestStringsFallback3(t *testing.T) { func TestSplitString(t *testing.T) { tests := map[string][]string{ - "": {}, - "test": {"test"}, - "test1 test2 test3": {"test1", "test2", "test3"}, - "test1,test2,test3": {"test1", "test2", "test3"}, - "test1, test2, test3": {"test1", "test2", "test3"}, - "test1 , test2 test3": {"test1", "test2", "test3"}, + "": {}, + "test": {"test"}, + " test1 test2 test3": {"test1", "test2", "test3"}, + "test1,test2,test3": {"test1", "test2", "test3"}, + "test1, test2, test3": {"test1", "test2", "test3"}, + "test1 , test2 test3": {"test1", "test2", "test3"}, + "foo, bar baz": {"foo", "bar", "baz"}, + `["foo", "bar baz"]`: {"foo", "bar baz"}, + `["foo", "bar \"baz\""]`: {"foo", "bar \"baz\""}, + ` ["foo", "bar baz"]`: {"foo", "bar baz"}, + `[]`: {}, } for input, expected := range tests { assert.EqualValues(t, expected, SplitString(input)) From 07bbc0716c8a5e105bbf96cf6965c03cd6f199aa Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Mon, 16 Jan 2023 15:16:06 +0100 Subject: [PATCH 16/65] Auth: Fix correct error for updateapikey in context handler (#61544) * fix: correct error for updateapikey * refactor: send the correct err forward * update: based on review --- pkg/services/contexthandler/contexthandler.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index b31a0e8b72a..aad500e1451 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -350,11 +350,17 @@ func (h *ContextHandler) initContextWithAPIKey(reqContext *models.ReqContext) bo return true } - // update api_key last used date - if err := h.apiKeyService.UpdateAPIKeyLastUsedDate(reqContext.Req.Context(), apikey.Id); err != nil { - reqContext.JsonApiErr(http.StatusInternalServerError, InvalidAPIKey, errKey) - return true - } + // non-blocking update api_key last used date + go func(id int64) { + defer func() { + if err := recover(); err != nil { + reqContext.Logger.Error("api key authentication panic", "err", err) + } + }() + 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) 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. From 7c2522c4771b3c54dfc7cccd87cc076067f17cd4 Mon Sep 17 00:00:00 2001 From: idafurjes <36131195+idafurjes@users.noreply.github.com> Date: Mon, 16 Jan 2023 16:33:55 +0100 Subject: [PATCH 17/65] Chore: Move dashboard models to dashboard pkg (#61458) * Copy dashboard models to dashboard pkg * Use some models from current pkg instead of models * Adjust api pkg * Adjust pkg services * Fix lint --- pkg/api/annotations.go | 18 +- pkg/api/annotations_test.go | 34 +- pkg/api/dashboard.go | 108 ++-- pkg/api/dashboard_permission.go | 2 +- pkg/api/dashboard_permission_test.go | 10 +- pkg/api/dashboard_snapshot_test.go | 20 +- pkg/api/dashboard_test.go | 80 +-- pkg/api/folder_test.go | 10 +- pkg/api/org_users_test.go | 11 +- pkg/api/playlist_play.go | 9 +- pkg/api/preferences.go | 23 +- pkg/api/preferences_test.go | 13 +- pkg/api/stars.go | 13 +- .../ossaccesscontrol/permissions_services.go | 12 +- pkg/services/alerting/eval_context.go | 10 +- pkg/services/alerting/extractor.go | 4 +- pkg/services/alerting/extractor_test.go | 25 +- pkg/services/alerting/models.go | 4 +- pkg/services/alerting/notifier.go | 2 +- pkg/services/alerting/notifier_test.go | 3 +- pkg/services/alerting/test_rule.go | 4 +- .../annotationsimpl/xorm_store_test.go | 35 +- .../comments/commentmodel/permissions.go | 9 +- .../dashboardimport/service/service.go | 20 +- .../dashboardimport/service/service_test.go | 50 +- pkg/services/dashboards/accesscontrol.go | 17 +- pkg/services/dashboards/accesscontrol_test.go | 17 +- pkg/services/dashboards/dashboard.go | 28 +- .../dashboards/dashboard_provisioning_mock.go | 20 +- .../dashboards/dashboard_service_mock.go | 40 +- pkg/services/dashboards/database/acl_test.go | 71 +-- pkg/services/dashboards/database/database.go | 130 ++--- .../database/database_folder_test.go | 182 +++---- .../database/database_provisioning_test.go | 43 +- .../dashboards/database/database_test.go | 200 ++++---- pkg/services/dashboards/models.go | 298 ++++++++++- .../dashboards/service/dashboard_service.go | 96 ++-- .../dashboard_service_integration_test.go | 470 +++++++++--------- .../service/dashboard_service_test.go | 42 +- pkg/services/dashboards/store_mock.go | 81 +-- pkg/services/folder/folderimpl/folder.go | 32 +- pkg/services/folder/folderimpl/folder_test.go | 80 +-- .../guardian/accesscontrol_guardian.go | 68 +-- .../guardian/accesscontrol_guardian_test.go | 32 +- pkg/services/guardian/guardian.go | 26 +- pkg/services/guardian/guardian_test.go | 34 +- pkg/services/guardian/guardian_util_test.go | 32 +- pkg/services/guardian/provider.go | 5 +- .../libraryelements_delete_test.go | 7 +- .../libraryelements_get_test.go | 5 +- .../libraryelements/libraryelements_test.go | 18 +- pkg/services/librarypanels/librarypanels.go | 7 +- .../librarypanels/librarypanels_test.go | 32 +- pkg/services/live/features/dashboard.go | 22 +- pkg/services/live/live.go | 18 +- pkg/services/navtree/navtreeimpl/navtree.go | 18 +- .../ngalert/state/historian/dashboard.go | 9 +- .../ngalert/state/historian/dashboard_test.go | 5 +- .../alerting/rules_provisioner.go | 15 +- .../provisioning/dashboards/file_reader.go | 30 +- .../dashboards/file_reader_test.go | 28 +- pkg/services/provisioning/dashboards/types.go | 9 +- .../provisioning/dashboards/validator_test.go | 8 +- pkg/services/provisioning/utils/utils.go | 4 +- pkg/services/publicdashboards/api/query.go | 2 +- .../publicdashboards/api/query_test.go | 16 +- .../publicdashboards/database/database.go | 5 +- .../database/database_test.go | 127 +++-- .../publicdashboards/models/models.go | 4 +- .../publicdashboards/models/models_test.go | 10 +- .../public_dashboard_service_mock.go | 28 +- .../public_dashboard_store_mock.go | 25 +- .../publicdashboards/publicdashboard.go | 10 +- .../publicdashboards/service/query.go | 17 +- .../publicdashboards/service/query_test.go | 48 +- .../publicdashboards/service/service.go | 5 +- .../publicdashboards/service/service_test.go | 71 ++- .../publicdashboards/validation/validation.go | 6 +- .../validation/validation_test.go | 6 +- .../querylibrary/querylibraryimpl/service.go | 4 +- pkg/services/querylibrary/types.go | 4 +- pkg/services/screenshot/screenshot.go | 9 +- pkg/services/screenshot/screenshot_test.go | 9 +- 83 files changed, 1770 insertions(+), 1444 deletions(-) diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index b86508acdc6..d48132b769b 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -50,7 +50,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 := models.GetDashboardQuery{Uid: query.DashboardUid, OrgId: c.OrgID} + dq := dashboards.GetDashboardQuery{UID: query.DashboardUid, OrgID: c.OrgID} err := hs.DashboardService.GetDashboard(c.Req.Context(), &dq) if err != nil { if hs.Features.IsEnabled(featuremgmt.FlagDashboardsFromStorage) { @@ -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 = dq.Result.ID } } @@ -79,11 +79,11 @@ func (hs *HTTPServer) GetAnnotations(c *models.ReqContext) response.Response { if val, ok := dashboardCache[item.DashboardId]; ok { item.DashboardUID = val } else { - query := models.GetDashboardQuery{Id: item.DashboardId, OrgId: c.OrgID} + 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 + item.DashboardUID = &query.Result.UID + dashboardCache[item.DashboardId] = &query.Result.UID } } } @@ -122,10 +122,10 @@ func (hs *HTTPServer) PostAnnotation(c *models.ReqContext) response.Response { // overwrite dashboardId when dashboardUID is not empty if cmd.DashboardUID != "" { - query := models.GetDashboardQuery{OrgId: c.OrgID, Uid: cmd.DashboardUID} + query := dashboards.GetDashboardQuery{OrgID: c.OrgID, UID: cmd.DashboardUID} err := hs.DashboardService.GetDashboard(c.Req.Context(), &query) if err == nil { - cmd.DashboardId = query.Result.Id + cmd.DashboardId = query.Result.ID } } @@ -379,10 +379,10 @@ func (hs *HTTPServer) MassDeleteAnnotations(c *models.ReqContext) response.Respo } if cmd.DashboardUID != "" { - query := models.GetDashboardQuery{OrgId: c.OrgID, Uid: cmd.DashboardUID} + query := dashboards.GetDashboardQuery{OrgID: c.OrgID, UID: cmd.DashboardUID} err := hs.DashboardService.GetDashboard(c.Req.Context(), &query) if err == nil { - cmd.DashboardId = query.Result.Id + cmd.DashboardId = query.Result.ID } } diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go index 7de7e945eba..b85bab9418e 100644 --- a/pkg/api/annotations_test.go +++ b/pkg/api/annotations_test.go @@ -227,11 +227,11 @@ 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("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - Uid: q.Uid, + 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) postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, cmd, store, dashSvc, func(sc *scenarioContext) { @@ -244,7 +244,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("*models.GetDashboardQuery")) + dashSvc.AssertCalled(t, "GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")) }) putAnnotationScenario(t, "When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId", role, updateCmd, func(sc *scenarioContext) { @@ -267,11 +267,11 @@ func TestAnnotationsAPIEndpoint(t *testing.T) { }) dashSvc = dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.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, + UID: deleteWithDashboardUIDCmd.DashboardUID, } }).Return(nil) deleteAnnotationsScenario(t, "When calling POST with dashboardUID on", "/api/annotations/mass-delete", @@ -279,7 +279,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("*models.GetDashboardQuery")) + dashSvc.AssertCalled(t, "GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")) }) }) }) @@ -687,11 +687,11 @@ func setUpACL() { {Role: &editorRole, Permission: models.PERMISSION_EDIT}, } }).Return(nil) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - Uid: q.Uid, + 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) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 1dd8e021ece..addac788e9e 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -109,7 +109,7 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response { // If public dashboards is enabled and we have a public dashboard, update meta // values if hs.Features.IsEnabled(featuremgmt.FlagPublicDashboards) { - publicDashboard, err := hs.PublicDashboardsApi.PublicDashboardService.FindByDashboardUid(c.Req.Context(), c.OrgID, dash.Uid) + publicDashboard, err := hs.PublicDashboardsApi.PublicDashboardService.FindByDashboardUid(c.Req.Context(), c.OrgID, dash.UID) if err != nil && !errors.Is(err, publicdashboardModels.ErrPublicDashboardNotFound) { return response.Error(500, "Error while retrieving public dashboards", err) } @@ -146,7 +146,7 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response { canAdmin, _ := guardian.CanAdmin() canDelete, _ := guardian.CanDelete() - isStarred, err := hs.isDashboardStarredByUser(c, dash.Id) + isStarred, err := hs.isDashboardStarredByUser(c, dash.ID) if err != nil { return response.Error(500, "Error while checking if dashboard was starred by user", err) } @@ -182,8 +182,8 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response { Version: dash.Version, HasACL: dash.HasACL, IsFolder: dash.IsFolder, - FolderId: dash.FolderId, - Url: dash.GetUrl(), + FolderId: dash.FolderID, + Url: dash.GetURL(), FolderTitle: "General", AnnotationsPermissions: annotationPermissions, PublicDashboardEnabled: publicDashboardEnabled, @@ -191,20 +191,20 @@ func (hs *HTTPServer) GetDashboard(c *models.ReqContext) response.Response { } // lookup folder title - if dash.FolderId > 0 { - query := models.GetDashboardQuery{Id: dash.FolderId, OrgId: c.OrgID} + if dash.FolderID > 0 { + query := dashboards.GetDashboardQuery{ID: dash.FolderID, OrgID: c.OrgID} if err := hs.DashboardService.GetDashboard(c.Req.Context(), &query); 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.FolderUid = query.Result.UID meta.FolderTitle = query.Result.Title - meta.FolderUrl = query.Result.GetUrl() + meta.FolderUrl = query.Result.GetURL() } - provisioningData, err := hs.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardID(c.Req.Context(), dash.Id) + provisioningData, err := hs.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardID(c.Req.Context(), dash.ID) if err != nil { return response.Error(500, "Error while checking if dashboard is provisioned", err) } @@ -275,13 +275,13 @@ func (hs *HTTPServer) getUserLogin(ctx context.Context, userID int64) string { return user.Login } -func (hs *HTTPServer) getDashboardHelper(ctx context.Context, orgID int64, id int64, uid string) (*models.Dashboard, response.Response) { - var query models.GetDashboardQuery +func (hs *HTTPServer) getDashboardHelper(ctx context.Context, orgID int64, id int64, uid string) (*dashboards.Dashboard, response.Response) { + var query dashboards.GetDashboardQuery if len(uid) > 0 { - query = models.GetDashboardQuery{Uid: uid, Id: id, OrgId: orgID} + query = dashboards.GetDashboardQuery{UID: uid, ID: id, OrgID: orgID} } else { - query = models.GetDashboardQuery{Id: id, OrgId: orgID} + query = dashboards.GetDashboardQuery{ID: id, OrgID: orgID} } if err := hs.DashboardService.GetDashboard(ctx, &query); err != nil { @@ -322,12 +322,12 @@ func (hs *HTTPServer) deleteDashboard(c *models.ReqContext) response.Response { } // disconnect all library elements for this dashboard - err = hs.LibraryElementService.DisconnectElementsFromDashboard(c.Req.Context(), dash.Id) + err = hs.LibraryElementService.DisconnectElementsFromDashboard(c.Req.Context(), dash.ID) if err != nil { - hs.log.Error("Failed to disconnect library elements", "dashboard", dash.Id, "user", c.SignedInUser.UserID, "error", err) + hs.log.Error("Failed to disconnect library elements", "dashboard", dash.ID, "user", c.SignedInUser.UserID, "error", err) } - err = hs.DashboardService.DeleteDashboard(c.Req.Context(), dash.Id, c.OrgID) + err = hs.DashboardService.DeleteDashboard(c.Req.Context(), dash.ID, c.OrgID) if err != nil { var dashboardErr dashboards.DashboardErr if ok := errors.As(err, &dashboardErr); ok { @@ -339,15 +339,15 @@ func (hs *HTTPServer) deleteDashboard(c *models.ReqContext) response.Response { } if hs.Live != nil { - err := hs.Live.GrafanaScope.Dashboards.DashboardDeleted(c.OrgID, c.ToUserDisplayDTO(), dash.Uid) + err := hs.Live.GrafanaScope.Dashboards.DashboardDeleted(c.OrgID, c.ToUserDisplayDTO(), dash.UID) if err != nil { - hs.log.Error("Failed to broadcast delete info", "dashboard", dash.Uid, "error", err) + hs.log.Error("Failed to broadcast delete info", "dashboard", dash.UID, "error", err) } } return response.JSON(http.StatusOK, util.DynMap{ "title": dash.Title, "message": fmt.Sprintf("Dashboard %s deleted", dash.Title), - "id": dash.Id, + "id": dash.ID, }) } @@ -367,7 +367,7 @@ func (hs *HTTPServer) deleteDashboard(c *models.ReqContext) response.Response { // 422: unprocessableEntityError // 500: internalServerError func (hs *HTTPServer) PostDashboard(c *models.ReqContext) response.Response { - cmd := models.SaveDashboardCommand{} + cmd := dashboards.SaveDashboardCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } @@ -397,15 +397,15 @@ func (hs *HTTPServer) PostDashboard(c *models.ReqContext) response.Response { return hs.postDashboard(c, cmd) } -func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboardCommand) response.Response { +func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd dashboards.SaveDashboardCommand) response.Response { ctx := c.Req.Context() var err error - cmd.OrgId = c.OrgID - cmd.UserId = c.UserID - if cmd.FolderUid != "" { + cmd.OrgID = c.OrgID + cmd.UserID = c.UserID + if cmd.FolderUID != "" { folder, err := hs.folderService.Get(ctx, &folder.GetFolderQuery{ OrgID: c.OrgID, - UID: &cmd.FolderUid, + UID: &cmd.FolderUID, SignedInUser: c.SignedInUser, }) if err != nil { @@ -414,11 +414,11 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa } return response.Error(500, "Error while checking folder ID", err) } - cmd.FolderId = folder.ID + cmd.FolderID = folder.ID } dash := cmd.GetDashboardModel() - newDashboard := dash.Id == 0 + newDashboard := dash.ID == 0 if newDashboard { limitReached, err := hs.QuotaService.QuotaReached(c, dashboards.QuotaTargetSrv) if err != nil { @@ -430,14 +430,14 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa } var provisioningData *models.DashboardProvisioning - if dash.Id != 0 { - data, err := hs.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardID(c.Req.Context(), dash.Id) + if dash.ID != 0 { + data, err := hs.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardID(c.Req.Context(), dash.ID) if err != nil { return response.Error(500, "Error while checking if dashboard is provisioned using ID", err) } provisioningData = data - } else if dash.Uid != "" { - data, err := hs.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardUID(c.Req.Context(), dash.OrgId, dash.Uid) + } else if dash.UID != "" { + data, err := hs.dashboardProvisioningService.GetProvisionedDashboardDataByDashboardUID(c.Req.Context(), dash.OrgID, dash.UID) if err != nil && !errors.Is(err, dashboards.ErrProvisionedDashboardNotFound) && !errors.Is(err, dashboards.ErrDashboardNotFound) { return response.Error(500, "Error while checking if dashboard is provisioned", err) } @@ -452,7 +452,7 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa dashItem := &dashboards.SaveDashboardDTO{ Dashboard: dash, Message: cmd.Message, - OrgId: c.OrgID, + OrgID: c.OrgID, User: c.SignedInUser, Overwrite: cmd.Overwrite, } @@ -479,7 +479,7 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa } if liveerr != nil { - hs.log.Warn("unable to broadcast save event", "uid", dashboard.Uid, "error", liveerr) + hs.log.Warn("unable to broadcast save event", "uid", dashboard.UID, "error", liveerr) } } @@ -504,9 +504,9 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa "status": "success", "slug": dashboard.Slug, "version": dashboard.Version, - "id": dashboard.Id, - "uid": dashboard.Uid, - "url": dashboard.GetUrl(), + "id": dashboard.ID, + "uid": dashboard.UID, + "url": dashboard.GetURL(), }) } @@ -533,10 +533,10 @@ func (hs *HTTPServer) GetHomeDashboard(c *models.ReqContext) response.Response { } if preference.HomeDashboardID != 0 { - slugQuery := models.GetDashboardRefByIdQuery{Id: preference.HomeDashboardID} - err := hs.DashboardService.GetDashboardUIDById(c.Req.Context(), &slugQuery) + slugQuery := dashboards.GetDashboardRefByIDQuery{ID: preference.HomeDashboardID} + err := hs.DashboardService.GetDashboardUIDByID(c.Req.Context(), &slugQuery) if err == nil { - url := models.GetDashboardUrl(slugQuery.Result.Uid, slugQuery.Result.Slug) + url := models.GetDashboardUrl(slugQuery.Result.UID, slugQuery.Result.Slug) dashRedirect := dtos.DashboardRedirect{RedirectUri: url} return response.JSON(http.StatusOK, &dashRedirect) } @@ -655,15 +655,15 @@ func (hs *HTTPServer) GetDashboardVersions(c *models.ReqContext) response.Respon query := dashver.ListDashboardVersionsQuery{ OrgID: c.OrgID, - DashboardID: dash.Id, - DashboardUID: dash.Uid, + DashboardID: dash.ID, + DashboardUID: dash.UID, Limit: c.QueryInt("limit"), Start: c.QueryInt("start"), } res, err := hs.dashboardVersionService.List(c.Req.Context(), &query) if err != nil { - return response.Error(404, fmt.Sprintf("No versions found for dashboardId %d", dash.Id), err) + return response.Error(404, fmt.Sprintf("No versions found for dashboardId %d", dash.ID), err) } for _, version := range res { @@ -716,7 +716,7 @@ func (hs *HTTPServer) GetDashboardVersion(c *models.ReqContext) response.Respons var err error dashUID := web.Params(c.Req)[":uid"] - var dash *models.Dashboard + var dash *dashboards.Dashboard if dashUID == "" { dashID, err = strconv.ParseInt(web.Params(c.Req)[":dashboardId"], 10, 64) if err != nil { @@ -741,13 +741,13 @@ func (hs *HTTPServer) GetDashboardVersion(c *models.ReqContext) response.Respons version, _ := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 32) query := dashver.GetDashboardVersionQuery{ OrgID: c.OrgID, - DashboardID: dash.Id, + DashboardID: dash.ID, Version: int(version), } res, err := hs.dashboardVersionService.Get(c.Req.Context(), &query) if err != nil { - return response.Error(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dash.Id), err) + return response.Error(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dash.ID), err) } creator := anonString @@ -996,15 +996,15 @@ func (hs *HTTPServer) RestoreDashboardVersion(c *models.ReqContext) response.Res return response.Error(404, "Dashboard version not found", nil) } - saveCmd := models.SaveDashboardCommand{} + saveCmd := dashboards.SaveDashboardCommand{} saveCmd.RestoredFrom = version.Version - saveCmd.OrgId = c.OrgID - saveCmd.UserId = c.UserID + saveCmd.OrgID = c.OrgID + saveCmd.UserID = c.UserID saveCmd.Dashboard = version.Data saveCmd.Dashboard.Set("version", dash.Version) - saveCmd.Dashboard.Set("uid", dash.Uid) + saveCmd.Dashboard.Set("uid", dash.UID) saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version) - saveCmd.FolderId = dash.FolderId + saveCmd.FolderID = dash.FolderID return hs.postDashboard(c, saveCmd) } @@ -1033,18 +1033,18 @@ func (hs *HTTPServer) GetDashboardUIDs(c *models.ReqContext) { ids := strings.Split(web.Params(c.Req)[":ids"], ",") uids := make([]string, 0, len(ids)) - q := &models.GetDashboardRefByIdQuery{} + q := &dashboards.GetDashboardRefByIDQuery{} for _, idstr := range ids { id, err := strconv.ParseInt(idstr, 10, 64) if err != nil { continue } - q.Id = id - err = hs.DashboardService.GetDashboardUIDById(c.Req.Context(), q) + q.ID = id + err = hs.DashboardService.GetDashboardUIDByID(c.Req.Context(), q) if err != nil { continue } - uids = append(uids, q.Result.Uid) + uids = append(uids, q.Result.UID) } c.JSON(http.StatusOK, uids) } diff --git a/pkg/api/dashboard_permission.go b/pkg/api/dashboard_permission.go index 7307bc940aa..34a0ef066f3 100644 --- a/pkg/api/dashboard_permission.go +++ b/pkg/api/dashboard_permission.go @@ -192,7 +192,7 @@ func (hs *HTTPServer) UpdateDashboardPermissions(c *models.ReqContext) response. if err != nil { return response.Error(500, "Error while checking dashboard permissions", err) } - if err := hs.updateDashboardAccessControl(c.Req.Context(), dash.OrgId, dash.Uid, false, items, old); err != nil { + if err := hs.updateDashboardAccessControl(c.Req.Context(), dash.OrgID, dash.UID, false, items, old); err != nil { return response.Error(500, "Failed to update permissions", err) } return response.Success("Dashboard permissions updated") diff --git a/pkg/api/dashboard_permission_test.go b/pkg/api/dashboard_permission_test.go index 3b0ec28cb8a..ac250c18bfa 100644 --- a/pkg/api/dashboard_permission_test.go +++ b/pkg/api/dashboard_permission_test.go @@ -27,11 +27,11 @@ 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("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - Uid: q.Uid, + 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) defer dashboardStore.AssertExpectations(t) diff --git a/pkg/api/dashboard_snapshot_test.go b/pkg/api/dashboard_snapshot_test.go index a5c115401e8..72fb6199834 100644 --- a/pkg/api/dashboard_snapshot_test.go +++ b/pkg/api/dashboard_snapshot_test.go @@ -74,11 +74,11 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { teamSvc := &teamtest.FakeService{} dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - Uid: q.Uid, + 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).Maybe() dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*models.GetDashboardACLInfoListQuery")).Return(nil).Maybe() @@ -136,11 +136,11 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { externalRequest = req }) dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - OrgId: q.OrgId, + 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("*models.GetDashboardACLInfoListQuery")).Run(func(args mock.Arguments) { diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index baeaa8b222d..e4ded0810c8 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -128,16 +128,16 @@ func newTestLive(t *testing.T, store db.DB) *live.GrafanaLive { func TestDashboardAPIEndpoint(t *testing.T) { t.Run("Given a dashboard with a parent folder which does not have an ACL", func(t *testing.T) { - fakeDash := models.NewDashboard("Child dash") - fakeDash.Id = 1 - fakeDash.FolderId = 1 + fakeDash := dashboards.NewDashboard("Child dash") + fakeDash.ID = 1 + fakeDash.FolderID = 1 fakeDash.HasACL = false fakeDashboardVersionService := dashvertest.NewDashboardVersionServiceFake() fakeDashboardVersionService.ExpectedDashboardVersion = &dashver.DashboardVersionDTO{} teamService := &teamtest.FakeService{} dashboardService := dashboards.NewFakeDashboardService(t) - dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) + 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) mockSQLStore := mockstore.NewSQLStoreMock() @@ -236,16 +236,16 @@ func TestDashboardAPIEndpoint(t *testing.T) { }) t.Run("Given a dashboard with a parent folder which has an ACL", func(t *testing.T) { - fakeDash := models.NewDashboard("Child dash") - fakeDash.Id = 1 - fakeDash.FolderId = 1 + fakeDash := dashboards.NewDashboard("Child dash") + fakeDash.ID = 1 + fakeDash.FolderID = 1 fakeDash.HasACL = true fakeDashboardVersionService := dashvertest.NewDashboardVersionServiceFake() fakeDashboardVersionService.ExpectedDashboardVersion = &dashver.DashboardVersionDTO{} teamService := &teamtest.FakeService{} dashboardService := dashboards.NewFakeDashboardService(t) - dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) + 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("*models.GetDashboardACLInfoListQuery")).Run(func(args mock.Arguments) { @@ -403,9 +403,9 @@ 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("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = models.NewDashboard("test") + 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) dashboardService.On("DeleteDashboard", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")).Return(nil) @@ -505,9 +505,9 @@ func TestDashboardAPIEndpoint(t *testing.T) { setUpInner() sc.sqlStore = mockSQLStore dashboardService := dashboards.NewFakeDashboardService(t) - dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = models.NewDashboard("test") + 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) dashboardService.On("DeleteDashboard", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")).Return(nil) hs.callDeleteDashboardByUID(t, sc, dashboardService) @@ -610,7 +610,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { dashboardService := dashboards.NewFakeDashboardService(t) dashboardService.On("SaveDashboard", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), mock.AnythingOfType("bool")). - Return(&models.Dashboard{Id: dashID, Uid: "uid", Title: "Dash", Slug: "dash", Version: 2}, nil) + Return(&dashboards.Dashboard{ID: dashID, UID: "uid", Title: "Dash", Slug: "dash", Version: 2}, nil) postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", cmd, dashboardService, nil, func(sc *scenarioContext) { callPostDashboardShouldReturnSuccess(sc) @@ -642,7 +642,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { dashboardService := dashboards.NewFakeDashboardService(t) dashboardService.On("SaveDashboard", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), mock.AnythingOfType("bool")). - Return(&models.Dashboard{Id: dashID, Uid: "uid", Title: "Dash", Slug: "dash", Version: 2}, nil) + Return(&dashboards.Dashboard{ID: dashID, UID: "uid", Title: "Dash", Slug: "dash", Version: 2}, nil) mockFolder := &foldertest.FakeService{ ExpectedFolder: &folder.Folder{ID: 1, UID: "folderUID", Title: "Folder"}, @@ -808,11 +808,11 @@ func TestDashboardAPIEndpoint(t *testing.T) { teamSvc := &teamtest.FakeService{} dashSvc := dashboards.NewFakeDashboardService(t) dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*models.GetDashboardACLInfoListQuery")).Return(nil) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - OrgId: q.OrgId, - Id: q.Id, + 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) guardian.InitLegacyGuardian(&sqlmock, dashSvc, teamSvc) @@ -853,20 +853,20 @@ func TestDashboardAPIEndpoint(t *testing.T) { t.Run("Given dashboard in folder being restored should restore to folder", func(t *testing.T) { const folderID int64 = 1 - fakeDash := models.NewDashboard("Child dash") - fakeDash.Id = 2 - fakeDash.FolderId = folderID + fakeDash := dashboards.NewDashboard("Child dash") + fakeDash.ID = 2 + fakeDash.FolderID = folderID fakeDash.HasACL = false dashboardService := dashboards.NewFakeDashboardService(t) - dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) + 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("SaveDashboard", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), mock.AnythingOfType("bool")).Run(func(args mock.Arguments) { cmd := args.Get(1).(*dashboards.SaveDashboardDTO) - cmd.Dashboard = &models.Dashboard{ - Id: 2, Uid: "uid", Title: "Dash", Slug: "dash", Version: 1, + cmd.Dashboard = &dashboards.Dashboard{ + ID: 2, UID: "uid", Title: "Dash", Slug: "dash", Version: 1, } }).Return(nil, nil) @@ -891,19 +891,19 @@ func TestDashboardAPIEndpoint(t *testing.T) { }) t.Run("Given dashboard in general folder being restored should restore to general folder", func(t *testing.T) { - fakeDash := models.NewDashboard("Child dash") - fakeDash.Id = 2 + fakeDash := dashboards.NewDashboard("Child dash") + fakeDash.ID = 2 fakeDash.HasACL = false dashboardService := dashboards.NewFakeDashboardService(t) - dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) + 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("SaveDashboard", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), mock.AnythingOfType("bool")).Run(func(args mock.Arguments) { cmd := args.Get(1).(*dashboards.SaveDashboardDTO) - cmd.Dashboard = &models.Dashboard{ - Id: 2, Uid: "uid", Title: "Dash", Slug: "dash", Version: 1, + cmd.Dashboard = &dashboards.Dashboard{ + ID: 2, UID: "uid", Title: "Dash", Slug: "dash", Version: 1, } }).Return(nil, nil) @@ -936,9 +936,9 @@ 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("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{Id: 1, Data: dataValue} + 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("*models.GetDashboardACLInfoListQuery")).Run(func(args mock.Arguments) { q := args.Get(1).(*models.GetDashboardACLInfoListQuery) @@ -1256,7 +1256,7 @@ func (s mockDashboardProvisioningService) GetProvisionedDashboardDataByDashboard type mockLibraryPanelService struct { } -func (m *mockLibraryPanelService) ConnectLibraryPanelsForDashboard(c context.Context, signedInUser *user.SignedInUser, dash *models.Dashboard) error { +func (m *mockLibraryPanelService) ConnectLibraryPanelsForDashboard(c context.Context, signedInUser *user.SignedInUser, dash *dashboards.Dashboard) error { return nil } diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index 7ff1decbd26..2e66c071b4d 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -242,11 +242,11 @@ func createFolderScenario(t *testing.T, desc string, url string, routePattern st q := args.Get(1).(*models.GetDashboardACLInfoListQuery) q.Result = aclMockResp }).Return(nil) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - Uid: q.Uid, + 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) store := mockstore.NewSQLStoreMock() diff --git a/pkg/api/org_users_test.go b/pkg/api/org_users_test.go index d1097a57eeb..4b4b1a83081 100644 --- a/pkg/api/org_users_test.go +++ b/pkg/api/org_users_test.go @@ -18,6 +18,7 @@ import ( "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" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" @@ -203,9 +204,9 @@ func TestOrgUsersAPIEndpoint_LegacyAccessControl_FolderAdmin(t *testing.T) { setInitCtxSignedInViewer(sc.initCtx) // Create a dashboard folder - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, - FolderId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + FolderID: 1, IsFolder: true, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, @@ -220,7 +221,7 @@ func TestOrgUsersAPIEndpoint_LegacyAccessControl_FolderAdmin(t *testing.T) { // Grant our test Viewer with permission to admin the folder acls := []*models.DashboardACL{ { - DashboardID: folder.Id, + DashboardID: folder.ID, OrgID: testOrgID, UserID: testUserID, Permission: models.PERMISSION_ADMIN, @@ -228,7 +229,7 @@ func TestOrgUsersAPIEndpoint_LegacyAccessControl_FolderAdmin(t *testing.T) { Updated: time.Now(), }, } - err = sc.dashboardsStore.UpdateDashboardACL(context.Background(), folder.Id, acls) + err = sc.dashboardsStore.UpdateDashboardACL(context.Background(), folder.ID, acls) require.NoError(t, err) response := callAPI(sc.server, http.MethodGet, "/api/org/users/lookup", nil, t) diff --git a/pkg/api/playlist_play.go b/pkg/api/playlist_play.go index 36259a458a8..4a17235aa91 100644 --- a/pkg/api/playlist_play.go +++ b/pkg/api/playlist_play.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" _ "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/playlist" "github.com/grafana/grafana/pkg/services/search" "github.com/grafana/grafana/pkg/services/user" @@ -18,19 +19,19 @@ func (hs *HTTPServer) populateDashboardsByID(ctx context.Context, dashboardByIDs result := make(dtos.PlaylistDashboardsSlice, 0) if len(dashboardByIDs) > 0 { - dashboardQuery := models.GetDashboardsQuery{DashboardIds: dashboardByIDs} + dashboardQuery := dashboards.GetDashboardsQuery{DashboardIDs: dashboardByIDs} if err := hs.DashboardService.GetDashboards(ctx, &dashboardQuery); err != nil { return result, err } for _, item := range dashboardQuery.Result { result = append(result, dtos.PlaylistDashboard{ - Id: item.Id, + Id: item.ID, Slug: item.Slug, Title: item.Title, Uri: "db/" + item.Slug, - Url: models.GetDashboardUrl(item.Uid, item.Slug), - Order: dashboardIDOrder[item.Id], + Url: models.GetDashboardUrl(item.UID, item.Slug), + Order: dashboardIDOrder[item.ID], }) } } diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go index e5e2a40a009..a005b9cdbc9 100644 --- a/pkg/api/preferences.go +++ b/pkg/api/preferences.go @@ -7,6 +7,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" pref "github.com/grafana/grafana/pkg/services/preference" "github.com/grafana/grafana/pkg/web" ) @@ -30,15 +31,15 @@ func (hs *HTTPServer) SetHomeDashboard(c *models.ReqContext) response.Response { // UID is used in preference to identify dashboard dashboardID := cmd.HomeDashboardID if cmd.HomeDashboardUID != nil { - query := models.GetDashboardQuery{Uid: *cmd.HomeDashboardUID} - if query.Uid == "" { + query := dashboards.GetDashboardQuery{UID: *cmd.HomeDashboardUID} + if query.UID == "" { dashboardID = 0 // clear the value } else { err := hs.DashboardService.GetDashboard(c.Req.Context(), &query) if err != nil { return response.Error(404, "Dashboard not found", err) } - dashboardID = query.Result.Id + dashboardID = query.Result.ID } } @@ -75,10 +76,10 @@ 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 := models.GetDashboardQuery{Id: preference.HomeDashboardID, OrgId: orgID} + query := dashboards.GetDashboardQuery{ID: preference.HomeDashboardID, OrgID: orgID} err = hs.DashboardService.GetDashboard(ctx, &query) if err == nil { - dashboardUID = query.Result.Uid + dashboardUID = query.Result.UID } } @@ -130,8 +131,8 @@ func (hs *HTTPServer) updatePreferencesFor(ctx context.Context, orgID, userID, t dashboardID := dtoCmd.HomeDashboardID if dtoCmd.HomeDashboardUID != nil { - query := models.GetDashboardQuery{Uid: *dtoCmd.HomeDashboardUID, OrgId: orgID} - if query.Uid == "" { + query := dashboards.GetDashboardQuery{UID: *dtoCmd.HomeDashboardUID, OrgID: orgID} + if query.UID == "" { // clear the value dashboardID = 0 } else { @@ -139,7 +140,7 @@ func (hs *HTTPServer) updatePreferencesFor(ctx context.Context, orgID, userID, t if err != nil { return response.Error(404, "Dashboard not found", err) } - dashboardID = query.Result.Id + dashboardID = query.Result.ID } } dtoCmd.HomeDashboardID = dashboardID @@ -189,8 +190,8 @@ func (hs *HTTPServer) patchPreferencesFor(ctx context.Context, orgID, userID, te // convert dashboard UID to ID in order to store internally if it exists in the query, otherwise take the id from query dashboardID := dtoCmd.HomeDashboardID if dtoCmd.HomeDashboardUID != nil { - query := models.GetDashboardQuery{Uid: *dtoCmd.HomeDashboardUID, OrgId: orgID} - if query.Uid == "" { + query := dashboards.GetDashboardQuery{UID: *dtoCmd.HomeDashboardUID, OrgID: orgID} + if query.UID == "" { // clear the value defaultDash := int64(0) dashboardID = &defaultDash @@ -199,7 +200,7 @@ func (hs *HTTPServer) patchPreferencesFor(ctx context.Context, orgID, userID, te if err != nil { return response.Error(404, "Dashboard not found", err) } - dashboardID = &query.Result.Id + dashboardID = &query.Result.ID } } dtoCmd.HomeDashboardID = dashboardID diff --git a/pkg/api/preferences_test.go b/pkg/api/preferences_test.go index 751f23f9a5a..ffeb42d8818 100644 --- a/pkg/api/preferences_test.go +++ b/pkg/api/preferences_test.go @@ -14,7 +14,6 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "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" @@ -41,9 +40,9 @@ func TestAPIEndpoint_GetCurrentOrgPreferences_LegacyAccessControl(t *testing.T) cfg.RBACEnabled = false sc := setupHTTPServerWithCfg(t, true, cfg) dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{Uid: "home", Id: 1} + 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) sc.hs.DashboardService = dashSvc @@ -175,9 +174,9 @@ func TestAPIEndpoint_PatchUserPreferences(t *testing.T) { }) input = strings.NewReader(testUpdateOrgPreferencesWithHomeDashboardUIDCmd) dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{Uid: "home", Id: 1} + 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) sc.hs.DashboardService = dashSvc t.Run("Returns 200 on success", func(t *testing.T) { diff --git a/pkg/api/stars.go b/pkg/api/stars.go index c1a98c239aa..47bd698b969 100644 --- a/pkg/api/stars.go +++ b/pkg/api/stars.go @@ -6,6 +6,7 @@ import ( "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/star" "github.com/grafana/grafana/pkg/web" ) @@ -22,15 +23,15 @@ func (hs *HTTPServer) GetStars(c *models.ReqContext) response.Response { uids := []string{} for dashboardId := range iuserstars.UserStars { - query := &models.GetDashboardQuery{ - Id: dashboardId, - OrgId: c.OrgID, + query := &dashboards.GetDashboardQuery{ + ID: dashboardId, + OrgID: c.OrgID, } 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, query.Result.UID) } } return response.JSON(200, uids) @@ -91,7 +92,7 @@ func (hs *HTTPServer) StarDashboardByUID(c *models.ReqContext) response.Response return rsp } - cmd := star.StarDashboardCommand{UserID: c.UserID, DashboardID: dash.Id} + cmd := star.StarDashboardCommand{UserID: c.UserID, DashboardID: dash.ID} if err := hs.starService.Add(c.Req.Context(), &cmd); err != nil { return response.Error(http.StatusInternalServerError, "Failed to star dashboard", err) @@ -156,7 +157,7 @@ func (hs *HTTPServer) UnstarDashboardByUID(c *models.ReqContext) response.Respon return rsp } - cmd := star.UnstarDashboardCommand{UserID: c.UserID, DashboardID: dash.Id} + cmd := star.UnstarDashboardCommand{UserID: c.UserID, DashboardID: dash.ID} if err := hs.starService.Delete(c.Req.Context(), &cmd); err != nil { return response.Error(http.StatusInternalServerError, "Failed to unstar dashboard", err) diff --git a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go index 476477980ee..bf3d8fe217c 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go @@ -117,8 +117,8 @@ func ProvideDashboardPermissions( license models.Licensing, dashboardStore dashboards.Store, service accesscontrol.Service, teamService team.Service, userService user.Service, ) (*DashboardPermissionsService, error) { - getDashboard := func(ctx context.Context, orgID int64, resourceID string) (*models.Dashboard, error) { - query := &models.GetDashboardQuery{Uid: resourceID, OrgId: orgID} + 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 { return nil, err } @@ -145,12 +145,12 @@ func ProvideDashboardPermissions( if err != nil { return nil, err } - if dashboard.FolderId > 0 { - query := &models.GetDashboardQuery{Id: dashboard.FolderId, OrgId: orgID} + if dashboard.FolderID > 0 { + query := &dashboards.GetDashboardQuery{ID: dashboard.FolderID, OrgID: orgID} if _, err := dashboardStore.GetDashboard(ctx, query); err != nil { return nil, err } - return []string{dashboards.ScopeFoldersProvider.GetResourceScopeUID(query.Result.Uid)}, nil + return []string{dashboards.ScopeFoldersProvider.GetResourceScopeUID(query.Result.UID)}, nil } return []string{}, nil }, @@ -200,7 +200,7 @@ func ProvideFolderPermissions( Resource: "folders", ResourceAttribute: "uid", ResourceValidator: func(ctx context.Context, orgID int64, resourceID string) error { - query := &models.GetDashboardQuery{Uid: resourceID, OrgId: orgID} + query := &dashboards.GetDashboardQuery{UID: resourceID, OrgID: orgID} if _, err := dashboardStore.GetDashboard(ctx, query); err != nil { return err } diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index c67804c163c..41aec12ccea 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -29,7 +29,7 @@ type EvalContext struct { Rule *Rule Log log.Logger - dashboardRef *models.DashboardRef + dashboardRef *dashboards.DashboardRef ImagePublicURL string ImageOnDiskPath string @@ -116,13 +116,13 @@ func (c *EvalContext) GetNotificationTitle() string { } // GetDashboardUID returns the dashboard uid for the alert rule. -func (c *EvalContext) GetDashboardUID() (*models.DashboardRef, error) { +func (c *EvalContext) GetDashboardUID() (*dashboards.DashboardRef, error) { if c.dashboardRef != nil { return c.dashboardRef, nil } - uidQuery := &models.GetDashboardRefByIdQuery{Id: c.Rule.DashboardID} - if err := c.dashboardService.GetDashboardUIDById(c.Ctx, uidQuery); err != nil { + uidQuery := &dashboards.GetDashboardRefByIDQuery{ID: c.Rule.DashboardID} + if err := c.dashboardService.GetDashboardUIDByID(c.Ctx, uidQuery); err != nil { return nil, err } @@ -142,7 +142,7 @@ func (c *EvalContext) GetRuleURL() (string, error) { if err != nil { return "", err } - return fmt.Sprintf(urlFormat, models.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelID, c.Rule.OrgID), nil + return fmt.Sprintf(urlFormat, models.GetFullDashboardUrl(ref.UID, ref.Slug), c.Rule.PanelID, c.Rule.OrgID), nil } // GetNewState returns the new state from the alert rule evaluation. diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index b91a4578aa7..edb9c82dc01 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -149,7 +149,7 @@ func (e *DashAlertExtractorService) getAlertFromPanels(ctx context.Context, json PanelID: panelID, } if dashAlertInfo.Dash != nil { - ve.DashboardID = dashAlertInfo.Dash.Id + ve.DashboardID = dashAlertInfo.Dash.ID } return ve } @@ -175,7 +175,7 @@ func (e *DashAlertExtractorService) getAlertFromPanels(ctx context.Context, json } alert := &models.Alert{ - DashboardId: dashAlertInfo.Dash.Id, + DashboardId: dashAlertInfo.Dash.ID, OrgId: dashAlertInfo.OrgID, PanelId: panelID, Id: jsonAlert.Get("id").MustInt64(), diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go index 85f5d9e0eef..42d8b57154b 100644 --- a/pkg/services/alerting/extractor_test.go +++ b/pkg/services/alerting/extractor_test.go @@ -13,6 +13,7 @@ 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/datasources" "github.com/grafana/grafana/pkg/services/datasources/permissions" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" @@ -59,7 +60,7 @@ func TestAlertRuleExtraction(t *testing.T) { _, _ = extractor.GetAlerts(context.Background(), DashAlertInfo{ User: nil, - Dash: models.NewDashboardFromJson(dashJSON), + Dash: dashboards.NewDashboardFromJson(dashJSON), OrgID: 1, }) @@ -73,7 +74,7 @@ func TestAlertRuleExtraction(t *testing.T) { dsService.ExpectedDatasource = &datasources.DataSource{Id: 12} alerts, err := extractor.GetAlerts(context.Background(), DashAlertInfo{ User: nil, - Dash: models.NewDashboardFromJson(dashJSON), + Dash: dashboards.NewDashboardFromJson(dashJSON), OrgID: 1, }) @@ -125,7 +126,7 @@ func TestAlertRuleExtraction(t *testing.T) { _, err = extractor.GetAlerts(context.Background(), DashAlertInfo{ User: nil, - Dash: models.NewDashboardFromJson(dashJSON), + Dash: dashboards.NewDashboardFromJson(dashJSON), OrgID: 1, }) @@ -141,7 +142,7 @@ func TestAlertRuleExtraction(t *testing.T) { _, err = extractor.GetAlerts(context.Background(), DashAlertInfo{ User: nil, - Dash: models.NewDashboardFromJson(dashJSON), + Dash: dashboards.NewDashboardFromJson(dashJSON), OrgID: 1, }) @@ -156,7 +157,7 @@ func TestAlertRuleExtraction(t *testing.T) { _, err = extractor.GetAlerts(WithUAEnabled(context.Background(), true), DashAlertInfo{ User: nil, - Dash: models.NewDashboardFromJson(dashJSON), + Dash: dashboards.NewDashboardFromJson(dashJSON), OrgID: 1, }) require.Equal(t, "alert validation error: Alert on PanelId: 2 refers to query(B) that cannot be found. Legacy alerting queries are not able to be removed at this time in order to preserve the ability to rollback to previous versions of Grafana", err.Error()) @@ -172,7 +173,7 @@ func TestAlertRuleExtraction(t *testing.T) { dsService.ExpectedDatasource = &datasources.DataSource{Id: 12} alerts, err := extractor.GetAlerts(context.Background(), DashAlertInfo{ User: nil, - Dash: models.NewDashboardFromJson(dashJSON), + Dash: dashboards.NewDashboardFromJson(dashJSON), OrgID: 1, }) require.Nil(t, err) @@ -191,7 +192,7 @@ func TestAlertRuleExtraction(t *testing.T) { alerts, err := extractor.GetAlerts(context.Background(), DashAlertInfo{ User: nil, - Dash: models.NewDashboardFromJson(dashJSON), + Dash: dashboards.NewDashboardFromJson(dashJSON), OrgID: 1, }) require.Nil(t, err) @@ -218,7 +219,7 @@ func TestAlertRuleExtraction(t *testing.T) { alerts, err := extractor.GetAlerts(context.Background(), DashAlertInfo{ User: nil, - Dash: models.NewDashboardFromJson(dashJSON), + Dash: dashboards.NewDashboardFromJson(dashJSON), OrgID: 1, }) require.Nil(t, err) @@ -242,7 +243,7 @@ func TestAlertRuleExtraction(t *testing.T) { dashJSON, err := simplejson.NewJson(json) require.Nil(t, err) - dash := models.NewDashboardFromJson(dashJSON) + dash := dashboards.NewDashboardFromJson(dashJSON) alerts, err := extractor.GetAlerts(context.Background(), DashAlertInfo{ User: nil, @@ -263,7 +264,7 @@ func TestAlertRuleExtraction(t *testing.T) { dashAlertInfo := DashAlertInfo{ User: nil, - Dash: models.NewDashboardFromJson(dashJSON), + Dash: dashboards.NewDashboardFromJson(dashJSON), OrgID: 1, } @@ -284,7 +285,7 @@ func TestAlertRuleExtraction(t *testing.T) { dsService.ExpectedDatasource = graphite2Ds dashAlertInfo := DashAlertInfo{ User: nil, - Dash: models.NewDashboardFromJson(dashJSON), + Dash: dashboards.NewDashboardFromJson(dashJSON), OrgID: 1, } @@ -355,7 +356,7 @@ func TestFilterPermissionsErrors(t *testing.T) { dsPermissions.ErrResult = test.err _, err = extractor.GetAlerts(WithUAEnabled(context.Background(), true), DashAlertInfo{ User: nil, - Dash: models.NewDashboardFromJson(dashJSON), + Dash: dashboards.NewDashboardFromJson(dashJSON), OrgID: 1, }) assert.Equal(t, err, test.expectedErr) diff --git a/pkg/services/alerting/models.go b/pkg/services/alerting/models.go index c484c10a23a..3193b54c655 100644 --- a/pkg/services/alerting/models.go +++ b/pkg/services/alerting/models.go @@ -4,7 +4,7 @@ import ( "sync" "github.com/grafana/grafana/pkg/components/null" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/user" ) @@ -47,6 +47,6 @@ type EvalMatch struct { type DashAlertInfo struct { User *user.SignedInUser - Dash *models.Dashboard + Dash *dashboards.Dashboard OrgID int64 } diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index e938c8a2785..bed6ad22211 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -225,7 +225,7 @@ func (n *notificationService) renderAndUploadImage(evalCtx *EvalContext, timeout return err } - renderOpts.Path = fmt.Sprintf("d-solo/%s/%s?orgId=%d&panelId=%d", ref.Uid, ref.Slug, evalCtx.Rule.OrgID, evalCtx.Rule.PanelID) + renderOpts.Path = fmt.Sprintf("d-solo/%s/%s?orgId=%d&panelId=%d", ref.UID, ref.Slug, evalCtx.Rule.OrgID, evalCtx.Rule.PanelID) n.log.Debug("Rendering alert panel image", "ruleId", evalCtx.Rule.ID, "urlPath", renderOpts.Path) start := time.Now() diff --git a/pkg/services/alerting/notifier_test.go b/pkg/services/alerting/notifier_test.go index 334bac71823..070b8454488 100644 --- a/pkg/services/alerting/notifier_test.go +++ b/pkg/services/alerting/notifier_test.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/annotations/annotationstest" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/validations" @@ -176,7 +177,7 @@ func notificationServiceScenario(t *testing.T, name string, evalCtx *EvalContext Factory: newTestNotifier, }) - evalCtx.dashboardRef = &models.DashboardRef{Uid: "db-uid"} + evalCtx.dashboardRef = &dashboards.DashboardRef{UID: "db-uid"} store := evalCtx.Store.(*AlertStoreMock) diff --git a/pkg/services/alerting/test_rule.go b/pkg/services/alerting/test_rule.go index 9e3c3e1e4e7..3632d030d5d 100644 --- a/pkg/services/alerting/test_rule.go +++ b/pkg/services/alerting/test_rule.go @@ -5,14 +5,14 @@ import ( "fmt" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/annotations/annotationstest" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/user" ) // AlertTest makes a test alert. func (e *AlertEngine) AlertTest(orgID int64, dashboard *simplejson.Json, panelID int64, user *user.SignedInUser) (*EvalContext, error) { - dash := models.NewDashboardFromJson(dashboard) + dash := dashboards.NewDashboardFromJson(dashboard) dashInfo := DashAlertInfo{ User: user, Dash: dash, diff --git a/pkg/services/annotations/annotationsimpl/xorm_store_test.go b/pkg/services/annotations/annotationsimpl/xorm_store_test.go index fbe44f9c680..cb752e988ce 100644 --- a/pkg/services/annotations/annotationsimpl/xorm_store_test.go +++ b/pkg/services/annotations/annotationsimpl/xorm_store_test.go @@ -14,7 +14,6 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "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/annotations" "github.com/grafana/grafana/pkg/services/dashboards" @@ -61,9 +60,9 @@ func TestIntegrationAnnotations(t *testing.T) { dashboardStore, err := dashboardstore.ProvideDashboardStore(sql, sql.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sql, sql.Cfg), quotaService) require.NoError(t, err) - testDashboard1 := models.SaveDashboardCommand{ - UserId: 1, - OrgId: 1, + testDashboard1 := dashboards.SaveDashboardCommand{ + UserID: 1, + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "title": "Dashboard 1", }), @@ -72,9 +71,9 @@ func TestIntegrationAnnotations(t *testing.T) { dashboard, err := dashboardStore.SaveDashboard(context.Background(), testDashboard1) require.NoError(t, err) - testDashboard2 := models.SaveDashboardCommand{ - UserId: 1, - OrgId: 1, + testDashboard2 := dashboards.SaveDashboardCommand{ + UserID: 1, + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "title": "Dashboard 2", }), @@ -85,7 +84,7 @@ func TestIntegrationAnnotations(t *testing.T) { annotation := &annotations.Item{ OrgId: 1, UserId: 1, - DashboardId: dashboard.Id, + DashboardId: dashboard.ID, Text: "hello", Type: "alert", Epoch: 10, @@ -100,7 +99,7 @@ func TestIntegrationAnnotations(t *testing.T) { annotation2 := &annotations.Item{ OrgId: 1, UserId: 1, - DashboardId: dashboard2.Id, + DashboardId: dashboard2.ID, Text: "hello", Type: "alert", Epoch: 21, // Should swap epoch & epochEnd @@ -139,7 +138,7 @@ func TestIntegrationAnnotations(t *testing.T) { t.Run("Can query for annotation by dashboard id", func(t *testing.T) { items, err := repo.Get(context.Background(), &annotations.ItemQuery{ OrgId: 1, - DashboardId: dashboard.Id, + DashboardId: dashboard.ID, From: 0, To: 15, SignedInUser: testUser, @@ -417,7 +416,7 @@ func TestIntegrationAnnotations(t *testing.T) { annotation3 := &annotations.Item{ OrgId: 1, UserId: 1, - DashboardId: dashboard2.Id, + DashboardId: dashboard2.ID, Text: "toBeDeletedWithPanelId", Type: "alert", Epoch: 11, @@ -501,9 +500,9 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) { dashboardStore, err := dashboardstore.ProvideDashboardStore(sql, sql.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sql, sql.Cfg), quotaService) require.NoError(t, err) - testDashboard1 := models.SaveDashboardCommand{ - UserId: 1, - OrgId: 1, + testDashboard1 := dashboards.SaveDashboardCommand{ + UserID: 1, + OrgID: 1, IsFolder: false, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "title": "Dashboard 1", @@ -511,11 +510,11 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) { } dashboard, err := dashboardStore.SaveDashboard(context.Background(), testDashboard1) require.NoError(t, err) - dash1UID := dashboard.Uid + dash1UID := dashboard.UID - testDashboard2 := models.SaveDashboardCommand{ - UserId: 1, - OrgId: 1, + testDashboard2 := dashboards.SaveDashboardCommand{ + UserID: 1, + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "title": "Dashboard 2", }), diff --git a/pkg/services/comments/commentmodel/permissions.go b/pkg/services/comments/commentmodel/permissions.go index a57d19645e8..c11f54179e4 100644 --- a/pkg/services/comments/commentmodel/permissions.go +++ b/pkg/services/comments/commentmodel/permissions.go @@ -5,7 +5,6 @@ import ( "strconv" "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/annotations" "github.com/grafana/grafana/pkg/services/dashboards" @@ -29,16 +28,16 @@ func NewPermissionChecker(sqlStore db.DB, features featuremgmt.FeatureToggles, return &PermissionChecker{sqlStore: sqlStore, features: features, accessControl: accessControl, annotationsRepo: annotationsRepo} } -func (c *PermissionChecker) getDashboardByUid(ctx context.Context, orgID int64, uid string) (*models.Dashboard, error) { - query := models.GetDashboardQuery{Uid: uid, OrgId: orgID} +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 { return nil, err } return query.Result, nil } -func (c *PermissionChecker) getDashboardById(ctx context.Context, orgID int64, id int64) (*models.Dashboard, error) { - query := models.GetDashboardQuery{Id: id, OrgId: orgID} +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 { return nil, err } diff --git a/pkg/services/dashboardimport/service/service.go b/pkg/services/dashboardimport/service/service.go index 1178654d299..19234f0463c 100644 --- a/pkg/services/dashboardimport/service/service.go +++ b/pkg/services/dashboardimport/service/service.go @@ -106,17 +106,17 @@ func (s *ImportDashboardService) ImportDashboard(ctx context.Context, req *dashb req.FolderUid = folder.UID } - saveCmd := models.SaveDashboardCommand{ + saveCmd := dashboards.SaveDashboardCommand{ Dashboard: generatedDash, - OrgId: req.User.OrgID, - UserId: req.User.UserID, + OrgID: req.User.OrgID, + UserID: req.User.UserID, Overwrite: req.Overwrite, - PluginId: req.PluginId, - FolderId: req.FolderId, + PluginID: req.PluginId, + FolderID: req.FolderId, } dto := &dashboards.SaveDashboardDTO{ - OrgId: saveCmd.OrgId, + OrgID: saveCmd.OrgID, Dashboard: saveCmd.GetDashboardModel(), Overwrite: saveCmd.Overwrite, User: req.User, @@ -138,18 +138,18 @@ func (s *ImportDashboardService) ImportDashboard(ctx context.Context, req *dashb } return &dashboardimport.ImportDashboardResponse{ - UID: savedDashboard.Uid, + UID: savedDashboard.UID, PluginId: req.PluginId, Title: savedDashboard.Title, Path: req.Path, Revision: savedDashboard.Data.Get("revision").MustInt64(1), - FolderId: savedDashboard.FolderId, + FolderId: savedDashboard.FolderID, FolderUID: req.FolderUid, ImportedUri: "db/" + savedDashboard.Slug, - ImportedUrl: savedDashboard.GetUrl(), + ImportedUrl: savedDashboard.GetURL(), ImportedRevision: savedDashboard.Data.Get("revision").MustInt64(1), Imported: true, - DashboardId: savedDashboard.Id, + DashboardId: savedDashboard.ID, Slug: savedDashboard.Slug, }, nil } diff --git a/pkg/services/dashboardimport/service/service_test.go b/pkg/services/dashboardimport/service/service_test.go index 1222ccada0e..41c4fedec52 100644 --- a/pkg/services/dashboardimport/service/service_test.go +++ b/pkg/services/dashboardimport/service/service_test.go @@ -27,16 +27,16 @@ func TestImportDashboardService(t *testing.T) { var importDashboardArg *dashboards.SaveDashboardDTO dashboardService := &dashboardServiceMock{ - importDashboardFunc: func(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*models.Dashboard, error) { + importDashboardFunc: func(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*dashboards.Dashboard, error) { importDashboardArg = dto - return &models.Dashboard{ - Id: 4, - Uid: dto.Dashboard.Uid, + return &dashboards.Dashboard{ + ID: 4, + UID: dto.Dashboard.UID, Slug: dto.Dashboard.Slug, - OrgId: 3, + OrgID: 3, Version: dto.Dashboard.Version, - PluginId: "prometheus", - FolderId: dto.Dashboard.FolderId, + PluginID: "prometheus", + FolderID: dto.Dashboard.FolderID, Title: dto.Dashboard.Title, Data: dto.Dashboard.Data, }, nil @@ -50,7 +50,7 @@ func TestImportDashboardService(t *testing.T) { importLibraryPanelsForDashboard = true return nil }, - connectLibraryPanelsForDashboardFunc: func(ctx context.Context, signedInUser *user.SignedInUser, dash *models.Dashboard) error { + connectLibraryPanelsForDashboardFunc: func(ctx context.Context, signedInUser *user.SignedInUser, dash *dashboards.Dashboard) error { connectLibraryPanelsForDashboardCalled = true return nil }, @@ -84,10 +84,10 @@ func TestImportDashboardService(t *testing.T) { require.Equal(t, "UDdpyzz7z", resp.UID) require.NotNil(t, importDashboardArg) - require.Equal(t, int64(3), importDashboardArg.OrgId) + require.Equal(t, int64(3), importDashboardArg.OrgID) require.Equal(t, int64(2), importDashboardArg.User.UserID) - require.Equal(t, "prometheus", importDashboardArg.Dashboard.PluginId) - require.Equal(t, int64(5), importDashboardArg.Dashboard.FolderId) + require.Equal(t, "prometheus", importDashboardArg.Dashboard.PluginID) + require.Equal(t, int64(5), importDashboardArg.Dashboard.FolderID) panel := importDashboardArg.Dashboard.Data.Get("panels").GetIndex(0) require.Equal(t, "prom", panel.Get("datasource").MustString()) @@ -99,16 +99,16 @@ func TestImportDashboardService(t *testing.T) { t.Run("When importing a non-plugin dashboard should save dashboard and sync library panels", func(t *testing.T) { var importDashboardArg *dashboards.SaveDashboardDTO dashboardService := &dashboardServiceMock{ - importDashboardFunc: func(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*models.Dashboard, error) { + importDashboardFunc: func(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*dashboards.Dashboard, error) { importDashboardArg = dto - return &models.Dashboard{ - Id: 4, - Uid: dto.Dashboard.Uid, + return &dashboards.Dashboard{ + ID: 4, + UID: dto.Dashboard.UID, Slug: dto.Dashboard.Slug, - OrgId: 3, + OrgID: 3, Version: dto.Dashboard.Version, - PluginId: "prometheus", - FolderId: dto.Dashboard.FolderId, + PluginID: "prometheus", + FolderID: dto.Dashboard.FolderID, Title: dto.Dashboard.Title, Data: dto.Dashboard.Data, }, nil @@ -148,10 +148,10 @@ func TestImportDashboardService(t *testing.T) { require.Equal(t, "UDdpyzz7z", resp.UID) require.NotNil(t, importDashboardArg) - require.Equal(t, int64(3), importDashboardArg.OrgId) + require.Equal(t, int64(3), importDashboardArg.OrgID) require.Equal(t, int64(2), importDashboardArg.User.UserID) - require.Equal(t, "", importDashboardArg.Dashboard.PluginId) - require.Equal(t, int64(5), importDashboardArg.Dashboard.FolderId) + require.Equal(t, "", importDashboardArg.Dashboard.PluginID) + require.Equal(t, int64(5), importDashboardArg.Dashboard.FolderID) panel := importDashboardArg.Dashboard.Data.Get("panels").GetIndex(0) require.Equal(t, "prom", panel.Get("datasource").MustString()) @@ -191,10 +191,10 @@ func (m *pluginDashboardServiceMock) LoadPluginDashboard(ctx context.Context, re type dashboardServiceMock struct { dashboards.DashboardService - importDashboardFunc func(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*models.Dashboard, error) + importDashboardFunc func(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*dashboards.Dashboard, error) } -func (s *dashboardServiceMock) ImportDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*models.Dashboard, error) { +func (s *dashboardServiceMock) ImportDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*dashboards.Dashboard, error) { if s.importDashboardFunc != nil { return s.importDashboardFunc(ctx, dto) } @@ -204,11 +204,11 @@ func (s *dashboardServiceMock) ImportDashboard(ctx context.Context, dto *dashboa type libraryPanelServiceMock struct { librarypanels.Service - connectLibraryPanelsForDashboardFunc func(c context.Context, signedInUser *user.SignedInUser, dash *models.Dashboard) error + connectLibraryPanelsForDashboardFunc func(c context.Context, signedInUser *user.SignedInUser, dash *dashboards.Dashboard) error importLibraryPanelsForDashboardFunc func(c context.Context, signedInUser *user.SignedInUser, libraryPanels *simplejson.Json, panels []interface{}, folderID int64) error } -func (s *libraryPanelServiceMock) ConnectLibraryPanelsForDashboard(ctx context.Context, signedInUser *user.SignedInUser, dash *models.Dashboard) error { +func (s *libraryPanelServiceMock) ConnectLibraryPanelsForDashboard(ctx context.Context, signedInUser *user.SignedInUser, dash *dashboards.Dashboard) error { if s.connectLibraryPanelsForDashboardFunc != nil { return s.connectLibraryPanelsForDashboardFunc(ctx, signedInUser, dash) } diff --git a/pkg/services/dashboards/accesscontrol.go b/pkg/services/dashboards/accesscontrol.go index 49df72b9b45..6350c35188d 100644 --- a/pkg/services/dashboards/accesscontrol.go +++ b/pkg/services/dashboards/accesscontrol.go @@ -4,7 +4,6 @@ import ( "context" "strings" - "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" ) @@ -97,7 +96,7 @@ func NewDashboardIDScopeResolver(db Store) (string, ac.ScopeAttributeResolver) { return nil, err } - dashboard, err := db.GetDashboard(ctx, &models.GetDashboardQuery{Id: id, OrgId: orgID}) + dashboard, err := db.GetDashboard(ctx, &GetDashboardQuery{ID: id, OrgID: orgID}) if err != nil { return nil, err } @@ -120,7 +119,7 @@ func NewDashboardUIDScopeResolver(db Store) (string, ac.ScopeAttributeResolver) return nil, err } - dashboard, err := db.GetDashboard(ctx, &models.GetDashboardQuery{Uid: uid, OrgId: orgID}) + dashboard, err := db.GetDashboard(ctx, &GetDashboardQuery{UID: uid, OrgID: orgID}) if err != nil { return nil, err } @@ -129,16 +128,16 @@ func NewDashboardUIDScopeResolver(db Store) (string, ac.ScopeAttributeResolver) }) } -func resolveDashboardScope(ctx context.Context, db Store, orgID int64, dashboard *models.Dashboard) ([]string, error) { +func resolveDashboardScope(ctx context.Context, db Store, orgID int64, dashboard *Dashboard) ([]string, error) { var folderUID string - if dashboard.FolderId < 0 { - return []string{ScopeDashboardsProvider.GetResourceScopeUID(dashboard.Uid)}, nil + if dashboard.FolderID < 0 { + return []string{ScopeDashboardsProvider.GetResourceScopeUID(dashboard.UID)}, nil } - if dashboard.FolderId == 0 { + if dashboard.FolderID == 0 { folderUID = ac.GeneralFolderUID } else { - folder, err := db.GetFolderByID(ctx, orgID, dashboard.FolderId) + folder, err := db.GetFolderByID(ctx, orgID, dashboard.FolderID) if err != nil { return nil, err } @@ -146,7 +145,7 @@ func resolveDashboardScope(ctx context.Context, db Store, orgID int64, dashboard } return []string{ - ScopeDashboardsProvider.GetResourceScopeUID(dashboard.Uid), + ScopeDashboardsProvider.GetResourceScopeUID(dashboard.UID), ScopeFoldersProvider.GetResourceScopeUID(folderUID), }, nil } diff --git a/pkg/services/dashboards/accesscontrol_test.go b/pkg/services/dashboards/accesscontrol_test.go index d59f739aec0..062994c87c3 100644 --- a/pkg/services/dashboards/accesscontrol_test.go +++ b/pkg/services/dashboards/accesscontrol_test.go @@ -10,7 +10,6 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/util" @@ -157,16 +156,16 @@ func TestNewDashboardIDScopeResolver(t *testing.T) { orgID := rand.Int63() folder := &folder.Folder{ID: 2, UID: "2"} - dashboard := &models.Dashboard{Id: 1, FolderId: folder.ID, Uid: "1"} + dashboard := &Dashboard{ID: 1, FolderID: folder.ID, UID: "1"} store.On("GetDashboard", mock.Anything, mock.Anything).Return(dashboard, nil).Once() store.On("GetFolderByID", mock.Anything, orgID, folder.ID).Return(folder, nil).Once() - scope := ac.Scope("dashboards", "id", strconv.FormatInt(dashboard.Id, 10)) + scope := ac.Scope("dashboards", "id", strconv.FormatInt(dashboard.ID, 10)) resolvedScopes, err := resolver.Resolve(context.Background(), orgID, scope) require.NoError(t, err) require.Len(t, resolvedScopes, 2) - require.Equal(t, fmt.Sprintf("dashboards:uid:%s", dashboard.Uid), resolvedScopes[0]) + require.Equal(t, fmt.Sprintf("dashboards:uid:%s", dashboard.UID), resolvedScopes[0]) require.Equal(t, fmt.Sprintf("folders:uid:%s", folder.UID), resolvedScopes[1]) }) @@ -180,7 +179,7 @@ func TestNewDashboardIDScopeResolver(t *testing.T) { store := &FakeDashboardStore{} _, resolver := NewDashboardIDScopeResolver(store) - dashboard := &models.Dashboard{Id: 1, FolderId: 0, Uid: "1"} + dashboard := &Dashboard{ID: 1, FolderID: 0, UID: "1"} store.On("GetDashboard", mock.Anything, mock.Anything).Return(dashboard, nil) resolved, err := resolver.Resolve(context.Background(), 1, ac.Scope("dashboards", "id", "1")) require.NoError(t, err) @@ -203,16 +202,16 @@ func TestNewDashboardUIDScopeResolver(t *testing.T) { orgID := rand.Int63() folder := &folder.Folder{ID: 2, UID: "2"} - dashboard := &models.Dashboard{Id: 1, FolderId: folder.ID, Uid: "1"} + dashboard := &Dashboard{ID: 1, FolderID: folder.ID, UID: "1"} store.On("GetDashboard", mock.Anything, mock.Anything).Return(dashboard, nil).Once() store.On("GetFolderByID", mock.Anything, orgID, folder.ID).Return(folder, nil).Once() - scope := ac.Scope("dashboards", "uid", dashboard.Uid) + scope := ac.Scope("dashboards", "uid", dashboard.UID) resolvedScopes, err := resolver.Resolve(context.Background(), orgID, scope) require.NoError(t, err) require.Len(t, resolvedScopes, 2) - require.Equal(t, fmt.Sprintf("dashboards:uid:%s", dashboard.Uid), resolvedScopes[0]) + require.Equal(t, fmt.Sprintf("dashboards:uid:%s", dashboard.UID), resolvedScopes[0]) require.Equal(t, fmt.Sprintf("folders:uid:%s", folder.UID), resolvedScopes[1]) }) @@ -226,7 +225,7 @@ func TestNewDashboardUIDScopeResolver(t *testing.T) { store := &FakeDashboardStore{} _, resolver := NewDashboardUIDScopeResolver(store) - dashboard := &models.Dashboard{Id: 1, FolderId: 0, Uid: "1"} + dashboard := &Dashboard{ID: 1, FolderID: 0, UID: "1"} store.On("GetDashboard", mock.Anything, mock.Anything).Return(dashboard, nil) resolved, err := resolver.Resolve(context.Background(), 1, ac.Scope("dashboards", "uid", "1")) require.NoError(t, err) diff --git a/pkg/services/dashboards/dashboard.go b/pkg/services/dashboards/dashboard.go index 6493b51b9d7..7c91092f410 100644 --- a/pkg/services/dashboards/dashboard.go +++ b/pkg/services/dashboards/dashboard.go @@ -12,19 +12,19 @@ import ( // //go:generate mockery --name DashboardService --structname FakeDashboardService --inpackage --filename dashboard_service_mock.go type DashboardService interface { - BuildSaveDashboardCommand(ctx context.Context, dto *SaveDashboardDTO, shouldValidateAlerts bool, validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) + 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 *models.GetDashboardQuery) error + GetDashboard(ctx context.Context, query *GetDashboardQuery) error GetDashboardACLInfoList(ctx context.Context, query *models.GetDashboardACLInfoListQuery) error - GetDashboards(ctx context.Context, query *models.GetDashboardsQuery) error + GetDashboards(ctx context.Context, query *GetDashboardsQuery) error GetDashboardTags(ctx context.Context, query *models.GetDashboardTagsQuery) error - GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error + GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) error HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *models.HasAdminPermissionInDashboardsOrFoldersQuery) error HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error - ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*models.Dashboard, 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) (*models.Dashboard, error) + SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*Dashboard, error) SearchDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) error UpdateDashboardACL(ctx context.Context, uid int64, items []*models.DashboardACL) error DeleteACLByUser(ctx context.Context, userID int64) error @@ -45,8 +45,8 @@ type DashboardProvisioningService interface { GetProvisionedDashboardData(ctx context.Context, name string) ([]*models.DashboardProvisioning, error) GetProvisionedDashboardDataByDashboardID(ctx context.Context, dashboardID int64) (*models.DashboardProvisioning, error) GetProvisionedDashboardDataByDashboardUID(ctx context.Context, orgID int64, dashboardUID string) (*models.DashboardProvisioning, error) - SaveFolderForProvisionedDashboards(context.Context, *SaveDashboardDTO) (*models.Dashboard, error) - SaveProvisionedDashboard(ctx context.Context, dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) + SaveFolderForProvisionedDashboards(context.Context, *SaveDashboardDTO) (*Dashboard, error) + SaveProvisionedDashboard(ctx context.Context, dto *SaveDashboardDTO, provisioning *DashboardProvisioning) (*Dashboard, error) UnprovisionDashboard(ctx context.Context, dashboardID int64) error } @@ -57,10 +57,10 @@ type Store interface { DeleteDashboard(ctx context.Context, cmd *models.DeleteDashboardCommand) error DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *models.DeleteOrphanedProvisionedDashboardsCommand) error FindDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) - GetDashboard(ctx context.Context, query *models.GetDashboardQuery) (*models.Dashboard, error) + GetDashboard(ctx context.Context, query *GetDashboardQuery) (*Dashboard, error) GetDashboardACLInfoList(ctx context.Context, query *models.GetDashboardACLInfoListQuery) error - GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error - GetDashboards(ctx context.Context, query *models.GetDashboardsQuery) error + GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) error + GetDashboards(ctx context.Context, query *GetDashboardsQuery) error // GetDashboardsByPluginID retrieves dashboards identified by plugin. GetDashboardsByPluginID(ctx context.Context, query *models.GetDashboardsByPluginIdQuery) error GetDashboardTags(ctx context.Context, query *models.GetDashboardTagsQuery) error @@ -71,12 +71,12 @@ type Store interface { HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error // SaveAlerts saves dashboard alerts. SaveAlerts(ctx context.Context, dashID int64, alerts []*models.Alert) error - SaveDashboard(ctx context.Context, cmd models.SaveDashboardCommand) (*models.Dashboard, error) - SaveProvisionedDashboard(ctx context.Context, cmd models.SaveDashboardCommand, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) + SaveDashboard(ctx context.Context, cmd SaveDashboardCommand) (*Dashboard, error) + SaveProvisionedDashboard(ctx context.Context, cmd SaveDashboardCommand, provisioning *DashboardProvisioning) (*Dashboard, error) UnprovisionDashboard(ctx context.Context, id int64) error UpdateDashboardACL(ctx context.Context, uid int64, items []*models.DashboardACL) error // ValidateDashboardBeforeSave validates a dashboard before save. - ValidateDashboardBeforeSave(ctx context.Context, dashboard *models.Dashboard, overwrite bool) (bool, error) + ValidateDashboardBeforeSave(ctx context.Context, dashboard *Dashboard, overwrite bool) (bool, error) DeleteACLByUser(context.Context, int64) error Count(context.Context, *quota.ScopeParameters) (*quota.Map, error) diff --git a/pkg/services/dashboards/dashboard_provisioning_mock.go b/pkg/services/dashboards/dashboard_provisioning_mock.go index 32493be45a7..b1e95a608b2 100644 --- a/pkg/services/dashboards/dashboard_provisioning_mock.go +++ b/pkg/services/dashboards/dashboard_provisioning_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.16.0. DO NOT EDIT. package dashboards @@ -112,15 +112,15 @@ func (_m *FakeDashboardProvisioning) GetProvisionedDashboardDataByDashboardUID(c } // SaveFolderForProvisionedDashboards provides a mock function with given fields: _a0, _a1 -func (_m *FakeDashboardProvisioning) SaveFolderForProvisionedDashboards(_a0 context.Context, _a1 *SaveDashboardDTO) (*models.Dashboard, error) { +func (_m *FakeDashboardProvisioning) SaveFolderForProvisionedDashboards(_a0 context.Context, _a1 *SaveDashboardDTO) (*Dashboard, error) { ret := _m.Called(_a0, _a1) - var r0 *models.Dashboard - if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO) *models.Dashboard); ok { + var r0 *Dashboard + if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO) *Dashboard); ok { r0 = rf(_a0, _a1) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.Dashboard) + r0 = ret.Get(0).(*Dashboard) } } @@ -135,20 +135,20 @@ func (_m *FakeDashboardProvisioning) SaveFolderForProvisionedDashboards(_a0 cont } // SaveProvisionedDashboard provides a mock function with given fields: ctx, dto, provisioning -func (_m *FakeDashboardProvisioning) SaveProvisionedDashboard(ctx context.Context, dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) { +func (_m *FakeDashboardProvisioning) SaveProvisionedDashboard(ctx context.Context, dto *SaveDashboardDTO, provisioning *DashboardProvisioning) (*Dashboard, error) { ret := _m.Called(ctx, dto, provisioning) - var r0 *models.Dashboard - if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO, *models.DashboardProvisioning) *models.Dashboard); ok { + var r0 *Dashboard + if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO, *DashboardProvisioning) *Dashboard); ok { r0 = rf(ctx, dto, provisioning) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.Dashboard) + r0 = ret.Get(0).(*Dashboard) } } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *SaveDashboardDTO, *models.DashboardProvisioning) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, *SaveDashboardDTO, *DashboardProvisioning) error); ok { r1 = rf(ctx, dto, provisioning) } else { r1 = ret.Error(1) diff --git a/pkg/services/dashboards/dashboard_service_mock.go b/pkg/services/dashboards/dashboard_service_mock.go index 180dd8016bd..ddf875a84c0 100644 --- a/pkg/services/dashboards/dashboard_service_mock.go +++ b/pkg/services/dashboards/dashboard_service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.16.0. DO NOT EDIT. package dashboards @@ -15,15 +15,15 @@ type FakeDashboardService struct { } // BuildSaveDashboardCommand provides a mock function with given fields: ctx, dto, shouldValidateAlerts, validateProvisionedDashboard -func (_m *FakeDashboardService) BuildSaveDashboardCommand(ctx context.Context, dto *SaveDashboardDTO, shouldValidateAlerts bool, validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) { +func (_m *FakeDashboardService) BuildSaveDashboardCommand(ctx context.Context, dto *SaveDashboardDTO, shouldValidateAlerts bool, validateProvisionedDashboard bool) (*SaveDashboardCommand, error) { ret := _m.Called(ctx, dto, shouldValidateAlerts, validateProvisionedDashboard) - var r0 *models.SaveDashboardCommand - if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO, bool, bool) *models.SaveDashboardCommand); ok { + var r0 *SaveDashboardCommand + if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO, bool, bool) *SaveDashboardCommand); ok { r0 = rf(ctx, dto, shouldValidateAlerts, validateProvisionedDashboard) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.SaveDashboardCommand) + r0 = ret.Get(0).(*SaveDashboardCommand) } } @@ -110,11 +110,11 @@ 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 *models.GetDashboardQuery) error { +func (_m *FakeDashboardService) GetDashboard(ctx context.Context, query *GetDashboardQuery) error { ret := _m.Called(ctx, query) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *models.GetDashboardQuery) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardQuery) error); ok { r0 = rf(ctx, query) } else { r0 = ret.Error(0) @@ -151,12 +151,12 @@ func (_m *FakeDashboardService) GetDashboardTags(ctx context.Context, query *mod return r0 } -// GetDashboardUIDById provides a mock function with given fields: ctx, query -func (_m *FakeDashboardService) GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error { +// GetDashboardUIDByID provides a mock function with given fields: ctx, query +func (_m *FakeDashboardService) GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) error { ret := _m.Called(ctx, query) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *models.GetDashboardRefByIdQuery) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardRefByIDQuery) error); ok { r0 = rf(ctx, query) } else { r0 = ret.Error(0) @@ -166,11 +166,11 @@ func (_m *FakeDashboardService) GetDashboardUIDById(ctx context.Context, query * } // GetDashboards provides a mock function with given fields: ctx, query -func (_m *FakeDashboardService) GetDashboards(ctx context.Context, query *models.GetDashboardsQuery) error { +func (_m *FakeDashboardService) GetDashboards(ctx context.Context, query *GetDashboardsQuery) error { ret := _m.Called(ctx, query) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *models.GetDashboardsQuery) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardsQuery) error); ok { r0 = rf(ctx, query) } else { r0 = ret.Error(0) @@ -208,15 +208,15 @@ func (_m *FakeDashboardService) HasEditPermissionInFolders(ctx context.Context, } // ImportDashboard provides a mock function with given fields: ctx, dto -func (_m *FakeDashboardService) ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*models.Dashboard, error) { +func (_m *FakeDashboardService) ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*Dashboard, error) { ret := _m.Called(ctx, dto) - var r0 *models.Dashboard - if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO) *models.Dashboard); ok { + var r0 *Dashboard + if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO) *Dashboard); ok { r0 = rf(ctx, dto) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.Dashboard) + r0 = ret.Get(0).(*Dashboard) } } @@ -245,15 +245,15 @@ func (_m *FakeDashboardService) MakeUserAdmin(ctx context.Context, orgID int64, } // SaveDashboard provides a mock function with given fields: ctx, dto, allowUiUpdate -func (_m *FakeDashboardService) SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error) { +func (_m *FakeDashboardService) SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*Dashboard, error) { ret := _m.Called(ctx, dto, allowUiUpdate) - var r0 *models.Dashboard - if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO, bool) *models.Dashboard); ok { + var r0 *Dashboard + if rf, ok := ret.Get(0).(func(context.Context, *SaveDashboardDTO, bool) *Dashboard); ok { r0 = rf(ctx, dto, allowUiUpdate) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.Dashboard) + r0 = ret.Get(0).(*Dashboard) } } diff --git a/pkg/services/dashboards/database/acl_test.go b/pkg/services/dashboards/database/acl_test.go index ae776c8a867..bb62752b290 100644 --- a/pkg/services/dashboards/database/acl_test.go +++ b/pkg/services/dashboards/database/acl_test.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/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotaimpl" @@ -25,7 +26,7 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { } var sqlStore *sqlstore.SQLStore var currentUser user.User - var savedFolder, childDash *models.Dashboard + var savedFolder, childDash *dashboards.Dashboard var dashboardStore *DashboardStore setup := func(t *testing.T) { @@ -36,14 +37,14 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { require.NoError(t, err) currentUser = createUser(t, sqlStore, "viewer", "Viewer", false) savedFolder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod", "webapp") - childDash = insertTestDashboard(t, dashboardStore, "2 test dash", 1, savedFolder.Id, false, "prod", "webapp") + childDash = insertTestDashboard(t, dashboardStore, "2 test dash", 1, savedFolder.ID, false, "prod", "webapp") } t.Run("Dashboard permission with userId and teamId set to 0", func(t *testing.T) { setup(t) - err := updateDashboardACL(t, dashboardStore, savedFolder.Id, models.DashboardACL{ + err := updateDashboardACL(t, dashboardStore, savedFolder.ID, models.DashboardACL{ OrgID: 1, - DashboardID: savedFolder.Id, + DashboardID: savedFolder.ID, Permission: models.PERMISSION_EDIT, }) require.Equal(t, models.ErrDashboardACLInfoMissing, err) @@ -51,7 +52,7 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { t.Run("Folder acl should include default acl", func(t *testing.T) { setup(t) - query := models.GetDashboardACLInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1} + query := models.GetDashboardACLInfoListQuery{DashboardID: savedFolder.ID, OrgID: 1} err := dashboardStore.GetDashboardACLInfoList(context.Background(), &query) require.Nil(t, err) @@ -68,7 +69,7 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { t.Run("Dashboard acl should include acl for parent folder", func(t *testing.T) { setup(t) - query := models.GetDashboardACLInfoListQuery{DashboardID: childDash.Id, OrgID: 1} + query := models.GetDashboardACLInfoListQuery{DashboardID: childDash.ID, OrgID: 1} err := dashboardStore.GetDashboardACLInfoList(context.Background(), &query) require.Nil(t, err) @@ -85,10 +86,10 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { t.Run("Folder with removed default permissions returns no acl items", func(t *testing.T) { setup(t) - err := dashboardStore.UpdateDashboardACL(context.Background(), savedFolder.Id, nil) + err := dashboardStore.UpdateDashboardACL(context.Background(), savedFolder.ID, nil) require.Nil(t, err) - query := models.GetDashboardACLInfoListQuery{DashboardID: childDash.Id, OrgID: 1} + query := models.GetDashboardACLInfoListQuery{DashboardID: childDash.ID, OrgID: 1} err = dashboardStore.GetDashboardACLInfoList(context.Background(), &query) require.Nil(t, err) @@ -98,43 +99,43 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { t.Run("Given a dashboard folder and a user", func(t *testing.T) { t.Run("Given dashboard folder permission", func(t *testing.T) { setup(t) - err := updateDashboardACL(t, dashboardStore, savedFolder.Id, models.DashboardACL{ + err := updateDashboardACL(t, dashboardStore, savedFolder.ID, models.DashboardACL{ OrgID: 1, UserID: currentUser.ID, - DashboardID: savedFolder.Id, + DashboardID: savedFolder.ID, Permission: models.PERMISSION_EDIT, }) require.Nil(t, err) t.Run("When reading dashboard acl should include acl for parent folder", func(t *testing.T) { - query := models.GetDashboardACLInfoListQuery{DashboardID: childDash.Id, OrgID: 1} + query := models.GetDashboardACLInfoListQuery{DashboardID: childDash.ID, OrgID: 1} 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, savedFolder.ID, query.Result[0].DashboardId) }) t.Run("Given child dashboard permission", func(t *testing.T) { - err := updateDashboardACL(t, dashboardStore, childDash.Id, models.DashboardACL{ + err := updateDashboardACL(t, dashboardStore, childDash.ID, models.DashboardACL{ OrgID: 1, UserID: currentUser.ID, - DashboardID: childDash.Id, + DashboardID: childDash.ID, Permission: models.PERMISSION_EDIT, }) require.Nil(t, err) t.Run("When reading dashboard acl should include acl for parent folder and child", func(t *testing.T) { - query := models.GetDashboardACLInfoListQuery{OrgID: 1, DashboardID: childDash.Id} + query := models.GetDashboardACLInfoListQuery{OrgID: 1, DashboardID: childDash.ID} 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.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.Equal(t, childDash.ID, query.Result[1].DashboardId) require.False(t, query.Result[1].Inherited) }) }) @@ -142,15 +143,15 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { t.Run("Reading dashboard acl should include default acl for parent folder and the child acl", func(t *testing.T) { setup(t) - err := updateDashboardACL(t, dashboardStore, childDash.Id, models.DashboardACL{ + err := updateDashboardACL(t, dashboardStore, childDash.ID, models.DashboardACL{ OrgID: 1, UserID: currentUser.ID, - DashboardID: childDash.Id, + DashboardID: childDash.ID, Permission: models.PERMISSION_EDIT, }) require.Nil(t, err) - query := models.GetDashboardACLInfoListQuery{OrgID: 1, DashboardID: childDash.Id} + query := models.GetDashboardACLInfoListQuery{OrgID: 1, DashboardID: childDash.ID} err = dashboardStore.GetDashboardACLInfoList(context.Background(), &query) require.Nil(t, err) @@ -163,35 +164,35 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { 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.Equal(t, childDash.ID, query.Result[2].DashboardId) require.False(t, query.Result[2].Inherited) }) t.Run("Add and delete dashboard permission", func(t *testing.T) { setup(t) - err := updateDashboardACL(t, dashboardStore, savedFolder.Id, models.DashboardACL{ + err := updateDashboardACL(t, dashboardStore, savedFolder.ID, models.DashboardACL{ OrgID: 1, UserID: currentUser.ID, - DashboardID: savedFolder.Id, + DashboardID: savedFolder.ID, Permission: models.PERMISSION_EDIT, }) require.Nil(t, err) - q1 := &models.GetDashboardACLInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1} + q1 := &models.GetDashboardACLInfoListQuery{DashboardID: savedFolder.ID, OrgID: 1} err = dashboardStore.GetDashboardACLInfoList(context.Background(), q1) require.Nil(t, err) - require.Equal(t, savedFolder.Id, q1.Result[0].DashboardId) + 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) - err = updateDashboardACL(t, dashboardStore, savedFolder.Id) + err = updateDashboardACL(t, dashboardStore, savedFolder.ID) require.Nil(t, err) - q3 := &models.GetDashboardACLInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1} + q3 := &models.GetDashboardACLInfoListQuery{DashboardID: savedFolder.ID, OrgID: 1} err = dashboardStore.GetDashboardACLInfoList(context.Background(), q3) require.Nil(t, err) require.Equal(t, 0, len(q3.Result)) @@ -203,18 +204,18 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { team1, err := teamSvc.CreateTeam("group1 name", "", 1) require.Nil(t, err) - err = updateDashboardACL(t, dashboardStore, savedFolder.Id, models.DashboardACL{ + err = updateDashboardACL(t, dashboardStore, savedFolder.ID, models.DashboardACL{ OrgID: 1, TeamID: team1.ID, - DashboardID: savedFolder.Id, + DashboardID: savedFolder.ID, Permission: models.PERMISSION_EDIT, }) require.Nil(t, err) - q1 := &models.GetDashboardACLInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1} + q1 := &models.GetDashboardACLInfoListQuery{DashboardID: savedFolder.ID, OrgID: 1} err = dashboardStore.GetDashboardACLInfoList(context.Background(), q1) require.Nil(t, err) - require.Equal(t, savedFolder.Id, q1.Result[0].DashboardId) + 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) }) @@ -224,19 +225,19 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) { teamSvc := teamimpl.ProvideService(sqlStore, sqlStore.Cfg) team1, err := teamSvc.CreateTeam("group1 name", "", 1) require.Nil(t, err) - err = updateDashboardACL(t, dashboardStore, savedFolder.Id, models.DashboardACL{ + err = updateDashboardACL(t, dashboardStore, savedFolder.ID, models.DashboardACL{ OrgID: 1, TeamID: team1.ID, - DashboardID: savedFolder.Id, + DashboardID: savedFolder.ID, Permission: models.PERMISSION_ADMIN, }) require.Nil(t, err) - q3 := &models.GetDashboardACLInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1} + q3 := &models.GetDashboardACLInfoListQuery{DashboardID: savedFolder.ID, OrgID: 1} 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, 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) }) diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index b63830bcea5..0959e97643c 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -69,11 +69,11 @@ func (d *DashboardStore) emitEntityEvent() bool { return d.features != nil && d.features.IsEnabled(featuremgmt.FlagPanelTitleSearch) } -func (d *DashboardStore) ValidateDashboardBeforeSave(ctx context.Context, dashboard *models.Dashboard, overwrite bool) (bool, error) { +func (d *DashboardStore) ValidateDashboardBeforeSave(ctx context.Context, dashboard *dashboards.Dashboard, overwrite bool) (bool, error) { isParentFolderChanged := false err := d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error { var err error - isParentFolderChanged, err = getExistingDashboardByIdOrUidForUpdate(sess, dashboard, d.store.GetDialect(), overwrite) + isParentFolderChanged, err = getExistingDashboardByIDOrUIDForUpdate(sess, dashboard, d.store.GetDialect(), overwrite) if err != nil { return err } @@ -204,7 +204,7 @@ func (d *DashboardStore) GetProvisionedDashboardData(ctx context.Context, name s return result, err } -func (d *DashboardStore) SaveProvisionedDashboard(ctx context.Context, cmd models.SaveDashboardCommand, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) { +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 { return err @@ -220,7 +220,7 @@ func (d *DashboardStore) SaveProvisionedDashboard(ctx context.Context, cmd model return cmd.Result, err } -func (d *DashboardStore) SaveDashboard(ctx context.Context, cmd models.SaveDashboardCommand) (*models.Dashboard, error) { +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()) }) @@ -354,14 +354,14 @@ func (d *DashboardStore) Count(ctx context.Context, scopeParams *quota.ScopePara return u, nil } -func getExistingDashboardByIdOrUidForUpdate(sess *db.Session, dash *models.Dashboard, dialect migrator.Dialect, overwrite bool) (bool, error) { +func getExistingDashboardByIDOrUIDForUpdate(sess *db.Session, dash *dashboards.Dashboard, dialect migrator.Dialect, overwrite bool) (bool, error) { dashWithIdExists := false isParentFolderChanged := false var existingById models.Dashboard - if dash.Id > 0 { + if dash.ID > 0 { var err error - dashWithIdExists, err = sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existingById) + dashWithIdExists, err = sess.Where("id=? AND org_id=?", dash.ID, dash.OrgID).Get(&existingById) if err != nil { return false, fmt.Errorf("SQL query for existing dashboard by ID failed: %w", err) } @@ -370,25 +370,25 @@ func getExistingDashboardByIdOrUidForUpdate(sess *db.Session, dash *models.Dashb return false, dashboards.ErrDashboardNotFound } - if dash.Uid == "" { - dash.SetUid(existingById.Uid) + if dash.UID == "" { + dash.SetUID(existingById.Uid) } } dashWithUidExists := false var existingByUid models.Dashboard - if dash.Uid != "" { + if dash.UID != "" { var err error - dashWithUidExists, err = sess.Where("org_id=? AND uid=?", dash.OrgId, dash.Uid).Get(&existingByUid) + dashWithUidExists, err = sess.Where("org_id=? AND uid=?", dash.OrgID, dash.UID).Get(&existingByUid) if err != nil { return false, fmt.Errorf("SQL query for existing dashboard by UID failed: %w", err) } } - if dash.FolderId > 0 { + if dash.FolderID > 0 { var existingFolder models.Dashboard - folderExists, err := sess.Where("org_id=? AND id=? AND is_folder=?", dash.OrgId, dash.FolderId, + folderExists, err := sess.Where("org_id=? AND id=? AND is_folder=?", dash.OrgID, dash.FolderID, dialect.BooleanStr(true)).Get(&existingFolder) if err != nil { return false, fmt.Errorf("SQL query for folder failed: %w", err) @@ -410,8 +410,8 @@ func getExistingDashboardByIdOrUidForUpdate(sess *db.Session, dash *models.Dashb existing := existingById if !dashWithIdExists && dashWithUidExists { - dash.SetId(existingByUid.Id) - dash.SetUid(existingByUid.Uid) + dash.SetID(existingByUid.Id) + dash.SetUID(existingByUid.Uid) existing = existingByUid } @@ -420,7 +420,7 @@ func getExistingDashboardByIdOrUidForUpdate(sess *db.Session, dash *models.Dashb return isParentFolderChanged, dashboards.ErrDashboardTypeMismatch } - if !dash.IsFolder && dash.FolderId != existing.FolderId { + if !dash.IsFolder && dash.FolderID != existing.FolderId { isParentFolderChanged = true } @@ -441,16 +441,16 @@ func getExistingDashboardByIdOrUidForUpdate(sess *db.Session, dash *models.Dashb return isParentFolderChanged, nil } -func getExistingDashboardByTitleAndFolder(sess *db.Session, dash *models.Dashboard, dialect migrator.Dialect, overwrite, +func getExistingDashboardByTitleAndFolder(sess *db.Session, dash *dashboards.Dashboard, dialect migrator.Dialect, overwrite, isParentFolderChanged bool) (bool, error) { var existing models.Dashboard - exists, err := sess.Where("org_id=? AND slug=? AND (is_folder=? OR folder_id=?)", dash.OrgId, dash.Slug, - dialect.BooleanStr(true), dash.FolderId).Get(&existing) + exists, err := sess.Where("org_id=? AND slug=? AND (is_folder=? OR folder_id=?)", dash.OrgID, dash.Slug, + dialect.BooleanStr(true), dash.FolderID).Get(&existing) if err != nil { return isParentFolderChanged, fmt.Errorf("SQL query for existing dashboard by org ID or folder ID failed: %w", err) } - if exists && dash.Id != existing.Id { + if exists && dash.ID != existing.Id { if existing.IsFolder && !dash.IsFolder { return isParentFolderChanged, dashboards.ErrDashboardWithSameNameAsFolder } @@ -459,13 +459,13 @@ func getExistingDashboardByTitleAndFolder(sess *db.Session, dash *models.Dashboa return isParentFolderChanged, dashboards.ErrDashboardFolderWithSameNameAsDashboard } - if !dash.IsFolder && (dash.FolderId != existing.FolderId || dash.Id == 0) { + if !dash.IsFolder && (dash.FolderID != existing.FolderId || dash.ID == 0) { isParentFolderChanged = true } if overwrite { - dash.SetId(existing.Id) - dash.SetUid(existing.Uid) + dash.SetID(existing.Id) + dash.SetUID(existing.Uid) dash.SetVersion(existing.Version) } else { return isParentFolderChanged, dashboards.ErrDashboardWithSameNameInFolderExists @@ -475,18 +475,18 @@ func getExistingDashboardByTitleAndFolder(sess *db.Session, dash *models.Dashboa return isParentFolderChanged, nil } -func saveDashboard(sess *db.Session, cmd *models.SaveDashboardCommand, emitEntityEvent bool) error { +func saveDashboard(sess *db.Session, cmd *dashboards.SaveDashboardCommand, emitEntityEvent bool) error { dash := cmd.GetDashboardModel() - userId := cmd.UserId + userId := cmd.UserID if userId == 0 { userId = -1 } - if dash.Id > 0 { + if dash.ID > 0 { var existing models.Dashboard - dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existing) + dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.ID, dash.OrgID).Get(&existing) if err != nil { return err } @@ -509,19 +509,19 @@ func saveDashboard(sess *db.Session, cmd *models.SaveDashboardCommand, emitEntit } } - if dash.Uid == "" { - uid, err := generateNewDashboardUid(sess, dash.OrgId) + if dash.UID == "" { + uid, err := generateNewDashboardUid(sess, dash.OrgID) if err != nil { return err } - dash.SetUid(uid) + dash.SetUID(uid) } parentVersion := dash.Version var affectedRows int64 var err error - if dash.Id == 0 { + if dash.ID == 0 { dash.SetVersion(1) dash.Created = time.Now() dash.CreatedBy = userId @@ -540,7 +540,7 @@ func saveDashboard(sess *db.Session, cmd *models.SaveDashboardCommand, emitEntit dash.UpdatedBy = userId - affectedRows, err = sess.MustCols("folder_id").ID(dash.Id).Update(dash) + affectedRows, err = sess.MustCols("folder_id").ID(dash.ID).Update(dash) } if err != nil { @@ -552,7 +552,7 @@ func saveDashboard(sess *db.Session, cmd *models.SaveDashboardCommand, emitEntit } dashVersion := &dashver.DashboardVersion{ - DashboardID: dash.Id, + DashboardID: dash.ID, ParentVersion: parentVersion, RestoredFrom: cmd.RestoredFrom, Version: dash.Version, @@ -570,7 +570,7 @@ func saveDashboard(sess *db.Session, cmd *models.SaveDashboardCommand, emitEntit } // delete existing tags - if _, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.Id); err != nil { + if _, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.ID); err != nil { return err } @@ -578,7 +578,7 @@ func saveDashboard(sess *db.Session, cmd *models.SaveDashboardCommand, emitEntit tags := dash.GetTags() if len(tags) > 0 { for _, tag := range tags { - if _, err := sess.Insert(DashboardTag{DashboardId: dash.Id, Term: tag}); err != nil { + if _, err := sess.Insert(DashboardTag{DashboardId: dash.ID, Term: tag}); err != nil { return err } } @@ -612,16 +612,16 @@ func generateNewDashboardUid(sess *db.Session, orgId int64) (string, error) { return "", dashboards.ErrDashboardFailedGenerateUniqueUid } -func saveProvisionedData(sess *db.Session, provisioning *models.DashboardProvisioning, dashboard *models.Dashboard) error { +func saveProvisionedData(sess *db.Session, provisioning *dashboards.DashboardProvisioning, dashboard *dashboards.Dashboard) error { result := &models.DashboardProvisioning{} - exist, err := sess.Where("dashboard_id=? AND name = ?", dashboard.Id, provisioning.Name).Get(result) + exist, err := sess.Where("dashboard_id=? AND name = ?", dashboard.ID, provisioning.Name).Get(result) if err != nil { return err } - provisioning.Id = result.Id - provisioning.DashboardId = dashboard.Id + provisioning.ID = result.Id + provisioning.DashboardID = dashboard.ID if exist { _, err = sess.ID(result.Id).Update(provisioning) @@ -767,7 +767,7 @@ func (d *DashboardStore) DeleteDashboard(ctx context.Context, cmd *models.Delete } func (d *DashboardStore) deleteDashboard(cmd *models.DeleteDashboardCommand, sess *db.Session, emitEntityEvent bool) error { - dashboard := models.Dashboard{Id: cmd.Id, OrgId: cmd.OrgId} + dashboard := dashboards.Dashboard{ID: cmd.Id, OrgID: cmd.OrgId} has, err := sess.Get(&dashboard) if err != nil { return err @@ -794,7 +794,7 @@ func (d *DashboardStore) deleteDashboard(cmd *models.DeleteDashboardCommand, ses Id int64 Uid string } - err := sess.SQL("SELECT id, uid FROM dashboard WHERE folder_id = ?", dashboard.Id).Find(&dashIds) + err := sess.SQL("SELECT id, uid FROM dashboard WHERE folder_id = ?", dashboard.ID).Find(&dashIds) if err != nil { return err } @@ -806,7 +806,7 @@ func (d *DashboardStore) deleteDashboard(cmd *models.DeleteDashboardCommand, ses } // remove all access control permission with folder scope - _, err = sess.Exec("DELETE FROM permission WHERE scope = ?", dashboards.ScopeFoldersProvider.GetResourceScopeUID(dashboard.Uid)) + _, err = sess.Exec("DELETE FROM permission WHERE scope = ?", dashboards.ScopeFoldersProvider.GetResourceScopeUID(dashboard.UID)) if err != nil { return err } @@ -830,7 +830,7 @@ func (d *DashboardStore) deleteDashboard(cmd *models.DeleteDashboardCommand, ses "DELETE FROM dashboard_public WHERE dashboard_uid IN (SELECT uid FROM dashboard WHERE org_id = ? AND folder_id = ?)", } for _, sql := range childrenDeletes { - _, err := sess.Exec(sql, dashboard.OrgId, dashboard.Id) + _, err := sess.Exec(sql, dashboard.OrgID, dashboard.ID) if err != nil { return err } @@ -838,7 +838,7 @@ func (d *DashboardStore) deleteDashboard(cmd *models.DeleteDashboardCommand, ses } var existingRuleID int64 - exists, err := sess.Table("alert_rule").Where("namespace_uid = (SELECT uid FROM dashboard WHERE id = ?)", dashboard.Id).Cols("id").Get(&existingRuleID) + exists, err := sess.Table("alert_rule").Where("namespace_uid = (SELECT uid FROM dashboard WHERE id = ?)", dashboard.ID).Cols("id").Get(&existingRuleID) if err != nil { return err } @@ -854,25 +854,25 @@ func (d *DashboardStore) deleteDashboard(cmd *models.DeleteDashboardCommand, ses } for _, sql := range deleteNGAlertsByFolder { - _, err := sess.Exec(sql, dashboard.Id) + _, err := sess.Exec(sql, dashboard.ID) if err != nil { return err } } } } else { - _, err = sess.Exec("DELETE FROM permission WHERE scope = ?", ac.GetResourceScopeUID("dashboards", dashboard.Uid)) + _, err = sess.Exec("DELETE FROM permission WHERE scope = ?", ac.GetResourceScopeUID("dashboards", dashboard.UID)) if err != nil { return err } } - if err := d.deleteAlertDefinition(dashboard.Id, sess); err != nil { + if err := d.deleteAlertDefinition(dashboard.ID, sess); err != nil { return err } for _, sql := range deletes { - _, err := sess.Exec(sql, dashboard.Id) + _, err := sess.Exec(sql, dashboard.ID) if err != nil { return err } @@ -887,18 +887,18 @@ func (d *DashboardStore) deleteDashboard(cmd *models.DeleteDashboardCommand, ses return nil } -func createEntityEvent(dashboard *models.Dashboard, eventType store.EntityEventType) *store.EntityEvent { +func createEntityEvent(dashboard *dashboards.Dashboard, eventType store.EntityEventType) *store.EntityEvent { var entityEvent *store.EntityEvent if dashboard.IsFolder { entityEvent = &store.EntityEvent{ EventType: eventType, - EntityId: store.CreateDatabaseEntityId(dashboard.Uid, dashboard.OrgId, store.EntityTypeFolder), + EntityId: store.CreateDatabaseEntityId(dashboard.UID, dashboard.OrgID, store.EntityTypeFolder), Created: time.Now().Unix(), } } else { entityEvent = &store.EntityEvent{ EventType: eventType, - EntityId: store.CreateDatabaseEntityId(dashboard.Uid, dashboard.OrgId, store.EntityTypeDashboard), + EntityId: store.CreateDatabaseEntityId(dashboard.UID, dashboard.OrgID, store.EntityTypeDashboard), Created: time.Now().Unix(), } } @@ -922,13 +922,13 @@ func (d *DashboardStore) deleteAlertDefinition(dashboardId int64, sess *db.Sessi return nil } -func (d *DashboardStore) GetDashboard(ctx context.Context, query *models.GetDashboardQuery) (*models.Dashboard, error) { +func (d *DashboardStore) GetDashboard(ctx context.Context, query *dashboards.GetDashboardQuery) (*dashboards.Dashboard, error) { err := d.store.WithDbSession(ctx, func(sess *db.Session) error { - if query.Id == 0 && len(query.Slug) == 0 && len(query.Uid) == 0 { + if query.ID == 0 && len(query.Slug) == 0 && len(query.UID) == 0 { return dashboards.ErrDashboardIdentifierNotSet } - dashboard := models.Dashboard{Slug: query.Slug, OrgId: query.OrgId, Id: query.Id, Uid: query.Uid} + dashboard := dashboards.Dashboard{Slug: query.Slug, OrgID: query.OrgID, ID: query.ID, UID: query.UID} has, err := sess.Get(&dashboard) if err != nil { @@ -937,8 +937,8 @@ func (d *DashboardStore) GetDashboard(ctx context.Context, query *models.GetDash return dashboards.ErrDashboardNotFound } - dashboard.SetId(dashboard.Id) - dashboard.SetUid(dashboard.Uid) + dashboard.SetID(dashboard.ID) + dashboard.SetUID(dashboard.UID) query.Result = &dashboard return nil }) @@ -946,11 +946,11 @@ func (d *DashboardStore) GetDashboard(ctx context.Context, query *models.GetDash return query.Result, err } -func (d *DashboardStore) GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error { +func (d *DashboardStore) GetDashboardUIDByID(ctx context.Context, query *dashboards.GetDashboardRefByIDQuery) error { return d.store.WithDbSession(ctx, func(sess *db.Session) error { var rawSQL = `SELECT uid, slug from dashboard WHERE Id=?` - us := &models.DashboardRef{} - exists, err := sess.SQL(rawSQL, query.Id).Get(us) + us := &dashboards.DashboardRef{} + exists, err := sess.SQL(rawSQL, query.ID).Get(us) if err != nil { return err } else if !exists { @@ -961,18 +961,18 @@ func (d *DashboardStore) GetDashboardUIDById(ctx context.Context, query *models. }) } -func (d *DashboardStore) GetDashboards(ctx context.Context, query *models.GetDashboardsQuery) error { +func (d *DashboardStore) GetDashboards(ctx context.Context, query *dashboards.GetDashboardsQuery) error { return d.store.WithDbSession(ctx, func(sess *db.Session) error { - if len(query.DashboardIds) == 0 && len(query.DashboardUIds) == 0 { + if len(query.DashboardIDs) == 0 && len(query.DashboardUIDs) == 0 { return star.ErrCommandValidationFailed } - var dashboards = make([]*models.Dashboard, 0) + var dashboards = make([]*dashboards.Dashboard, 0) var session *xorm.Session - if len(query.DashboardIds) > 0 { - session = sess.In("id", query.DashboardIds) + if len(query.DashboardIDs) > 0 { + session = sess.In("id", query.DashboardIDs) } else { - session = sess.In("uid", query.DashboardUIds) + session = sess.In("uid", query.DashboardUIDs) } err := session.Find(&dashboards) diff --git a/pkg/services/dashboards/database/database_folder_test.go b/pkg/services/dashboards/database/database_folder_test.go index 2b0addedad7..24c9059d87d 100644 --- a/pkg/services/dashboards/database/database_folder_test.go +++ b/pkg/services/dashboards/database/database_folder_test.go @@ -27,7 +27,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { } t.Run("Testing DB", func(t *testing.T) { var sqlStore *sqlstore.SQLStore - var folder, dashInRoot, childDash *models.Dashboard + var folder, dashInRoot, childDash *dashboards.Dashboard var currentUser user.User var dashboardStore *DashboardStore @@ -40,8 +40,8 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { require.NoError(t, err) folder = 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, folder.ID, false, "prod", "webapp") + insertTestDashboard(t, dashboardStore, "test dash 45", 1, folder.ID, false, "prod") currentUser = createUser(t, sqlStore, "viewer", "Viewer", false) } @@ -53,20 +53,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{folder.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[1].ID, dashInRoot.Id) + require.Equal(t, query.Result[0].ID, folder.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, models.DashboardACL{ - DashboardID: folder.Id, + err := updateDashboardACL(t, dashboardStore, folder.ID, models.DashboardACL{ + DashboardID: folder.ID, OrgID: 1, UserID: otherUser, Permission: models.PERMISSION_EDIT, @@ -76,18 +76,18 @@ 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{folder.ID, dashInRoot.ID}, } err := testSearchDashboards(dashboardStore, query) require.NoError(t, err) require.Equal(t, len(query.Result), 1) - require.Equal(t, query.Result[0].ID, dashInRoot.Id) + require.Equal(t, query.Result[0].ID, dashInRoot.ID) }) t.Run("when the user is given permission", func(t *testing.T) { - err := updateDashboardACL(t, dashboardStore, folder.Id, models.DashboardACL{ - DashboardID: folder.Id, OrgID: 1, UserID: currentUser.ID, Permission: models.PERMISSION_EDIT, + err := updateDashboardACL(t, dashboardStore, folder.ID, models.DashboardACL{ + DashboardID: folder.ID, OrgID: 1, UserID: currentUser.ID, Permission: models.PERMISSION_EDIT, }) require.NoError(t, err) @@ -95,13 +95,13 @@ 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{folder.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[1].ID, dashInRoot.Id) + require.Equal(t, query.Result[0].ID, folder.ID) + require.Equal(t, query.Result[1].ID, dashInRoot.ID) }) }) @@ -114,49 +114,49 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { OrgRole: org.RoleAdmin, }, OrgId: 1, - DashboardIds: []int64{folder.Id, dashInRoot.Id}, + DashboardIds: []int64{folder.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[1].ID, dashInRoot.Id) + require.Equal(t, query.Result[0].ID, folder.ID) + require.Equal(t, query.Result[1].ID, dashInRoot.ID) }) }) }) 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, folder.ID) require.NoError(t, err) - err = updateDashboardACL(t, dashboardStore, childDash.Id, models.DashboardACL{ - DashboardID: folder.Id, OrgID: 1, UserID: otherUser, Permission: models.PERMISSION_EDIT, + err = updateDashboardACL(t, dashboardStore, childDash.ID, models.DashboardACL{ + DashboardID: folder.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{folder.ID, childDash.ID, dashInRoot.ID}, } err := testSearchDashboards(dashboardStore, query) require.NoError(t, err) require.Equal(t, len(query.Result), 1) - require.Equal(t, query.Result[0].ID, dashInRoot.Id) + require.Equal(t, query.Result[0].ID, dashInRoot.ID) }) t.Run("when the user is given permission to child", func(t *testing.T) { - err := updateDashboardACL(t, dashboardStore, childDash.Id, models.DashboardACL{ - DashboardID: childDash.Id, OrgID: 1, UserID: currentUser.ID, Permission: models.PERMISSION_EDIT, + err := updateDashboardACL(t, dashboardStore, childDash.ID, models.DashboardACL{ + DashboardID: childDash.ID, OrgID: 1, UserID: currentUser.ID, Permission: models.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{folder.Id, childDash.Id, dashInRoot.Id}} + query := &models.FindPersistedDashboardsQuery{SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, DashboardIds: []int64{folder.ID, childDash.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, childDash.Id) - require.Equal(t, query.Result[1].ID, dashInRoot.Id) + require.Equal(t, query.Result[0].ID, childDash.ID) + require.Equal(t, query.Result[1].ID, dashInRoot.ID) }) }) @@ -169,14 +169,14 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { OrgRole: org.RoleAdmin, }, OrgId: 1, - DashboardIds: []int64{folder.Id, dashInRoot.Id, childDash.Id}, + DashboardIds: []int64{folder.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[1].ID, childDash.Id) - require.Equal(t, query.Result[2].ID, dashInRoot.Id) + require.Equal(t, query.Result[0].ID, folder.ID) + require.Equal(t, query.Result[1].ID, childDash.ID) + require.Equal(t, query.Result[2].ID, dashInRoot.ID) }) }) }) @@ -184,7 +184,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { t.Run("Given two dashboard folders with one dashboard each and one dashboard in the root folder", func(t *testing.T) { var sqlStore *sqlstore.SQLStore - var folder1, folder2, dashInRoot, childDash1, childDash2 *models.Dashboard + var folder1, folder2, dashInRoot, childDash1, childDash2 *dashboards.Dashboard var currentUser user.User var rootFolderId int64 = 0 @@ -196,8 +196,8 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { folder1 = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod") folder2 = insertTestDashboard(t, dashboardStore, "2 test dash folder", 1, 0, true, "prod") dashInRoot = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, false, "prod") - childDash1 = insertTestDashboard(t, dashboardStore, "child dash 1", 1, folder1.Id, false, "prod") - childDash2 = insertTestDashboard(t, dashboardStore, "child dash 2", 1, folder2.Id, false, "prod") + childDash1 = insertTestDashboard(t, dashboardStore, "child dash 1", 1, folder1.ID, false, "prod") + childDash2 = insertTestDashboard(t, dashboardStore, "child dash 2", 1, folder2.ID, false, "prod") currentUser = createUser(t, sqlStore, "viewer", "Viewer", false) } @@ -207,7 +207,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { t.Run("should return dashboards in root and expanded folder", func(t *testing.T) { query := &models.FindPersistedDashboardsQuery{ FolderIds: []int64{ - rootFolderId, folder1.Id}, SignedInUser: &user.SignedInUser{UserID: currentUser.ID, + rootFolderId, folder1.ID}, SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer, }, OrgId: 1, @@ -215,76 +215,76 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { err := testSearchDashboards(dashboardStore, query) require.NoError(t, err) require.Equal(t, len(query.Result), 4) - require.Equal(t, query.Result[0].ID, folder1.Id) - require.Equal(t, query.Result[1].ID, folder2.Id) - require.Equal(t, query.Result[2].ID, childDash1.Id) - require.Equal(t, query.Result[3].ID, dashInRoot.Id) + require.Equal(t, query.Result[0].ID, folder1.ID) + require.Equal(t, query.Result[1].ID, folder2.ID) + require.Equal(t, query.Result[2].ID, childDash1.ID) + require.Equal(t, query.Result[3].ID, dashInRoot.ID) }) }) t.Run("and acl is set for one dashboard folder", func(t *testing.T) { const otherUser int64 = 999 - err := updateDashboardACL(t, dashboardStore, folder1.Id, models.DashboardACL{ - DashboardID: folder1.Id, OrgID: 1, UserID: otherUser, Permission: models.PERMISSION_EDIT, + err := updateDashboardACL(t, dashboardStore, folder1.ID, models.DashboardACL{ + DashboardID: folder1.ID, OrgID: 1, UserID: otherUser, Permission: models.PERMISSION_EDIT, }) require.NoError(t, err) t.Run("and a dashboard is moved from folder without acl to the folder with an acl", func(t *testing.T) { - moveDashboard(t, dashboardStore, 1, childDash2.Data, folder1.Id) + 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{ SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, - DashboardIds: []int64{folder1.Id, childDash1.Id, childDash2.Id, dashInRoot.Id}, + DashboardIds: []int64{folder1.ID, childDash1.ID, childDash2.ID, dashInRoot.ID}, } err := testSearchDashboards(dashboardStore, query) require.NoError(t, err) require.Equal(t, len(query.Result), 1) - require.Equal(t, query.Result[0].ID, dashInRoot.Id) + require.Equal(t, query.Result[0].ID, dashInRoot.ID) }) }) t.Run("and a dashboard is moved from folder with acl to the folder without an acl", func(t *testing.T) { setup2() - moveDashboard(t, dashboardStore, 1, childDash1.Data, folder2.Id) + 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{ SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, - DashboardIds: []int64{folder2.Id, childDash1.Id, childDash2.Id, dashInRoot.Id}, + DashboardIds: []int64{folder2.ID, childDash1.ID, childDash2.ID, dashInRoot.ID}, } err := testSearchDashboards(dashboardStore, query) require.NoError(t, err) require.Equal(t, len(query.Result), 4) - require.Equal(t, query.Result[0].ID, folder2.Id) - require.Equal(t, query.Result[1].ID, childDash1.Id) - require.Equal(t, query.Result[2].ID, childDash2.Id) - require.Equal(t, query.Result[3].ID, dashInRoot.Id) + require.Equal(t, query.Result[0].ID, folder2.ID) + require.Equal(t, query.Result[1].ID, childDash1.ID) + require.Equal(t, query.Result[2].ID, childDash2.ID) + require.Equal(t, query.Result[3].ID, dashInRoot.ID) }) }) 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, models.DashboardACL{ - DashboardID: childDash1.Id, OrgID: 1, UserID: otherUser, Permission: models.PERMISSION_EDIT, + err := updateDashboardACL(t, dashboardStore, childDash1.ID, models.DashboardACL{ + DashboardID: childDash1.ID, OrgID: 1, UserID: otherUser, Permission: models.PERMISSION_EDIT, }) require.NoError(t, err) - moveDashboard(t, dashboardStore, 1, childDash1.Data, folder2.Id) + 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{ SignedInUser: &user.SignedInUser{UserID: currentUser.ID, OrgID: 1, OrgRole: org.RoleViewer}, OrgId: 1, - DashboardIds: []int64{folder2.Id, childDash1.Id, childDash2.Id, dashInRoot.Id}, + DashboardIds: []int64{folder2.ID, childDash1.ID, childDash2.ID, dashInRoot.ID}, } err = testSearchDashboards(dashboardStore, query) require.NoError(t, err) require.Equal(t, len(query.Result), 4) - require.Equal(t, query.Result[0].ID, folder2.Id) - require.Equal(t, query.Result[1].ID, childDash1.Id) - require.Equal(t, query.Result[2].ID, childDash2.Id) - require.Equal(t, query.Result[3].ID, dashInRoot.Id) + require.Equal(t, query.Result[0].ID, folder2.ID) + require.Equal(t, query.Result[1].ID, childDash1.ID) + require.Equal(t, query.Result[2].ID, childDash2.ID) + require.Equal(t, query.Result[3].ID, dashInRoot.ID) }) }) }) @@ -292,7 +292,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { t.Run("Given two dashboard folders", func(t *testing.T) { var sqlStore *sqlstore.SQLStore - var folder1, folder2 *models.Dashboard + var folder1, folder2 *dashboards.Dashboard var adminUser, editorUser, viewerUser user.User setup3 := func() { @@ -323,8 +323,8 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { require.NoError(t, err) require.Equal(t, len(query.Result), 2) - require.Equal(t, query.Result[0].ID, folder1.Id) - require.Equal(t, query.Result[1].ID, folder2.Id) + require.Equal(t, query.Result[0].ID, folder1.ID) + require.Equal(t, query.Result[1].ID, folder2.ID) }) t.Run("should have edit permission in folders", func(t *testing.T) { @@ -358,13 +358,13 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { require.NoError(t, err) require.Equal(t, len(query.Result), 2) - require.Equal(t, query.Result[0].ID, folder1.Id) - require.Equal(t, query.Result[1].ID, folder2.Id) + require.Equal(t, query.Result[0].ID, folder1.ID) + require.Equal(t, query.Result[1].ID, folder2.ID) }) 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, models.DashboardACL{ - DashboardID: folder1.Id, OrgID: 1, UserID: editorUser.ID, Permission: models.PERMISSION_VIEW, + err := updateDashboardACL(t, dashboardStore, folder1.ID, models.DashboardACL{ + DashboardID: folder1.ID, OrgID: 1, UserID: editorUser.ID, Permission: models.PERMISSION_VIEW, }) require.NoError(t, err) @@ -372,7 +372,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { require.NoError(t, err) require.Equal(t, len(query.Result), 1) - require.Equal(t, query.Result[0].ID, folder2.Id) + require.Equal(t, query.Result[0].ID, folder2.ID) }) t.Run("should have edit permission in folders", func(t *testing.T) { @@ -409,8 +409,8 @@ 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, models.DashboardACL{ - DashboardID: folder1.Id, OrgID: 1, UserID: viewerUser.ID, Permission: models.PERMISSION_EDIT, + err := updateDashboardACL(t, dashboardStore, folder1.ID, models.DashboardACL{ + DashboardID: folder1.ID, OrgID: 1, UserID: viewerUser.ID, Permission: models.PERMISSION_EDIT, }) require.NoError(t, err) @@ -418,7 +418,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { require.NoError(t, err) require.Equal(t, len(query.Result), 1) - require.Equal(t, query.Result[0].ID, folder1.Id) + require.Equal(t, query.Result[0].ID, folder1.ID) }) t.Run("should not have edit permission in folders", func(t *testing.T) { @@ -442,8 +442,8 @@ 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, models.DashboardACL{ - DashboardID: folder1.Id, OrgID: 1, UserID: viewerUser.ID, Permission: models.PERMISSION_ADMIN, + err := updateDashboardACL(t, dashboardStore, folder1.ID, models.DashboardACL{ + DashboardID: folder1.ID, OrgID: 1, UserID: viewerUser.ID, Permission: models.PERMISSION_ADMIN, }) require.NoError(t, err) @@ -458,8 +458,8 @@ 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, models.DashboardACL{ - DashboardID: folder1.Id, OrgID: 1, UserID: viewerUser.ID, Permission: models.PERMISSION_EDIT, + err := updateDashboardACL(t, dashboardStore, folder1.ID, models.DashboardACL{ + DashboardID: folder1.ID, OrgID: 1, UserID: viewerUser.ID, Permission: models.PERMISSION_EDIT, }) require.NoError(t, err) @@ -479,19 +479,19 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { var orgId int64 = 1 title := "Very Unique Name" var sqlStore *sqlstore.SQLStore - var folder1, folder2 *models.Dashboard + var folder1, folder2 *dashboards.Dashboard sqlStore = db.InitTestDB(t) quotaService := quotatest.New(false, nil) dashboardStore, err := ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) folder2 = insertTestDashboard(t, dashboardStore, "TEST", orgId, 0, true, "prod") - _ = insertTestDashboard(t, dashboardStore, title, orgId, folder2.Id, false, "prod") + _ = insertTestDashboard(t, dashboardStore, title, orgId, folder2.ID, false, "prod") folder1 = insertTestDashboard(t, dashboardStore, title, orgId, 0, true, "prod") t.Run("GetFolderByTitle should find the folder", func(t *testing.T) { result, err := dashboardStore.GetFolderByTitle(context.Background(), orgId, title) require.NoError(t, err) - require.Equal(t, folder1.Id, result.ID) + require.Equal(t, folder1.ID, result.ID) }) }) @@ -502,20 +502,20 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { dashboardStore, err := ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) folder := insertTestDashboard(t, dashboardStore, "TEST", orgId, 0, true, "prod") - dash := insertTestDashboard(t, dashboardStore, "Very Unique Name", orgId, folder.Id, false, "prod") + dash := insertTestDashboard(t, dashboardStore, "Very Unique Name", orgId, folder.ID, false, "prod") t.Run("should return folder by UID", func(t *testing.T) { - d, err := dashboardStore.GetFolderByUID(context.Background(), orgId, folder.Uid) - require.Equal(t, folder.Id, d.ID) + d, err := dashboardStore.GetFolderByUID(context.Background(), orgId, folder.UID) + require.Equal(t, folder.ID, d.ID) require.NoError(t, err) }) t.Run("should not find dashboard", func(t *testing.T) { - d, err := dashboardStore.GetFolderByUID(context.Background(), orgId, dash.Uid) + d, err := dashboardStore.GetFolderByUID(context.Background(), orgId, dash.UID) require.Nil(t, d) require.ErrorIs(t, err, dashboards.ErrFolderNotFound) }) t.Run("should search in organization", func(t *testing.T) { - d, err := dashboardStore.GetFolderByUID(context.Background(), orgId+1, folder.Uid) + d, err := dashboardStore.GetFolderByUID(context.Background(), orgId+1, folder.UID) require.Nil(t, d) require.ErrorIs(t, err, dashboards.ErrFolderNotFound) }) @@ -528,20 +528,20 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { dashboardStore, err := ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) folder := insertTestDashboard(t, dashboardStore, "TEST", orgId, 0, true, "prod") - dash := insertTestDashboard(t, dashboardStore, "Very Unique Name", orgId, folder.Id, false, "prod") + dash := insertTestDashboard(t, dashboardStore, "Very Unique Name", orgId, folder.ID, false, "prod") t.Run("should return folder by ID", func(t *testing.T) { - d, err := dashboardStore.GetFolderByID(context.Background(), orgId, folder.Id) - require.Equal(t, folder.Id, d.ID) + d, err := dashboardStore.GetFolderByID(context.Background(), orgId, folder.ID) + require.Equal(t, folder.ID, d.ID) require.NoError(t, err) }) t.Run("should not find dashboard", func(t *testing.T) { - d, err := dashboardStore.GetFolderByID(context.Background(), orgId, dash.Id) + d, err := dashboardStore.GetFolderByID(context.Background(), orgId, dash.ID) require.Nil(t, d) require.ErrorIs(t, err, dashboards.ErrFolderNotFound) }) t.Run("should search in organization", func(t *testing.T) { - d, err := dashboardStore.GetFolderByID(context.Background(), orgId+1, folder.Id) + d, err := dashboardStore.GetFolderByID(context.Background(), orgId+1, folder.ID) require.Nil(t, d) require.ErrorIs(t, err, dashboards.ErrFolderNotFound) }) @@ -550,12 +550,12 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { } func moveDashboard(t *testing.T, dashboardStore *DashboardStore, orgId int64, dashboard *simplejson.Json, - newFolderId int64) *models.Dashboard { + newFolderId int64) *dashboards.Dashboard { t.Helper() - cmd := models.SaveDashboardCommand{ - OrgId: orgId, - FolderId: newFolderId, + cmd := dashboards.SaveDashboardCommand{ + OrgID: orgId, + FolderID: newFolderId, Dashboard: dashboard, Overwrite: true, } diff --git a/pkg/services/dashboards/database/database_provisioning_test.go b/pkg/services/dashboards/database/database_provisioning_test.go index 2bfd0feb0cf..bd911d8e27c 100644 --- a/pkg/services/dashboards/database/database_provisioning_test.go +++ b/pkg/services/dashboards/database/database_provisioning_test.go @@ -10,6 +10,7 @@ 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/quota/quotatest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" ) @@ -23,9 +24,9 @@ func TestIntegrationDashboardProvisioningTest(t *testing.T) { dashboardStore, err := ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService) require.NoError(t, err) - folderCmd := models.SaveDashboardCommand{ - OrgId: 1, - FolderId: 0, + folderCmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + FolderID: 0, IsFolder: true, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, @@ -36,10 +37,10 @@ func TestIntegrationDashboardProvisioningTest(t *testing.T) { dash, err := dashboardStore.SaveDashboard(context.Background(), folderCmd) require.Nil(t, err) - saveDashboardCmd := models.SaveDashboardCommand{ - OrgId: 1, + saveDashboardCmd := dashboards.SaveDashboardCommand{ + OrgID: 1, IsFolder: false, - FolderId: dash.Id, + FolderID: dash.ID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": "test dashboard", @@ -49,38 +50,38 @@ func TestIntegrationDashboardProvisioningTest(t *testing.T) { t.Run("Saving dashboards with provisioning meta data", func(t *testing.T) { now := time.Now() - provisioning := &models.DashboardProvisioning{ + provisioning := &dashboards.DashboardProvisioning{ Name: "default", - ExternalId: "/var/grafana.json", + ExternalID: "/var/grafana.json", Updated: now.Unix(), } dash, err := dashboardStore.SaveProvisionedDashboard(context.Background(), saveDashboardCmd, provisioning) require.Nil(t, err) require.NotNil(t, dash) - require.NotEqual(t, 0, dash.Id) - dashId := dash.Id + require.NotEqual(t, 0, dash.ID) + dashId := dash.ID t.Run("Deleting orphaned provisioned dashboards", func(t *testing.T) { - saveCmd := models.SaveDashboardCommand{ - OrgId: 1, + saveCmd := dashboards.SaveDashboardCommand{ + OrgID: 1, IsFolder: false, - FolderId: dash.Id, + FolderID: dash.ID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": "another_dashboard", }), } - provisioning := &models.DashboardProvisioning{ + provisioning := &dashboards.DashboardProvisioning{ Name: "another_reader", - ExternalId: "/var/grafana.json", + ExternalID: "/var/grafana.json", Updated: now.Unix(), } anotherDash, err := dashboardStore.SaveProvisionedDashboard(context.Background(), saveCmd, provisioning) require.Nil(t, err) - query := &models.GetDashboardsQuery{DashboardIds: []int64{anotherDash.Id}} + query := &dashboards.GetDashboardsQuery{DashboardIDs: []int64{anotherDash.ID}} err = dashboardStore.GetDashboards(context.Background(), query) require.Nil(t, err) require.NotNil(t, query.Result) @@ -88,12 +89,12 @@ func TestIntegrationDashboardProvisioningTest(t *testing.T) { deleteCmd := &models.DeleteOrphanedProvisionedDashboardsCommand{ReaderNames: []string{"default"}} require.Nil(t, dashboardStore.DeleteOrphanedProvisionedDashboards(context.Background(), deleteCmd)) - query = &models.GetDashboardsQuery{DashboardIds: []int64{dash.Id, anotherDash.Id}} + query = &dashboards.GetDashboardsQuery{DashboardIDs: []int64{dash.ID, anotherDash.ID}} 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, dashId, query.Result[0].ID) }) t.Run("Can query for provisioned dashboards", func(t *testing.T) { @@ -106,7 +107,7 @@ func TestIntegrationDashboardProvisioningTest(t *testing.T) { }) t.Run("Can query for one provisioned dashboard", func(t *testing.T) { - data, err := dashboardStore.GetProvisionedDataByDashboardID(context.Background(), dash.Id) + data, err := dashboardStore.GetProvisionedDataByDashboardID(context.Background(), dash.ID) require.Nil(t, err) require.NotNil(t, data) }) @@ -119,13 +120,13 @@ func TestIntegrationDashboardProvisioningTest(t *testing.T) { t.Run("Deleting folder should delete provision meta data", func(t *testing.T) { deleteCmd := &models.DeleteDashboardCommand{ - Id: dash.Id, + Id: dash.ID, OrgId: 1, } require.Nil(t, dashboardStore.DeleteDashboard(context.Background(), deleteCmd)) - data, err := dashboardStore.GetProvisionedDataByDashboardID(context.Background(), dash.Id) + data, err := dashboardStore.GetProvisionedDataByDashboardID(context.Background(), dash.ID) require.Nil(t, err) require.Nil(t, data) }) diff --git a/pkg/services/dashboards/database/database_test.go b/pkg/services/dashboards/database/database_test.go index dac7c7b784e..0be79da7727 100644 --- a/pkg/services/dashboards/database/database_test.go +++ b/pkg/services/dashboards/database/database_test.go @@ -35,7 +35,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { } var sqlStore *sqlstore.SQLStore var cfg *setting.Cfg - var savedFolder, savedDash, savedDash2 *models.Dashboard + var savedFolder, savedDash, savedDash2 *dashboards.Dashboard var dashboardStore *DashboardStore var starService star.Service var publicDashboardStore *database.PublicDashboardStoreImpl @@ -48,10 +48,10 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { dashboardStore, err = ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, cfg), quotaService) require.NoError(t, err) savedFolder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod", "webapp") - savedDash = insertTestDashboard(t, dashboardStore, "test dash 23", 1, savedFolder.Id, false, "prod", "webapp") - insertTestDashboard(t, dashboardStore, "test dash 45", 1, savedFolder.Id, false, "prod") + savedDash = insertTestDashboard(t, dashboardStore, "test dash 23", 1, savedFolder.ID, false, "prod", "webapp") + insertTestDashboard(t, dashboardStore, "test dash 45", 1, savedFolder.ID, false, "prod") savedDash2 = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, false, "prod") - insertTestRule(t, sqlStore, savedFolder.OrgId, savedFolder.Uid) + insertTestRule(t, sqlStore, savedFolder.OrgID, savedFolder.UID) publicDashboardStore = database.ProvideStore(sqlStore) } @@ -60,24 +60,24 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { setup() require.Equal(t, savedDash.Title, "test dash 23") require.Equal(t, savedDash.Slug, "test-dash-23") - require.NotEqual(t, savedDash.Id, 0) + require.NotEqual(t, savedDash.ID, 0) require.False(t, savedDash.IsFolder) - require.Positive(t, savedDash.FolderId) - require.Positive(t, len(savedDash.Uid)) + require.Positive(t, savedDash.FolderID) + require.Positive(t, len(savedDash.UID)) require.Equal(t, savedFolder.Title, "1 test dash folder") require.Equal(t, savedFolder.Slug, "1-test-dash-folder") - require.NotEqual(t, savedFolder.Id, 0) + require.NotEqual(t, savedFolder.ID, 0) require.True(t, savedFolder.IsFolder) - require.EqualValues(t, savedFolder.FolderId, 0) - require.Positive(t, len(savedFolder.Uid)) + require.EqualValues(t, savedFolder.FolderID, 0) + require.Positive(t, len(savedFolder.UID)) }) t.Run("Should be able to get dashboard by id", func(t *testing.T) { setup() - query := models.GetDashboardQuery{ - Id: savedDash.Id, - OrgId: 1, + query := dashboards.GetDashboardQuery{ + ID: savedDash.ID, + OrgID: 1, } _, err := dashboardStore.GetDashboard(context.Background(), &query) @@ -85,16 +85,16 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { 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.Equal(t, query.Result.ID, savedDash.ID) + require.Equal(t, query.Result.UID, savedDash.UID) require.False(t, query.Result.IsFolder) }) t.Run("Should be able to get dashboard by slug", func(t *testing.T) { setup() - query := models.GetDashboardQuery{ + query := dashboards.GetDashboardQuery{ Slug: "test-dash-23", - OrgId: 1, + OrgID: 1, } _, err := dashboardStore.GetDashboard(context.Background(), &query) @@ -102,16 +102,16 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { 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.Equal(t, query.Result.ID, savedDash.ID) + require.Equal(t, query.Result.UID, savedDash.UID) require.False(t, query.Result.IsFolder) }) t.Run("Should be able to get dashboard by uid", func(t *testing.T) { setup() - query := models.GetDashboardQuery{ - Uid: savedDash.Uid, - OrgId: 1, + query := dashboards.GetDashboardQuery{ + UID: savedDash.UID, + OrgID: 1, } _, err := dashboardStore.GetDashboard(context.Background(), &query) @@ -119,23 +119,23 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { 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.Equal(t, query.Result.ID, savedDash.ID) + require.Equal(t, query.Result.UID, savedDash.UID) require.False(t, query.Result.IsFolder) }) t.Run("Should be able to get a dashboard UID by ID", func(t *testing.T) { setup() - query := models.GetDashboardRefByIdQuery{Id: savedDash.Id} - err := dashboardStore.GetDashboardUIDById(context.Background(), &query) + query := dashboards.GetDashboardRefByIDQuery{ID: savedDash.ID} + err := dashboardStore.GetDashboardUIDByID(context.Background(), &query) require.NoError(t, err) - require.Equal(t, query.Result.Uid, savedDash.Uid) + require.Equal(t, query.Result.UID, savedDash.UID) }) t.Run("Shouldn't be able to get a dashboard with just an OrgID", func(t *testing.T) { setup() - query := models.GetDashboardQuery{ - OrgId: 1, + query := dashboards.GetDashboardQuery{ + OrgID: 1, } _, err := dashboardStore.GetDashboard(context.Background(), &query) @@ -144,12 +144,12 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { t.Run("Should be able to get dashboards by IDs & UIDs", func(t *testing.T) { setup() - query := models.GetDashboardsQuery{DashboardIds: []int64{savedDash.Id, savedDash2.Id}} + query := dashboards.GetDashboardsQuery{DashboardIDs: []int64{savedDash.ID, savedDash2.ID}} err := dashboardStore.GetDashboards(context.Background(), &query) require.NoError(t, err) assert.Equal(t, len(query.Result), 2) - query = models.GetDashboardsQuery{DashboardUIds: []string{savedDash.Uid, savedDash2.Uid}} + query = dashboards.GetDashboardsQuery{DashboardUIDs: []string{savedDash.UID, savedDash2.UID}} err = dashboardStore.GetDashboards(context.Background(), &query) require.NoError(t, err) assert.Equal(t, len(query.Result), 2) @@ -160,7 +160,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { dash := insertTestDashboard(t, dashboardStore, "delete me", 1, 0, false, "delete this") err := dashboardStore.DeleteDashboard(context.Background(), &models.DeleteDashboardCommand{ - Id: dash.Id, + Id: dash.ID, OrgId: 1, }) require.NoError(t, err) @@ -168,13 +168,13 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { t.Run("Should be able to create dashboard", func(t *testing.T) { setup() - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "title": "folderId", "tags": []interface{}{}, }), - UserId: 100, + UserID: 100, } dashboard, err := dashboardStore.SaveDashboard(context.Background(), cmd) require.NoError(t, err) @@ -186,43 +186,43 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { t.Run("Should be able to update dashboard by id and remove folderId", func(t *testing.T) { setup() - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": savedDash.Id, + "id": savedDash.ID, "title": "folderId", "tags": []interface{}{}, }), Overwrite: true, - FolderId: 2, - UserId: 100, + FolderID: 2, + UserID: 100, } dash, err := dashboardStore.SaveDashboard(context.Background(), cmd) require.NoError(t, err) - require.EqualValues(t, dash.FolderId, 2) + require.EqualValues(t, dash.FolderID, 2) - cmd = models.SaveDashboardCommand{ - OrgId: 1, + cmd = dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": savedDash.Id, + "id": savedDash.ID, "title": "folderId", "tags": []interface{}{}, }), - FolderId: 0, + FolderID: 0, Overwrite: true, - UserId: 100, + UserID: 100, } _, err = dashboardStore.SaveDashboard(context.Background(), cmd) require.NoError(t, err) - query := models.GetDashboardQuery{ - Id: savedDash.Id, - OrgId: 1, + query := dashboards.GetDashboardQuery{ + ID: savedDash.ID, + OrgID: 1, } _, err = dashboardStore.GetDashboard(context.Background(), &query) require.NoError(t, err) - require.Equal(t, query.Result.FolderId, int64(0)) + 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)) @@ -233,14 +233,14 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { setup() emptyFolder := insertTestDashboard(t, dashboardStore, "2 test dash folder", 1, 0, true, "prod", "webapp") - deleteCmd := &models.DeleteDashboardCommand{Id: emptyFolder.Id} + deleteCmd := &models.DeleteDashboardCommand{Id: emptyFolder.ID} err := dashboardStore.DeleteDashboard(context.Background(), deleteCmd) require.NoError(t, err) }) t.Run("Should be not able to delete a dashboard if force delete rules is disabled", func(t *testing.T) { setup() - deleteCmd := &models.DeleteDashboardCommand{Id: savedFolder.Id, ForceDeleteFolderRules: false} + deleteCmd := &models.DeleteDashboardCommand{Id: savedFolder.ID, ForceDeleteFolderRules: false} err := dashboardStore.DeleteDashboard(context.Background(), deleteCmd) require.True(t, errors.Is(err, dashboards.ErrFolderContainsAlertRules)) }) @@ -252,8 +252,8 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { cmd := publicDashboardModels.SavePublicDashboardCommand{ PublicDashboard: publicDashboardModels.PublicDashboard{ Uid: uid, - DashboardUid: savedDash.Uid, - OrgId: savedDash.OrgId, + DashboardUid: savedDash.UID, + OrgId: savedDash.OrgID, IsEnabled: true, TimeSettings: &publicDashboardModels.TimeSettings{}, CreatedBy: 1, @@ -266,11 +266,11 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { pubdashConfig, _ := publicDashboardStore.FindByAccessToken(context.Background(), "an-access-token") require.NotNil(t, pubdashConfig) - deleteCmd := &models.DeleteDashboardCommand{Id: savedDash.Id, OrgId: savedDash.OrgId} + deleteCmd := &models.DeleteDashboardCommand{Id: savedDash.ID, OrgId: savedDash.OrgID} err = dashboardStore.DeleteDashboard(context.Background(), deleteCmd) require.NoError(t, err) - query := models.GetDashboardQuery{Uid: savedDash.Uid, OrgId: savedDash.OrgId} + query := dashboards.GetDashboardQuery{UID: savedDash.UID, OrgID: savedDash.OrgID} dash, getErr := dashboardStore.GetDashboard(context.Background(), &query) require.Equal(t, getErr, dashboards.ErrDashboardNotFound) assert.Nil(t, dash) @@ -287,8 +287,8 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { cmd := publicDashboardModels.SavePublicDashboardCommand{ PublicDashboard: publicDashboardModels.PublicDashboard{ Uid: uid, - DashboardUid: savedDash.Uid, - OrgId: savedDash.OrgId, + DashboardUid: savedDash.UID, + OrgId: savedDash.OrgID, IsEnabled: true, TimeSettings: &publicDashboardModels.TimeSettings{}, CreatedBy: 1, @@ -301,12 +301,12 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { pubdashConfig, _ := publicDashboardStore.FindByAccessToken(context.Background(), "an-access-token") require.NotNil(t, pubdashConfig) - deleteCmd := &models.DeleteDashboardCommand{Id: savedFolder.Id, ForceDeleteFolderRules: true} + deleteCmd := &models.DeleteDashboardCommand{Id: savedFolder.ID, ForceDeleteFolderRules: true} err = dashboardStore.DeleteDashboard(context.Background(), deleteCmd) require.NoError(t, err) - query := models.GetDashboardsQuery{ - DashboardIds: []int64{savedFolder.Id, savedDash.Id}, + query := dashboards.GetDashboardsQuery{ + DashboardIDs: []int64{savedFolder.ID, savedDash.ID}, } err = dashboardStore.GetDashboards(context.Background(), &query) require.NoError(t, err) @@ -319,13 +319,13 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { t.Run("Should be able to delete a dashboard folder and its children if force delete rules is enabled", func(t *testing.T) { setup() - deleteCmd := &models.DeleteDashboardCommand{Id: savedFolder.Id, ForceDeleteFolderRules: true} + deleteCmd := &models.DeleteDashboardCommand{Id: savedFolder.ID, ForceDeleteFolderRules: true} err := dashboardStore.DeleteDashboard(context.Background(), deleteCmd) require.NoError(t, err) query := models.FindPersistedDashboardsQuery{ OrgId: 1, - FolderIds: []int64{savedFolder.Id}, + FolderIds: []int64{savedFolder.ID}, SignedInUser: &user.SignedInUser{}, } @@ -335,12 +335,12 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { var existingRuleID int64 - exists, err := sess.Table("alert_rule").Where("namespace_uid = (SELECT uid FROM dashboard WHERE id = ?)", savedFolder.Id).Cols("id").Get(&existingRuleID) + exists, err := sess.Table("alert_rule").Where("namespace_uid = (SELECT uid FROM dashboard WHERE id = ?)", savedFolder.ID).Cols("id").Get(&existingRuleID) require.NoError(t, err) require.False(t, exists) var existingRuleVersionID int64 - exists, err = sess.Table("alert_rule_version").Where("rule_namespace_uid = (SELECT uid FROM dashboard WHERE id = ?)", savedFolder.Id).Cols("id").Get(&existingRuleVersionID) + exists, err = sess.Table("alert_rule_version").Where("rule_namespace_uid = (SELECT uid FROM dashboard WHERE id = ?)", savedFolder.ID).Cols("id").Get(&existingRuleVersionID) require.NoError(t, err) require.False(t, exists) @@ -350,8 +350,8 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { }) t.Run("Should return error if no dashboard is found for update when dashboard id is greater than zero", func(t *testing.T) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Overwrite: true, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": float64(123412321), @@ -365,8 +365,8 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { }) t.Run("Should not return error if no dashboard is found for update when dashboard id is zero", func(t *testing.T) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Overwrite: true, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": 0, @@ -408,7 +408,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { require.Equal(t, len(query.Result), 1) hit := query.Result[0] require.Equal(t, hit.Type, models.DashHitFolder) - require.Equal(t, hit.URL, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.Uid, savedFolder.Slug)) + require.Equal(t, hit.URL, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.UID, savedFolder.Slug)) require.Equal(t, hit.FolderTitle, "") }) @@ -484,7 +484,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { setup() query := models.FindPersistedDashboardsQuery{ OrgId: 1, - FolderIds: []int64{savedFolder.Id}, + FolderIds: []int64{savedFolder.ID}, SignedInUser: &user.SignedInUser{ OrgID: 1, OrgRole: org.RoleEditor, @@ -499,18 +499,18 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { require.Equal(t, len(query.Result), 2) hit := query.Result[0] - require.Equal(t, hit.ID, savedDash.Id) - require.Equal(t, hit.URL, fmt.Sprintf("/d/%s/%s", savedDash.Uid, savedDash.Slug)) - require.Equal(t, hit.FolderID, savedFolder.Id) - require.Equal(t, hit.FolderUID, savedFolder.Uid) + require.Equal(t, hit.ID, savedDash.ID) + require.Equal(t, hit.URL, fmt.Sprintf("/d/%s/%s", savedDash.UID, savedDash.Slug)) + require.Equal(t, hit.FolderID, savedFolder.ID) + require.Equal(t, hit.FolderUID, savedFolder.UID) require.Equal(t, hit.FolderTitle, savedFolder.Title) - require.Equal(t, hit.FolderURL, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.Uid, savedFolder.Slug)) + require.Equal(t, hit.FolderURL, fmt.Sprintf("/dashboards/f/%s/%s", savedFolder.UID, savedFolder.Slug)) }) t.Run("Should be able to find dashboards by ids", func(t *testing.T) { setup() query := models.FindPersistedDashboardsQuery{ - DashboardIds: []int64{savedDash.Id, savedDash2.Id}, + DashboardIds: []int64{savedDash.ID, savedDash2.ID}, SignedInUser: &user.SignedInUser{ OrgID: 1, OrgRole: org.RoleEditor, @@ -536,13 +536,13 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { setup() starredDash := insertTestDashboard(t, dashboardStore, "starred dash", 1, 0, false) err := starService.Add(context.Background(), &star.StarDashboardCommand{ - DashboardID: starredDash.Id, + DashboardID: starredDash.ID, UserID: 10, }) require.NoError(t, err) err = starService.Add(context.Background(), &star.StarDashboardCommand{ - DashboardID: savedDash.Id, + DashboardID: savedDash.ID, UserID: 1, }) require.NoError(t, err) @@ -576,7 +576,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { count, err = dashboardStore.CountDashboardsInFolder( context.Background(), - &dashboards.CountDashboardsInFolderRequest{FolderID: savedFolder.Id, OrgID: 1}) + &dashboards.CountDashboardsInFolderRequest{FolderID: savedFolder.ID, OrgID: 1}) require.NoError(t, err) require.Equal(t, int64(2), count) }) @@ -595,8 +595,8 @@ func TestIntegrationDashboardDataAccessGivenPluginWithImportedDashboards(t *test pluginId := "test-app" appFolder := insertTestDashboardForPlugin(t, dashboardStore, "app-test", 1, 0, true, pluginId) - insertTestDashboardForPlugin(t, dashboardStore, "app-dash1", 1, appFolder.Id, false, pluginId) - insertTestDashboardForPlugin(t, dashboardStore, "app-dash2", 1, appFolder.Id, false, pluginId) + insertTestDashboardForPlugin(t, dashboardStore, "app-dash1", 1, appFolder.ID, false, pluginId) + insertTestDashboardForPlugin(t, dashboardStore, "app-dash2", 1, appFolder.ID, false, pluginId) query := models.GetDashboardsByPluginIdQuery{ PluginId: pluginId, @@ -621,8 +621,8 @@ func TestIntegrationDashboard_SortingOptions(t *testing.T) { dashB := insertTestDashboard(t, dashboardStore, "Beta", 1, 0, false) dashA := insertTestDashboard(t, dashboardStore, "Alfa", 1, 0, false) - assert.NotZero(t, dashA.Id) - assert.Less(t, dashB.Id, dashA.Id) + assert.NotZero(t, dashA.ID) + assert.Less(t, dashB.ID, dashA.ID) qNoSort := &models.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{ OrgID: 1, @@ -636,8 +636,8 @@ func TestIntegrationDashboard_SortingOptions(t *testing.T) { results, err := dashboardStore.FindDashboards(context.Background(), qNoSort) require.NoError(t, err) require.Len(t, results, 2) - assert.Equal(t, dashA.Id, results[0].ID) - assert.Equal(t, dashB.Id, results[1].ID) + assert.Equal(t, dashA.ID, results[0].ID) + assert.Equal(t, dashB.ID, results[1].ID) qSort := &models.FindPersistedDashboardsQuery{ SignedInUser: &user.SignedInUser{ @@ -657,8 +657,8 @@ func TestIntegrationDashboard_SortingOptions(t *testing.T) { results, err = dashboardStore.FindDashboards(context.Background(), qSort) require.NoError(t, err) require.Len(t, results, 2) - assert.Equal(t, dashB.Id, results[0].ID) - assert.Equal(t, dashA.Id, results[1].ID) + assert.Equal(t, dashB.ID, results[0].ID) + assert.Equal(t, dashA.ID, results[1].ID) } func TestIntegrationDashboard_Filter(t *testing.T) { @@ -706,7 +706,7 @@ func TestIntegrationDashboard_Filter(t *testing.T) { results, err = dashboardStore.FindDashboards(context.Background(), qFilter) require.NoError(t, err) require.Len(t, results, 1) - assert.Equal(t, dashB.Id, results[0].ID) + assert.Equal(t, dashB.ID, results[0].ID) } func insertTestRule(t *testing.T, sqlStore db.DB, foderOrgID int64, folderUID string) { @@ -783,11 +783,11 @@ func insertTestRule(t *testing.T, sqlStore db.DB, foderOrgID int64, folderUID st } func insertTestDashboard(t *testing.T, dashboardStore *DashboardStore, title string, orgId int64, - folderId int64, isFolder bool, tags ...interface{}) *models.Dashboard { + folderId int64, isFolder bool, tags ...interface{}) *dashboards.Dashboard { t.Helper() - cmd := models.SaveDashboardCommand{ - OrgId: orgId, - FolderId: folderId, + cmd := dashboards.SaveDashboardCommand{ + OrgID: orgId, + FolderID: folderId, IsFolder: isFolder, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, @@ -798,23 +798,23 @@ func insertTestDashboard(t *testing.T, dashboardStore *DashboardStore, title str dash, err := dashboardStore.SaveDashboard(context.Background(), cmd) require.NoError(t, err) require.NotNil(t, dash) - dash.Data.Set("id", dash.Id) - dash.Data.Set("uid", dash.Uid) + dash.Data.Set("id", dash.ID) + dash.Data.Set("uid", dash.UID) return dash } func insertTestDashboardForPlugin(t *testing.T, dashboardStore *DashboardStore, title string, orgId int64, - folderId int64, isFolder bool, pluginId string) *models.Dashboard { + folderId int64, isFolder bool, pluginId string) *dashboards.Dashboard { t.Helper() - cmd := models.SaveDashboardCommand{ - OrgId: orgId, - FolderId: folderId, + cmd := dashboards.SaveDashboardCommand{ + OrgID: orgId, + FolderID: folderId, IsFolder: isFolder, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": title, }), - PluginId: pluginId, + PluginID: pluginId, } dash, err := dashboardStore.SaveDashboard(context.Background(), cmd) diff --git a/pkg/services/dashboards/models.go b/pkg/services/dashboards/models.go index f35cfcd686a..417c9416a14 100644 --- a/pkg/services/dashboards/models.go +++ b/pkg/services/dashboards/models.go @@ -1,20 +1,299 @@ package dashboards import ( + "fmt" "time" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" ) +const RootFolderName = "General" + +const ( + DashTypeDB = "db" + DashTypeSnapshot = "snapshot" +) + +// Dashboard model +type Dashboard struct { + ID int64 `xorm:"pk autoincr 'id'"` + UID string `xorm:"uid"` + Slug string + OrgID int64 `xorm:"org_id"` + GnetID int64 `xorm:"gnet_id"` + Version int + PluginID string `xorm:"plugin_id"` + + Created time.Time + Updated time.Time + + UpdatedBy int64 + CreatedBy int64 + FolderID int64 `xorm:"folder_id"` + IsFolder bool + HasACL bool `xorm:"has_acl"` + + Title string + Data *simplejson.Json +} + +func (d *Dashboard) SetID(id int64) { + d.ID = id + d.Data.Set("id", id) +} + +func (d *Dashboard) SetUID(uid string) { + d.UID = uid + d.Data.Set("uid", uid) +} + +func (d *Dashboard) SetVersion(version int) { + d.Version = version + d.Data.Set("version", version) +} + +// NewDashboard creates a new dashboard +func NewDashboard(title string) *Dashboard { + dash := &Dashboard{} + dash.Data = simplejson.New() + dash.Data.Set("title", title) + dash.Title = title + dash.Created = time.Now() + dash.Updated = time.Now() + dash.UpdateSlug() + return dash +} + +// NewDashboardFolder creates a new dashboard folder +func NewDashboardFolder(title string) *Dashboard { + folder := NewDashboard(title) + folder.IsFolder = true + folder.Data.Set("schemaVersion", 17) + folder.Data.Set("version", 0) + folder.IsFolder = true + return folder +} + +// GetTags turns the tags in data json into go string array +func (d *Dashboard) GetTags() []string { + return d.Data.Get("tags").MustStringArray() +} + +func NewDashboardFromJson(data *simplejson.Json) *Dashboard { + dash := &Dashboard{} + dash.Data = data + dash.Title = dash.Data.Get("title").MustString() + dash.UpdateSlug() + update := false + + if id, err := dash.Data.Get("id").Float64(); err == nil { + dash.ID = int64(id) + update = true + } + + if uid, err := dash.Data.Get("uid").String(); err == nil { + dash.UID = uid + update = true + } + + if version, err := dash.Data.Get("version").Float64(); err == nil && update { + dash.Version = int(version) + dash.Updated = time.Now() + } else { + dash.Data.Set("version", 0) + dash.Created = time.Now() + dash.Updated = time.Now() + } + + if gnetId, err := dash.Data.Get("gnetId").Float64(); err == nil { + dash.GnetID = int64(gnetId) + } + + return dash +} + +// GetDashboardModel turns the command into the saveable model +func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard { + dash := NewDashboardFromJson(cmd.Dashboard) + userID := cmd.UserID + + if userID == 0 { + userID = -1 + } + + dash.UpdatedBy = userID + dash.OrgID = cmd.OrgID + dash.PluginID = cmd.PluginID + dash.IsFolder = cmd.IsFolder + dash.FolderID = cmd.FolderID + dash.UpdateSlug() + return dash +} + +// UpdateSlug updates the slug +func (d *Dashboard) UpdateSlug() { + title := d.Data.Get("title").MustString() + d.Slug = slugify.Slugify(title) +} + +// GetURL return the html url for a folder if it's folder, otherwise for a dashboard +func (d *Dashboard) GetURL() string { + return GetDashboardFolderURL(d.IsFolder, d.UID, d.Slug) +} + +// GetDashboardFolderURL return the html url for a folder if it's folder, otherwise for a dashboard +func GetDashboardFolderURL(isFolder bool, uid string, slug string) string { + if isFolder { + return GetFolderURL(uid, slug) + } + + return GetDashboardURL(uid, slug) +} + +// GetDashboardURL returns the HTML url for a dashboard. +func GetDashboardURL(uid string, slug string) string { + return fmt.Sprintf("%s/d/%s/%s", setting.AppSubUrl, uid, slug) +} + +// GetKioskModeDashboardUrl returns the HTML url for a dashboard in kiosk mode. +func GetKioskModeDashboardURL(uid string, slug string, theme models.Theme) string { + return fmt.Sprintf("%s?kiosk&theme=%s", GetDashboardURL(uid, slug), string(theme)) +} + +// GetFullDashboardURL returns the full URL for a dashboard. +func GetFullDashboardURL(uid string, slug string) string { + return fmt.Sprintf("%sd/%s/%s", setting.AppUrl, uid, slug) +} + +// GetFolderURL returns the HTML url for a folder. +func GetFolderURL(folderUID string, slug string) string { + return fmt.Sprintf("%s/dashboards/f/%s/%s", setting.AppSubUrl, folderUID, slug) +} + +type ValidateDashboardBeforeSaveResult struct { + IsParentFolderChanged bool +} + +// +// COMMANDS +// + +type SaveDashboardCommand struct { + Dashboard *simplejson.Json `json:"dashboard" binding:"Required"` + UserID int64 `json:"userId" xorm:"user_id"` + Overwrite bool `json:"overwrite"` + Message string `json:"message"` + OrgID int64 `json:"-" xorm:"org_id"` + RestoredFrom int `json:"-"` + PluginID string `json:"-" xorm:"plugin_id"` + FolderID int64 `json:"folderId" xorm:"folder_id"` + FolderUID string `json:"folderUid" xorm:"folder_uid"` + IsFolder bool `json:"isFolder"` + + UpdatedAt time.Time + + Result *Dashboard `json:"-"` +} + +type ValidateDashboardCommand struct { + Dashboard string `json:"dashboard" binding:"Required"` +} + +type TrimDashboardCommand struct { + Dashboard *simplejson.Json `json:"dashboard" binding:"Required"` + Meta *simplejson.Json `json:"meta"` + Result *Dashboard `json:"-"` +} + +type DashboardProvisioning struct { + ID int64 `xorm:"pk autoincr 'id'"` + DashboardID int64 `xorm:"dashboard_id"` + Name string + ExternalID string `xorm:"external_id"` + CheckSum string + Updated int64 +} + +type DeleteDashboardCommand struct { + ID int64 + OrgID int64 + ForceDeleteFolderRules bool +} + +type DeleteOrphanedProvisionedDashboardsCommand struct { + ReaderNames []string +} + +// +// QUERIES +// + +type GetDashboardQuery struct { + Slug string // required if no ID or Uid is specified + ID int64 // optional if slug is set + UID string // optional if slug is set + OrgID int64 + + Result *Dashboard +} + +type DashboardTagCloudItem struct { + Term string `json:"term"` + Count int `json:"count"` +} + +type GetDashboardTagsQuery struct { + OrgID int64 + Result []*DashboardTagCloudItem +} + +type GetDashboardsQuery struct { + DashboardIDs []int64 + DashboardUIDs []string + 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 { + UID string `xorm:"uid"` + Slug string +} + +type GetDashboardRefByIDQuery struct { + ID int64 + Result *DashboardRef +} + type SaveDashboardDTO struct { - OrgId int64 + OrgID int64 UpdatedAt time.Time User *user.SignedInUser Message string Overwrite bool - Dashboard *models.Dashboard + Dashboard *Dashboard } type DashboardSearchProjection struct { @@ -48,3 +327,18 @@ type CountDashboardsInFolderRequest struct { FolderID int64 OrgID int64 } + +func FromDashboard(dash *Dashboard) *folder.Folder { + return &folder.Folder{ + ID: dash.ID, + UID: dash.UID, + Title: dash.Title, + HasACL: dash.HasACL, + Url: models.GetFolderUrl(dash.UID, dash.Slug), + Version: dash.Version, + Created: dash.Created, + CreatedBy: dash.CreatedBy, + Updated: dash.Updated, + UpdatedBy: dash.UpdatedBy, + } +} diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 8a3f7ebf55b..a5eb63bebdc 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -77,19 +77,19 @@ func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardUID(ctx co } func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, dto *dashboards.SaveDashboardDTO, shouldValidateAlerts bool, - validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) { + validateProvisionedDashboard bool) (*dashboards.SaveDashboardCommand, error) { dash := dto.Dashboard - dash.OrgId = dto.OrgId + dash.OrgID = dto.OrgID dash.Title = strings.TrimSpace(dash.Title) dash.Data.Set("title", dash.Title) - dash.SetUid(strings.TrimSpace(dash.Uid)) + dash.SetUID(strings.TrimSpace(dash.UID)) if dash.Title == "" { return nil, dashboards.ErrDashboardTitleEmpty } - if dash.IsFolder && dash.FolderId > 0 { + if dash.IsFolder && dash.FolderID > 0 { return nil, dashboards.ErrDashboardFolderCannotHaveParent } @@ -97,9 +97,9 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d return nil, dashboards.ErrDashboardFolderNameExists } - if !util.IsValidShortUID(dash.Uid) { + if !util.IsValidShortUID(dash.UID) { return nil, dashboards.ErrDashboardInvalidUid - } else if util.IsShortUIDTooLong(dash.Uid) { + } else if util.IsShortUIDTooLong(dash.UID) { return nil, dashboards.ErrDashboardUidTooLong } @@ -108,7 +108,7 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d } if shouldValidateAlerts { - dashAlertInfo := alerting.DashAlertInfo{Dash: dash, User: dto.User, OrgID: dash.OrgId} + dashAlertInfo := alerting.DashAlertInfo{Dash: dash, User: dto.User, OrgID: dash.OrgID} if err := dr.dashAlertExtractor.ValidateAlerts(ctx, dashAlertInfo); err != nil { return nil, err } @@ -121,11 +121,11 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d if isParentFolderChanged { // Check that the user is allowed to add a dashboard to the folder - guardian, err := guardian.NewByDashboard(ctx, dash, dto.OrgId, dto.User) + guardian, err := guardian.NewByDashboard(ctx, dash, dto.OrgID, dto.User) if err != nil { return nil, err } - if canSave, err := guardian.CanCreate(dash.FolderId, dash.IsFolder); err != nil || !canSave { + if canSave, err := guardian.CanCreate(dash.FolderID, dash.IsFolder); err != nil || !canSave { if err != nil { return nil, err } @@ -134,7 +134,7 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d } if validateProvisionedDashboard { - provisionedData, err := dr.GetProvisionedDashboardDataByDashboardID(ctx, dash.Id) + provisionedData, err := dr.GetProvisionedDashboardDataByDashboardID(ctx, dash.ID) if err != nil { return nil, err } @@ -149,8 +149,8 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d return nil, err } - if dash.Id == 0 { - if canCreate, err := guard.CanCreate(dash.FolderId, dash.IsFolder); err != nil || !canCreate { + if dash.ID == 0 { + if canCreate, err := guard.CanCreate(dash.FolderID, dash.IsFolder); err != nil || !canCreate { if err != nil { return nil, err } @@ -165,15 +165,15 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d } } - cmd := &models.SaveDashboardCommand{ + cmd := &dashboards.SaveDashboardCommand{ Dashboard: dash.Data, Message: dto.Message, - OrgId: dto.OrgId, + OrgID: dto.OrgID, Overwrite: dto.Overwrite, - UserId: dto.User.UserID, - FolderId: dash.FolderId, + UserID: dto.User.UserID, + FolderID: dash.FolderID, IsFolder: dash.IsFolder, - PluginId: dash.PluginId, + PluginID: dash.PluginID, } if !dto.UpdatedAt.IsZero() { @@ -193,25 +193,25 @@ func (dr *DashboardServiceImpl) DeleteOrphanedProvisionedDashboards(ctx context. // getGuardianForSavePermissionCheck returns the guardian to be used for checking permission of dashboard // It replaces deleted Dashboard.GetDashboardIdForSavePermissionCheck() -func getGuardianForSavePermissionCheck(ctx context.Context, d *models.Dashboard, user *user.SignedInUser) (guardian.DashboardGuardian, error) { - newDashboard := d.Id == 0 +func getGuardianForSavePermissionCheck(ctx context.Context, d *dashboards.Dashboard, user *user.SignedInUser) (guardian.DashboardGuardian, error) { + newDashboard := d.ID == 0 if newDashboard { // if it's a new dashboard/folder check the parent folder permissions - guard, err := guardian.New(ctx, d.FolderId, d.OrgId, user) + guard, err := guardian.New(ctx, d.FolderID, d.OrgID, user) if err != nil { return nil, err } return guard, nil } - guard, err := guardian.NewByDashboard(ctx, d, d.OrgId, user) + guard, err := guardian.NewByDashboard(ctx, d, d.OrgID, user) if err != nil { return nil, err } return guard, nil } -func validateDashboardRefreshInterval(dash *models.Dashboard) error { +func validateDashboardRefreshInterval(dash *dashboards.Dashboard) error { if setting.MinRefreshInterval == "" { return nil } @@ -239,14 +239,14 @@ func validateDashboardRefreshInterval(dash *models.Dashboard) error { } func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO, - provisioning *models.DashboardProvisioning) (*models.Dashboard, error) { + provisioning *dashboards.DashboardProvisioning) (*dashboards.Dashboard, error) { if err := validateDashboardRefreshInterval(dto.Dashboard); err != nil { dr.log.Warn("Changing refresh interval for provisioned dashboard to minimum refresh interval", "dashboardUid", - dto.Dashboard.Uid, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval", setting.MinRefreshInterval) + dto.Dashboard.UID, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval", setting.MinRefreshInterval) dto.Dashboard.Data.Set("refresh", setting.MinRefreshInterval) } - dto.User = accesscontrol.BackgroundUser("dashboard_provisioning", dto.OrgId, org.RoleAdmin, provisionerPermissions) + dto.User = accesscontrol.BackgroundUser("dashboard_provisioning", dto.OrgID, org.RoleAdmin, provisionerPermissions) cmd, err := dr.BuildSaveDashboardCommand(ctx, dto, setting.IsLegacyAlertingEnabled(), false) if err != nil { @@ -263,7 +263,7 @@ func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dt dashAlertInfo := alerting.DashAlertInfo{ User: dto.User, Dash: dash, - OrgID: dto.OrgId, + OrgID: dto.OrgID, } // extract/save legacy alerts only if legacy alerting is enabled @@ -273,13 +273,13 @@ func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dt return nil, err } - err = dr.dashboardStore.SaveAlerts(ctx, dash.Id, alerts) + err = dr.dashboardStore.SaveAlerts(ctx, dash.ID, alerts) if err != nil { return nil, err } } - if dto.Dashboard.Id == 0 { + if dto.Dashboard.ID == 0 { if err := dr.setDefaultPermissions(ctx, dto, dash, true); err != nil { dr.log.Error("Could not make user admin", "dashboard", dash.Title, "user", dto.User.UserID, "error", err) } @@ -288,8 +288,8 @@ func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dt return dash, nil } -func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*models.Dashboard, error) { - dto.User = accesscontrol.BackgroundUser("dashboard_provisioning", dto.OrgId, org.RoleAdmin, provisionerPermissions) +func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*dashboards.Dashboard, error) { + dto.User = accesscontrol.BackgroundUser("dashboard_provisioning", dto.OrgID, org.RoleAdmin, provisionerPermissions) cmd, err := dr.BuildSaveDashboardCommand(ctx, dto, false, false) if err != nil { return nil, err @@ -303,7 +303,7 @@ func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.C dashAlertInfo := alerting.DashAlertInfo{ User: dto.User, Dash: dash, - OrgID: dto.OrgId, + OrgID: dto.OrgID, } // extract/save legacy alerts only if legacy alerting is enabled @@ -313,13 +313,13 @@ func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.C return nil, err } - err = dr.dashboardStore.SaveAlerts(ctx, dash.Id, alerts) + err = dr.dashboardStore.SaveAlerts(ctx, dash.ID, alerts) if err != nil { return nil, err } } - if dto.Dashboard.Id == 0 { + if dto.Dashboard.ID == 0 { if err := dr.setDefaultPermissions(ctx, dto, dash, true); err != nil { dr.log.Error("Could not make user admin", "dashboard", dash.Title, "user", dto.User.UserID, "error", err) } @@ -329,10 +329,10 @@ func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.C } func (dr *DashboardServiceImpl) SaveDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO, - allowUiUpdate bool) (*models.Dashboard, error) { + allowUiUpdate bool) (*dashboards.Dashboard, error) { if err := validateDashboardRefreshInterval(dto.Dashboard); err != nil { dr.log.Warn("Changing refresh interval for imported dashboard to minimum refresh interval", - "dashboardUid", dto.Dashboard.Uid, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval", + "dashboardUid", dto.Dashboard.UID, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval", setting.MinRefreshInterval) dto.Dashboard.Data.Set("refresh", setting.MinRefreshInterval) } @@ -350,7 +350,7 @@ func (dr *DashboardServiceImpl) SaveDashboard(ctx context.Context, dto *dashboar dashAlertInfo := alerting.DashAlertInfo{ User: dto.User, Dash: dash, - OrgID: dto.OrgId, + OrgID: dto.OrgID, } // extract/save legacy alerts only if legacy alerting is enabled @@ -360,14 +360,14 @@ func (dr *DashboardServiceImpl) SaveDashboard(ctx context.Context, dto *dashboar return nil, err } - err = dr.dashboardStore.SaveAlerts(ctx, dash.Id, alerts) + err = dr.dashboardStore.SaveAlerts(ctx, dash.ID, alerts) if err != nil { return nil, err } } // new dashboard created - if dto.Dashboard.Id == 0 { + if dto.Dashboard.ID == 0 { if err := dr.setDefaultPermissions(ctx, dto, dash, false); err != nil { dr.log.Error("Could not make user admin", "dashboard", dash.Title, "user", dto.User.UserID, "error", err) } @@ -450,10 +450,10 @@ func (dr *DashboardServiceImpl) deleteDashboard(ctx context.Context, dashboardId } func (dr *DashboardServiceImpl) ImportDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO) ( - *models.Dashboard, error) { + *dashboards.Dashboard, error) { if err := validateDashboardRefreshInterval(dto.Dashboard); err != nil { dr.log.Warn("Changing refresh interval for imported dashboard to minimum refresh interval", - "dashboardUid", dto.Dashboard.Uid, "dashboardTitle", dto.Dashboard.Title, + "dashboardUid", dto.Dashboard.UID, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval", setting.MinRefreshInterval) dto.Dashboard.Data.Set("refresh", setting.MinRefreshInterval) } @@ -485,8 +485,8 @@ func (dr *DashboardServiceImpl) GetDashboardsByPluginID(ctx context.Context, que return dr.dashboardStore.GetDashboardsByPluginID(ctx, query) } -func (dr *DashboardServiceImpl) setDefaultPermissions(ctx context.Context, dto *dashboards.SaveDashboardDTO, dash *models.Dashboard, provisioned bool) error { - inFolder := dash.FolderId > 0 +func (dr *DashboardServiceImpl) setDefaultPermissions(ctx context.Context, dto *dashboards.SaveDashboardDTO, dash *dashboards.Dashboard, provisioned bool) error { + inFolder := dash.FolderID > 0 if !accesscontrol.IsDisabled(dr.cfg) { var permissions []accesscontrol.SetResourcePermissionCommand if !provisioned && dto.User.IsRealUser() && !dto.User.IsAnonymous { @@ -507,12 +507,12 @@ func (dr *DashboardServiceImpl) setDefaultPermissions(ctx context.Context, dto * svc = dr.folderPermissions } - _, err := svc.SetPermissions(ctx, dto.OrgId, dash.Uid, permissions...) + _, err := svc.SetPermissions(ctx, dto.OrgID, dash.UID, permissions...) if err != nil { return err } } else if dr.cfg.EditorsCanAdmin && !provisioned && dto.User.IsRealUser() && !dto.User.IsAnonymous { - if err := dr.MakeUserAdmin(ctx, dto.OrgId, dto.User.UserID, dash.Id, !inFolder); err != nil { + if err := dr.MakeUserAdmin(ctx, dto.OrgID, dto.User.UserID, dash.ID, !inFolder); err != nil { return err } } @@ -520,16 +520,16 @@ func (dr *DashboardServiceImpl) setDefaultPermissions(ctx context.Context, dto * return nil } -func (dr *DashboardServiceImpl) GetDashboard(ctx context.Context, query *models.GetDashboardQuery) error { +func (dr *DashboardServiceImpl) GetDashboard(ctx context.Context, query *dashboards.GetDashboardQuery) error { _, err := dr.dashboardStore.GetDashboard(ctx, query) return err } -func (dr *DashboardServiceImpl) GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error { - return dr.dashboardStore.GetDashboardUIDById(ctx, query) +func (dr *DashboardServiceImpl) GetDashboardUIDByID(ctx context.Context, query *dashboards.GetDashboardRefByIDQuery) error { + return dr.dashboardStore.GetDashboardUIDByID(ctx, query) } -func (dr *DashboardServiceImpl) GetDashboards(ctx context.Context, query *models.GetDashboardsQuery) error { +func (dr *DashboardServiceImpl) GetDashboards(ctx context.Context, query *dashboards.GetDashboardsQuery) error { return dr.dashboardStore.GetDashboards(ctx, query) } diff --git a/pkg/services/dashboards/service/dashboard_service_integration_test.go b/pkg/services/dashboards/service/dashboard_service_integration_test.go index e70e4f391c2..51d34a60e4a 100644 --- a/pkg/services/dashboards/service/dashboard_service_integration_test.go +++ b/pkg/services/dashboards/service/dashboard_service_integration_test.go @@ -35,8 +35,8 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When saving a dashboard with non-existing id", true, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": float64(123412321), "title": "Expect error", @@ -54,10 +54,10 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When creating a dashboard with same id as dashboard in organization A", true, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: otherOrgId, + cmd := dashboards.SaveDashboardCommand{ + OrgID: otherOrgId, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": sc.savedDashInFolder.Id, + "id": sc.savedDashInFolder.ID, "title": "Expect error", }), Overwrite: false, @@ -70,10 +70,10 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When creating a dashboard with same uid as dashboard in organization A, it should create a new dashboard in org B", true, func(t *testing.T, sc *permissionScenarioContext) { const otherOrgId int64 = 2 - cmd := models.SaveDashboardCommand{ - OrgId: otherOrgId, + cmd := dashboards.SaveDashboardCommand{ + OrgID: otherOrgId, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "uid": sc.savedDashInFolder.Uid, + "uid": sc.savedDashInFolder.UID, "title": "Dash with existing uid in other org", }), Overwrite: false, @@ -82,9 +82,9 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { res := callSaveWithResult(t, cmd, sc.sqlStore) require.NotNil(t, res) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &models.GetDashboardQuery{ - OrgId: otherOrgId, - Uid: sc.savedDashInFolder.Uid, + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + OrgID: otherOrgId, + UID: sc.savedDashInFolder.UID, }) require.NoError(t, err) }) @@ -96,12 +96,12 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When creating a new dashboard in the General folder", canSave, func(t *testing.T, sc *permissionScenarioContext) { sqlStore := db.InitTestDB(t) - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "title": "Dash", }), - UserId: 10000, + UserID: 10000, Overwrite: true, } @@ -109,195 +109,195 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) assert.Equal(t, "", sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) - assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID) + assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserID, sc.dashboardGuardianMock.User.UserID) }) permissionScenario(t, "When creating a new dashboard in other folder, it should create dashboard guardian for other folder with correct arguments and rsult in access denied error", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "title": "Dash", }), - FolderId: sc.otherSavedFolder.Id, - UserId: 10000, + FolderID: sc.otherSavedFolder.ID, + UserID: 10000, Overwrite: true, } err := callSaveWithError(t, cmd, sc.sqlStore) require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - assert.Equal(t, sc.otherSavedFolder.Id, sc.dashboardGuardianMock.DashID) - assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) - assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID) + assert.Equal(t, sc.otherSavedFolder.ID, sc.dashboardGuardianMock.DashID) + assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserID, sc.dashboardGuardianMock.User.UserID) }) permissionScenario(t, "When creating a new dashboard by existing title in folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "title": sc.savedDashInFolder.Title, }), - FolderId: sc.savedFolder.Id, - UserId: 10000, + FolderID: sc.savedFolder.ID, + UserID: 10000, Overwrite: true, } err := callSaveWithError(t, cmd, sc.sqlStore) require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - assert.Equal(t, sc.savedDashInFolder.Uid, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) - assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID) + assert.Equal(t, sc.savedDashInFolder.UID, sc.dashboardGuardianMock.DashUID) + assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserID, sc.dashboardGuardianMock.User.UserID) }) permissionScenario(t, "When creating a new dashboard by existing UID in folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "uid": sc.savedDashInFolder.Uid, + "uid": sc.savedDashInFolder.UID, "title": "New dash", }), - FolderId: sc.savedFolder.Id, - UserId: 10000, + FolderID: sc.savedFolder.ID, + UserID: 10000, Overwrite: true, } err := callSaveWithError(t, cmd, sc.sqlStore) require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - assert.Equal(t, sc.savedDashInFolder.Uid, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) - assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID) + assert.Equal(t, sc.savedDashInFolder.UID, sc.dashboardGuardianMock.DashUID) + assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserID, sc.dashboardGuardianMock.User.UserID) }) permissionScenario(t, "When updating a dashboard by existing id in the General folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": sc.savedDashInGeneralFolder.Id, + "id": sc.savedDashInGeneralFolder.ID, "title": "Dash", }), - FolderId: sc.savedDashInGeneralFolder.FolderId, - UserId: 10000, + FolderID: sc.savedDashInGeneralFolder.FolderID, + UserID: 10000, Overwrite: true, } err := callSaveWithError(t, cmd, sc.sqlStore) assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - assert.Equal(t, sc.savedDashInGeneralFolder.Uid, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) - assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID) + assert.Equal(t, sc.savedDashInGeneralFolder.UID, sc.dashboardGuardianMock.DashUID) + assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserID, sc.dashboardGuardianMock.User.UserID) }) permissionScenario(t, "When updating a dashboard by existing id in other folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": sc.savedDashInFolder.Id, + "id": sc.savedDashInFolder.ID, "title": "Dash", }), - FolderId: sc.savedDashInFolder.FolderId, - UserId: 10000, + FolderID: sc.savedDashInFolder.FolderID, + UserID: 10000, Overwrite: true, } err := callSaveWithError(t, cmd, sc.sqlStore) require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - assert.Equal(t, sc.savedDashInFolder.Uid, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) - assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID) + assert.Equal(t, sc.savedDashInFolder.UID, sc.dashboardGuardianMock.DashUID) + assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserID, sc.dashboardGuardianMock.User.UserID) }) permissionScenario(t, "When moving a dashboard by existing ID to other folder from General folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": sc.savedDashInGeneralFolder.Id, + "id": sc.savedDashInGeneralFolder.ID, "title": "Dash", }), - FolderId: sc.otherSavedFolder.Id, - UserId: 10000, + FolderID: sc.otherSavedFolder.ID, + UserID: 10000, Overwrite: true, } err := callSaveWithError(t, cmd, sc.sqlStore) require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - assert.Equal(t, sc.savedDashInGeneralFolder.Uid, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) - assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID) + assert.Equal(t, sc.savedDashInGeneralFolder.UID, sc.dashboardGuardianMock.DashUID) + assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserID, sc.dashboardGuardianMock.User.UserID) }) permissionScenario(t, "When moving a dashboard by existing id to the General folder from other folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": sc.savedDashInFolder.Id, + "id": sc.savedDashInFolder.ID, "title": "Dash", }), - FolderId: 0, - UserId: 10000, + FolderID: 0, + UserID: 10000, Overwrite: true, } err := callSaveWithError(t, cmd, sc.sqlStore) assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - assert.Equal(t, sc.savedDashInFolder.Uid, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) - assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID) + assert.Equal(t, sc.savedDashInFolder.UID, sc.dashboardGuardianMock.DashUID) + assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserID, sc.dashboardGuardianMock.User.UserID) }) permissionScenario(t, "When moving a dashboard by existing uid to other folder from General folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "uid": sc.savedDashInGeneralFolder.Uid, + "uid": sc.savedDashInGeneralFolder.UID, "title": "Dash", }), - FolderId: sc.otherSavedFolder.Id, - UserId: 10000, + FolderID: sc.otherSavedFolder.ID, + UserID: 10000, Overwrite: true, } err := callSaveWithError(t, cmd, sc.sqlStore) require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - assert.Equal(t, sc.savedDashInGeneralFolder.Uid, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) - assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID) + assert.Equal(t, sc.savedDashInGeneralFolder.UID, sc.dashboardGuardianMock.DashUID) + assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserID, sc.dashboardGuardianMock.User.UserID) }) permissionScenario(t, "When moving a dashboard by existing UID to the General folder from other folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "uid": sc.savedDashInFolder.Uid, + "uid": sc.savedDashInFolder.UID, "title": "Dash", }), - FolderId: 0, - UserId: 10000, + FolderID: 0, + UserID: 10000, Overwrite: true, } err := callSaveWithError(t, cmd, sc.sqlStore) require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - assert.Equal(t, sc.savedDashInFolder.Uid, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgId, sc.dashboardGuardianMock.OrgId) - assert.Equal(t, cmd.UserId, sc.dashboardGuardianMock.User.UserID) + assert.Equal(t, sc.savedDashInFolder.UID, sc.dashboardGuardianMock.DashUID) + assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgId) + assert.Equal(t, cmd.UserID, sc.dashboardGuardianMock.User.UserID) }) }) @@ -309,22 +309,22 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When creating a dashboard in General folder with same name as dashboard in other folder", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": sc.savedDashInFolder.Title, }), - FolderId: 0, + FolderID: 0, Overwrite: shouldOverwrite, } res := callSaveWithResult(t, cmd, sc.sqlStore) require.NotNil(t, res) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &models.GetDashboardQuery{ - Id: res.Id, - OrgId: cmd.OrgId, + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, }) require.NoError(t, err) @@ -332,32 +332,32 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When creating a dashboard in other folder with same name as dashboard in General folder", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": sc.savedDashInGeneralFolder.Title, }), - FolderId: sc.savedFolder.Id, + FolderID: sc.savedFolder.ID, Overwrite: shouldOverwrite, } res := callSaveWithResult(t, cmd, sc.sqlStore) require.NotNil(t, res) - assert.NotEqual(t, sc.savedDashInGeneralFolder.Id, res.Id) + assert.NotEqual(t, sc.savedDashInGeneralFolder.ID, res.ID) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &models.GetDashboardQuery{ - Id: res.Id, - OrgId: cmd.OrgId, + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, }) require.NoError(t, err) }) permissionScenario(t, "When creating a folder with same name as dashboard in other folder", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": sc.savedDashInFolder.Title, @@ -369,20 +369,20 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { res := callSaveWithResult(t, cmd, sc.sqlStore) require.NotNil(t, res) - assert.NotEqual(t, sc.savedDashInGeneralFolder.Id, res.Id) + assert.NotEqual(t, sc.savedDashInGeneralFolder.ID, res.ID) assert.True(t, res.IsFolder) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &models.GetDashboardQuery{ - Id: res.Id, - OrgId: cmd.OrgId, + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, }) require.NoError(t, err) }) permissionScenario(t, "When saving a dashboard without id and uid and unique title in folder", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "title": "Dash without id and uid", }), @@ -392,19 +392,19 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { res := callSaveWithResult(t, cmd, sc.sqlStore) require.NotNil(t, res) - assert.Greater(t, res.Id, int64(0)) - assert.NotEmpty(t, res.Uid) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &models.GetDashboardQuery{ - Id: res.Id, - OrgId: cmd.OrgId, + assert.Greater(t, res.ID, int64(0)) + assert.NotEmpty(t, res.UID) + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, }) require.NoError(t, err) }) permissionScenario(t, "When saving a dashboard when dashboard id is zero ", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": 0, "title": "Dash with zero id", @@ -415,21 +415,21 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { res := callSaveWithResult(t, cmd, sc.sqlStore) require.NotNil(t, res) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &models.GetDashboardQuery{ - Id: res.Id, - OrgId: cmd.OrgId, + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, }) require.NoError(t, err) }) permissionScenario(t, "When saving a dashboard in non-existing folder", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "title": "Expect error", }), - FolderId: 123412321, + FolderID: 123412321, Overwrite: shouldOverwrite, } @@ -439,13 +439,13 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When updating an existing dashboard by id without current version", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": sc.savedDashInGeneralFolder.Id, + "id": sc.savedDashInGeneralFolder.ID, "title": "test dash 23", }), - FolderId: sc.savedFolder.Id, + FolderID: sc.savedFolder.ID, Overwrite: shouldOverwrite, } @@ -455,23 +455,23 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When updating an existing dashboard by id with current version", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": sc.savedDashInGeneralFolder.Id, + "id": sc.savedDashInGeneralFolder.ID, "title": "Updated title", "version": sc.savedDashInGeneralFolder.Version, }), - FolderId: sc.savedFolder.Id, + FolderID: sc.savedFolder.ID, Overwrite: shouldOverwrite, } res := callSaveWithResult(t, cmd, sc.sqlStore) require.NotNil(t, res) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &models.GetDashboardQuery{ - Id: sc.savedDashInGeneralFolder.Id, - OrgId: cmd.OrgId, + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: sc.savedDashInGeneralFolder.ID, + OrgID: cmd.OrgID, }) require.NoError(t, err) @@ -479,13 +479,13 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When updating an existing dashboard by uid without current version", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "uid": sc.savedDashInFolder.Uid, + "uid": sc.savedDashInFolder.UID, "title": "test dash 23", }), - FolderId: 0, + FolderID: 0, Overwrite: shouldOverwrite, } @@ -495,36 +495,36 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When updating an existing dashboard by uid with current version", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "uid": sc.savedDashInFolder.Uid, + "uid": sc.savedDashInFolder.UID, "title": "Updated title", "version": sc.savedDashInFolder.Version, }), - FolderId: 0, + FolderID: 0, Overwrite: shouldOverwrite, } res := callSaveWithResult(t, cmd, sc.sqlStore) require.NotNil(t, res) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &models.GetDashboardQuery{ - Id: sc.savedDashInFolder.Id, - OrgId: cmd.OrgId, + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: sc.savedDashInFolder.ID, + OrgID: cmd.OrgID, }) require.NoError(t, err) }) permissionScenario(t, "When creating a dashboard with same name as dashboard in other folder", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": sc.savedDashInFolder.Title, }), - FolderId: sc.savedDashInFolder.FolderId, + FolderID: sc.savedDashInFolder.FolderID, Overwrite: shouldOverwrite, } @@ -534,13 +534,13 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When creating a dashboard with same name as dashboard in General folder", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": sc.savedDashInGeneralFolder.Title, }), - FolderId: sc.savedDashInGeneralFolder.FolderId, + FolderID: sc.savedDashInGeneralFolder.FolderID, Overwrite: shouldOverwrite, } @@ -550,8 +550,8 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When creating a folder with same name as existing folder", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": sc.savedFolder.Title, @@ -570,54 +570,54 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When updating an existing dashboard by id without current version", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": sc.savedDashInGeneralFolder.Id, + "id": sc.savedDashInGeneralFolder.ID, "title": "Updated title", }), - FolderId: sc.savedFolder.Id, + FolderID: sc.savedFolder.ID, Overwrite: shouldOverwrite, } res := callSaveWithResult(t, cmd, sc.sqlStore) require.NotNil(t, res) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &models.GetDashboardQuery{ - Id: sc.savedDashInGeneralFolder.Id, - OrgId: cmd.OrgId, + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: sc.savedDashInGeneralFolder.ID, + OrgID: cmd.OrgID, }) require.NoError(t, err) }) permissionScenario(t, "When updating an existing dashboard by uid without current version", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "uid": sc.savedDashInFolder.Uid, + "uid": sc.savedDashInFolder.UID, "title": "Updated title", }), - FolderId: 0, + FolderID: 0, Overwrite: shouldOverwrite, } res := callSaveWithResult(t, cmd, sc.sqlStore) require.NotNil(t, res) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &models.GetDashboardQuery{ - Id: sc.savedDashInFolder.Id, - OrgId: cmd.OrgId, + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: sc.savedDashInFolder.ID, + OrgID: cmd.OrgID, }) require.NoError(t, err) }) permissionScenario(t, "When updating uid for existing dashboard using id", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": sc.savedDashInFolder.Id, + "id": sc.savedDashInFolder.ID, "uid": "new-uid", "title": sc.savedDashInFolder.Title, }), @@ -626,23 +626,23 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { res := callSaveWithResult(t, cmd, sc.sqlStore) require.NotNil(t, res) - assert.Equal(t, sc.savedDashInFolder.Id, res.Id) - assert.Equal(t, "new-uid", res.Uid) + assert.Equal(t, sc.savedDashInFolder.ID, res.ID) + assert.Equal(t, "new-uid", res.UID) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &models.GetDashboardQuery{ - Id: sc.savedDashInFolder.Id, - OrgId: cmd.OrgId, + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: sc.savedDashInFolder.ID, + OrgID: cmd.OrgID, }) require.NoError(t, err) }) permissionScenario(t, "When updating uid to an existing uid for existing dashboard using id", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": sc.savedDashInFolder.Id, - "uid": sc.savedDashInGeneralFolder.Uid, + "id": sc.savedDashInFolder.ID, + "uid": sc.savedDashInGeneralFolder.UID, "title": sc.savedDashInFolder.Title, }), Overwrite: shouldOverwrite, @@ -654,58 +654,58 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When creating a dashboard with same name as dashboard in other folder", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": sc.savedDashInFolder.Title, }), - FolderId: sc.savedDashInFolder.FolderId, + FolderID: sc.savedDashInFolder.FolderID, Overwrite: shouldOverwrite, } res := callSaveWithResult(t, cmd, sc.sqlStore) require.NotNil(t, res) - assert.Equal(t, sc.savedDashInFolder.Id, res.Id) - assert.Equal(t, sc.savedDashInFolder.Uid, res.Uid) + assert.Equal(t, sc.savedDashInFolder.ID, res.ID) + assert.Equal(t, sc.savedDashInFolder.UID, res.UID) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &models.GetDashboardQuery{ - Id: res.Id, - OrgId: cmd.OrgId, + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, }) require.NoError(t, err) }) permissionScenario(t, "When creating a dashboard with same name as dashboard in General folder", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: testOrgID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": sc.savedDashInGeneralFolder.Title, }), - FolderId: sc.savedDashInGeneralFolder.FolderId, + FolderID: sc.savedDashInGeneralFolder.FolderID, Overwrite: shouldOverwrite, } res := callSaveWithResult(t, cmd, sc.sqlStore) require.NotNil(t, res) - assert.Equal(t, sc.savedDashInGeneralFolder.Id, res.Id) - assert.Equal(t, sc.savedDashInGeneralFolder.Uid, res.Uid) + assert.Equal(t, sc.savedDashInGeneralFolder.ID, res.ID) + assert.Equal(t, sc.savedDashInGeneralFolder.UID, res.UID) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &models.GetDashboardQuery{ - Id: res.Id, - OrgId: cmd.OrgId, + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, }) require.NoError(t, err) }) permissionScenario(t, "When updating existing folder to a dashboard using id", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": sc.savedFolder.Id, + "id": sc.savedFolder.ID, "title": "new title", }), IsFolder: false, @@ -718,10 +718,10 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When updating existing dashboard to a folder using id", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": sc.savedDashInFolder.Id, + "id": sc.savedDashInFolder.ID, "title": "new folder title", }), IsFolder: true, @@ -734,10 +734,10 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When updating existing folder to a dashboard using uid", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "uid": sc.savedFolder.Uid, + "uid": sc.savedFolder.UID, "title": "new title", }), IsFolder: false, @@ -750,10 +750,10 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When updating existing dashboard to a folder using uid", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "uid": sc.savedDashInFolder.Uid, + "uid": sc.savedDashInFolder.UID, "title": "new folder title", }), IsFolder: true, @@ -766,8 +766,8 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When updating existing folder to a dashboard using title", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "title": sc.savedFolder.Title, }), @@ -781,8 +781,8 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { permissionScenario(t, "When updating existing dashboard to a folder using title", canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := models.SaveDashboardCommand{ - OrgId: 1, + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "title": sc.savedDashInGeneralFolder.Title, }), @@ -802,10 +802,10 @@ type permissionScenarioContext struct { dashboardGuardianMock *guardian.FakeDashboardGuardian sqlStore db.DB dashboardStore dashboards.Store - savedFolder *models.Dashboard - savedDashInFolder *models.Dashboard - otherSavedFolder *models.Dashboard - savedDashInGeneralFolder *models.Dashboard + savedFolder *dashboards.Dashboard + savedDashInFolder *dashboards.Dashboard + otherSavedFolder *dashboards.Dashboard + savedDashInGeneralFolder *dashboards.Dashboard } type permissionScenarioFunc func(t *testing.T, sc *permissionScenarioContext) @@ -835,24 +835,24 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc guardian.InitLegacyGuardian(sqlStore, service, &teamtest.FakeService{}) savedFolder := saveTestFolder(t, "Saved folder", testOrgID, sqlStore) - savedDashInFolder := saveTestDashboard(t, "Saved dash in folder", testOrgID, savedFolder.Id, sqlStore) - saveTestDashboard(t, "Other saved dash in folder", testOrgID, savedFolder.Id, sqlStore) + savedDashInFolder := saveTestDashboard(t, "Saved dash in folder", testOrgID, savedFolder.ID, sqlStore) + saveTestDashboard(t, "Other saved dash in folder", testOrgID, savedFolder.ID, sqlStore) savedDashInGeneralFolder := saveTestDashboard(t, "Saved dashboard in general folder", testOrgID, 0, sqlStore) otherSavedFolder := saveTestFolder(t, "Other saved folder", testOrgID, sqlStore) require.Equal(t, "Saved folder", savedFolder.Title) require.Equal(t, "saved-folder", savedFolder.Slug) - require.NotEqual(t, int64(0), savedFolder.Id) + require.NotEqual(t, int64(0), savedFolder.ID) require.True(t, savedFolder.IsFolder) - require.Equal(t, int64(0), savedFolder.FolderId) - require.NotEmpty(t, savedFolder.Uid) + require.Equal(t, int64(0), savedFolder.FolderID) + require.NotEmpty(t, savedFolder.UID) require.Equal(t, "Saved dash in folder", savedDashInFolder.Title) require.Equal(t, "saved-dash-in-folder", savedDashInFolder.Slug) - require.NotEqual(t, int64(0), savedDashInFolder.Id) + require.NotEqual(t, int64(0), savedDashInFolder.ID) require.False(t, savedDashInFolder.IsFolder) - require.Equal(t, savedFolder.Id, savedDashInFolder.FolderId) - require.NotEmpty(t, savedDashInFolder.Uid) + require.Equal(t, savedFolder.ID, savedDashInFolder.FolderID) + require.NotEmpty(t, savedDashInFolder.UID) origNewDashboardGuardian := guardian.New t.Cleanup(func() { @@ -874,7 +874,7 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc }) } -func callSaveWithResult(t *testing.T, cmd models.SaveDashboardCommand, sqlStore db.DB) *models.Dashboard { +func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlStore db.DB) *dashboards.Dashboard { t.Helper() dto := toSaveDashboardDto(cmd) @@ -897,7 +897,7 @@ func callSaveWithResult(t *testing.T, cmd models.SaveDashboardCommand, sqlStore return res } -func callSaveWithError(t *testing.T, cmd models.SaveDashboardCommand, sqlStore db.DB) error { +func callSaveWithError(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlStore db.DB) error { dto := toSaveDashboardDto(cmd) cfg := setting.NewCfg() cfg.RBACEnabled = false @@ -916,12 +916,12 @@ func callSaveWithError(t *testing.T, cmd models.SaveDashboardCommand, sqlStore d return err } -func saveTestDashboard(t *testing.T, title string, orgID, folderID int64, sqlStore db.DB) *models.Dashboard { +func saveTestDashboard(t *testing.T, title string, orgID, folderID int64, sqlStore db.DB) *dashboards.Dashboard { t.Helper() - cmd := models.SaveDashboardCommand{ - OrgId: orgID, - FolderId: folderID, + cmd := dashboards.SaveDashboardCommand{ + OrgID: orgID, + FolderID: folderID, IsFolder: false, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, @@ -930,7 +930,7 @@ func saveTestDashboard(t *testing.T, title string, orgID, folderID int64, sqlSto } dto := dashboards.SaveDashboardDTO{ - OrgId: orgID, + OrgID: orgID, Dashboard: cmd.GetDashboardModel(), User: &user.SignedInUser{ UserID: 1, @@ -957,11 +957,11 @@ func saveTestDashboard(t *testing.T, title string, orgID, folderID int64, sqlSto return res } -func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *models.Dashboard { +func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *dashboards.Dashboard { t.Helper() - cmd := models.SaveDashboardCommand{ - OrgId: orgID, - FolderId: 0, + cmd := dashboards.SaveDashboardCommand{ + OrgID: orgID, + FolderID: 0, IsFolder: true, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, @@ -970,7 +970,7 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *mo } dto := dashboards.SaveDashboardDTO{ - OrgId: orgID, + OrgID: orgID, Dashboard: cmd.GetDashboardModel(), User: &user.SignedInUser{ UserID: 1, @@ -997,14 +997,14 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *mo return res } -func toSaveDashboardDto(cmd models.SaveDashboardCommand) dashboards.SaveDashboardDTO { +func toSaveDashboardDto(cmd dashboards.SaveDashboardCommand) dashboards.SaveDashboardDTO { dash := (&cmd).GetDashboardModel() return dashboards.SaveDashboardDTO{ Dashboard: dash, Message: cmd.Message, - OrgId: cmd.OrgId, - User: &user.SignedInUser{UserID: cmd.UserId}, + OrgID: cmd.OrgID, + User: &user.SignedInUser{UserID: cmd.UserID}, Overwrite: cmd.Overwrite, } } diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index 3464677f40e..fd9c4a87b98 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -43,21 +43,21 @@ func TestDashboardService(t *testing.T) { titles := []string{"", " ", " \t "} for _, title := range titles { - dto.Dashboard = models.NewDashboard(title) + dto.Dashboard = dashboards.NewDashboard(title) _, err := service.SaveDashboard(context.Background(), dto, false) require.Equal(t, err, dashboards.ErrDashboardTitleEmpty) } }) t.Run("Should return validation error if it's a folder and have a folder id", func(t *testing.T) { - dto.Dashboard = models.NewDashboardFolder("Folder") - dto.Dashboard.FolderId = 1 + dto.Dashboard = dashboards.NewDashboardFolder("Folder") + dto.Dashboard.FolderID = 1 _, err := service.SaveDashboard(context.Background(), dto, false) require.Equal(t, err, dashboards.ErrDashboardFolderCannotHaveParent) }) t.Run("Should return validation error if folder is named General", func(t *testing.T) { - dto.Dashboard = models.NewDashboardFolder("General") + dto.Dashboard = dashboards.NewDashboardFolder("General") _, err := service.SaveDashboard(context.Background(), dto, false) require.Equal(t, err, dashboards.ErrDashboardFolderNameExists) }) @@ -77,8 +77,8 @@ func TestDashboardService(t *testing.T) { } for _, tc := range testCases { - dto.Dashboard = models.NewDashboard("title") - dto.Dashboard.SetUid(tc.Uid) + dto.Dashboard = dashboards.NewDashboard("title") + dto.Dashboard.SetUID(tc.Uid) dto.User = &user.SignedInUser{} if tc.Error == nil { @@ -93,8 +93,8 @@ func TestDashboardService(t *testing.T) { fakeStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything, mock.AnythingOfType("bool")).Return(true, nil).Once() fakeStore.On("GetProvisionedDataByDashboardID", mock.Anything, mock.AnythingOfType("int64")).Return(&models.DashboardProvisioning{}, nil).Once() - dto.Dashboard = models.NewDashboard("Dash") - dto.Dashboard.SetId(3) + dto.Dashboard = dashboards.NewDashboard("Dash") + dto.Dashboard.SetID(3) dto.User = &user.SignedInUser{UserID: 1} _, err := service.SaveDashboard(context.Background(), dto, false) require.Equal(t, err, dashboards.ErrDashboardCannotSaveProvisionedDashboard) @@ -102,10 +102,10 @@ func TestDashboardService(t *testing.T) { t.Run("Should not return validation error if dashboard is provisioned but UI updates allowed", func(t *testing.T) { fakeStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything, mock.AnythingOfType("bool")).Return(true, nil).Once() - fakeStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("models.SaveDashboardCommand")).Return(&models.Dashboard{Data: simplejson.New()}, nil).Once() + fakeStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(&dashboards.Dashboard{Data: simplejson.New()}, nil).Once() - dto.Dashboard = models.NewDashboard("Dash") - dto.Dashboard.SetId(3) + dto.Dashboard = dashboards.NewDashboard("Dash") + dto.Dashboard.SetID(3) dto.User = &user.SignedInUser{UserID: 1} _, err := service.SaveDashboard(context.Background(), dto, true) require.NoError(t, err) @@ -128,10 +128,10 @@ func TestDashboardService(t *testing.T) { fakeStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything, mock.AnythingOfType("bool")).Return(true, nil).Once() fakeStore.On("GetProvisionedDataByDashboardID", mock.Anything, mock.AnythingOfType("int64")).Return(nil, nil).Once() - fakeStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("models.SaveDashboardCommand")).Return(&models.Dashboard{Data: simplejson.New()}, nil).Once() + fakeStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(&dashboards.Dashboard{Data: simplejson.New()}, nil).Once() fakeStore.On("SaveAlerts", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("alert validation error")).Once() - dto.Dashboard = models.NewDashboard("Dash") + dto.Dashboard = dashboards.NewDashboard("Dash") dto.User = &user.SignedInUser{UserID: 1} _, err := service.SaveDashboard(context.Background(), dto, false) require.Error(t, err) @@ -144,10 +144,10 @@ func TestDashboardService(t *testing.T) { t.Run("Should not return validation error if dashboard is provisioned", func(t *testing.T) { fakeStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything, mock.AnythingOfType("bool")).Return(true, nil).Once() - fakeStore.On("SaveProvisionedDashboard", mock.Anything, mock.AnythingOfType("models.SaveDashboardCommand"), mock.AnythingOfType("*models.DashboardProvisioning")).Return(&models.Dashboard{Data: simplejson.New()}, nil).Once() + fakeStore.On("SaveProvisionedDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand"), mock.AnythingOfType("*dashboards.DashboardProvisioning")).Return(&dashboards.Dashboard{Data: simplejson.New()}, nil).Once() - dto.Dashboard = models.NewDashboard("Dash") - dto.Dashboard.SetId(3) + dto.Dashboard = dashboards.NewDashboard("Dash") + dto.Dashboard.SetID(3) dto.User = &user.SignedInUser{UserID: 1} _, err := service.SaveProvisionedDashboard(context.Background(), dto, nil) require.NoError(t, err) @@ -155,14 +155,14 @@ func TestDashboardService(t *testing.T) { t.Run("Should override invalid refresh interval if dashboard is provisioned", func(t *testing.T) { fakeStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything, mock.AnythingOfType("bool")).Return(true, nil).Once() - fakeStore.On("SaveProvisionedDashboard", mock.Anything, mock.AnythingOfType("models.SaveDashboardCommand"), mock.AnythingOfType("*models.DashboardProvisioning")).Return(&models.Dashboard{Data: simplejson.New()}, nil).Once() + fakeStore.On("SaveProvisionedDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand"), mock.AnythingOfType("*dashboards.DashboardProvisioning")).Return(&dashboards.Dashboard{Data: simplejson.New()}, nil).Once() oldRefreshInterval := setting.MinRefreshInterval setting.MinRefreshInterval = "5m" defer func() { setting.MinRefreshInterval = oldRefreshInterval }() - dto.Dashboard = models.NewDashboard("Dash") - dto.Dashboard.SetId(3) + dto.Dashboard = dashboards.NewDashboard("Dash") + dto.Dashboard.SetID(3) dto.User = &user.SignedInUser{UserID: 1} dto.Dashboard.Data.Set("refresh", "1s") _, err := service.SaveProvisionedDashboard(context.Background(), dto, nil) @@ -178,8 +178,8 @@ func TestDashboardService(t *testing.T) { fakeStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything, mock.AnythingOfType("bool")).Return(true, nil).Once() fakeStore.On("GetProvisionedDataByDashboardID", mock.Anything, mock.AnythingOfType("int64")).Return(&models.DashboardProvisioning{}, nil).Once() - dto.Dashboard = models.NewDashboard("Dash") - dto.Dashboard.SetId(3) + dto.Dashboard = dashboards.NewDashboard("Dash") + dto.Dashboard.SetID(3) dto.User = &user.SignedInUser{UserID: 1} _, err := service.ImportDashboard(context.Background(), dto) require.Equal(t, err, dashboards.ErrDashboardCannotSaveProvisionedDashboard) diff --git a/pkg/services/dashboards/store_mock.go b/pkg/services/dashboards/store_mock.go index 9cdd1a9aa41..d6ee0dc6434 100644 --- a/pkg/services/dashboards/store_mock.go +++ b/pkg/services/dashboards/store_mock.go @@ -1,14 +1,16 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.16.0. DO NOT EDIT. package dashboards import ( context "context" - models "github.com/grafana/grafana/pkg/models" folder "github.com/grafana/grafana/pkg/services/folder" - "github.com/grafana/grafana/pkg/services/quota" mock "github.com/stretchr/testify/mock" + + models "github.com/grafana/grafana/pkg/models" + + quota "github.com/grafana/grafana/pkg/services/quota" ) // FakeDashboardStore is an autogenerated mock type for the Store type @@ -16,6 +18,29 @@ type FakeDashboardStore struct { mock.Mock } +// Count provides a mock function with given fields: _a0, _a1 +func (_m *FakeDashboardStore) Count(_a0 context.Context, _a1 *quota.ScopeParameters) (*quota.Map, error) { + ret := _m.Called(_a0, _a1) + + var r0 *quota.Map + if rf, ok := ret.Get(0).(func(context.Context, *quota.ScopeParameters) *quota.Map); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*quota.Map) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *quota.ScopeParameters) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // CountDashboardsInFolder provides a mock function with given fields: ctx, request func (_m *FakeDashboardStore) CountDashboardsInFolder(ctx context.Context, request *CountDashboardsInFolderRequest) (int64, error) { ret := _m.Called(ctx, request) @@ -103,20 +128,20 @@ func (_m *FakeDashboardStore) FindDashboards(ctx context.Context, query *models. } // GetDashboard provides a mock function with given fields: ctx, query -func (_m *FakeDashboardStore) GetDashboard(ctx context.Context, query *models.GetDashboardQuery) (*models.Dashboard, error) { +func (_m *FakeDashboardStore) GetDashboard(ctx context.Context, query *GetDashboardQuery) (*Dashboard, error) { ret := _m.Called(ctx, query) - var r0 *models.Dashboard - if rf, ok := ret.Get(0).(func(context.Context, *models.GetDashboardQuery) *models.Dashboard); ok { + var r0 *Dashboard + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardQuery) *Dashboard); ok { r0 = rf(ctx, query) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.Dashboard) + r0 = ret.Get(0).(*Dashboard) } } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *models.GetDashboardQuery) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, *GetDashboardQuery) error); ok { r1 = rf(ctx, query) } else { r1 = ret.Error(1) @@ -153,12 +178,12 @@ func (_m *FakeDashboardStore) GetDashboardTags(ctx context.Context, query *model return r0 } -// GetDashboardUIDById provides a mock function with given fields: ctx, query -func (_m *FakeDashboardStore) GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error { +// GetDashboardUIDByID provides a mock function with given fields: ctx, query +func (_m *FakeDashboardStore) GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) error { ret := _m.Called(ctx, query) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *models.GetDashboardRefByIdQuery) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardRefByIDQuery) error); ok { r0 = rf(ctx, query) } else { r0 = ret.Error(0) @@ -168,11 +193,11 @@ func (_m *FakeDashboardStore) GetDashboardUIDById(ctx context.Context, query *mo } // GetDashboards provides a mock function with given fields: ctx, query -func (_m *FakeDashboardStore) GetDashboards(ctx context.Context, query *models.GetDashboardsQuery) error { +func (_m *FakeDashboardStore) GetDashboards(ctx context.Context, query *GetDashboardsQuery) error { ret := _m.Called(ctx, query) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *models.GetDashboardsQuery) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardsQuery) error); ok { r0 = rf(ctx, query) } else { r0 = ret.Error(0) @@ -376,20 +401,20 @@ func (_m *FakeDashboardStore) SaveAlerts(ctx context.Context, dashID int64, aler } // SaveDashboard provides a mock function with given fields: ctx, cmd -func (_m *FakeDashboardStore) SaveDashboard(ctx context.Context, cmd models.SaveDashboardCommand) (*models.Dashboard, error) { +func (_m *FakeDashboardStore) SaveDashboard(ctx context.Context, cmd SaveDashboardCommand) (*Dashboard, error) { ret := _m.Called(ctx, cmd) - var r0 *models.Dashboard - if rf, ok := ret.Get(0).(func(context.Context, models.SaveDashboardCommand) *models.Dashboard); ok { + var r0 *Dashboard + if rf, ok := ret.Get(0).(func(context.Context, SaveDashboardCommand) *Dashboard); ok { r0 = rf(ctx, cmd) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.Dashboard) + r0 = ret.Get(0).(*Dashboard) } } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, models.SaveDashboardCommand) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, SaveDashboardCommand) error); ok { r1 = rf(ctx, cmd) } else { r1 = ret.Error(1) @@ -399,20 +424,20 @@ func (_m *FakeDashboardStore) SaveDashboard(ctx context.Context, cmd models.Save } // SaveProvisionedDashboard provides a mock function with given fields: ctx, cmd, provisioning -func (_m *FakeDashboardStore) SaveProvisionedDashboard(ctx context.Context, cmd models.SaveDashboardCommand, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) { +func (_m *FakeDashboardStore) SaveProvisionedDashboard(ctx context.Context, cmd SaveDashboardCommand, provisioning *DashboardProvisioning) (*Dashboard, error) { ret := _m.Called(ctx, cmd, provisioning) - var r0 *models.Dashboard - if rf, ok := ret.Get(0).(func(context.Context, models.SaveDashboardCommand, *models.DashboardProvisioning) *models.Dashboard); ok { + var r0 *Dashboard + if rf, ok := ret.Get(0).(func(context.Context, SaveDashboardCommand, *DashboardProvisioning) *Dashboard); ok { r0 = rf(ctx, cmd, provisioning) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*models.Dashboard) + r0 = ret.Get(0).(*Dashboard) } } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, models.SaveDashboardCommand, *models.DashboardProvisioning) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, SaveDashboardCommand, *DashboardProvisioning) error); ok { r1 = rf(ctx, cmd, provisioning) } else { r1 = ret.Error(1) @@ -450,18 +475,18 @@ func (_m *FakeDashboardStore) UpdateDashboardACL(ctx context.Context, uid int64, } // ValidateDashboardBeforeSave provides a mock function with given fields: ctx, dashboard, overwrite -func (_m *FakeDashboardStore) ValidateDashboardBeforeSave(ctx context.Context, dashboard *models.Dashboard, overwrite bool) (bool, error) { +func (_m *FakeDashboardStore) ValidateDashboardBeforeSave(ctx context.Context, dashboard *Dashboard, overwrite bool) (bool, error) { ret := _m.Called(ctx, dashboard, overwrite) var r0 bool - if rf, ok := ret.Get(0).(func(context.Context, *models.Dashboard, bool) bool); ok { + if rf, ok := ret.Get(0).(func(context.Context, *Dashboard, bool) bool); ok { r0 = rf(ctx, dashboard, overwrite) } else { r0 = ret.Get(0).(bool) } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *models.Dashboard, bool) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, *Dashboard, bool) error); ok { r1 = rf(ctx, dashboard, overwrite) } else { r1 = ret.Error(1) @@ -475,10 +500,6 @@ type mockConstructorTestingTNewFakeDashboardStore interface { Cleanup(func()) } -func (_m *FakeDashboardStore) Count(context.Context, *quota.ScopeParameters) (*quota.Map, error) { - return nil, nil -} - // NewFakeDashboardStore creates a new instance of FakeDashboardStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. func NewFakeDashboardStore(t mockConstructorTestingTNewFakeDashboardStore) *FakeDashboardStore { mock := &FakeDashboardStore{} diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 3890db59bc1..23dfaf60606 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -293,15 +293,15 @@ func (s *Service) getFolderByTitle(ctx context.Context, user *user.SignedInUser, func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) (*folder.Folder, error) { logger := s.log.FromContext(ctx) - dashFolder := models.NewDashboardFolder(cmd.Title) - dashFolder.OrgId = cmd.OrgID + dashFolder := dashboards.NewDashboardFolder(cmd.Title) + dashFolder.OrgID = cmd.OrgID trimmedUID := strings.TrimSpace(cmd.UID) if trimmedUID == accesscontrol.GeneralFolderUID { return nil, dashboards.ErrFolderInvalidUID } - dashFolder.SetUid(trimmedUID) + dashFolder.SetUID(trimmedUID) if cmd.SignedInUser == nil { return nil, folder.ErrBadRequest.Errorf("missing signed in user") @@ -317,7 +317,7 @@ func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) ( dto := &dashboards.SaveDashboardDTO{ Dashboard: dashFolder, - OrgId: cmd.OrgID, + OrgID: cmd.OrgID, User: user, } @@ -332,7 +332,7 @@ func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) ( } var createdFolder *folder.Folder - createdFolder, err = s.dashboardStore.GetFolderByID(ctx, cmd.OrgID, dash.Id) + createdFolder, err = s.dashboardStore.GetFolderByID(ctx, cmd.OrgID, dash.ID) if err != nil { return nil, err } @@ -366,7 +366,7 @@ func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) ( // TODO: Today, if a UID isn't specified, the dashboard store // generates a new UID. The new folder store will need to do this as // well, but for now we take the UID from the newly created folder. - UID: dash.Uid, + UID: dash.UID, OrgID: cmd.OrgID, Title: cmd.Title, Description: cmd.Description, @@ -384,11 +384,11 @@ func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) ( }); deleteErr != nil { logger.Error("error deleting folder after failed save to nested folder store", "error", err) } - return folder.FromDashboard(dash), err + return dashboards.FromDashboard(dash), err } } - f := folder.FromDashboard(dash) + f := dashboards.FromDashboard(dash) if nestedFolder != nil && nestedFolder.ParentUID != "" { f.ParentUID = nestedFolder.ParentUID } @@ -439,7 +439,7 @@ func (s *Service) Update(ctx context.Context, cmd *folder.UpdateFolderCommand) ( func (s *Service) legacyUpdate(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) { logger := s.log.FromContext(ctx) - query := models.GetDashboardQuery{OrgId: cmd.OrgID, Uid: cmd.UID} + query := dashboards.GetDashboardQuery{OrgID: cmd.OrgID, UID: cmd.UID} _, err := s.dashboardStore.GetDashboard(ctx, &query) if err != nil { return nil, toFolderError(err) @@ -461,7 +461,7 @@ func (s *Service) legacyUpdate(ctx context.Context, cmd *folder.UpdateFolderComm dto := &dashboards.SaveDashboardDTO{ Dashboard: dashFolder, - OrgId: cmd.OrgID, + OrgID: cmd.OrgID, User: cmd.SignedInUser, Overwrite: cmd.Overwrite, } @@ -477,7 +477,7 @@ func (s *Service) legacyUpdate(ctx context.Context, cmd *folder.UpdateFolderComm } var foldr *folder.Folder - foldr, err = s.dashboardStore.GetFolderByID(ctx, cmd.OrgID, dash.Id) + foldr, err = s.dashboardStore.GetFolderByID(ctx, cmd.OrgID, dash.ID) if err != nil { return nil, err } @@ -486,8 +486,8 @@ func (s *Service) legacyUpdate(ctx context.Context, cmd *folder.UpdateFolderComm if err := s.bus.Publish(ctx, &events.FolderTitleUpdated{ Timestamp: foldr.Updated, Title: foldr.Title, - ID: dash.Id, - UID: dash.Uid, + ID: dash.ID, + UID: dash.UID, OrgID: cmd.OrgID, }); err != nil { logger.Error("failed to publish FolderTitleUpdated event", "folder", foldr.Title, "user", user.UserID, "error", err) @@ -497,8 +497,8 @@ func (s *Service) legacyUpdate(ctx context.Context, cmd *folder.UpdateFolderComm } // prepareForUpdate updates an existing dashboard model from command into model for folder update -func prepareForUpdate(dashFolder *models.Dashboard, orgId int64, userId int64, cmd *folder.UpdateFolderCommand) { - dashFolder.OrgId = orgId +func prepareForUpdate(dashFolder *dashboards.Dashboard, orgId int64, userId int64, cmd *folder.UpdateFolderCommand) { + dashFolder.OrgID = orgId title := dashFolder.Title if cmd.NewTitle != nil && *cmd.NewTitle != "" { @@ -508,7 +508,7 @@ func prepareForUpdate(dashFolder *models.Dashboard, orgId int64, userId int64, c dashFolder.Data.Set("title", dashFolder.Title) if cmd.NewUID != nil && *cmd.NewUID != "" { - dashFolder.SetUid(*cmd.NewUID) + dashFolder.SetUID(*cmd.NewUID) } dashFolder.SetVersion(cmd.Version) diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index 99c10c86422..2a7a99657cd 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -116,7 +116,7 @@ func TestIntegrationFolderService(t *testing.T) { }) t.Run("When creating folder should return access denied error", func(t *testing.T) { - dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*models.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil).Times(2) + dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil).Times(2) _, err := service.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, Title: f.Title, @@ -128,11 +128,11 @@ 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("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - folder := args.Get(1).(*models.GetDashboardQuery) - folder.Result = models.NewDashboard("dashboard-test") + 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(&models.Dashboard{}, nil) + }).Return(&dashboards.Dashboard{}, nil) _, err := service.Update(context.Background(), &folder.UpdateFolderCommand{ UID: folderUID, OrgID: orgID, @@ -169,13 +169,13 @@ func TestIntegrationFolderService(t *testing.T) { guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) t.Run("When creating folder should not return access denied error", func(t *testing.T) { - dash := models.NewDashboardFolder("Test-Folder") - dash.Id = rand.Int63() - f := folder.FromDashboard(dash) + dash := dashboards.NewDashboardFolder("Test-Folder") + dash.ID = rand.Int63() + f := dashboards.FromDashboard(dash) - dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*models.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil) - dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("models.SaveDashboardCommand")).Return(dash, nil).Once() - dashStore.On("GetFolderByID", mock.Anything, orgID, dash.Id).Return(f, nil) + dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil) + dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(dash, nil).Once() + dashStore.On("GetFolderByID", mock.Anything, orgID, dash.ID).Return(f, nil) actualFolder, err := service.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, @@ -188,8 +188,8 @@ func TestIntegrationFolderService(t *testing.T) { }) t.Run("When creating folder should return error if uid is general", func(t *testing.T) { - dash := models.NewDashboardFolder("Test-Folder") - dash.Id = rand.Int63() + dash := dashboards.NewDashboardFolder("Test-Folder") + dash.ID = rand.Int63() _, err := service.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, @@ -201,18 +201,18 @@ func TestIntegrationFolderService(t *testing.T) { }) t.Run("When updating folder should not return access denied error", func(t *testing.T) { - dashboardFolder := models.NewDashboardFolder("Folder") - dashboardFolder.Id = rand.Int63() - dashboardFolder.Uid = util.GenerateShortUID() - f := folder.FromDashboard(dashboardFolder) + dashboardFolder := dashboards.NewDashboardFolder("Folder") + dashboardFolder.ID = rand.Int63() + dashboardFolder.UID = util.GenerateShortUID() + f := dashboards.FromDashboard(dashboardFolder) - dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*models.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil) - dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("models.SaveDashboardCommand")).Return(dashboardFolder, nil) - dashStore.On("GetFolderByID", mock.Anything, orgID, dashboardFolder.Id).Return(f, nil) + dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil) + dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(dashboardFolder, nil) + dashStore.On("GetFolderByID", mock.Anything, orgID, dashboardFolder.ID).Return(f, nil) title := "TEST-Folder" req := &folder.UpdateFolderCommand{ - UID: dashboardFolder.Uid, + UID: dashboardFolder.UID, OrgID: orgID, NewTitle: &title, SignedInUser: usr, @@ -324,9 +324,9 @@ func TestNestedFolderServiceFeatureToggle(t *testing.T) { dashboardsvc := dashboards.FakeDashboardService{} dashboardsvc.On("BuildSaveDashboardCommand", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), - mock.AnythingOfType("bool"), mock.AnythingOfType("bool")).Return(&models.SaveDashboardCommand{}, nil) + mock.AnythingOfType("bool"), mock.AnythingOfType("bool")).Return(&dashboards.SaveDashboardCommand{}, nil) dashStore := dashboards.FakeDashboardStore{} - dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("models.SaveDashboardCommand")).Return(&models.Dashboard{}, nil) + dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(&dashboards.Dashboard{}, nil) dashStore.On("GetFolderByID", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")).Return(&folder.Folder{}, nil) cfg := setting.NewCfg() cfg.RBACEnabled = false @@ -368,8 +368,8 @@ func TestNestedFolderService(t *testing.T) { // dashboard store & service commands that should be called. dashboardsvc.On("BuildSaveDashboardCommand", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), - mock.AnythingOfType("bool"), mock.AnythingOfType("bool")).Return(&models.SaveDashboardCommand{}, nil) - dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("models.SaveDashboardCommand")).Return(&models.Dashboard{}, nil) + mock.AnythingOfType("bool"), mock.AnythingOfType("bool")).Return(&dashboards.SaveDashboardCommand{}, nil) + dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(&dashboards.Dashboard{}, nil) dashStore.On("GetFolderByID", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")).Return(&folder.Folder{}, nil) _, err := foldersvc.Create(context.Background(), &folder.CreateFolderCommand{ @@ -427,8 +427,8 @@ func TestNestedFolderService(t *testing.T) { // dashboard store & service commands that should be called. dashboardsvc.On("BuildSaveDashboardCommand", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), - mock.AnythingOfType("bool"), mock.AnythingOfType("bool")).Return(&models.SaveDashboardCommand{}, nil) - dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("models.SaveDashboardCommand")).Return(&models.Dashboard{}, nil) + mock.AnythingOfType("bool"), mock.AnythingOfType("bool")).Return(&dashboards.SaveDashboardCommand{}, nil) + dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(&dashboards.Dashboard{}, nil) dashStore.On("GetFolderByID", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")).Return(&folder.Folder{}, nil) _, err := foldersvc.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, @@ -447,8 +447,8 @@ func TestNestedFolderService(t *testing.T) { foldersvc.dashboardStore = dashStore dashboardsvc.On("BuildSaveDashboardCommand", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), - mock.AnythingOfType("bool"), mock.AnythingOfType("bool")).Return(&models.SaveDashboardCommand{}, nil) - dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("models.SaveDashboardCommand")).Return(&models.Dashboard{Uid: "newUID"}, nil) + mock.AnythingOfType("bool"), mock.AnythingOfType("bool")).Return(&dashboards.SaveDashboardCommand{}, nil) + dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(&dashboards.Dashboard{UID: "newUID"}, nil) dashStore.On("GetFolderByID", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")).Return(&folder.Folder{}, nil) f, err := foldersvc.Create(context.Background(), &folder.CreateFolderCommand{ OrgID: orgID, @@ -463,16 +463,16 @@ func TestNestedFolderService(t *testing.T) { t.Run("create failed because of circular reference", func(t *testing.T) { // dashboard store & service commands that should be called. - dashboardFolder := models.NewDashboardFolder("myFolder") - dashboardFolder.Id = rand.Int63() - dashboardFolder.Uid = "myFolder" - f := folder.FromDashboard(dashboardFolder) + dashboardFolder := dashboards.NewDashboardFolder("myFolder") + dashboardFolder.ID = rand.Int63() + dashboardFolder.UID = "myFolder" + f := dashboards.FromDashboard(dashboardFolder) dashStore = &dashboards.FakeDashboardStore{} foldersvc.dashboardStore = dashStore - dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*models.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil) - dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("models.SaveDashboardCommand")).Return(dashboardFolder, nil) - dashStore.On("GetFolderByID", mock.Anything, orgID, dashboardFolder.Id).Return(f, nil) + dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil) + dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(dashboardFolder, nil) + dashStore.On("GetFolderByID", mock.Anything, orgID, dashboardFolder.ID).Return(f, nil) var actualCmd *models.DeleteDashboardCommand dashStore.On("DeleteDashboard", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { actualCmd = args.Get(1).(*models.DeleteDashboardCommand) @@ -509,8 +509,8 @@ func TestNestedFolderService(t *testing.T) { // dashboard store & service commands that should be called. dashboardsvc.On("BuildSaveDashboardCommand", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), - mock.AnythingOfType("bool"), mock.AnythingOfType("bool")).Return(&models.SaveDashboardCommand{}, nil) - dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("models.SaveDashboardCommand")).Return(&models.Dashboard{}, nil) + mock.AnythingOfType("bool"), mock.AnythingOfType("bool")).Return(&dashboards.SaveDashboardCommand{}, nil) + dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(&dashboards.Dashboard{}, nil) dashStore.On("GetFolderByID", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")).Return(&folder.Folder{}, nil) dashStore.On("GetFolderByUID", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).Return(&folder.Folder{}, nil) var actualCmd *models.DeleteDashboardCommand @@ -670,8 +670,8 @@ func TestNestedFolderService(t *testing.T) { // dashboard store & service commands that should be called. dashboardsvc.On("BuildSaveDashboardCommand", mock.Anything, mock.AnythingOfType("*dashboards.SaveDashboardDTO"), - mock.AnythingOfType("bool"), mock.AnythingOfType("bool")).Return(&models.SaveDashboardCommand{}, nil) - dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("models.SaveDashboardCommand")).Return(&models.Dashboard{}, nil) + mock.AnythingOfType("bool"), mock.AnythingOfType("bool")).Return(&dashboards.SaveDashboardCommand{}, nil) + dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(&dashboards.Dashboard{}, nil) dashStore.On("GetFolderByID", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("int64")).Return(&folder.Folder{}, nil) dashStore.On("GetFolderByUID", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).Return(&folder.Folder{}, nil) var actualCmd *models.DeleteDashboardCommand diff --git a/pkg/services/guardian/accesscontrol_guardian.go b/pkg/services/guardian/accesscontrol_guardian.go index e2f1018dae3..e583b18f0b9 100644 --- a/pkg/services/guardian/accesscontrol_guardian.go +++ b/pkg/services/guardian/accesscontrol_guardian.go @@ -30,11 +30,11 @@ func NewAccessControlDashboardGuardian( dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardService dashboards.DashboardService, ) (*AccessControlDashboardGuardian, error) { - var dashboard *models.Dashboard + var dashboard *dashboards.Dashboard if dashboardId != 0 { - q := &models.GetDashboardQuery{ - Id: dashboardId, - OrgId: user.OrgID, + q := &dashboards.GetDashboardQuery{ + ID: dashboardId, + OrgID: user.OrgID, } if err := dashboardService.GetDashboard(ctx, q); err != nil { @@ -67,11 +67,11 @@ func NewAccessControlDashboardGuardianByUID( dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardService dashboards.DashboardService, ) (*AccessControlDashboardGuardian, error) { - var dashboard *models.Dashboard + var dashboard *dashboards.Dashboard if dashboardUID != "" { - q := &models.GetDashboardQuery{ - Uid: dashboardUID, - OrgId: user.OrgID, + q := &dashboards.GetDashboardQuery{ + UID: dashboardUID, + OrgID: user.OrgID, } if err := dashboardService.GetDashboard(ctx, q); err != nil { @@ -100,7 +100,7 @@ func NewAccessControlDashboardGuardianByUID( // This constructor should be preferred over the other two if the dashboard in available // since it avoids querying the database for fetching the dashboard. func NewAccessControlDashboardGuardianByDashboard( - ctx context.Context, dashboard *models.Dashboard, user *user.SignedInUser, + ctx context.Context, dashboard *dashboards.Dashboard, user *user.SignedInUser, store db.DB, ac accesscontrol.AccessControl, folderPermissionsService accesscontrol.FolderPermissionsService, dashboardPermissionsService accesscontrol.DashboardPermissionsService, @@ -122,7 +122,7 @@ func NewAccessControlDashboardGuardianByDashboard( type AccessControlDashboardGuardian struct { ctx context.Context log log.Logger - dashboard *models.Dashboard + dashboard *dashboards.Dashboard user *user.SignedInUser store db.DB ac accesscontrol.AccessControl @@ -137,11 +137,11 @@ func (a *AccessControlDashboardGuardian) CanSave() (bool, error) { } if a.dashboard.IsFolder { - return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.Uid))) + return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.UID))) } return a.evaluate( - accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.Uid)), + accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.UID)), ) } @@ -155,11 +155,11 @@ func (a *AccessControlDashboardGuardian) CanEdit() (bool, error) { } if a.dashboard.IsFolder { - return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.Uid))) + return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.UID))) } return a.evaluate( - accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.Uid)), + accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.UID)), ) } @@ -169,11 +169,11 @@ func (a *AccessControlDashboardGuardian) CanView() (bool, error) { } if a.dashboard.IsFolder { - return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.Uid))) + return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.UID))) } return a.evaluate( - accesscontrol.EvalPermission(dashboards.ActionDashboardsRead, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.Uid)), + accesscontrol.EvalPermission(dashboards.ActionDashboardsRead, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.UID)), ) } @@ -184,14 +184,14 @@ func (a *AccessControlDashboardGuardian) CanAdmin() (bool, error) { if a.dashboard.IsFolder { return a.evaluate(accesscontrol.EvalAll( - accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsRead, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.Uid)), - accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.Uid)), + accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsRead, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.UID)), + accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.UID)), )) } return a.evaluate(accesscontrol.EvalAll( - accesscontrol.EvalPermission(dashboards.ActionDashboardsPermissionsRead, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.Uid)), - accesscontrol.EvalPermission(dashboards.ActionDashboardsPermissionsWrite, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.Uid)), + accesscontrol.EvalPermission(dashboards.ActionDashboardsPermissionsRead, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.UID)), + accesscontrol.EvalPermission(dashboards.ActionDashboardsPermissionsWrite, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.UID)), )) } @@ -201,11 +201,11 @@ func (a *AccessControlDashboardGuardian) CanDelete() (bool, error) { } if a.dashboard.IsFolder { - return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.Uid))) + return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.dashboard.UID))) } return a.evaluate( - accesscontrol.EvalPermission(dashboards.ActionDashboardsDelete, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.Uid)), + accesscontrol.EvalPermission(dashboards.ActionDashboardsDelete, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.UID)), ) } @@ -217,17 +217,17 @@ func (a *AccessControlDashboardGuardian) CanCreate(folderID int64, isFolder bool if err != nil { return false, err } - return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionDashboardsCreate, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.Uid))) + return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionDashboardsCreate, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.UID))) } func (a *AccessControlDashboardGuardian) evaluate(evaluator accesscontrol.Evaluator) (bool, error) { ok, err := a.ac.Evaluate(a.ctx, a.user, evaluator) if err != nil { - a.log.Debug("Failed to evaluate access control to folder or dashboard", "error", err, "userId", a.user.UserID, "id", a.dashboard.Id) + a.log.Debug("Failed to evaluate access control to folder or dashboard", "error", err, "userId", a.user.UserID, "id", a.dashboard.ID) } if !ok && err == nil { - a.log.Debug("Access denied to folder or dashboard", "userId", a.user.UserID, "id", a.dashboard.Id, "permissions", evaluator.GoString()) + a.log.Debug("Access denied to folder or dashboard", "userId", a.user.UserID, "id", a.dashboard.ID, "permissions", evaluator.GoString()) } return ok, err @@ -251,7 +251,7 @@ func (a *AccessControlDashboardGuardian) GetACL() ([]*models.DashboardACLInfoDTO svc = a.dashboardPermissionsService } - permissions, err := svc.GetPermissions(a.ctx, a.user, a.dashboard.Uid) + permissions, err := svc.GetPermissions(a.ctx, a.user, a.dashboard.UID) if err != nil { return nil, err } @@ -269,9 +269,9 @@ func (a *AccessControlDashboardGuardian) GetACL() ([]*models.DashboardACLInfoDTO } acl = append(acl, &models.DashboardACLInfoDTO{ - OrgId: a.dashboard.OrgId, - DashboardId: a.dashboard.Id, - FolderId: a.dashboard.FolderId, + OrgId: a.dashboard.OrgID, + DashboardId: a.dashboard.ID, + FolderId: a.dashboard.FolderID, Created: p.Created, Updated: p.Updated, UserId: p.UserId, @@ -283,11 +283,11 @@ func (a *AccessControlDashboardGuardian) GetACL() ([]*models.DashboardACLInfoDTO Role: role, Permission: permissionMap[svc.MapActions(p)], PermissionName: permissionMap[svc.MapActions(p)].String(), - Uid: a.dashboard.Uid, + Uid: a.dashboard.UID, Title: a.dashboard.Title, Slug: a.dashboard.Slug, IsFolder: a.dashboard.IsFolder, - Url: a.dashboard.GetUrl(), + Url: a.dashboard.GetURL(), Inherited: false, }) } @@ -332,11 +332,11 @@ func (a *AccessControlDashboardGuardian) GetHiddenACL(cfg *setting.Cfg) ([]*mode return hiddenACL, nil } -func (a *AccessControlDashboardGuardian) loadParentFolder(folderID int64) (*models.Dashboard, error) { +func (a *AccessControlDashboardGuardian) loadParentFolder(folderID int64) (*dashboards.Dashboard, error) { if folderID == 0 { - return &models.Dashboard{Uid: accesscontrol.GeneralFolderUID}, nil + return &dashboards.Dashboard{UID: accesscontrol.GeneralFolderUID}, nil } - folderQuery := &models.GetDashboardQuery{Id: folderID, OrgId: a.user.OrgID} + folderQuery := &dashboards.GetDashboardQuery{ID: folderID, OrgID: a.user.OrgID} if err := a.dashboardService.GetDashboard(a.ctx, folderQuery); err != nil { return nil, err } diff --git a/pkg/services/guardian/accesscontrol_guardian_test.go b/pkg/services/guardian/accesscontrol_guardian_test.go index 41ad86297be..9a45bae5878 100644 --- a/pkg/services/guardian/accesscontrol_guardian_test.go +++ b/pkg/services/guardian/accesscontrol_guardian_test.go @@ -584,7 +584,7 @@ func TestAccessControlDashboardGuardian_GetHiddenACL(t *testing.T) { } } -func setupAccessControlGuardianTest(t *testing.T, uid string, permissions []accesscontrol.Permission, dashboardSvc dashboards.DashboardService) (*AccessControlDashboardGuardian, *models.Dashboard) { +func setupAccessControlGuardianTest(t *testing.T, uid string, permissions []accesscontrol.Permission, dashboardSvc dashboards.DashboardService) (*AccessControlDashboardGuardian, *dashboards.Dashboard) { t.Helper() store := db.InitTestDB(t) @@ -595,10 +595,10 @@ func setupAccessControlGuardianTest(t *testing.T, uid string, permissions []acce quotaService := quotatest.New(false, nil) dashStore, err := dashdb.ProvideDashboardStore(store, store.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(store, store.Cfg), quotaService) require.NoError(t, err) - dash, err := dashStore.SaveDashboard(context.Background(), models.SaveDashboardCommand{ + dash, err := dashStore.SaveDashboard(context.Background(), dashboards.SaveDashboardCommand{ Dashboard: toSave.Data, - UserId: 1, - OrgId: 1, + UserID: 1, + OrgID: 1, }) require.NoError(t, err) ac := accesscontrolmock.New().WithPermissions(permissions) @@ -617,29 +617,29 @@ func setupAccessControlGuardianTest(t *testing.T, uid string, permissions []acce require.NoError(t, err) if dashboardSvc == nil { fakeDashboardService := dashboards.NewFakeDashboardService(t) - fakeDashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - Uid: q.Uid, - OrgId: q.OrgId, + fakeDashboardService.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, + OrgID: q.OrgID, } }).Return(nil) dashboardSvc = fakeDashboardService } - g, err := NewAccessControlDashboardGuardian(context.Background(), dash.Id, &user.SignedInUser{OrgID: 1}, store, ac, folderPermissions, dashboardPermissions, dashboardSvc) + g, err := NewAccessControlDashboardGuardian(context.Background(), dash.ID, &user.SignedInUser{OrgID: 1}, store, ac, folderPermissions, dashboardPermissions, dashboardSvc) require.NoError(t, err) return g, dash } func testDashSvc(t *testing.T) dashboards.DashboardService { dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - d := models.NewDashboard("mocked") - d.Id = 1 - d.Uid = "1" + 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 dashSvc diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index 5bc5f790b3a..24d1cb7761d 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -69,16 +69,16 @@ var NewByUID = func(ctx context.Context, dashUID string, orgId int64, user *user // NewByDashboard factory for creating a new dashboard guardian instance // When using access control this function is replaced on startup and the AccessControlDashboardGuardian is returned -var NewByDashboard = func(ctx context.Context, dash *models.Dashboard, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) { +var NewByDashboard = func(ctx context.Context, dash *dashboards.Dashboard, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) { panic("no guardian factory implementation provided") } // newDashboardGuardian creates a dashboard guardian by the provided dashId. func newDashboardGuardian(ctx context.Context, dashId int64, orgId int64, user *user.SignedInUser, store db.DB, dashSvc dashboards.DashboardService, teamSvc team.Service) (*dashboardGuardianImpl, error) { if dashId != 0 { - q := &models.GetDashboardQuery{ - Id: dashId, - OrgId: orgId, + q := &dashboards.GetDashboardQuery{ + ID: dashId, + OrgID: orgId, } if err := dashSvc.GetDashboard(ctx, q); err != nil { @@ -105,9 +105,9 @@ func newDashboardGuardian(ctx context.Context, dashId int64, orgId int64, user * func newDashboardGuardianByUID(ctx context.Context, dashUID string, orgId int64, user *user.SignedInUser, store db.DB, dashSvc dashboards.DashboardService, teamSvc team.Service) (*dashboardGuardianImpl, error) { dashID := int64(0) if dashUID != "" { - q := &models.GetDashboardQuery{ - Uid: dashUID, - OrgId: orgId, + q := &dashboards.GetDashboardQuery{ + UID: dashUID, + OrgID: orgId, } if err := dashSvc.GetDashboard(ctx, q); err != nil { @@ -116,7 +116,7 @@ func newDashboardGuardianByUID(ctx context.Context, dashUID string, orgId int64, } return nil, ErrGuardianGetDashboardFailure.Errorf("failed to get dashboard by UID: %w", err) } - dashID = q.Result.Id + dashID = q.Result.ID } return &dashboardGuardianImpl{ @@ -134,10 +134,10 @@ func newDashboardGuardianByUID(ctx context.Context, dashUID string, orgId int64, // newDashboardGuardianByDashboard creates a dashboard guardian by the provided dashboard. // This constructor should be preferred over the other two if the dashboard in available // since it avoids querying the database for fetching the dashboard. -func newDashboardGuardianByDashboard(ctx context.Context, dash *models.Dashboard, orgId int64, user *user.SignedInUser, store db.DB, dashSvc dashboards.DashboardService, teamSvc team.Service) (*dashboardGuardianImpl, error) { +func newDashboardGuardianByDashboard(ctx context.Context, dash *dashboards.Dashboard, orgId int64, user *user.SignedInUser, store db.DB, dashSvc dashboards.DashboardService, teamSvc team.Service) (*dashboardGuardianImpl, error) { return &dashboardGuardianImpl{ user: user, - dashId: dash.Id, + dashId: dash.ID, orgId: orgId, log: log.New("dashboard.permissions"), ctx: ctx, @@ -470,10 +470,10 @@ func MockDashboardGuardian(mock *FakeDashboardGuardian) { return mock, nil } - NewByDashboard = func(_ context.Context, dash *models.Dashboard, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) { + NewByDashboard = func(_ context.Context, dash *dashboards.Dashboard, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) { mock.OrgId = orgId - mock.DashUID = dash.Uid - mock.DashID = dash.Id + mock.DashUID = dash.UID + mock.DashID = dash.ID mock.User = user return mock, nil } diff --git a/pkg/services/guardian/guardian_test.go b/pkg/services/guardian/guardian_test.go index 85b82e44252..126b3b165ae 100644 --- a/pkg/services/guardian/guardian_test.go +++ b/pkg/services/guardian/guardian_test.go @@ -698,12 +698,12 @@ func TestGuardianGetHiddenACL(t *testing.T) { {Inherited: true, UserId: 3, UserLogin: "user3", Permission: models.PERMISSION_VIEW}, } }).Return(nil) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - Uid: q.Uid, - OrgId: q.OrgId, + 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, + OrgID: q.OrgID, } }).Return(nil) @@ -734,11 +734,11 @@ func TestGuardianGetHiddenACL(t *testing.T) { IsGrafanaAdmin: true, } dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - Uid: q.Uid, + 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) g, err := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store, dashSvc, &teamtest.FakeService{}) @@ -769,12 +769,12 @@ func TestGuardianGetACLWithoutDuplicates(t *testing.T) { {Inherited: false, UserId: 6, UserLogin: "user6", Permission: models.PERMISSION_EDIT}, } }).Return(nil) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - Uid: q.Uid, - OrgId: q.OrgId, + 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, + OrgID: q.OrgID, } }).Return(nil) diff --git a/pkg/services/guardian/guardian_util_test.go b/pkg/services/guardian/guardian_util_test.go index 18af2889dfe..aa69dc3a5b3 100644 --- a/pkg/services/guardian/guardian_util_test.go +++ b/pkg/services/guardian/guardian_util_test.go @@ -47,11 +47,11 @@ func orgRoleScenario(desc string, t *testing.T, role org.RoleType, fn scenarioFu store := dbtest.NewFakeDB() fakeDashboardService := dashboards.NewFakeDashboardService(t) - fakeDashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - Uid: q.Uid, + fakeDashboardService.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) guard, err := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store, fakeDashboardService, &teamtest.FakeService{}) @@ -78,11 +78,11 @@ func apiKeyScenario(desc string, t *testing.T, role org.RoleType, fn scenarioFun } store := dbtest.NewFakeDB() dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - Uid: q.Uid, + 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) guard, err := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store, dashSvc, &teamtest.FakeService{}) @@ -118,12 +118,12 @@ func permissionScenario(desc string, dashboardID int64, sc *scenarioContext, q := args.Get(1).(*models.GetDashboardACLInfoListQuery) q.Result = permissions }).Return(nil) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - Uid: q.Uid, - OrgId: q.OrgId, + 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, + OrgID: q.OrgID, } }).Return(nil) diff --git a/pkg/services/guardian/provider.go b/pkg/services/guardian/provider.go index 4dc0cdc71b7..82d839db7b0 100644 --- a/pkg/services/guardian/provider.go +++ b/pkg/services/guardian/provider.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/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/team" @@ -36,7 +35,7 @@ func InitLegacyGuardian(store db.DB, dashSvc dashboards.DashboardService, teamSv return newDashboardGuardianByUID(ctx, dashUID, orgId, user, store, dashSvc, teamSvc) } - NewByDashboard = func(ctx context.Context, dash *models.Dashboard, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) { + NewByDashboard = func(ctx context.Context, dash *dashboards.Dashboard, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) { return newDashboardGuardianByDashboard(ctx, dash, orgId, user, store, dashSvc, teamSvc) } } @@ -53,7 +52,7 @@ func InitAccessControlGuardian( return NewAccessControlDashboardGuardianByUID(ctx, dashUID, user, store, ac, folderPermissionsService, dashboardPermissionsService, dashboardService) } - NewByDashboard = func(ctx context.Context, dash *models.Dashboard, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) { + NewByDashboard = func(ctx context.Context, dash *dashboards.Dashboard, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) { return NewAccessControlDashboardGuardianByDashboard(ctx, dash, user, store, ac, folderPermissionsService, dashboardPermissionsService, dashboardService) } } diff --git a/pkg/services/libraryelements/libraryelements_delete_test.go b/pkg/services/libraryelements/libraryelements_delete_test.go index 0bd640e646d..42f76f77179 100644 --- a/pkg/services/libraryelements/libraryelements_delete_test.go +++ b/pkg/services/libraryelements/libraryelements_delete_test.go @@ -5,11 +5,10 @@ import ( "testing" "github.com/grafana/grafana/pkg/components/simplejson" + "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" - - "github.com/grafana/grafana/pkg/models" ) func TestDeleteLibraryElement(t *testing.T) { @@ -69,12 +68,12 @@ func TestDeleteLibraryElement(t *testing.T) { }, }, } - dash := models.Dashboard{ + dash := dashboards.Dashboard{ Title: "Testing deleteHandler ", Data: simplejson.NewFromAny(dashJSON), } dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) - err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.Id) + err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.ID) require.NoError(t, err) sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID}) diff --git a/pkg/services/libraryelements/libraryelements_get_test.go b/pkg/services/libraryelements/libraryelements_get_test.go index a1ae8276351..ff587a20d85 100644 --- a/pkg/services/libraryelements/libraryelements_get_test.go +++ b/pkg/services/libraryelements/libraryelements_get_test.go @@ -5,6 +5,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/grafana/grafana/pkg/components/simplejson" + "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" @@ -115,12 +116,12 @@ func TestGetLibraryElement(t *testing.T) { }, }, } - dash := models.Dashboard{ + dash := dashboards.Dashboard{ Title: "Testing getHandler", Data: simplejson.NewFromAny(dashJSON), } dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) - err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.Id) + err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.ID) require.NoError(t, err) expected := func(res libraryElementResult) libraryElementResult { diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 180188d6454..1ca4a0c8304 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -72,12 +72,12 @@ func TestDeleteLibraryPanelsInFolder(t *testing.T) { }, }, } - dash := models.Dashboard{ + dash := dashboards.Dashboard{ Title: "Testing DeleteLibraryElementsInFolder", Data: simplejson.NewFromAny(dashJSON), } dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) - err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.Id) + err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.ID) require.NoError(t, err) err = sc.service.DeleteLibraryElementsInFolder(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, sc.folder.UID) @@ -145,12 +145,12 @@ func TestGetLibraryPanelConnections(t *testing.T) { }, }, } - dash := models.Dashboard{ + dash := dashboards.Dashboard{ Title: "Testing GetLibraryPanelConnections", Data: simplejson.NewFromAny(dashJSON), } dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID) - err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.Id) + err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.ID) require.NoError(t, err) var expected = func(res LibraryElementConnectionsResponse) LibraryElementConnectionsResponse { @@ -160,8 +160,8 @@ func TestGetLibraryPanelConnections(t *testing.T) { ID: sc.initialResult.Result.ID, Kind: sc.initialResult.Result.Kind, ElementID: 1, - ConnectionID: dashInDB.Id, - ConnectionUID: dashInDB.Uid, + ConnectionID: dashInDB.ID, + ConnectionUID: dashInDB.UID, Created: res.Result[0].Created, CreatedBy: LibraryElementDTOMetaUser{ ID: 1, @@ -269,12 +269,12 @@ type folderACLItem struct { permission models.PermissionType } -func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash *models.Dashboard, folderID int64) *models.Dashboard { - dash.FolderId = folderID +func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash *dashboards.Dashboard, folderID int64) *dashboards.Dashboard { + dash.FolderID = folderID dashItem := &dashboards.SaveDashboardDTO{ Dashboard: dash, Message: "", - OrgId: user.OrgID, + OrgID: user.OrgID, User: &user, Overwrite: false, } diff --git a/pkg/services/librarypanels/librarypanels.go b/pkg/services/librarypanels/librarypanels.go index 514272e156f..41209c98898 100644 --- a/pkg/services/librarypanels/librarypanels.go +++ b/pkg/services/librarypanels/librarypanels.go @@ -10,6 +10,7 @@ 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/libraryelements" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -28,7 +29,7 @@ func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.Rout // Service is a service for operating on library panels. type Service interface { - ConnectLibraryPanelsForDashboard(c context.Context, signedInUser *user.SignedInUser, dash *models.Dashboard) error + ConnectLibraryPanelsForDashboard(c context.Context, signedInUser *user.SignedInUser, dash *dashboards.Dashboard) error ImportLibraryPanelsForDashboard(c context.Context, signedInUser *user.SignedInUser, libraryPanels *simplejson.Json, panels []interface{}, folderID int64) error } @@ -47,7 +48,7 @@ type LibraryPanelService struct { } // ConnectLibraryPanelsForDashboard loops through all panels in dashboard JSON and connects any library panels to the dashboard. -func (lps *LibraryPanelService) ConnectLibraryPanelsForDashboard(c context.Context, signedInUser *user.SignedInUser, dash *models.Dashboard) error { +func (lps *LibraryPanelService) ConnectLibraryPanelsForDashboard(c context.Context, signedInUser *user.SignedInUser, dash *dashboards.Dashboard) error { panels := dash.Data.Get("panels").MustArray() libraryPanels := make(map[string]string) err := connectLibraryPanelsRecursively(c, panels, libraryPanels) @@ -60,7 +61,7 @@ func (lps *LibraryPanelService) ConnectLibraryPanelsForDashboard(c context.Conte elementUIDs = append(elementUIDs, libraryPanel) } - return lps.LibraryElementService.ConnectElementsToDashboard(c, signedInUser, elementUIDs, dash.Id) + return lps.LibraryElementService.ConnectElementsToDashboard(c, signedInUser, elementUIDs, dash.ID) } func isLibraryPanelOrRow(panel *simplejson.Json, panelType string) bool { diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 90eb5f4125a..ed1c760d64d 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -72,7 +72,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { }, }, } - dash := models.Dashboard{ + dash := dashboards.Dashboard{ Title: "Testing ConnectLibraryPanelsForDashboard", Data: simplejson.NewFromAny(dashJSON), } @@ -81,7 +81,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { err := sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB) require.NoError(t, err) - elements, err := sc.elementService.GetElementsForDashboard(sc.ctx, dashInDB.Id) + elements, err := sc.elementService.GetElementsForDashboard(sc.ctx, dashInDB.ID) require.NoError(t, err) require.Len(t, elements, 1) require.Equal(t, sc.initialResult.Result.UID, elements[sc.initialResult.Result.UID].UID) @@ -170,7 +170,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { }, }, } - dash := models.Dashboard{ + dash := dashboards.Dashboard{ Title: "Testing ConnectLibraryPanelsForDashboard", Data: simplejson.NewFromAny(dashJSON), } @@ -179,7 +179,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { err = sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB) require.NoError(t, err) - elements, err := sc.elementService.GetElementsForDashboard(sc.ctx, dashInDB.Id) + elements, err := sc.elementService.GetElementsForDashboard(sc.ctx, dashInDB.ID) require.NoError(t, err) require.Len(t, elements, 2) require.Equal(t, sc.initialResult.Result.UID, elements[sc.initialResult.Result.UID].UID) @@ -216,7 +216,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { }, }, } - dash := models.Dashboard{ + dash := dashboards.Dashboard{ Title: "Testing ConnectLibraryPanelsForDashboard", Data: simplejson.NewFromAny(dashJSON), } @@ -272,12 +272,12 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { }, } - dash := models.Dashboard{ + dash := dashboards.Dashboard{ Title: "Testing ConnectLibraryPanelsForDashboard", Data: simplejson.NewFromAny(dashJSON), } 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) + err = sc.elementService.ConnectElementsToDashboard(sc.ctx, sc.user, []string{sc.initialResult.Result.UID}, dashInDB.ID) require.NoError(t, err) panelJSON := []interface{}{ @@ -310,7 +310,7 @@ func TestConnectLibraryPanelsForDashboard(t *testing.T) { err = sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB) require.NoError(t, err) - elements, err := sc.elementService.GetElementsForDashboard(sc.ctx, dashInDB.Id) + elements, err := sc.elementService.GetElementsForDashboard(sc.ctx, dashInDB.ID) require.NoError(t, err) require.Len(t, elements, 1) require.Equal(t, sc.initialResult.Result.UID, elements[sc.initialResult.Result.UID].UID) @@ -684,12 +684,12 @@ func getExpected(t *testing.T, res libraryelements.LibraryElementDTO, UID string } } -func createDashboard(t *testing.T, sqlStore db.DB, user *user.SignedInUser, dash *models.Dashboard, folderID int64) *models.Dashboard { - dash.FolderId = folderID +func createDashboard(t *testing.T, sqlStore db.DB, user *user.SignedInUser, dash *dashboards.Dashboard, folderID int64) *dashboards.Dashboard { + dash.FolderID = folderID dashItem := &dashboards.SaveDashboardDTO{ Dashboard: dash, Message: "", - OrgId: user.OrgID, + OrgID: user.OrgID, User: user, Overwrite: false, } @@ -767,11 +767,11 @@ func scenarioWithLibraryPanel(t *testing.T, desc string, fn func(t *testing.T, s store := dbtest.NewFakeDB() dashSvc := dashboards.NewFakeDashboardService(t) - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{ - Id: q.Id, - Uid: q.Uid, + 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) guardian.InitLegacyGuardian(store, dashSvc, &teamtest.FakeService{}) diff --git a/pkg/services/live/features/dashboard.go b/pkg/services/live/features/dashboard.go index c2d81dbf3ff..2474bf23085 100644 --- a/pkg/services/live/features/dashboard.go +++ b/pkg/services/live/features/dashboard.go @@ -29,13 +29,13 @@ const ( // DashboardEvent events related to dashboards type dashboardEvent struct { - UID string `json:"uid"` - Action actionType `json:"action"` // saved, editing, deleted - User *user.UserDisplayDTO `json:"user,omitempty"` - SessionID string `json:"sessionId,omitempty"` - Message string `json:"message,omitempty"` - Dashboard *models.Dashboard `json:"dashboard,omitempty"` - Error string `json:"error,omitempty"` + UID string `json:"uid"` + Action actionType `json:"action"` // saved, editing, deleted + User *user.UserDisplayDTO `json:"user,omitempty"` + SessionID string `json:"sessionId,omitempty"` + Message string `json:"message,omitempty"` + Dashboard *dashboards.Dashboard `json:"dashboard,omitempty"` + Error string `json:"error,omitempty"` } // DashboardHandler manages all the `grafana/dashboard/*` channels @@ -66,7 +66,7 @@ func (h *DashboardHandler) OnSubscribe(ctx context.Context, user *user.SignedInU // make sure can view this dashboard if len(parts) == 2 && parts[0] == "uid" { - query := models.GetDashboardQuery{Uid: parts[1], OrgId: user.OrgID} + query := dashboards.GetDashboardQuery{UID: parts[1], OrgID: user.OrgID} if err := h.DashboardService.GetDashboard(ctx, &query); err != nil { logger.Error("Error getting dashboard", "query", query, "error", err) return models.SubscribeReply{}, backend.SubscribeStreamStatusNotFound, nil @@ -116,7 +116,7 @@ func (h *DashboardHandler) OnPublish(ctx context.Context, user *user.SignedInUse // just ignore the event return models.PublishReply{}, backend.PublishStreamStatusNotFound, fmt.Errorf("ignore???") } - query := models.GetDashboardQuery{Uid: parts[1], OrgId: user.OrgID} + query := dashboards.GetDashboardQuery{UID: parts[1], OrgID: user.OrgID} if err := h.DashboardService.GetDashboard(ctx, &query); err != nil { logger.Error("Unknown dashboard", "query", query) return models.PublishReply{}, backend.PublishStreamStatusNotFound, nil @@ -171,13 +171,13 @@ func (h *DashboardHandler) publish(orgID int64, event dashboardEvent) error { } // DashboardSaved will broadcast to all connected dashboards -func (h *DashboardHandler) DashboardSaved(orgID int64, user *user.UserDisplayDTO, message string, dashboard *models.Dashboard, err error) error { +func (h *DashboardHandler) DashboardSaved(orgID int64, user *user.UserDisplayDTO, message string, dashboard *dashboards.Dashboard, err error) error { if err != nil && !h.HasGitOpsObserver(orgID) { return nil // only broadcast if it was OK } msg := dashboardEvent{ - UID: dashboard.Uid, + UID: dashboard.UID, Action: ActionSaved, User: user, Message: message, diff --git a/pkg/services/live/live.go b/pkg/services/live/live.go index f56c35c3ad3..40020873f89 100644 --- a/pkg/services/live/live.go +++ b/pkg/services/live/live.go @@ -69,7 +69,7 @@ type CoreGrafanaScope struct { Features map[string]models.ChannelHandlerFactory // The generic service to advertise dashboard changes - Dashboards models.DashboardActivityChannel + Dashboards DashboardActivityChannel } func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, routeRegister routing.RouteRegister, @@ -437,6 +437,22 @@ type GrafanaLive struct { usageStats usageStats } +// DashboardActivityChannel is a service to advertise dashboard activity +type DashboardActivityChannel interface { + // Called when a dashboard is saved -- this includes the error so we can support a + // gitops workflow that knows if the value was saved to the local database or not + // in many cases all direct save requests will fail, but the request should be forwarded + // to any gitops observers + DashboardSaved(orgID int64, user *user.UserDisplayDTO, message string, dashboard *dashboards.Dashboard, err error) error + + // Called when a dashboard is deleted + DashboardDeleted(orgID int64, user *user.UserDisplayDTO, uid string) error + + // Experimental! Indicate is GitOps is active. This really means + // someone is subscribed to the `grafana/dashboards/gitops` channel + HasGitOpsObserver(orgID int64) bool +} + func (g *GrafanaLive) getStreamPlugin(ctx context.Context, pluginID string) (backend.StreamHandler, error) { plugin, exists := g.pluginStore.Plugin(ctx, pluginID) if !exists { diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index f77e85e1d28..d1e8f740182 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -219,10 +219,10 @@ func (s *ServiceImpl) getHomeNode(c *models.ReqContext, prefs *pref.Preference) } if prefs.HomeDashboardID != 0 { - slugQuery := models.GetDashboardRefByIdQuery{Id: prefs.HomeDashboardID} - err := s.dashboardService.GetDashboardUIDById(c.Req.Context(), &slugQuery) + slugQuery := dashboards.GetDashboardRefByIDQuery{ID: prefs.HomeDashboardID} + err := s.dashboardService.GetDashboardUIDByID(c.Req.Context(), &slugQuery) if err == nil { - homeUrl = models.GetDashboardUrl(slugQuery.Result.Uid, slugQuery.Result.Slug) + homeUrl = models.GetDashboardUrl(slugQuery.Result.UID, slugQuery.Result.Slug) } } @@ -331,7 +331,7 @@ func (s *ServiceImpl) buildStarredItemsNavLinks(c *models.ReqContext) ([]*navtre return nil, err } - starredDashboards := []*models.Dashboard{} + starredDashboards := []*dashboards.Dashboard{} starredDashboardsCounter := 0 for dashboardId := range starredDashboardResult.UserStars { // Set a loose limit to the first 50 starred dashboards found @@ -339,9 +339,9 @@ func (s *ServiceImpl) buildStarredItemsNavLinks(c *models.ReqContext) ([]*navtre break } starredDashboardsCounter++ - query := &models.GetDashboardQuery{ - Id: dashboardId, - OrgId: c.OrgID, + query := &dashboards.GetDashboardQuery{ + ID: dashboardId, + OrgID: c.OrgID, } err := s.dashboardService.GetDashboard(c.Req.Context(), query) if err == nil { @@ -355,9 +355,9 @@ func (s *ServiceImpl) buildStarredItemsNavLinks(c *models.ReqContext) ([]*navtre }) for _, starredItem := range starredDashboards { starredItemsChildNavs = append(starredItemsChildNavs, &navtree.NavLink{ - Id: "starred/" + starredItem.Uid, + Id: "starred/" + starredItem.UID, Text: starredItem.Title, - Url: starredItem.GetUrl(), + Url: starredItem.GetURL(), }) } } diff --git a/pkg/services/ngalert/state/historian/dashboard.go b/pkg/services/ngalert/state/historian/dashboard.go index e2d3213a3a2..ec311e6ff05 100644 --- a/pkg/services/ngalert/state/historian/dashboard.go +++ b/pkg/services/ngalert/state/historian/dashboard.go @@ -8,7 +8,6 @@ import ( "time" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/patrickmn/go-cache" "golang.org/x/sync/singleflight" @@ -51,9 +50,9 @@ func (r *dashboardResolver) getID(ctx context.Context, orgID int64, uid string) r.log.Debug("Dashboard cache miss, querying dashboards", "dashboardUID", uid) var result interface{} - query := &models.GetDashboardQuery{ - Uid: uid, - OrgId: orgID, + query := &dashboards.GetDashboardQuery{ + UID: uid, + OrgID: orgID, } err := r.dashboards.GetDashboard(ctx, query) // We also cache lookups where we don't find anything. @@ -64,7 +63,7 @@ func (r *dashboardResolver) getID(ctx context.Context, orgID int64, uid string) } else if query.Result == nil { result = dashboards.ErrDashboardNotFound } else { - result = query.Result.Id + result = query.Result.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 904e8929982..f8e7dba20a4 100644 --- a/pkg/services/ngalert/state/historian/dashboard_test.go +++ b/pkg/services/ngalert/state/historian/dashboard_test.go @@ -5,7 +5,6 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -16,7 +15,7 @@ func TestDashboardResolver(t *testing.T) { dbs := &dashboards.FakeDashboardService{} exp := int64(14) dbs.On("GetDashboard", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { - args.Get(1).(*models.GetDashboardQuery).Result = &models.Dashboard{Id: exp} + args.Get(1).(*dashboards.GetDashboardQuery).Result = &dashboards.Dashboard{ID: exp} }).Return(nil) sut := createDashboardResolverSut(dbs) @@ -29,7 +28,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).(*models.GetDashboardQuery).Result = nil + args.Get(1).(*dashboards.GetDashboardQuery).Result = nil }).Return(dashboards.ErrDashboardNotFound) sut := createDashboardResolverSut(dbs) diff --git a/pkg/services/provisioning/alerting/rules_provisioner.go b/pkg/services/provisioning/alerting/rules_provisioner.go index 071c549edad..5d816b325be 100644 --- a/pkg/services/provisioning/alerting/rules_provisioner.go +++ b/pkg/services/provisioning/alerting/rules_provisioner.go @@ -7,7 +7,6 @@ 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/dashboards" alert_models "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/provisioning" @@ -99,9 +98,9 @@ func (prov *defaultAlertRuleProvisioner) provisionRule( func (prov *defaultAlertRuleProvisioner) getOrCreateFolderUID( ctx context.Context, folderName string, orgID int64) (string, error) { - cmd := &models.GetDashboardQuery{ + cmd := &dashboards.GetDashboardQuery{ Slug: slugify.Slugify(folderName), - OrgId: orgID, + OrgID: orgID, } err := prov.dashboardService.GetDashboard(ctx, cmd) if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) { @@ -111,22 +110,22 @@ func (prov *defaultAlertRuleProvisioner) getOrCreateFolderUID( // dashboard folder not found. create one. if errors.Is(err, dashboards.ErrDashboardNotFound) { dash := &dashboards.SaveDashboardDTO{} - dash.Dashboard = models.NewDashboardFolder(folderName) + dash.Dashboard = dashboards.NewDashboardFolder(folderName) dash.Dashboard.IsFolder = true dash.Overwrite = true - dash.OrgId = orgID - dash.Dashboard.SetUid(util.GenerateShortUID()) + dash.OrgID = orgID + dash.Dashboard.SetUID(util.GenerateShortUID()) dbDash, err := prov.dashboardProvService.SaveFolderForProvisionedDashboards(ctx, dash) if err != nil { return "", err } - return dbDash.Uid, nil + return dbDash.UID, nil } if !cmd.Result.IsFolder { return "", fmt.Errorf("got invalid response. expected folder, found dashboard") } - return cmd.Result.Uid, nil + return cmd.Result.UID, nil } diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index d87b4407e7c..3f1772c84c6 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -244,26 +244,26 @@ func (fr *FileReader) saveDashboard(ctx context.Context, path string, folderID i // keeps track of which UIDs and titles we have already provisioned dash := jsonFile.dashboard - provisioningMetadata.uid = dash.Dashboard.Uid - provisioningMetadata.identity = dashboardIdentity{title: dash.Dashboard.Title, folderID: dash.Dashboard.FolderId} + provisioningMetadata.uid = dash.Dashboard.UID + provisioningMetadata.identity = dashboardIdentity{title: dash.Dashboard.Title, folderID: dash.Dashboard.FolderID} if upToDate { return provisioningMetadata, nil } - if dash.Dashboard.Id != 0 { + if dash.Dashboard.ID != 0 { dash.Dashboard.Data.Set("id", nil) - dash.Dashboard.Id = 0 + dash.Dashboard.ID = 0 } if alreadyProvisioned { - dash.Dashboard.SetId(provisionedData.DashboardId) + dash.Dashboard.SetID(provisionedData.DashboardId) } if !fr.isDatabaseAccessRestricted() { - fr.log.Debug("saving new dashboard", "provisioner", fr.Cfg.Name, "file", path, "folderId", dash.Dashboard.FolderId) - dp := &models.DashboardProvisioning{ - ExternalId: path, + fr.log.Debug("saving new dashboard", "provisioner", fr.Cfg.Name, "file", path, "folderId", dash.Dashboard.FolderID) + dp := &dashboards.DashboardProvisioning{ + ExternalID: path, Name: fr.Cfg.Name, Updated: resolvedFileInfo.ModTime().Unix(), CheckSum: jsonFile.checkSum, @@ -274,7 +274,7 @@ func (fr *FileReader) saveDashboard(ctx context.Context, path string, folderID i } } else { fr.log.Warn("Not saving new dashboard due to restricted database access", "provisioner", fr.Cfg.Name, - "file", path, "folderId", dash.Dashboard.FolderId) + "file", path, "folderId", dash.Dashboard.FolderID) } return provisioningMetadata, nil @@ -300,7 +300,7 @@ func (fr *FileReader) getOrCreateFolderID(ctx context.Context, cfg *config, serv return 0, ErrFolderNameMissing } - cmd := &models.GetDashboardQuery{Slug: slugify.Slugify(folderName), OrgId: cfg.OrgID} + cmd := &dashboards.GetDashboardQuery{Slug: slugify.Slugify(folderName), OrgID: cfg.OrgID} err := fr.dashboardStore.GetDashboard(ctx, cmd) if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) { @@ -310,28 +310,28 @@ func (fr *FileReader) getOrCreateFolderID(ctx context.Context, cfg *config, serv // dashboard folder not found. create one. if errors.Is(err, dashboards.ErrDashboardNotFound) { dash := &dashboards.SaveDashboardDTO{} - dash.Dashboard = models.NewDashboardFolder(folderName) + dash.Dashboard = dashboards.NewDashboardFolder(folderName) dash.Dashboard.IsFolder = true dash.Overwrite = true - dash.OrgId = cfg.OrgID + dash.OrgID = cfg.OrgID // set dashboard folderUid if given if cfg.FolderUID == accesscontrol.GeneralFolderUID { return 0, dashboards.ErrFolderInvalidUID } - dash.Dashboard.SetUid(cfg.FolderUID) + dash.Dashboard.SetUID(cfg.FolderUID) dbDash, err := service.SaveFolderForProvisionedDashboards(ctx, dash) if err != nil { return 0, err } - return dbDash.Id, nil + return dbDash.ID, nil } if !cmd.Result.IsFolder { return 0, fmt.Errorf("got invalid response. expected folder, found dashboard") } - return cmd.Result.Id, nil + return cmd.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 d29015b88bd..463bf88ea47 100644 --- a/pkg/services/provisioning/dashboards/file_reader_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_test.go @@ -114,8 +114,8 @@ func TestDashboardFileReader(t *testing.T) { cfg.Folder = "Team A" fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(nil, nil).Once() - fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{Id: 1}, nil).Once() - fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{Id: 2}, nil).Times(2) + fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{ID: 1}, nil).Once() + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{ID: 2}, nil).Times(2) reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) reader.dashboardProvisioningService = fakeService @@ -132,7 +132,7 @@ func TestDashboardFileReader(t *testing.T) { inserted := 0 fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(nil, nil).Once() fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything). - Return(&models.Dashboard{}, nil).Once(). + Return(&dashboards.Dashboard{}, nil).Once(). Run(func(args mock.Arguments) { inserted++ }) @@ -200,7 +200,7 @@ func TestDashboardFileReader(t *testing.T) { } fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(provisionedDashboard, nil).Once() - fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once() + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Once() reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) reader.dashboardProvisioningService = fakeService @@ -263,7 +263,7 @@ func TestDashboardFileReader(t *testing.T) { } fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(provisionedDashboard, nil).Once() - fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once() + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Once() reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) reader.dashboardProvisioningService = fakeService @@ -278,7 +278,7 @@ func TestDashboardFileReader(t *testing.T) { cfg.Options["path"] = containingID fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(nil, nil).Once() - fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once() + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Once() reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) reader.dashboardProvisioningService = fakeService @@ -294,8 +294,8 @@ func TestDashboardFileReader(t *testing.T) { cfg.Options["foldersFromFilesStructure"] = true fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(nil, nil).Once() - fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(2) - fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(3) + fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(2) + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(3) reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) reader.dashboardProvisioningService = fakeService @@ -332,8 +332,8 @@ func TestDashboardFileReader(t *testing.T) { cfg2 := &config{Name: "2", Type: "file", OrgID: 1, Folder: "f2", Options: map[string]interface{}{"path": containingID}} fakeService.On("GetProvisionedDashboardData", mock.Anything, mock.AnythingOfType("string")).Return(nil, nil).Times(2) - fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(2) - fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(2) + fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(2) + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(2) reader1, err := NewDashboardFileReader(cfg1, logger, nil, fakeStore) reader1.dashboardProvisioningService = fakeService @@ -380,7 +380,7 @@ func TestDashboardFileReader(t *testing.T) { "folder": defaultDashboards, }, } - fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{Id: 1}, nil).Once() + fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{ID: 1}, nil).Once() r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) require.NoError(t, err) @@ -453,7 +453,7 @@ func TestDashboardFileReader(t *testing.T) { fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(provisionedDashboard, nil).Once() fakeService.On("UnprovisionDashboard", mock.Anything, mock.Anything).Return(nil).Once() - fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once() + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Once() cfg.DisableDeletion = true @@ -469,7 +469,7 @@ func TestDashboardFileReader(t *testing.T) { setupFakeService() fakeService.On("GetProvisionedDashboardData", mock.Anything, configName).Return(provisionedDashboard, nil).Once() - fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Once() + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Once() fakeService.On("DeleteProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() reader, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) @@ -513,6 +513,6 @@ func (ffi FakeFileInfo) Sys() interface{} { type fakeDashboardStore struct{} -func (fds *fakeDashboardStore) GetDashboard(_ context.Context, _ *models.GetDashboardQuery) error { +func (fds *fakeDashboardStore) GetDashboard(_ context.Context, _ *dashboards.GetDashboardQuery) error { return dashboards.ErrDashboardNotFound } diff --git a/pkg/services/provisioning/dashboards/types.go b/pkg/services/provisioning/dashboards/types.go index 8a4c200d5f8..8e259379b2a 100644 --- a/pkg/services/provisioning/dashboards/types.go +++ b/pkg/services/provisioning/dashboards/types.go @@ -5,7 +5,6 @@ import ( "time" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/provisioning/values" ) @@ -59,12 +58,12 @@ type configs struct { func createDashboardJSON(data *simplejson.Json, lastModified time.Time, cfg *config, folderID int64) (*dashboards.SaveDashboardDTO, error) { dash := &dashboards.SaveDashboardDTO{} - dash.Dashboard = models.NewDashboardFromJson(data) + dash.Dashboard = dashboards.NewDashboardFromJson(data) dash.UpdatedAt = lastModified dash.Overwrite = true - dash.OrgId = cfg.OrgID - dash.Dashboard.OrgId = cfg.OrgID - dash.Dashboard.FolderId = folderID + dash.OrgID = cfg.OrgID + dash.Dashboard.OrgID = cfg.OrgID + dash.Dashboard.FolderID = folderID if dash.Dashboard.Title == "" { return nil, dashboards.ErrDashboardTitleEmpty diff --git a/pkg/services/provisioning/dashboards/validator_test.go b/pkg/services/provisioning/dashboards/validator_test.go index 380bb1db995..0c8243a7140 100644 --- a/pkg/services/provisioning/dashboards/validator_test.go +++ b/pkg/services/provisioning/dashboards/validator_test.go @@ -37,9 +37,9 @@ func TestDuplicatesValidator(t *testing.T) { fakeStore := &fakeDashboardStore{} r, err := NewDashboardFileReader(cfg, logger, nil, fakeStore) require.NoError(t, err) - fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(6) + fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(6) fakeService.On("GetProvisionedDashboardData", mock.Anything, mock.AnythingOfType("string")).Return([]*models.DashboardProvisioning{}, nil).Times(4) - fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(5) + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(5) folderID, err := r.getOrCreateFolderID(context.Background(), cfg, fakeService, folderName) require.NoError(t, err) @@ -151,9 +151,9 @@ func TestDuplicatesValidator(t *testing.T) { }) t.Run("Duplicates validator should restrict write access only for readers with duplicates", func(t *testing.T) { - fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(5) + fakeService.On("SaveFolderForProvisionedDashboards", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(5) fakeService.On("GetProvisionedDashboardData", mock.Anything, mock.AnythingOfType("string")).Return([]*models.DashboardProvisioning{}, nil).Times(3) - fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil).Times(5) + fakeService.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil).Times(5) fakeStore := &fakeDashboardStore{} cfg1 := &config{ diff --git a/pkg/services/provisioning/utils/utils.go b/pkg/services/provisioning/utils/utils.go index a1054436ea9..40153769f6f 100644 --- a/pkg/services/provisioning/utils/utils.go +++ b/pkg/services/provisioning/utils/utils.go @@ -5,12 +5,12 @@ import ( "errors" "fmt" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" ) type DashboardStore interface { - GetDashboard(context.Context, *models.GetDashboardQuery) error + GetDashboard(context.Context, *dashboards.GetDashboardQuery) error } func CheckOrgExists(ctx context.Context, orgService org.Service, orgID int64) error { diff --git a/pkg/services/publicdashboards/api/query.go b/pkg/services/publicdashboards/api/query.go index a1bf190112f..233e067604b 100644 --- a/pkg/services/publicdashboards/api/query.go +++ b/pkg/services/publicdashboards/api/query.go @@ -40,7 +40,7 @@ func (api *Api) ViewPublicDashboard(c *models.ReqContext) response.Response { Updated: dash.Updated, Version: dash.Version, IsFolder: false, - FolderId: dash.FolderId, + FolderId: dash.FolderID, PublicDashboardAccessToken: pubdash.AccessToken, } dash.Data.Get("timepicker").Set("hidden", !pubdash.TimeSelectionEnabled) diff --git a/pkg/services/publicdashboards/api/query_test.go b/pkg/services/publicdashboards/api/query_test.go index 44b9466920b..b69e7a91918 100644 --- a/pkg/services/publicdashboards/api/query_test.go +++ b/pkg/services/publicdashboards/api/query_test.go @@ -17,9 +17,9 @@ 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" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/annotations/annotationstest" + "github.com/grafana/grafana/pkg/services/dashboards" dashboardStore "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/datasources" datasourcesService "github.com/grafana/grafana/pkg/services/datasources/service" @@ -46,7 +46,7 @@ func TestAPIViewPublicDashboard(t *testing.T) { Name string AccessToken string ExpectedHttpResponse int - DashboardResult *models.Dashboard + DashboardResult *dashboards.Dashboard Err error FixedErrorResponse string }{ @@ -54,7 +54,7 @@ func TestAPIViewPublicDashboard(t *testing.T) { Name: "It gets a public dashboard", AccessToken: validAccessToken, ExpectedHttpResponse: http.StatusOK, - DashboardResult: &models.Dashboard{ + DashboardResult: &dashboards.Dashboard{ Data: simplejson.NewFromAny(map[string]interface{}{ "Uid": DashboardUid, }), @@ -279,9 +279,9 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) }) // Create Dashboard - saveDashboardCmd := models.SaveDashboardCommand{ - OrgId: 1, - FolderId: 1, + saveDashboardCmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + FolderID: 1, IsFolder: false, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, @@ -311,8 +311,8 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) // Create public dashboard savePubDashboardCmd := &SavePublicDashboardDTO{ - DashboardUid: dashboard.Uid, - OrgId: dashboard.OrgId, + DashboardUid: dashboard.UID, + OrgId: dashboard.OrgID, PublicDashboard: &PublicDashboard{ IsEnabled: true, }, diff --git a/pkg/services/publicdashboards/database/database.go b/pkg/services/publicdashboards/database/database.go index b0f4ade154f..681dbea3cc2 100644 --- a/pkg/services/publicdashboards/database/database.go +++ b/pkg/services/publicdashboards/database/database.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/publicdashboards" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" @@ -56,8 +55,8 @@ func (d *PublicDashboardStoreImpl) FindAll(ctx context.Context, orgId int64) ([] } // FindDashboard returns a dashboard by orgId and dashboardUid -func (d *PublicDashboardStoreImpl) FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*models.Dashboard, error) { - dashboard := &models.Dashboard{OrgId: orgId, Uid: dashboardUid} +func (d *PublicDashboardStoreImpl) FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*dashboards.Dashboard, error) { + dashboard := &dashboards.Dashboard{OrgID: orgId, UID: dashboardUid} var found bool err := d.sqlStore.WithDbSession(ctx, func(sess *db.Session) error { diff --git a/pkg/services/publicdashboards/database/database_test.go b/pkg/services/publicdashboards/database/database_test.go index 37526c5fdc8..ba4a6eab5ad 100644 --- a/pkg/services/publicdashboards/database/database_test.go +++ b/pkg/services/publicdashboards/database/database_test.go @@ -7,7 +7,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" dashboardsDB "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -48,9 +47,9 @@ func TestIntegrationListPublicDashboard(t *testing.T) { cDash := insertTestDashboard(t, dashboardStore, "c", orgId, 0, true) // these are in order of how they should be returned from ListPUblicDashboards - a := insertPublicDashboard(t, publicdashboardStore, bDash.Uid, orgId, true) - b := insertPublicDashboard(t, publicdashboardStore, cDash.Uid, orgId, true) - c := insertPublicDashboard(t, publicdashboardStore, aDash.Uid, orgId, false) + a := insertPublicDashboard(t, publicdashboardStore, bDash.UID, orgId, true) + b := insertPublicDashboard(t, publicdashboardStore, cDash.UID, orgId, true) + c := insertPublicDashboard(t, publicdashboardStore, aDash.UID, orgId, false) // this is case that can happen as of now, however, postgres and mysql sort // null in the exact opposite fashion and there is no shared syntax to sort @@ -77,7 +76,7 @@ func TestIntegrationFindDashboard(t *testing.T) { var cfg *setting.Cfg var dashboardStore *dashboardsDB.DashboardStore var publicdashboardStore *PublicDashboardStoreImpl - var savedDashboard *models.Dashboard + var savedDashboard *dashboards.Dashboard setup := func() { sqlStore, cfg = db.InitTestDBwithCfg(t) @@ -92,10 +91,10 @@ func TestIntegrationFindDashboard(t *testing.T) { t.Run("FindDashboard can get original dashboard by uid", func(t *testing.T) { setup() - dashboard, err := publicdashboardStore.FindDashboard(context.Background(), savedDashboard.OrgId, savedDashboard.Uid) + dashboard, err := publicdashboardStore.FindDashboard(context.Background(), savedDashboard.OrgID, savedDashboard.UID) require.NoError(t, err) - require.Equal(t, savedDashboard.Uid, dashboard.Uid) + require.Equal(t, savedDashboard.UID, dashboard.UID) }) } @@ -107,7 +106,7 @@ func TestIntegrationExistsEnabledByAccessToken(t *testing.T) { var cfg *setting.Cfg var dashboardStore *dashboardsDB.DashboardStore var publicdashboardStore *PublicDashboardStoreImpl - var savedDashboard *models.Dashboard + var savedDashboard *dashboards.Dashboard setup := func() { sqlStore, cfg = db.InitTestDBwithCfg(t) @@ -125,8 +124,8 @@ func TestIntegrationExistsEnabledByAccessToken(t *testing.T) { PublicDashboard: PublicDashboard{ IsEnabled: true, Uid: "abc123", - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, + DashboardUid: savedDashboard.UID, + OrgId: savedDashboard.OrgID, CreatedAt: time.Now(), CreatedBy: 7, AccessToken: "accessToken", @@ -147,8 +146,8 @@ func TestIntegrationExistsEnabledByAccessToken(t *testing.T) { PublicDashboard: PublicDashboard{ IsEnabled: false, Uid: "abc123", - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, + DashboardUid: savedDashboard.UID, + OrgId: savedDashboard.OrgID, CreatedAt: time.Now(), CreatedBy: 7, AccessToken: "accessToken", @@ -180,7 +179,7 @@ func TestIntegrationExistsEnabledByDashboardUid(t *testing.T) { var cfg *setting.Cfg var dashboardStore *dashboardsDB.DashboardStore var publicdashboardStore *PublicDashboardStoreImpl - var savedDashboard *models.Dashboard + var savedDashboard *dashboards.Dashboard setup := func() { sqlStore, cfg = db.InitTestDBwithCfg(t) @@ -199,8 +198,8 @@ func TestIntegrationExistsEnabledByDashboardUid(t *testing.T) { PublicDashboard: PublicDashboard{ IsEnabled: true, Uid: "abc123", - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, + DashboardUid: savedDashboard.UID, + OrgId: savedDashboard.OrgID, CreatedAt: time.Now(), CreatedBy: 7, AccessToken: "NOTAREALUUID", @@ -208,7 +207,7 @@ func TestIntegrationExistsEnabledByDashboardUid(t *testing.T) { }) require.NoError(t, err) - res, err := publicdashboardStore.ExistsEnabledByDashboardUid(context.Background(), savedDashboard.Uid) + res, err := publicdashboardStore.ExistsEnabledByDashboardUid(context.Background(), savedDashboard.UID) require.NoError(t, err) require.True(t, res) @@ -221,8 +220,8 @@ func TestIntegrationExistsEnabledByDashboardUid(t *testing.T) { PublicDashboard: PublicDashboard{ IsEnabled: false, Uid: "abc123", - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, + DashboardUid: savedDashboard.UID, + OrgId: savedDashboard.OrgID, CreatedAt: time.Now(), CreatedBy: 7, AccessToken: "NOTAREALUUID", @@ -230,7 +229,7 @@ func TestIntegrationExistsEnabledByDashboardUid(t *testing.T) { }) require.NoError(t, err) - res, err := publicdashboardStore.ExistsEnabledByDashboardUid(context.Background(), savedDashboard.Uid) + res, err := publicdashboardStore.ExistsEnabledByDashboardUid(context.Background(), savedDashboard.UID) require.NoError(t, err) require.False(t, res) @@ -245,7 +244,7 @@ func TestIntegrationFindByDashboardUid(t *testing.T) { var cfg *setting.Cfg var dashboardStore *dashboardsDB.DashboardStore var publicdashboardStore *PublicDashboardStoreImpl - var savedDashboard *models.Dashboard + var savedDashboard *dashboards.Dashboard setup := func() { sqlStore, cfg = db.InitTestDBwithCfg(t) @@ -259,15 +258,15 @@ func TestIntegrationFindByDashboardUid(t *testing.T) { t.Run("returns public dashboard by dashboardUid", func(t *testing.T) { setup() - savedPubdash := insertPublicDashboard(t, publicdashboardStore, savedDashboard.Uid, savedDashboard.OrgId, false) - pubdash, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard.OrgId, savedDashboard.Uid) + savedPubdash := insertPublicDashboard(t, publicdashboardStore, savedDashboard.UID, savedDashboard.OrgID, false) + pubdash, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard.OrgID, savedDashboard.UID) require.NoError(t, err) assert.Equal(t, savedPubdash, pubdash) }) t.Run("returns nil when identifier is not set", func(t *testing.T) { setup() - pubdash, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard.OrgId, "") + pubdash, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard.OrgID, "") assert.Nil(t, err) assert.Nil(t, pubdash) }) @@ -278,8 +277,8 @@ func TestIntegrationFindByDashboardUid(t *testing.T) { PublicDashboard: PublicDashboard{ IsEnabled: true, Uid: "pubdash-uid", - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, + DashboardUid: savedDashboard.UID, + OrgId: savedDashboard.OrgID, TimeSettings: DefaultTimeSettings, CreatedAt: DefaultTime, CreatedBy: 7, @@ -291,7 +290,7 @@ func TestIntegrationFindByDashboardUid(t *testing.T) { require.NoError(t, err) // retrieve from db - pubdash, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard.OrgId, savedDashboard.Uid) + pubdash, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard.OrgID, savedDashboard.UID) require.NoError(t, err) assert.True(t, assert.ObjectsAreEqualValues(&cmd.PublicDashboard, pubdash)) @@ -313,7 +312,7 @@ func TestIntegrationFindByAccessToken(t *testing.T) { var cfg *setting.Cfg var dashboardStore *dashboardsDB.DashboardStore var publicdashboardStore *PublicDashboardStoreImpl - var savedDashboard *models.Dashboard + var savedDashboard *dashboards.Dashboard var err error setup := func() { @@ -326,7 +325,7 @@ func TestIntegrationFindByAccessToken(t *testing.T) { t.Run("returns public dashboard by accessToken", func(t *testing.T) { setup() - savedPubdash := insertPublicDashboard(t, publicdashboardStore, savedDashboard.Uid, savedDashboard.OrgId, false) + savedPubdash := insertPublicDashboard(t, publicdashboardStore, savedDashboard.UID, savedDashboard.OrgID, false) pubdash, err := publicdashboardStore.FindByAccessToken(context.Background(), savedPubdash.AccessToken) require.NoError(t, err) assert.Equal(t, savedPubdash, pubdash) @@ -345,8 +344,8 @@ func TestIntegrationFindByAccessToken(t *testing.T) { PublicDashboard: PublicDashboard{ IsEnabled: true, Uid: "pubdash-uid", - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, + DashboardUid: savedDashboard.UID, + OrgId: savedDashboard.OrgID, TimeSettings: DefaultTimeSettings, CreatedAt: DefaultTime, CreatedBy: 7, @@ -381,8 +380,8 @@ func TestIntegrationCreatePublicDashboard(t *testing.T) { var cfg *setting.Cfg var dashboardStore *dashboardsDB.DashboardStore var publicdashboardStore *PublicDashboardStoreImpl - var savedDashboard *models.Dashboard - var savedDashboard2 *models.Dashboard + var savedDashboard *dashboards.Dashboard + var savedDashboard2 *dashboards.Dashboard setup := func() { sqlStore, cfg = db.InitTestDBwithCfg(t, db.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagPublicDashboards}}) @@ -393,7 +392,7 @@ func TestIntegrationCreatePublicDashboard(t *testing.T) { publicdashboardStore = ProvideStore(sqlStore) savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) savedDashboard2 = insertTestDashboard(t, dashboardStore, "testDashie2", 1, 0, true) - insertPublicDashboard(t, publicdashboardStore, savedDashboard2.Uid, savedDashboard2.OrgId, false) + insertPublicDashboard(t, publicdashboardStore, savedDashboard2.UID, savedDashboard2.OrgID, false) } t.Run("saves new public dashboard", func(t *testing.T) { @@ -404,8 +403,8 @@ func TestIntegrationCreatePublicDashboard(t *testing.T) { AnnotationsEnabled: true, TimeSelectionEnabled: true, Uid: "pubdash-uid", - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, + DashboardUid: savedDashboard.UID, + OrgId: savedDashboard.OrgID, TimeSettings: DefaultTimeSettings, CreatedAt: DefaultTime, CreatedBy: 7, @@ -416,7 +415,7 @@ func TestIntegrationCreatePublicDashboard(t *testing.T) { require.NoError(t, err) assert.EqualValues(t, affectedRows, 1) - pubdash, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard.OrgId, savedDashboard.Uid) + pubdash, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard.OrgID, savedDashboard.UID) require.NoError(t, err) assert.Equal(t, pubdash.AccessToken, "NOTAREALUUID") assert.True(t, pubdash.IsEnabled) @@ -424,7 +423,7 @@ func TestIntegrationCreatePublicDashboard(t *testing.T) { assert.True(t, pubdash.TimeSelectionEnabled) // verify we didn't update all dashboards - pubdash2, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard2.OrgId, savedDashboard2.Uid) + pubdash2, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard2.OrgID, savedDashboard2.UID) require.NoError(t, err) assert.False(t, pubdash2.IsEnabled) }) @@ -436,7 +435,7 @@ func TestIntegrationCreatePublicDashboard(t *testing.T) { IsEnabled: true, Uid: "pubdash-uid", DashboardUid: "", - OrgId: savedDashboard.OrgId, + OrgId: savedDashboard.OrgID, TimeSettings: DefaultTimeSettings, CreatedAt: DefaultTime, CreatedBy: 7, @@ -458,8 +457,8 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) { var cfg *setting.Cfg var dashboardStore *dashboardsDB.DashboardStore var publicdashboardStore *PublicDashboardStoreImpl - var savedDashboard *models.Dashboard - var anotherSavedDashboard *models.Dashboard + var savedDashboard *dashboards.Dashboard + var anotherSavedDashboard *dashboards.Dashboard var err error setup := func() { @@ -479,8 +478,8 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) { cmd := SavePublicDashboardCommand{ PublicDashboard: PublicDashboard{ Uid: pdUid, - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, + DashboardUid: savedDashboard.UID, + OrgId: savedDashboard.OrgID, IsEnabled: false, AnnotationsEnabled: true, TimeSelectionEnabled: true, @@ -498,8 +497,8 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) { cmd = SavePublicDashboardCommand{ PublicDashboard: PublicDashboard{ Uid: anotherPdUid, - DashboardUid: anotherSavedDashboard.Uid, - OrgId: anotherSavedDashboard.OrgId, + DashboardUid: anotherSavedDashboard.UID, + OrgId: anotherSavedDashboard.OrgID, IsEnabled: true, AnnotationsEnabled: false, TimeSelectionEnabled: false, @@ -515,8 +514,8 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) { updatedPublicDashboard := PublicDashboard{ Uid: pdUid, - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, + DashboardUid: savedDashboard.UID, + OrgId: savedDashboard.OrgID, IsEnabled: false, AnnotationsEnabled: true, TimeSelectionEnabled: true, @@ -532,7 +531,7 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) { assert.EqualValues(t, rowsAffected, 1) // updated dashboard should have changed - pdRetrieved, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard.OrgId, savedDashboard.Uid) + pdRetrieved, err := publicdashboardStore.FindByDashboardUid(context.Background(), savedDashboard.OrgID, savedDashboard.UID) require.NoError(t, err) assert.Equal(t, updatedPublicDashboard.UpdatedAt, pdRetrieved.UpdatedAt) @@ -543,7 +542,7 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) { assert.Equal(t, updatedPublicDashboard.TimeSelectionEnabled, pdRetrieved.TimeSelectionEnabled) // not updated dashboard shouldn't have changed - pdNotUpdatedRetrieved, err := publicdashboardStore.FindByDashboardUid(context.Background(), anotherSavedDashboard.OrgId, anotherSavedDashboard.Uid) + pdNotUpdatedRetrieved, err := publicdashboardStore.FindByDashboardUid(context.Background(), anotherSavedDashboard.OrgID, anotherSavedDashboard.UID) require.NoError(t, err) assert.NotEqual(t, updatedPublicDashboard.UpdatedAt, pdNotUpdatedRetrieved.UpdatedAt) assert.NotEqual(t, updatedPublicDashboard.IsEnabled, pdNotUpdatedRetrieved.IsEnabled) @@ -559,7 +558,7 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) { var cfg *setting.Cfg var dashboardStore *dashboardsDB.DashboardStore var publicdashboardStore *PublicDashboardStoreImpl - var savedDashboard *models.Dashboard + var savedDashboard *dashboards.Dashboard var err error setup := func() { @@ -576,8 +575,8 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) { PublicDashboard: PublicDashboard{ IsEnabled: true, Uid: "abc123", - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, + DashboardUid: savedDashboard.UID, + OrgId: savedDashboard.OrgID, CreatedAt: time.Now(), CreatedBy: 7, AccessToken: "accessToken", @@ -589,7 +588,7 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) { orgId, err := publicdashboardStore.GetOrgIdByAccessToken(context.Background(), "accessToken") require.NoError(t, err) - assert.Equal(t, savedDashboard.OrgId, orgId) + assert.Equal(t, savedDashboard.OrgID, orgId) }) t.Run("GetOrgIdByAccessToken will return 0 when IsEnabled=false", func(t *testing.T) { @@ -598,8 +597,8 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) { PublicDashboard: PublicDashboard{ IsEnabled: false, Uid: "abc123", - DashboardUid: savedDashboard.Uid, - OrgId: savedDashboard.OrgId, + DashboardUid: savedDashboard.UID, + OrgId: savedDashboard.OrgID, CreatedAt: time.Now(), CreatedBy: 7, AccessToken: "accessToken", @@ -611,7 +610,7 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) { orgId, err := publicdashboardStore.GetOrgIdByAccessToken(context.Background(), "accessToken") require.NoError(t, err) - assert.NotEqual(t, savedDashboard.OrgId, orgId) + assert.NotEqual(t, savedDashboard.OrgID, orgId) }) t.Run("GetOrgIdByAccessToken will return 0 when no public dashboard has matching access token", func(t *testing.T) { @@ -619,7 +618,7 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) { orgId, err := publicdashboardStore.GetOrgIdByAccessToken(context.Background(), "nonExistentAccessToken") require.NoError(t, err) - assert.NotEqual(t, savedDashboard.OrgId, orgId) + assert.NotEqual(t, savedDashboard.OrgID, orgId) }) } @@ -631,7 +630,7 @@ func TestIntegrationDelete(t *testing.T) { var cfg *setting.Cfg var dashboardStore *dashboardsDB.DashboardStore var publicdashboardStore *PublicDashboardStoreImpl - var savedDashboard *models.Dashboard + var savedDashboard *dashboards.Dashboard var savedPublicDashboard *PublicDashboard var err error @@ -641,7 +640,7 @@ func TestIntegrationDelete(t *testing.T) { require.NoError(t, err) publicdashboardStore = ProvideStore(sqlStore) savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true) - savedPublicDashboard = insertPublicDashboard(t, publicdashboardStore, savedDashboard.Uid, savedDashboard.OrgId, true) + savedPublicDashboard = insertPublicDashboard(t, publicdashboardStore, savedDashboard.UID, savedDashboard.OrgID, true) } t.Run("Delete success", func(t *testing.T) { @@ -668,11 +667,11 @@ func TestIntegrationDelete(t *testing.T) { // helper function to insert a dashboard func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardStore, title string, orgId int64, - folderId int64, isFolder bool, tags ...interface{}) *models.Dashboard { + folderId int64, isFolder bool, tags ...interface{}) *dashboards.Dashboard { t.Helper() - cmd := models.SaveDashboardCommand{ - OrgId: orgId, - FolderId: folderId, + cmd := dashboards.SaveDashboardCommand{ + OrgID: orgId, + FolderID: folderId, IsFolder: isFolder, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, @@ -683,8 +682,8 @@ func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardSto dash, err := dashboardStore.SaveDashboard(context.Background(), cmd) require.NoError(t, err) require.NotNil(t, dash) - dash.Data.Set("id", dash.Id) - dash.Data.Set("uid", dash.Uid) + dash.Data.Set("id", dash.ID) + dash.Data.Set("uid", dash.UID) return dash } diff --git a/pkg/services/publicdashboards/models/models.go b/pkg/services/publicdashboards/models/models.go index 1de844522d5..8274e6c8551 100644 --- a/pkg/services/publicdashboards/models/models.go +++ b/pkg/services/publicdashboards/models/models.go @@ -6,7 +6,7 @@ import ( "time" "github.com/grafana/grafana/pkg/kinds/dashboard" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/tsdb/legacydata" ) @@ -95,7 +95,7 @@ func (ts *TimeSettings) ToDB() ([]byte, error) { } // BuildTimeSettings build time settings object using selected values if enabled and are valid or dashboard default values -func (pd PublicDashboard) BuildTimeSettings(dashboard *models.Dashboard, reqDTO PublicDashboardQueryDTO) TimeSettings { +func (pd PublicDashboard) BuildTimeSettings(dashboard *dashboards.Dashboard, reqDTO PublicDashboardQueryDTO) TimeSettings { from := dashboard.Data.GetPath("time", "from").MustString() to := dashboard.Data.GetPath("time", "to").MustString() diff --git a/pkg/services/publicdashboards/models/models_test.go b/pkg/services/publicdashboards/models/models_test.go index c7fc470e084..fe60b4aa9c0 100644 --- a/pkg/services/publicdashboards/models/models_test.go +++ b/pkg/services/publicdashboards/models/models_test.go @@ -6,7 +6,7 @@ import ( "time" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/publicdashboards/internal" "github.com/stretchr/testify/assert" ) @@ -24,14 +24,14 @@ func TestBuildTimeSettings(t *testing.T) { testCases := []struct { name string - dashboard *models.Dashboard + dashboard *dashboards.Dashboard pubdash *PublicDashboard timeResult TimeSettings reqDTO PublicDashboardQueryDTO }{ { name: "should use dashboard time if pubdash time empty", - dashboard: &models.Dashboard{Data: dashboardData}, + dashboard: &dashboards.Dashboard{Data: dashboardData}, pubdash: &PublicDashboard{TimeSelectionEnabled: false}, timeResult: TimeSettings{ From: defaultFromMs, @@ -41,7 +41,7 @@ func TestBuildTimeSettings(t *testing.T) { }, { name: "should use dashboard time when time selection is disabled", - dashboard: &models.Dashboard{Data: dashboardData}, + dashboard: &dashboards.Dashboard{Data: dashboardData}, pubdash: &PublicDashboard{TimeSelectionEnabled: false, TimeSettings: &TimeSettings{From: "now-12", To: "now"}}, timeResult: TimeSettings{ From: defaultFromMs, @@ -51,7 +51,7 @@ func TestBuildTimeSettings(t *testing.T) { }, { name: "should use selected values if time selection is enabled", - dashboard: &models.Dashboard{Data: dashboardData}, + dashboard: &dashboards.Dashboard{Data: dashboardData}, pubdash: &PublicDashboard{TimeSelectionEnabled: true, TimeSettings: &TimeSettings{From: "now-12", To: "now"}}, reqDTO: PublicDashboardQueryDTO{ TimeRange: TimeSettings{ diff --git a/pkg/services/publicdashboards/public_dashboard_service_mock.go b/pkg/services/publicdashboards/public_dashboard_service_mock.go index a8544cc2b0e..883c51e1b80 100644 --- a/pkg/services/publicdashboards/public_dashboard_service_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_service_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.16.0. DO NOT EDIT. package publicdashboards @@ -7,14 +7,14 @@ import ( backend "github.com/grafana/grafana-plugin-sdk-go/backend" + dashboards "github.com/grafana/grafana/pkg/services/dashboards" + dtos "github.com/grafana/grafana/pkg/api/dtos" mock "github.com/stretchr/testify/mock" models "github.com/grafana/grafana/pkg/services/publicdashboards/models" - pkgmodels "github.com/grafana/grafana/pkg/models" - user "github.com/grafana/grafana/pkg/services/user" ) @@ -195,15 +195,15 @@ func (_m *FakePublicDashboardService) FindByDashboardUid(ctx context.Context, or } // FindDashboard provides a mock function with given fields: ctx, orgId, dashboardUid -func (_m *FakePublicDashboardService) FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*pkgmodels.Dashboard, error) { +func (_m *FakePublicDashboardService) FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*dashboards.Dashboard, error) { ret := _m.Called(ctx, orgId, dashboardUid) - var r0 *pkgmodels.Dashboard - if rf, ok := ret.Get(0).(func(context.Context, int64, string) *pkgmodels.Dashboard); ok { + var r0 *dashboards.Dashboard + if rf, ok := ret.Get(0).(func(context.Context, int64, string) *dashboards.Dashboard); ok { r0 = rf(ctx, orgId, dashboardUid) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*pkgmodels.Dashboard) + r0 = ret.Get(0).(*dashboards.Dashboard) } } @@ -218,7 +218,7 @@ func (_m *FakePublicDashboardService) FindDashboard(ctx context.Context, orgId i } // FindPublicDashboardAndDashboardByAccessToken provides a mock function with given fields: ctx, accessToken -func (_m *FakePublicDashboardService) FindPublicDashboardAndDashboardByAccessToken(ctx context.Context, accessToken string) (*models.PublicDashboard, *pkgmodels.Dashboard, error) { +func (_m *FakePublicDashboardService) FindPublicDashboardAndDashboardByAccessToken(ctx context.Context, accessToken string) (*models.PublicDashboard, *dashboards.Dashboard, error) { ret := _m.Called(ctx, accessToken) var r0 *models.PublicDashboard @@ -230,12 +230,12 @@ func (_m *FakePublicDashboardService) FindPublicDashboardAndDashboardByAccessTok } } - var r1 *pkgmodels.Dashboard - if rf, ok := ret.Get(1).(func(context.Context, string) *pkgmodels.Dashboard); ok { + var r1 *dashboards.Dashboard + if rf, ok := ret.Get(1).(func(context.Context, string) *dashboards.Dashboard); ok { r1 = rf(ctx, accessToken) } else { if ret.Get(1) != nil { - r1 = ret.Get(1).(*pkgmodels.Dashboard) + r1 = ret.Get(1).(*dashboards.Dashboard) } } @@ -250,18 +250,18 @@ func (_m *FakePublicDashboardService) FindPublicDashboardAndDashboardByAccessTok } // GetMetricRequest provides a mock function with given fields: ctx, dashboard, publicDashboard, panelId, reqDTO -func (_m *FakePublicDashboardService) GetMetricRequest(ctx context.Context, dashboard *pkgmodels.Dashboard, publicDashboard *models.PublicDashboard, panelId int64, reqDTO models.PublicDashboardQueryDTO) (dtos.MetricRequest, error) { +func (_m *FakePublicDashboardService) GetMetricRequest(ctx context.Context, dashboard *dashboards.Dashboard, publicDashboard *models.PublicDashboard, panelId int64, reqDTO models.PublicDashboardQueryDTO) (dtos.MetricRequest, error) { ret := _m.Called(ctx, dashboard, publicDashboard, panelId, reqDTO) var r0 dtos.MetricRequest - if rf, ok := ret.Get(0).(func(context.Context, *pkgmodels.Dashboard, *models.PublicDashboard, int64, models.PublicDashboardQueryDTO) dtos.MetricRequest); ok { + if rf, ok := ret.Get(0).(func(context.Context, *dashboards.Dashboard, *models.PublicDashboard, int64, models.PublicDashboardQueryDTO) dtos.MetricRequest); ok { r0 = rf(ctx, dashboard, publicDashboard, panelId, reqDTO) } else { r0 = ret.Get(0).(dtos.MetricRequest) } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *pkgmodels.Dashboard, *models.PublicDashboard, int64, models.PublicDashboardQueryDTO) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, *dashboards.Dashboard, *models.PublicDashboard, int64, models.PublicDashboardQueryDTO) error); ok { r1 = rf(ctx, dashboard, publicDashboard, panelId, reqDTO) } else { r1 = ret.Error(1) diff --git a/pkg/services/publicdashboards/public_dashboard_store_mock.go b/pkg/services/publicdashboards/public_dashboard_store_mock.go index a664cb9cc2c..16a79872fea 100644 --- a/pkg/services/publicdashboards/public_dashboard_store_mock.go +++ b/pkg/services/publicdashboards/public_dashboard_store_mock.go @@ -1,16 +1,14 @@ -// Code generated by mockery v2.12.1. DO NOT EDIT. +// Code generated by mockery v2.16.0. DO NOT EDIT. package publicdashboards import ( context "context" - models "github.com/grafana/grafana/pkg/services/publicdashboards/models" + dashboards "github.com/grafana/grafana/pkg/services/dashboards" mock "github.com/stretchr/testify/mock" - pkgmodels "github.com/grafana/grafana/pkg/models" - - testing "testing" + models "github.com/grafana/grafana/pkg/services/publicdashboards/models" ) // FakePublicDashboardStore is an autogenerated mock type for the Store type @@ -195,15 +193,15 @@ func (_m *FakePublicDashboardStore) FindByDashboardUid(ctx context.Context, orgI } // FindDashboard provides a mock function with given fields: ctx, orgId, dashboardUid -func (_m *FakePublicDashboardStore) FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*pkgmodels.Dashboard, error) { +func (_m *FakePublicDashboardStore) FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*dashboards.Dashboard, error) { ret := _m.Called(ctx, orgId, dashboardUid) - var r0 *pkgmodels.Dashboard - if rf, ok := ret.Get(0).(func(context.Context, int64, string) *pkgmodels.Dashboard); ok { + var r0 *dashboards.Dashboard + if rf, ok := ret.Get(0).(func(context.Context, int64, string) *dashboards.Dashboard); ok { r0 = rf(ctx, orgId, dashboardUid) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*pkgmodels.Dashboard) + r0 = ret.Get(0).(*dashboards.Dashboard) } } @@ -259,8 +257,13 @@ func (_m *FakePublicDashboardStore) Update(ctx context.Context, cmd models.SaveP return r0, r1 } -// NewFakePublicDashboardStore creates a new instance of FakePublicDashboardStore. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. -func NewFakePublicDashboardStore(t testing.TB) *FakePublicDashboardStore { +type mockConstructorTestingTNewFakePublicDashboardStore interface { + mock.TestingT + Cleanup(func()) +} + +// NewFakePublicDashboardStore creates a new instance of FakePublicDashboardStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewFakePublicDashboardStore(t mockConstructorTestingTNewFakePublicDashboardStore) *FakePublicDashboardStore { mock := &FakePublicDashboardStore{} mock.Mock.Test(t) diff --git a/pkg/services/publicdashboards/publicdashboard.go b/pkg/services/publicdashboards/publicdashboard.go index e78ef487d4e..b301e9be8ad 100644 --- a/pkg/services/publicdashboards/publicdashboard.go +++ b/pkg/services/publicdashboards/publicdashboard.go @@ -5,7 +5,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/api/dtos" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/services/user" ) @@ -15,17 +15,17 @@ import ( //go:generate go run ./commands/generate_datasources/main.go //go:generate mockery --name Service --structname FakePublicDashboardService --inpackage --filename public_dashboard_service_mock.go type Service interface { - FindPublicDashboardAndDashboardByAccessToken(ctx context.Context, accessToken string) (*PublicDashboard, *models.Dashboard, error) + FindPublicDashboardAndDashboardByAccessToken(ctx context.Context, accessToken string) (*PublicDashboard, *dashboards.Dashboard, error) FindByAccessToken(ctx context.Context, accessToken string) (*PublicDashboard, error) FindByDashboardUid(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error) FindAnnotations(ctx context.Context, reqDTO AnnotationsQueryDTO, accessToken string) ([]AnnotationEvent, error) - FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*models.Dashboard, error) + FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*dashboards.Dashboard, error) FindAll(ctx context.Context, u *user.SignedInUser, orgId int64) ([]PublicDashboardListResponse, error) Create(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error) Update(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardDTO) (*PublicDashboard, error) Delete(ctx context.Context, orgId int64, uid string) error - GetMetricRequest(ctx context.Context, dashboard *models.Dashboard, publicDashboard *PublicDashboard, panelId int64, reqDTO PublicDashboardQueryDTO) (dtos.MetricRequest, error) + GetMetricRequest(ctx context.Context, dashboard *dashboards.Dashboard, publicDashboard *PublicDashboard, panelId int64, reqDTO PublicDashboardQueryDTO) (dtos.MetricRequest, error) GetQueryDataResponse(ctx context.Context, skipCache bool, reqDTO PublicDashboardQueryDTO, panelId int64, accessToken string) (*backend.QueryDataResponse, error) GetOrgIdByAccessToken(ctx context.Context, accessToken string) (int64, error) NewPublicDashboardAccessToken(ctx context.Context) (string, error) @@ -40,7 +40,7 @@ type Store interface { Find(ctx context.Context, uid string) (*PublicDashboard, error) FindByAccessToken(ctx context.Context, accessToken string) (*PublicDashboard, error) FindByDashboardUid(ctx context.Context, orgId int64, dashboardUid string) (*PublicDashboard, error) - FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*models.Dashboard, error) + FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*dashboards.Dashboard, error) FindAll(ctx context.Context, orgId int64) ([]PublicDashboardListResponse, error) Create(ctx context.Context, cmd SavePublicDashboardCommand) (int64, error) Update(ctx context.Context, cmd SavePublicDashboardCommand) (int64, error) diff --git a/pkg/services/publicdashboards/service/query.go b/pkg/services/publicdashboards/service/query.go index 42005beb02d..c6f1f1c92f8 100644 --- a/pkg/services/publicdashboards/service/query.go +++ b/pkg/services/publicdashboards/service/query.go @@ -6,7 +6,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/components/simplejson" - dashmodels "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/dashboards" @@ -44,9 +43,9 @@ func (pd *PublicDashboardServiceImpl) FindAnnotations(ctx context.Context, reqDT annoQuery := &annotations.ItemQuery{ From: reqDTO.From, To: reqDTO.To, - OrgId: dash.OrgId, - DashboardId: dash.Id, - DashboardUid: dash.Uid, + OrgId: dash.OrgID, + DashboardId: dash.ID, + DashboardUid: dash.UID, Limit: anno.Target.Limit, MatchAny: anno.Target.MatchAny, SignedInUser: anonymousUser, @@ -98,7 +97,7 @@ func (pd *PublicDashboardServiceImpl) FindAnnotations(ctx context.Context, reqDT } // GetMetricRequest returns a metric request for the given panel and query -func (pd *PublicDashboardServiceImpl) GetMetricRequest(ctx context.Context, dashboard *dashmodels.Dashboard, publicDashboard *models.PublicDashboard, panelId int64, queryDto models.PublicDashboardQueryDTO) (dtos.MetricRequest, error) { +func (pd *PublicDashboardServiceImpl) GetMetricRequest(ctx context.Context, dashboard *dashboards.Dashboard, publicDashboard *models.PublicDashboard, panelId int64, queryDto models.PublicDashboardQueryDTO) (dtos.MetricRequest, error) { err := validation.ValidateQueryPublicDashboardRequest(queryDto, publicDashboard) if err != nil { return dtos.MetricRequest{}, err @@ -150,7 +149,7 @@ func (pd *PublicDashboardServiceImpl) GetQueryDataResponse(ctx context.Context, } // buildMetricRequest merges public dashboard parameters with dashboard and returns a metrics request to be sent to query backend -func (pd *PublicDashboardServiceImpl) buildMetricRequest(ctx context.Context, dashboard *dashmodels.Dashboard, publicDashboard *models.PublicDashboard, panelId int64, reqDTO models.PublicDashboardQueryDTO) (dtos.MetricRequest, error) { +func (pd *PublicDashboardServiceImpl) buildMetricRequest(ctx context.Context, dashboard *dashboards.Dashboard, publicDashboard *models.PublicDashboard, panelId int64, reqDTO models.PublicDashboardQueryDTO) (dtos.MetricRequest, error) { // group queries by panel queriesByPanel := groupQueriesByPanelId(dashboard.Data) queries, ok := queriesByPanel[panelId] @@ -175,11 +174,11 @@ func (pd *PublicDashboardServiceImpl) buildMetricRequest(ctx context.Context, da } // buildAnonymousUser creates a user with permissions to read from all datasources used in the dashboard -func buildAnonymousUser(ctx context.Context, dashboard *dashmodels.Dashboard) *user.SignedInUser { +func buildAnonymousUser(ctx context.Context, dashboard *dashboards.Dashboard) *user.SignedInUser { datasourceUids := getUniqueDashboardDatasourceUids(dashboard.Data) // Create a user with blank permissions - anonymousUser := &user.SignedInUser{OrgID: dashboard.OrgId, Permissions: make(map[int64]map[string][]string)} + anonymousUser := &user.SignedInUser{OrgID: dashboard.OrgID, Permissions: make(map[int64]map[string][]string)} // Scopes needed for Annotation queries annotationScopes := []string{accesscontrol.ScopeAnnotationsTypeDashboard} @@ -202,7 +201,7 @@ func buildAnonymousUser(ctx context.Context, dashboard *dashmodels.Dashboard) *u permissions[accesscontrol.ActionAnnotationsRead] = annotationScopes permissions[dashboards.ActionDashboardsRead] = dashboardScopes - anonymousUser.Permissions[dashboard.OrgId] = permissions + anonymousUser.Permissions[dashboard.OrgID] = permissions return anonymousUser } diff --git a/pkg/services/publicdashboards/service/query_test.go b/pkg/services/publicdashboards/service/query_test.go index 81885ddcb65..137ce98f003 100644 --- a/pkg/services/publicdashboards/service/query_test.go +++ b/pkg/services/publicdashboards/service/query_test.go @@ -11,9 +11,9 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" dashboard2 "github.com/grafana/grafana/pkg/kinds/dashboard" - grafanamodels "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/annotations/annotationsimpl" + "github.com/grafana/grafana/pkg/services/dashboards" dashboardsDB "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/featuremgmt" . "github.com/grafana/grafana/pkg/services/publicdashboards" @@ -391,8 +391,8 @@ func TestGetQueryDataResponse(t *testing.T) { dashboard := insertTestDashboard(t, dashboardStore, "testDashWithHiddenQuery", 1, 0, true, []map[string]interface{}{}, customPanels) dto := &SavePublicDashboardDTO{ - DashboardUid: dashboard.Uid, - OrgId: dashboard.OrgId, + DashboardUid: dashboard.UID, + OrgId: dashboard.OrgID, UserId: 7, PublicDashboard: &PublicDashboard{ IsEnabled: true, @@ -426,13 +426,13 @@ func TestGetAnnotations(t *testing.T) { fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")). Return(&PublicDashboard{Uid: "uid1", IsEnabled: true}, nil) fakeStore.On("FindDashboard", mock.Anything, mock.Anything, mock.AnythingOfType("string")). - Return(grafanamodels.NewDashboard("dash1"), nil) + Return(dashboards.NewDashboard("dash1"), nil) reqDTO := AnnotationsQueryDTO{ From: 1, To: 2, } - dash := grafanamodels.NewDashboard("testDashboard") + dash := dashboards.NewDashboard("testDashboard") items, _ := service.FindAnnotations(context.Background(), reqDTO, "abc123") anonUser := buildAnonymousUser(context.Background(), dash) @@ -442,7 +442,7 @@ func TestGetAnnotations(t *testing.T) { }) t.Run("Test events from tag queries overwrite built-in annotation queries and duplicate events are not returned", func(t *testing.T) { - dash := grafanamodels.NewDashboard("test") + dash := dashboards.NewDashboard("test") grafanaAnnotation := DashAnnotation{ Datasource: CreateDatasource("grafana", "grafana"), Enable: true, @@ -478,7 +478,7 @@ func TestGetAnnotations(t *testing.T) { store: &fakeStore, AnnotationsRepo: &annotationsRepo, } - pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.Uid, AnnotationsEnabled: true} + pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.UID, AnnotationsEnabled: true} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeStore.On("FindDashboard", mock.Anything, mock.Anything, mock.AnythingOfType("string")).Return(dashboard, nil) @@ -515,7 +515,7 @@ func TestGetAnnotations(t *testing.T) { }) t.Run("Test panelId set to zero when annotation event is for a tags query", func(t *testing.T) { - dash := grafanamodels.NewDashboard("test") + dash := dashboards.NewDashboard("test") grafanaAnnotation := DashAnnotation{ Datasource: CreateDatasource("grafana", "grafana"), Enable: true, @@ -538,7 +538,7 @@ func TestGetAnnotations(t *testing.T) { store: &fakeStore, AnnotationsRepo: &annotationsRepo, } - pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.Uid, AnnotationsEnabled: true} + pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.UID, AnnotationsEnabled: true} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeStore.On("FindDashboard", mock.Anything, mock.Anything, mock.AnythingOfType("string")).Return(dashboard, nil) @@ -575,7 +575,7 @@ func TestGetAnnotations(t *testing.T) { }) t.Run("Test can get grafana annotations and will skip annotation queries and disabled annotations", func(t *testing.T) { - dash := grafanamodels.NewDashboard("test") + dash := dashboards.NewDashboard("test") disabledGrafanaAnnotation := DashAnnotation{ Datasource: CreateDatasource("grafana", "grafana"), Enable: false, @@ -610,7 +610,7 @@ func TestGetAnnotations(t *testing.T) { store: &fakeStore, AnnotationsRepo: &annotationsRepo, } - pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.Uid, AnnotationsEnabled: true} + pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.UID, AnnotationsEnabled: true} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeStore.On("FindDashboard", mock.Anything, mock.Anything, mock.AnythingOfType("string")).Return(dashboard, nil) @@ -654,8 +654,8 @@ func TestGetAnnotations(t *testing.T) { store: &fakeStore, AnnotationsRepo: &annotationsRepo, } - dashboard := grafanamodels.NewDashboard("dashWithNoAnnotations") - pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.Uid, AnnotationsEnabled: true} + dashboard := dashboards.NewDashboard("dashWithNoAnnotations") + pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.UID, AnnotationsEnabled: true} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeStore.On("FindDashboard", mock.Anything, mock.Anything, mock.AnythingOfType("string")).Return(dashboard, nil) @@ -674,7 +674,7 @@ func TestGetAnnotations(t *testing.T) { store: &fakeStore, AnnotationsRepo: &annotationsRepo, } - dash := grafanamodels.NewDashboard("test") + dash := dashboards.NewDashboard("test") grafanaAnnotation := DashAnnotation{ Datasource: CreateDatasource("grafana", "grafana"), Enable: true, @@ -690,7 +690,7 @@ func TestGetAnnotations(t *testing.T) { } annos := []DashAnnotation{grafanaAnnotation} dashboard := AddAnnotationsToDashboard(t, dash, annos) - pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.Uid, AnnotationsEnabled: false} + pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dashboard.UID, AnnotationsEnabled: false} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeStore.On("FindDashboard", mock.Anything, mock.Anything, mock.AnythingOfType("string")).Return(dashboard, nil) @@ -709,7 +709,7 @@ func TestGetAnnotations(t *testing.T) { store: &fakeStore, AnnotationsRepo: &annotationsRepo, } - dash := grafanamodels.NewDashboard("test") + dash := dashboards.NewDashboard("test") grafanaAnnotation := DashAnnotation{ Datasource: CreateDatasource("grafana", "grafana"), Enable: true, @@ -724,7 +724,7 @@ func TestGetAnnotations(t *testing.T) { } annos := []DashAnnotation{grafanaAnnotation} dash = AddAnnotationsToDashboard(t, dash, annos) - pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dash.Uid, AnnotationsEnabled: true} + pubdash := &PublicDashboard{Uid: "uid1", IsEnabled: true, OrgId: 1, DashboardUid: dash.UID, AnnotationsEnabled: true} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")).Return(pubdash, nil) fakeStore.On("FindDashboard", mock.Anything, mock.Anything, mock.AnythingOfType("string")).Return(dash, nil) @@ -746,7 +746,7 @@ func TestGetMetricRequest(t *testing.T) { dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil) publicDashboard := &PublicDashboard{ Uid: "1", - DashboardUid: dashboard.Uid, + DashboardUid: dashboard.UID, IsEnabled: true, AccessToken: "abc123", } @@ -834,8 +834,8 @@ func TestBuildMetricRequest(t *testing.T) { } dto := &SavePublicDashboardDTO{ - DashboardUid: publicDashboard.Uid, - OrgId: publicDashboard.OrgId, + DashboardUid: publicDashboard.UID, + OrgId: publicDashboard.OrgID, PublicDashboard: &PublicDashboard{ IsEnabled: true, DashboardUid: "NOTTHESAME", @@ -848,8 +848,8 @@ func TestBuildMetricRequest(t *testing.T) { require.NoError(t, err) nonPublicDto := &SavePublicDashboardDTO{ - DashboardUid: nonPublicDashboard.Uid, - OrgId: nonPublicDashboard.OrgId, + DashboardUid: nonPublicDashboard.UID, + OrgId: nonPublicDashboard.OrgID, PublicDashboard: &PublicDashboard{ IsEnabled: false, DashboardUid: "NOTTHESAME", @@ -1038,7 +1038,7 @@ func TestBuildAnonymousUser(t *testing.T) { t.Run("will add datasource read and query permissions to user for each datasource in dashboard", func(t *testing.T) { user := buildAnonymousUser(context.Background(), dashboard) - require.Equal(t, dashboard.OrgId, user.OrgID) + require.Equal(t, dashboard.OrgID, user.OrgID) require.Equal(t, "datasources:uid:ds1", user.Permissions[user.OrgID]["datasources:query"][0]) require.Equal(t, "datasources:uid:ds3", user.Permissions[user.OrgID]["datasources:query"][1]) require.Equal(t, "datasources:uid:ds1", user.Permissions[user.OrgID]["datasources:read"][0]) @@ -1047,7 +1047,7 @@ func TestBuildAnonymousUser(t *testing.T) { t.Run("will add dashboard and annotation permissions needed for getting annotations", func(t *testing.T) { user := buildAnonymousUser(context.Background(), dashboard) - require.Equal(t, dashboard.OrgId, user.OrgID) + require.Equal(t, dashboard.OrgID, user.OrgID) require.Equal(t, "annotations:type:dashboard", user.Permissions[user.OrgID]["annotations:read"][0]) require.Equal(t, "dashboards:*", user.Permissions[user.OrgID]["dashboards:read"][0]) }) diff --git a/pkg/services/publicdashboards/service/service.go b/pkg/services/publicdashboards/service/service.go index adbf48006c2..37ff8c755f9 100644 --- a/pkg/services/publicdashboards/service/service.go +++ b/pkg/services/publicdashboards/service/service.go @@ -7,7 +7,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "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/annotations" "github.com/grafana/grafana/pkg/services/dashboards" @@ -62,7 +61,7 @@ func ProvideService( } // FindDashboard Gets a dashboard by Uid -func (pd *PublicDashboardServiceImpl) FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*models.Dashboard, error) { +func (pd *PublicDashboardServiceImpl) FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*dashboards.Dashboard, error) { dash, err := pd.store.FindDashboard(ctx, orgId, dashboardUid) if err != nil { return nil, ErrInternalServerError.Errorf("FindDashboard: failed to find dashboard by orgId: %d and dashboardUid: %s: %w", orgId, dashboardUid, err) @@ -90,7 +89,7 @@ func (pd *PublicDashboardServiceImpl) FindByAccessToken(ctx context.Context, acc } // FindPublicDashboardAndDashboardByAccessToken Gets public dashboard and a dashboard by access token -func (pd *PublicDashboardServiceImpl) FindPublicDashboardAndDashboardByAccessToken(ctx context.Context, accessToken string) (*PublicDashboard, *models.Dashboard, error) { +func (pd *PublicDashboardServiceImpl) FindPublicDashboardAndDashboardByAccessToken(ctx context.Context, accessToken string) (*PublicDashboard, *dashboards.Dashboard, error) { pubdash, err := pd.FindByAccessToken(ctx, accessToken) if err != nil { return nil, nil, err diff --git a/pkg/services/publicdashboards/service/service_test.go b/pkg/services/publicdashboards/service/service_test.go index fcf700b36c4..771f916d53a 100644 --- a/pkg/services/publicdashboards/service/service_test.go +++ b/pkg/services/publicdashboards/service/service_test.go @@ -12,7 +12,6 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "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" dashboardsDB "github.com/grafana/grafana/pkg/services/dashboards/database" @@ -44,7 +43,7 @@ func TestLogPrefix(t *testing.T) { func TestGetPublicDashboard(t *testing.T) { type storeResp struct { pd *PublicDashboard - d *models.Dashboard + d *dashboards.Dashboard err error } @@ -53,25 +52,25 @@ func TestGetPublicDashboard(t *testing.T) { AccessToken string StoreResp *storeResp ErrResp error - DashResp *models.Dashboard + DashResp *dashboards.Dashboard }{ { Name: "returns a dashboard", AccessToken: "abc123", StoreResp: &storeResp{ pd: &PublicDashboard{AccessToken: "abcdToken", IsEnabled: true}, - d: &models.Dashboard{Uid: "mydashboard", Data: dashboardData}, + d: &dashboards.Dashboard{UID: "mydashboard", Data: dashboardData}, err: nil, }, ErrResp: nil, - DashResp: &models.Dashboard{Uid: "mydashboard", Data: dashboardData}, + DashResp: &dashboards.Dashboard{UID: "mydashboard", Data: dashboardData}, }, { Name: "returns ErrPublicDashboardNotFound when isEnabled is false", AccessToken: "abc123", StoreResp: &storeResp{ pd: &PublicDashboard{AccessToken: "abcdToken", IsEnabled: false}, - d: &models.Dashboard{Uid: "mydashboard"}, + d: &dashboards.Dashboard{UID: "mydashboard"}, err: nil, }, ErrResp: ErrPublicDashboardNotFound, @@ -138,8 +137,8 @@ func TestCreatePublicDashboard(t *testing.T) { } dto := &SavePublicDashboardDTO{ - DashboardUid: dashboard.Uid, - OrgId: dashboard.OrgId, + DashboardUid: dashboard.UID, + OrgId: dashboard.OrgID, UserId: 7, PublicDashboard: &PublicDashboard{ IsEnabled: true, @@ -153,12 +152,12 @@ func TestCreatePublicDashboard(t *testing.T) { _, err = service.Create(context.Background(), SignedInUser, dto) require.NoError(t, err) - pubdash, err := service.FindByDashboardUid(context.Background(), dashboard.OrgId, dashboard.Uid) + pubdash, err := service.FindByDashboardUid(context.Background(), dashboard.OrgID, dashboard.UID) require.NoError(t, err) // DashboardUid/OrgId/CreatedBy set by the command, not parameters - assert.Equal(t, dashboard.Uid, pubdash.DashboardUid) - assert.Equal(t, dashboard.OrgId, pubdash.OrgId) + assert.Equal(t, dashboard.UID, pubdash.DashboardUid) + assert.Equal(t, dashboard.OrgID, pubdash.OrgId) assert.Equal(t, dto.UserId, pubdash.CreatedBy) assert.Equal(t, dto.PublicDashboard.AnnotationsEnabled, pubdash.AnnotationsEnabled) // ExistsEnabledByDashboardUid set by parameters @@ -186,8 +185,8 @@ func TestCreatePublicDashboard(t *testing.T) { } dto := &SavePublicDashboardDTO{ - DashboardUid: dashboard.Uid, - OrgId: dashboard.OrgId, + DashboardUid: dashboard.UID, + OrgId: dashboard.OrgID, UserId: 7, PublicDashboard: &PublicDashboard{ IsEnabled: true, @@ -199,7 +198,7 @@ func TestCreatePublicDashboard(t *testing.T) { _, err = service.Create(context.Background(), SignedInUser, dto) require.NoError(t, err) - pubdash, err := service.FindByDashboardUid(context.Background(), dashboard.OrgId, dashboard.Uid) + pubdash, err := service.FindByDashboardUid(context.Background(), dashboard.OrgID, dashboard.UID) require.NoError(t, err) assert.Equal(t, defaultPubdashTimeSettings, pubdash.TimeSettings) }) @@ -219,8 +218,8 @@ func TestCreatePublicDashboard(t *testing.T) { } dto := &SavePublicDashboardDTO{ - DashboardUid: dashboard.Uid, - OrgId: dashboard.OrgId, + DashboardUid: dashboard.UID, + OrgId: dashboard.OrgID, UserId: 7, PublicDashboard: &PublicDashboard{ IsEnabled: true, @@ -234,7 +233,7 @@ func TestCreatePublicDashboard(t *testing.T) { }) t.Run("Throws an error when pubdash with generated access token already exists", func(t *testing.T) { - dashboard := models.NewDashboard("testDashie") + dashboard := dashboards.NewDashboard("testDashie") pubdash := &PublicDashboard{ IsEnabled: true, AnnotationsEnabled: false, @@ -283,8 +282,8 @@ func TestCreatePublicDashboard(t *testing.T) { } dto := &SavePublicDashboardDTO{ - DashboardUid: dashboard.Uid, - OrgId: dashboard.OrgId, + DashboardUid: dashboard.UID, + OrgId: dashboard.OrgID, UserId: 7, PublicDashboard: &PublicDashboard{ AnnotationsEnabled: false, @@ -298,8 +297,8 @@ func TestCreatePublicDashboard(t *testing.T) { // attempt to overwrite settings dto = &SavePublicDashboardDTO{ - DashboardUid: dashboard.Uid, - OrgId: dashboard.OrgId, + DashboardUid: dashboard.UID, + OrgId: dashboard.OrgID, UserId: 8, PublicDashboard: &PublicDashboard{ Uid: savedPubdash.Uid, @@ -336,8 +335,8 @@ func TestUpdatePublicDashboard(t *testing.T) { } dto := &SavePublicDashboardDTO{ - DashboardUid: dashboard.Uid, - OrgId: dashboard.OrgId, + DashboardUid: dashboard.UID, + OrgId: dashboard.OrgID, UserId: 7, PublicDashboard: &PublicDashboard{ AnnotationsEnabled: false, @@ -352,8 +351,8 @@ func TestUpdatePublicDashboard(t *testing.T) { // attempt to overwrite settings dto = &SavePublicDashboardDTO{ - DashboardUid: dashboard.Uid, - OrgId: dashboard.OrgId, + DashboardUid: dashboard.UID, + OrgId: dashboard.OrgID, UserId: 8, PublicDashboard: &PublicDashboard{ Uid: savedPubdash.Uid, @@ -400,8 +399,8 @@ func TestUpdatePublicDashboard(t *testing.T) { } dto := &SavePublicDashboardDTO{ - DashboardUid: dashboard.Uid, - OrgId: dashboard.OrgId, + DashboardUid: dashboard.UID, + OrgId: dashboard.OrgID, UserId: 7, PublicDashboard: &PublicDashboard{ IsEnabled: true, @@ -414,8 +413,8 @@ func TestUpdatePublicDashboard(t *testing.T) { // attempt to overwrite settings dto = &SavePublicDashboardDTO{ - DashboardUid: dashboard.Uid, - OrgId: dashboard.OrgId, + DashboardUid: dashboard.UID, + OrgId: dashboard.OrgID, UserId: 8, PublicDashboard: &PublicDashboard{ Uid: savedPubdash.Uid, @@ -933,7 +932,7 @@ func CreateDatasource(dsType string, uid string) struct { } } -func AddAnnotationsToDashboard(t *testing.T, dash *models.Dashboard, annotations []DashAnnotation) *models.Dashboard { +func AddAnnotationsToDashboard(t *testing.T, dash *dashboards.Dashboard, annotations []DashAnnotation) *dashboards.Dashboard { type annotationsDto struct { List []DashAnnotation `json:"list"` } @@ -951,7 +950,7 @@ func AddAnnotationsToDashboard(t *testing.T, dash *models.Dashboard, annotations } func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardStore, title string, orgId int64, - folderId int64, isFolder bool, templateVars []map[string]interface{}, customPanels []interface{}, tags ...interface{}) *models.Dashboard { + folderId int64, isFolder bool, templateVars []map[string]interface{}, customPanels []interface{}, tags ...interface{}) *dashboards.Dashboard { t.Helper() var dashboardPanels []interface{} @@ -999,9 +998,9 @@ func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardSto } } - cmd := models.SaveDashboardCommand{ - OrgId: orgId, - FolderId: folderId, + cmd := dashboards.SaveDashboardCommand{ + OrgID: orgId, + FolderID: folderId, IsFolder: isFolder, Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, @@ -1020,7 +1019,7 @@ func insertTestDashboard(t *testing.T, dashboardStore *dashboardsDB.DashboardSto dash, err := dashboardStore.SaveDashboard(context.Background(), cmd) require.NoError(t, err) require.NotNil(t, dash) - dash.Data.Set("id", dash.Id) - dash.Data.Set("uid", dash.Uid) + dash.Data.Set("id", dash.ID) + dash.Data.Set("uid", dash.UID) return dash } diff --git a/pkg/services/publicdashboards/validation/validation.go b/pkg/services/publicdashboards/validation/validation.go index 70c2d25b703..6f2c57c83dd 100644 --- a/pkg/services/publicdashboards/validation/validation.go +++ b/pkg/services/publicdashboards/validation/validation.go @@ -1,12 +1,12 @@ package validation import ( - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/tsdb/legacydata" ) -func ValidatePublicDashboard(dto *SavePublicDashboardDTO, dashboard *models.Dashboard) error { +func ValidatePublicDashboard(dto *SavePublicDashboardDTO, dashboard *dashboards.Dashboard) error { if hasTemplateVariables(dashboard) { return ErrPublicDashboardHasTemplateVariables.Errorf("ValidateSavePublicDashboard: public dashboard has template variables") } @@ -14,7 +14,7 @@ func ValidatePublicDashboard(dto *SavePublicDashboardDTO, dashboard *models.Dash return nil } -func hasTemplateVariables(dashboard *models.Dashboard) bool { +func hasTemplateVariables(dashboard *dashboards.Dashboard) bool { templateVariables := dashboard.Data.Get("templating").Get("list").MustArray() return len(templateVariables) > 0 diff --git a/pkg/services/publicdashboards/validation/validation_test.go b/pkg/services/publicdashboards/validation/validation_test.go index 8b606d05a27..4ee281e20fb 100644 --- a/pkg/services/publicdashboards/validation/validation_test.go +++ b/pkg/services/publicdashboards/validation/validation_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/stretchr/testify/require" ) @@ -21,7 +21,7 @@ func TestValidatePublicDashboard(t *testing.T) { } }`) dashboardData, _ := simplejson.NewJson(templateVars) - dashboard := models.NewDashboardFromJson(dashboardData) + dashboard := dashboards.NewDashboardFromJson(dashboardData) dto := &SavePublicDashboardDTO{DashboardUid: "abc123", OrgId: 1, UserId: 1, PublicDashboard: nil} err := ValidatePublicDashboard(dto, dashboard) @@ -35,7 +35,7 @@ func TestValidatePublicDashboard(t *testing.T) { } }`) dashboardData, _ := simplejson.NewJson(templateVars) - dashboard := models.NewDashboardFromJson(dashboardData) + dashboard := dashboards.NewDashboardFromJson(dashboardData) dto := &SavePublicDashboardDTO{DashboardUid: "abc123", OrgId: 1, UserId: 1, PublicDashboard: nil} err := ValidatePublicDashboard(dto, dashboard) diff --git a/pkg/services/querylibrary/querylibraryimpl/service.go b/pkg/services/querylibrary/querylibraryimpl/service.go index 6078c41af82..15b39bea04f 100644 --- a/pkg/services/querylibrary/querylibraryimpl/service.go +++ b/pkg/services/querylibrary/querylibraryimpl/service.go @@ -9,7 +9,7 @@ import ( "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/x/persistentcollection" - "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/querylibrary" "github.com/grafana/grafana/pkg/services/store/kind/dashboard" @@ -67,7 +67,7 @@ type queryLoader interface { byUID(uid string) (*querylibrary.Query, error) } -func (s *service) UpdateDashboardQueries(ctx context.Context, user *user.SignedInUser, dash *models.Dashboard) error { +func (s *service) UpdateDashboardQueries(ctx context.Context, user *user.SignedInUser, dash *dashboards.Dashboard) error { queryLoader := newPerRequestQueryLoader(ctx, user, s) return s.updateQueriesRecursively(queryLoader, dash.Data) } diff --git a/pkg/services/querylibrary/types.go b/pkg/services/querylibrary/types.go index 2dfbb6247fa..4f2bd11cfc3 100644 --- a/pkg/services/querylibrary/types.go +++ b/pkg/services/querylibrary/types.go @@ -5,8 +5,8 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/store/kind/dashboard" "github.com/grafana/grafana/pkg/services/user" ) @@ -78,7 +78,7 @@ type Service interface { GetBatch(ctx context.Context, user *user.SignedInUser, uids []string) ([]*Query, error) Update(ctx context.Context, user *user.SignedInUser, query *Query) error Delete(ctx context.Context, user *user.SignedInUser, uid string) error - UpdateDashboardQueries(ctx context.Context, user *user.SignedInUser, dash *models.Dashboard) error + UpdateDashboardQueries(ctx context.Context, user *user.SignedInUser, dash *dashboards.Dashboard) error registry.CanBeDisabled } diff --git a/pkg/services/screenshot/screenshot.go b/pkg/services/screenshot/screenshot.go index e8d6b41b695..eb5bf01ddfa 100644 --- a/pkg/services/screenshot/screenshot.go +++ b/pkg/services/screenshot/screenshot.go @@ -12,7 +12,6 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" - "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/rendering" @@ -86,7 +85,7 @@ func (s *HeadlessScreenshotService) Take(ctx context.Context, opts ScreenshotOpt start := time.Now() defer func() { s.duration.Observe(time.Since(start).Seconds()) }() - q := models.GetDashboardQuery{Uid: opts.DashboardUID} + q := dashboards.GetDashboardQuery{UID: opts.DashboardUID} if err := s.ds.GetDashboard(ctx, &q); err != nil { s.instrumentError(err) return nil, err @@ -95,9 +94,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", q.Result.UID, q.Result.Slug) p := u.Query() - p.Add("orgId", strconv.FormatInt(q.Result.OrgId, 10)) + p.Add("orgId", strconv.FormatInt(q.Result.OrgID, 10)) p.Add("panelId", strconv.FormatInt(opts.PanelID, 10)) p.Add("from", opts.From) p.Add("to", opts.To) @@ -105,7 +104,7 @@ func (s *HeadlessScreenshotService) Take(ctx context.Context, opts ScreenshotOpt renderOpts := rendering.Opts{ AuthOpts: rendering.AuthOpts{ - OrgID: q.Result.OrgId, + OrgID: q.Result.OrgID, OrgRole: org.RoleAdmin, }, ErrorOpts: rendering.ErrorOpts{ diff --git a/pkg/services/screenshot/screenshot_test.go b/pkg/services/screenshot/screenshot_test.go index d4382bbd960..42557f6956e 100644 --- a/pkg/services/screenshot/screenshot_test.go +++ b/pkg/services/screenshot/screenshot_test.go @@ -11,7 +11,6 @@ import ( "github.com/stretchr/testify/mock" "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/services/rendering" @@ -27,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("*models.GetDashboardQuery")).Return(dashboards.ErrDashboardNotFound).Once() + d.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(dashboards.ErrDashboardNotFound).Once() ctx := context.Background() opts := ScreenshotOptions{} screenshot, err := s.Take(ctx, opts) @@ -35,9 +34,9 @@ func TestHeadlessScreenshotService(t *testing.T) { assert.Nil(t, screenshot) // should take a screenshot - d.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*models.GetDashboardQuery) - q.Result = &models.Dashboard{Id: 1, Uid: "foo", Slug: "bar", OrgId: 2} + 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) renderOpts := rendering.Opts{ From 4454d5eb492a5e5556b1da2a369cf0fb88ce1405 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Mon, 16 Jan 2023 15:42:02 +0000 Subject: [PATCH 18/65] Build: Fix eslint in file (#61568) --- public/app/plugins/datasource/loki/responseUtils.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/loki/responseUtils.ts b/public/app/plugins/datasource/loki/responseUtils.ts index 29f462c0d9f..aa7db223bd5 100644 --- a/public/app/plugins/datasource/loki/responseUtils.ts +++ b/public/app/plugins/datasource/loki/responseUtils.ts @@ -1,8 +1,7 @@ import { DataFrame, FieldType, isValidGoDuration, Labels } from '@grafana/data'; -import { isLogLineJSON, isLogLineLogfmt } from './lineParser'; - import { isBytesString } from './languageUtils'; +import { isLogLineJSON, isLogLineLogfmt } from './lineParser'; export function dataFrameHasLokiError(frame: DataFrame): boolean { const labelSets: Labels[] = frame.fields.find((f) => f.name === 'labels')?.values.toArray() ?? []; From 8ae4b9060b8e61e44b7b56217d2c8b5240ee1097 Mon Sep 17 00:00:00 2001 From: Gareth Dawson Date: Mon, 16 Jan 2023 15:54:40 +0000 Subject: [PATCH 19/65] Elastic: Change elastic version 8.0+ to be more accurate (#61379) fix: change elastic version 8.0+ to be more accurate --- docs/sources/datasources/elasticsearch/_index.md | 2 +- .../datasource/elasticsearch/configuration/ElasticDetails.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/datasources/elasticsearch/_index.md b/docs/sources/datasources/elasticsearch/_index.md index 4ad5848e5b2..16b3a051472 100644 --- a/docs/sources/datasources/elasticsearch/_index.md +++ b/docs/sources/datasources/elasticsearch/_index.md @@ -31,7 +31,7 @@ Once you've added the Elasticsearch data source, you can [configure it]({{< relr This data source supports these versions of Elasticsearch: - v7.10+ -- v8.0+ +- v8.x ## Configure the data source diff --git a/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.tsx b/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.tsx index be770175068..bd7f5e76d43 100644 --- a/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.tsx +++ b/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.tsx @@ -17,7 +17,7 @@ const indexPatternTypes: Array> = [ const esVersions: SelectableValue[] = [ { label: '7.10+', value: '7.10.0' }, - { label: '8.0+', value: '8.0.0' }, + { label: '8.x', value: '8.0.0' }, ]; type Props = { From 0eeeeef08bb962d2278247b82feb7ff7c2df420c Mon Sep 17 00:00:00 2001 From: Alexa V <239999+axelavargas@users.noreply.github.com> Date: Mon, 16 Jan 2023 15:56:39 +0000 Subject: [PATCH 20/65] PanelChrome: Refactor and refine items next to title (#60514) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Torkel Ödegaard Co-authored-by: Polina Boneva <13227501+polibb@users.noreply.github.com> Co-authored-by: polinaboneva --- packages/grafana-data/src/types/icon.ts | 1 + .../grafana-ui/src/components/Icon/utils.ts | 10 +- .../PanelChrome/PanelChrome.story.tsx | 29 +---- .../PanelChrome/PanelChrome.test.tsx | 16 +-- .../components/PanelChrome/PanelChrome.tsx | 77 ++++++------- .../PanelChrome/PanelDescription.tsx | 75 ++++++++++++ .../src/components/PanelChrome/index.ts | 2 + .../src/components/Tooltip/Tooltip.tsx | 1 + .../PanelHeader/PanelHeaderNotice.tsx | 73 ++++++++++-- .../PanelHeader/PanelHeaderTitleItems.tsx | 107 ++++++++++++++++++ .../dashboard/dashgrid/PanelLinks.tsx | 77 +++++++++++++ .../dashboard/dashgrid/PanelStateWrapper.tsx | 44 ++++++- public/img/icons/mono/circle-mono.svg | 1 + public/locales/pseudo-LOCALE/grafana.json | 2 +- 14 files changed, 419 insertions(+), 96 deletions(-) create mode 100644 packages/grafana-ui/src/components/PanelChrome/PanelDescription.tsx create mode 100644 public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderTitleItems.tsx create mode 100644 public/app/features/dashboard/dashgrid/PanelLinks.tsx create mode 100644 public/img/icons/mono/circle-mono.svg diff --git a/packages/grafana-data/src/types/icon.ts b/packages/grafana-data/src/types/icon.ts index c58967fa1dc..74f9c258a33 100644 --- a/packages/grafana-data/src/types/icon.ts +++ b/packages/grafana-data/src/types/icon.ts @@ -44,6 +44,7 @@ export const availableIconsIndex = { 'check-circle': true, 'check-square': true, circle: true, + 'circle-mono': true, 'clipboard-alt': true, 'clock-nine': true, cloud: true, diff --git a/packages/grafana-ui/src/components/Icon/utils.ts b/packages/grafana-ui/src/components/Icon/utils.ts index f7ead12d92c..d31311a54d5 100644 --- a/packages/grafana-ui/src/components/Icon/utils.ts +++ b/packages/grafana-ui/src/components/Icon/utils.ts @@ -1,6 +1,14 @@ import { IconName, IconSize } from '../../types/icon'; -const alwaysMonoIcons: IconName[] = ['grafana', 'favorite', 'heart-break', 'heart', 'panel-add', 'library-panel']; +const alwaysMonoIcons: IconName[] = [ + 'grafana', + 'favorite', + 'heart-break', + 'heart', + 'panel-add', + 'library-panel', + 'circle-mono', +]; export function getIconSubDir(name: IconName, type: string): string { if (name?.startsWith('gf-')) { diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx index ab52fc5b442..d6f4b3d0493 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx @@ -12,8 +12,6 @@ import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; import { HorizontalGroup, VerticalGroup } from '../Layout/Layout'; import { Menu } from '../Menu/Menu'; -import { PanelChromeInfoState } from './PanelChrome'; - const meta: ComponentMeta = { title: 'Visualizations/PanelChrome', component: PanelChrome, @@ -235,29 +233,11 @@ const ErrorIcon = [ const leftItems = { LoadingIcon, ErrorIcon, Default }; -const titleItems: PanelChromeInfoState[] = [ - { - icon: 'info', - tooltip: - 'Description text with very long descriptive words that describe what is going on in the panel and not beyond. Or maybe beyond, not up to us.', - }, - { - icon: 'external-link-alt', - tooltip: 'wearegoingonanadventure.openanewtab.maybe', - onClick: () => {}, - }, - { - icon: 'clock-nine', - tooltip: 'Time range: 2021-09-01 00:00:00 to 2021-09-01 00:00:00', - onClick: () => {}, - }, - { - icon: 'heart', - tooltip: 'Health of the panel', - }, -]; +const description = + 'Description text with very long descriptive words that describe what is going on in the panel and not beyond. Or maybe beyond, not up to us.'; Basic.argTypes = { + description: { control: { type: 'text' } }, leftItems: { options: Object.keys(leftItems), mapping: leftItems, @@ -276,9 +256,8 @@ Basic.args = { width: 400, height: 200, title: 'Very long title that should get ellipsis when there is no more space', - titleItems, + description, menu, - loadingState: LoadingState.Loading, }; export default meta; diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.test.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.test.tsx index 6c4d000b151..2d2169e4812 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.test.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.test.tsx @@ -49,13 +49,7 @@ it('renders panel with a header with title in place if prop title', () => { it('renders panel with a header if prop titleItems', () => { setup({ - titleItems: [ - { - icon: 'info-circle', - tooltip: 'This is the panel description', - onClick: () => {}, - }, - ], + titleItems: [
This should be a self-contained node
], }); expect(screen.getByTestId('header-container')).toBeInTheDocument(); @@ -63,13 +57,7 @@ it('renders panel with a header if prop titleItems', () => { it('renders panel with a header with icons in place if prop titleItems', () => { setup({ - titleItems: [ - { - icon: 'info-circle', - tooltip: 'This is the panel description', - onClick: () => {}, - }, - ], + titleItems: [
This should be a self-contained node
], }); expect(screen.getByTestId('title-items-container')).toBeInTheDocument(); diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx index 1b94df2f5f6..d2f2b3716a6 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx @@ -1,18 +1,18 @@ import { css, cx } from '@emotion/css'; import { isEmpty } from 'lodash'; -import React, { CSSProperties, ReactElement, ReactNode } from 'react'; +import React, { CSSProperties, ReactNode, ReactElement } from 'react'; -import { GrafanaTheme2, isIconName, LoadingState } from '@grafana/data'; +import { GrafanaTheme2, LoadingState } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { useStyles2, useTheme2 } from '../../themes'; -import { IconName } from '../../types/icon'; import { Dropdown } from '../Dropdown/Dropdown'; import { Icon } from '../Icon/Icon'; -import { IconButton, IconButtonVariant } from '../IconButton/IconButton'; import { LoadingBar } from '../LoadingBar/LoadingBar'; import { ToolbarButton } from '../ToolbarButton'; -import { PopoverContent, Tooltip } from '../Tooltip'; +import { Tooltip } from '../Tooltip'; +import { PanelDescription } from './PanelDescription'; import { PanelStatus } from './PanelStatus'; interface Status { @@ -20,17 +20,6 @@ interface Status { onClick?: (e: React.SyntheticEvent) => void; } -/** - * @internal - */ -export interface PanelChromeInfoState { - icon: IconName; - label?: string | ReactNode; - tooltip?: PopoverContent; - variant?: IconButtonVariant; - onClick?: () => void; -} - /** * @internal */ @@ -40,7 +29,8 @@ export interface PanelChromeProps { children: (innerWidth: number, innerHeight: number) => ReactNode; padding?: PanelPadding; title?: string; - titleItems?: PanelChromeInfoState[]; + description?: string | (() => string); + titleItems?: ReactNode[]; menu?: ReactElement | (() => ReactElement); /** dragClass, hoverHeader not yet implemented */ // dragClass?: string; @@ -69,6 +59,7 @@ export function PanelChrome({ children, padding = 'md', title = '', + description = '', titleItems = [], menu, // dragClass, @@ -82,7 +73,16 @@ export function PanelChrome({ // To Do rely on hoverHeader prop for header, not separate props // once hoverHeader is implemented - const hasHeader = title.length > 0 || leftItems.length > 0; + // + // Backwards compatibility for having a designated space for the header + + const hasHeader = + hoverHeader === false && + (title.length > 0 || + titleItems.length > 0 || + description !== '' || + loadingState === LoadingState.Streaming || + leftItems.length > 0); const headerHeight = getHeaderHeight(theme, hasHeader); const { contentStyle, innerWidth, innerHeight } = getContentStyle(padding, theme, width, headerHeight, height); @@ -114,8 +114,10 @@ export function PanelChrome({ return null; } }; + + const ariaLabel = title ? selectors.components.Panels.Panel.containerByTitle(title) : 'Panel'; return ( -
+
{showLoading ? : null}
@@ -127,29 +129,19 @@ export function PanelChrome({ )} - {showStreaming && ( -
- - - + + + {titleItems && ( +
+ {titleItems.map((item) => item)}
)} - {titleItems.length > 0 && ( -
- {titleItems - .filter((item) => isIconName(item.icon)) - .map((item, i) => ( -
- {item.onClick ? ( - - ) : ( - - - - )} -
- ))} + {showStreaming && ( +
+ + +
)} @@ -172,7 +164,6 @@ export function PanelChrome({ {renderStatus()}
-
{children(innerWidth, innerHeight)}
@@ -299,5 +290,11 @@ const getStyles = (theme: GrafanaTheme2) => { display: 'flex', alignItems: 'center', }), + titleItems: css({ + display: 'flex', + alignItems: 'center', + overflow: 'hidden', + padding: theme.spacing(1), + }), }; }; diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelDescription.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelDescription.tsx new file mode 100644 index 00000000000..be82080da43 --- /dev/null +++ b/packages/grafana-ui/src/components/PanelChrome/PanelDescription.tsx @@ -0,0 +1,75 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; + +import { useTheme2 } from '../../themes'; +import { getFocusStyles, getMouseFocusStyles } from '../../themes/mixins'; +import { Icon } from '../Icon/Icon'; +import { Tooltip } from '../Tooltip'; + +interface Props { + description: string | (() => string); +} + +export function PanelDescription({ description }: Props) { + const theme = useTheme2(); + const styles = getStyles(theme); + + const getDescriptionContent = (): JSX.Element => { + // description + const panelDescription = typeof description === 'function' ? description() : description; + + return ( +
+
+
+ ); + }; + + return description !== '' ? ( + + + + + + ) : null; +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + description: css({ + color: `${theme.colors.text.secondary}`, + backgroundColor: `${theme.colors.background.primary}`, + cursor: 'auto', + border: 'none', + borderRadius: `${theme.shape.borderRadius()}`, + padding: `${theme.spacing(0, 1)}`, + height: ` ${theme.spacing(theme.components.height.md)}`, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + + '&:focus, &:focus-visible': { + ...getFocusStyles(theme), + zIndex: 1, + }, + '&: focus:not(:focus-visible)': getMouseFocusStyles(theme), + + '&:hover ': { + boxShadow: `${theme.shadows.z1}`, + color: `${theme.colors.text.primary}`, + background: `${theme.colors.background.secondary}`, + }, + + code: { + whiteSpace: 'normal', + wordWrap: 'break-word', + }, + + 'pre > code': { + display: 'block', + }, + }), + }; +}; diff --git a/packages/grafana-ui/src/components/PanelChrome/index.ts b/packages/grafana-ui/src/components/PanelChrome/index.ts index 89f7e995c40..22cf206bfbd 100644 --- a/packages/grafana-ui/src/components/PanelChrome/index.ts +++ b/packages/grafana-ui/src/components/PanelChrome/index.ts @@ -38,6 +38,8 @@ export { type ErrorIndicatorProps as PanelChromeErrorIndicatorProps, } from './ErrorIndicator'; +export { PanelDescription } from './PanelDescription'; + export { usePanelContext, PanelContextProvider, type PanelContext, PanelContextRoot } from './PanelContext'; export * from './types'; diff --git a/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx b/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx index 239002b718b..b5e3a78cab7 100644 --- a/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx +++ b/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx @@ -58,6 +58,7 @@ export const Tooltip = React.memo(({ children, theme, interactive, show, placeme <> {React.cloneElement(children, { ref: setTriggerRef, + tabIndex: 0, // tooltip should be keyboard focusable })} {visible && ( diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderNotice.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderNotice.tsx index 18e13ca1ccc..a59680e1290 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderNotice.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderNotice.tsx @@ -1,7 +1,9 @@ +import { css } from '@emotion/css'; import React, { FC } from 'react'; -import { QueryResultMetaNotice } from '@grafana/data'; -import { Icon, Tooltip } from '@grafana/ui'; +import { GrafanaTheme2, QueryResultMetaNotice } from '@grafana/data'; +import { Icon, ToolbarButton, Tooltip, useStyles2 } from '@grafana/ui'; +import { getFocusStyles, getMouseFocusStyles } from '@grafana/ui/src/themes/mixins'; interface Props { notice: QueryResultMetaNotice; @@ -9,20 +11,67 @@ interface Props { } export const PanelHeaderNotice: FC = ({ notice, onClick }) => { + const styles = useStyles2(getStyles); + const iconName = notice.severity === 'error' || notice.severity === 'warning' ? 'exclamation-triangle' : 'info-circle'; + if (notice.inspect && onClick) { + return ( + onClick(e, notice.inspect!)} + /> + ); + } + + if (notice.link) { + return ( + + + + ); + } + return ( - - {notice.inspect ? ( -
onClick(e, notice.inspect!)}> - -
- ) : ( - - - - )} + + + + ); }; + +const getStyles = (theme: GrafanaTheme2) => ({ + notice: css({ + border: 'none', + borderRadius: theme.shape.borderRadius(), + }), + iconTooltip: css({ + color: `${theme.colors.text.secondary}`, + backgroundColor: `${theme.colors.background.primary}`, + cursor: 'auto', + border: 'none', + borderRadius: `${theme.shape.borderRadius()}`, + padding: `${theme.spacing(0, 1)}`, + height: ` ${theme.spacing(theme.components.height.md)}`, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + + '&:focus, &:focus-visible': { + ...getFocusStyles(theme), + zIndex: 1, + }, + '&: focus:not(:focus-visible)': getMouseFocusStyles(theme), + + '&:hover ': { + boxShadow: `${theme.shadows.z1}`, + color: `${theme.colors.text.primary}`, + background: `${theme.colors.background.secondary}`, + }, + }), +}); diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderTitleItems.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderTitleItems.tsx new file mode 100644 index 00000000000..91967b2ab52 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderTitleItems.tsx @@ -0,0 +1,107 @@ +import { css, cx } from '@emotion/css'; +import React from 'react'; + +import { PanelData, GrafanaTheme2, PanelModel, LinkModel, AlertState, DataLink } from '@grafana/data'; +import { Icon, Tooltip, useStyles2 } from '@grafana/ui'; +import { getFocusStyles, getMouseFocusStyles } from '@grafana/ui/src/themes/mixins'; + +import { PanelLinks } from '../PanelLinks'; + +import { PanelHeaderNotices } from './PanelHeaderNotices'; + +export interface Props { + alertState?: string; + data: PanelData; + panelId: number; + onShowPanelLinks?: () => Array>; + panelLinks?: DataLink[]; +} + +export function PanelHeaderTitleItems(props: Props) { + const { alertState, data, panelId, onShowPanelLinks, panelLinks } = props; + const styles = useStyles2(getStyles); + + // panel health + const alertStateItem = ( + + + + + + ); + + const timeshift = ( + <> + + + + {data.request?.timeInfo} + + + + ); + + return ( + <> + {panelLinks && panelLinks.length > 0 && onShowPanelLinks && ( + + )} + + {} + {data.request && data.request.timeInfo && timeshift} + {alertState && alertStateItem} + + ); +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + item: css({ + label: 'panel-header-item', + backgroundColor: `${theme.colors.background.primary}`, + cursor: 'auto', + border: 'none', + borderRadius: `${theme.shape.borderRadius()}`, + padding: `${theme.spacing(0, 1)}`, + height: `${theme.spacing(theme.components.height.md)}`, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + + '&:focus, &:focus-visible': { + ...getFocusStyles(theme), + zIndex: 1, + }, + '&: focus:not(:focus-visible)': getMouseFocusStyles(theme), + + '&:hover ': { + boxShadow: `${theme.shadows.z1}`, + background: `${theme.colors.background.secondary}`, + }, + }), + ok: css({ + color: theme.colors.success.text, + }), + pending: css({ + color: theme.colors.warning.text, + }), + alerting: css({ + color: theme.colors.error.text, + }), + timeshift: css({ + color: theme.colors.text.link, + + '&:hover': { + color: theme.colors.emphasize(theme.colors.text.link, 0.03), + }, + }), + }; +}; diff --git a/public/app/features/dashboard/dashgrid/PanelLinks.tsx b/public/app/features/dashboard/dashgrid/PanelLinks.tsx new file mode 100644 index 00000000000..706d29fa0ba --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelLinks.tsx @@ -0,0 +1,77 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { DataLink, GrafanaTheme2, LinkModel } from '@grafana/data'; +import { Dropdown, Icon, Menu, ToolbarButton, useStyles2 } from '@grafana/ui'; +import { getFocusStyles, getMouseFocusStyles } from '@grafana/ui/src/themes/mixins'; + +interface Props { + panelLinks: DataLink[]; + onShowPanelLinks: () => LinkModel[]; +} + +export function PanelLinks({ panelLinks, onShowPanelLinks }: Props) { + const styles = useStyles2(getStyles); + + const getLinksContent = (): JSX.Element => { + const interpolatedLinks = onShowPanelLinks(); + return ( + + {interpolatedLinks?.map((link, idx) => { + return ; + })} + + ); + }; + + if (panelLinks.length === 1) { + const linkModel = onShowPanelLinks()[0]; + return ( + + + + ); + } else { + return ( + + + + ); + } +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + menuTrigger: css({ + border: 'none', + borderRadius: `${theme.shape.borderRadius()}`, + cursor: 'context-menu', + }), + singleLink: css({ + color: theme.colors.text.secondary, + padding: `${theme.spacing(0, 1)}`, + height: ` ${theme.spacing(theme.components.height.md)}`, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + + '&:focus, &:focus-visible': { + ...getFocusStyles(theme), + zIndex: 1, + }, + '&: focus:not(:focus-visible)': getMouseFocusStyles(theme), + + '&:hover ': { + boxShadow: `${theme.shadows.z1}`, + color: `${theme.colors.text.primary}`, + background: `${theme.colors.background.secondary}`, + }, + }), + }; +}; diff --git a/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx b/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx index a6aadfd2a05..0a0c3df1643 100644 --- a/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx +++ b/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx @@ -11,17 +11,19 @@ import { EventFilterOptions, FieldConfigSource, getDefaultTimeRange, + LinkModel, LoadingState, PanelData, PanelPlugin, PanelPluginMeta, PluginContextProvider, + renderMarkdown, TimeRange, toDataFrameDTO, toUtc, } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { config, locationService, RefreshEvent } from '@grafana/runtime'; +import { getTemplateSrv, config, locationService, RefreshEvent } from '@grafana/runtime'; import { VizLegendOptions } from '@grafana/schema'; import { ErrorBoundary, @@ -35,6 +37,7 @@ import { PANEL_BORDER } from 'app/core/constants'; import { profiler } from 'app/core/profiler'; import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel'; import { InspectTab } from 'app/features/inspector/types'; +import { getPanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers'; import { changeSeriesColorConfigFactory } from 'app/plugins/panel/timeseries/overrides/colorSeriesConfigFactory'; import { RenderEvent } from 'app/types/events'; @@ -47,6 +50,7 @@ import { loadSnapshotData } from '../utils/loadSnapshotData'; import { PanelHeader } from './PanelHeader/PanelHeader'; import { PanelHeaderMenuWrapper } from './PanelHeader/PanelHeaderMenuWrapper'; +import { PanelHeaderTitleItems } from './PanelHeader/PanelHeaderTitleItems'; import { seriesVisibilityConfigFactory } from './SeriesVisibilityConfigFactory'; import { liveTimer } from './liveTimer'; @@ -567,6 +571,27 @@ export class PanelStateWrapper extends PureComponent { return !panel.hasTitle(); } + onShowPanelDescription = () => { + const { panel } = this.props; + const descriptionMarkdown = getTemplateSrv().replace(panel.description, panel.scopedVars); + const interpolatedDescription = renderMarkdown(descriptionMarkdown); + return interpolatedDescription; + }; + + onShowPanelLinks = (): LinkModel[] => { + const { panel } = this.props; + const linkSupplier = getPanelLinksSupplier(panel); + if (linkSupplier) { + const panelLinks = linkSupplier && linkSupplier.getLinks(panel.replaceVariables); + return panelLinks; + } + return []; + }; + + onOpenInspector = (e: React.SyntheticEvent, tab: string) => { + e.stopPropagation(); + locationService.partial({ inspect: this.props.panel.id, inspectTab: tab }); + }; onOpenErrorInspect(e: React.SyntheticEvent, tab: string) { e.stopPropagation(); locationService.partial({ inspect: this.props.panel.id, inspectTab: tab }); @@ -590,6 +615,17 @@ export class PanelStateWrapper extends PureComponent { const title = panel.getDisplayTitle(); const padding: PanelPadding = plugin.noPadding ? 'none' : 'md'; + const titleItems = [ + , + ]; + let menu; if (!dashboard.meta.publicDashboardAccessToken) { menu = ( @@ -610,14 +646,16 @@ export class PanelStateWrapper extends PureComponent { this.onOpenErrorInspect(e, InspectTab.Error), }} + description={!!panel.description ? this.onShowPanelDescription : undefined} + titleItems={titleItems} + menu={menu} + padding={padding} > {(innerWidth, innerHeight) => ( <> diff --git a/public/img/icons/mono/circle-mono.svg b/public/img/icons/mono/circle-mono.svg new file mode 100644 index 00000000000..4f6385be522 --- /dev/null +++ b/public/img/icons/mono/circle-mono.svg @@ -0,0 +1 @@ + diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 983fb298314..21964cfc999 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -574,4 +574,4 @@ "option-tooltip": "Cľęäř şęľęčŧįőʼnş" } } -} \ No newline at end of file +} From 82d8b2036ff80f146e609bc93e42ff588ebbe504 Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Mon, 16 Jan 2023 17:20:19 +0100 Subject: [PATCH 21/65] Logs: Add possibility to download logs in JSON format (#61394) * add implementation of `logRowsToReadableJson` * add test for logRowsToReadableJson * add json, txt download buttons * changed downloadmenu to `Menu` * set closed state when menu closes * removed unused css * removed unused imports * remove isOpen state * remove unused import * add tests * remove untouched file --- .../app/features/explore/LogsMetaRow.test.tsx | 186 ++++++++++++++++++ public/app/features/explore/LogsMetaRow.tsx | 45 ++++- .../app/features/logs/components/logParser.ts | 2 +- public/app/features/logs/utils.test.ts | 56 ++++++ public/app/features/logs/utils.ts | 20 ++ 5 files changed, 300 insertions(+), 9 deletions(-) create mode 100644 public/app/features/explore/LogsMetaRow.test.tsx diff --git a/public/app/features/explore/LogsMetaRow.test.tsx b/public/app/features/explore/LogsMetaRow.test.tsx new file mode 100644 index 00000000000..4096e2ab5ed --- /dev/null +++ b/public/app/features/explore/LogsMetaRow.test.tsx @@ -0,0 +1,186 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import saveAs from 'file-saver'; +import React, { ComponentProps } from 'react'; + +import { LogLevel, LogsDedupStrategy, MutableDataFrame } from '@grafana/data'; + +import { MAX_CHARACTERS } from '../logs/components/LogRowMessage'; +import { logRowsToReadableJson } from '../logs/utils'; + +import { LogsMetaRow } from './LogsMetaRow'; + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + reportInteraction: () => null, +})); + +jest.mock('file-saver', () => jest.fn()); + +type LogsMetaRowProps = ComponentProps; +const defaultProps: LogsMetaRowProps = { + meta: [], + dedupStrategy: LogsDedupStrategy.none, + dedupCount: 0, + displayedFields: [], + hasUnescapedContent: false, + forceEscape: false, + logRows: [], + onEscapeNewlines: jest.fn(), + clearDetectedFields: jest.fn(), +}; + +const setup = (propOverrides?: object) => { + const props = { + ...defaultProps, + ...propOverrides, + }; + + return render(); +}; + +describe('LogsMetaRow', () => { + it('renders the dedupe number', async () => { + setup({ dedupStrategy: LogsDedupStrategy.numbers, dedupCount: 1234 }); + expect(await screen.findByText('1234')).toBeInTheDocument(); + }); + + it('renders a highlighting warning', async () => { + setup({ logRows: [{ entry: 'A'.repeat(MAX_CHARACTERS + 1) }] }); + expect( + await screen.findByText('Logs with more than 100,000 characters could not be parsed and highlighted') + ).toBeInTheDocument(); + }); + + it('renders the show original line button', () => { + setup({ displayedFields: ['test'] }); + expect( + screen.getByRole('button', { + name: 'Show original line', + }) + ).toBeInTheDocument(); + }); + + it('renders the displayedfield', async () => { + setup({ displayedFields: ['testField1234'] }); + expect(await screen.findByText('testField1234')).toBeInTheDocument(); + }); + + it('renders a button to clear displayedfields', () => { + const clearSpy = jest.fn(); + setup({ displayedFields: ['testField1234'], clearDetectedFields: clearSpy }); + fireEvent( + screen.getByRole('button', { + name: 'Show original line', + }), + new MouseEvent('click', { + bubbles: true, + cancelable: true, + }) + ); + expect(clearSpy).toBeCalled(); + }); + + it('renders a button to remove escaping', () => { + setup({ hasUnescapedContent: true, forceEscape: true }); + expect( + screen.getByRole('button', { + name: 'Remove escaping', + }) + ).toBeInTheDocument(); + }); + + it('renders a button to remove escaping', () => { + setup({ hasUnescapedContent: true, forceEscape: false }); + expect( + screen.getByRole('button', { + name: 'Escape newlines', + }) + ).toBeInTheDocument(); + }); + + it('renders a button to remove escaping', () => { + const escapeSpy = jest.fn(); + setup({ hasUnescapedContent: true, forceEscape: false, onEscapeNewlines: escapeSpy }); + fireEvent( + screen.getByRole('button', { + name: 'Escape newlines', + }), + new MouseEvent('click', { + bubbles: true, + cancelable: true, + }) + ); + expect(escapeSpy).toBeCalled(); + }); + + it('renders a button to show the download menu', () => { + setup(); + expect(screen.getByText('Download').closest('button')).toBeInTheDocument(); + }); + + it('renders a button to show the download menu', async () => { + setup(); + + expect(screen.queryAllByText('txt')).toHaveLength(0); + await userEvent.click(screen.getByText('Download').closest('button')!); + expect( + screen.getByRole('menuitem', { + name: 'txt', + }) + ).toBeInTheDocument(); + }); + + it('renders a button to download txt', async () => { + setup(); + + await userEvent.click(screen.getByText('Download').closest('button')!); + + await userEvent.click( + screen.getByRole('menuitem', { + name: 'txt', + }) + ); + + expect(saveAs).toBeCalled(); + }); + + it('renders a button to download json', async () => { + const rows = [ + { + rowIndex: 1, + entryFieldIndex: 0, + dataFrame: new MutableDataFrame(), + entry: 'test entry', + hasAnsi: false, + hasUnescapedContent: false, + labels: { + foo: 'bar', + }, + logLevel: LogLevel.info, + raw: '', + timeEpochMs: 10, + timeEpochNs: '123456789', + timeFromNow: '', + timeLocal: '', + timeUtc: '', + uid: '2', + }, + ]; + setup({ logRows: rows }); + + await userEvent.click(screen.getByText('Download').closest('button')!); + + await userEvent.click( + screen.getByRole('menuitem', { + name: 'json', + }) + ); + + expect(saveAs).toBeCalled(); + const blob = (saveAs as unknown as jest.Mock).mock.lastCall[0]; + expect(blob.type).toBe('application/json;charset=utf-8'); + const text = await blob.text(); + expect(text).toBe(JSON.stringify(logRowsToReadableJson(rows))); + }); +}); diff --git a/public/app/features/explore/LogsMetaRow.tsx b/public/app/features/explore/LogsMetaRow.tsx index 88642e197a8..402ed08aefb 100644 --- a/public/app/features/explore/LogsMetaRow.tsx +++ b/public/app/features/explore/LogsMetaRow.tsx @@ -1,13 +1,15 @@ import { css } from '@emotion/css'; +import saveAs from 'file-saver'; import React from 'react'; -import { LogsDedupStrategy, LogsMetaItem, LogsMetaKind, LogRowModel, CoreApp } from '@grafana/data'; +import { LogsDedupStrategy, LogsMetaItem, LogsMetaKind, LogRowModel, CoreApp, dateTimeFormat } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; -import { Button, ToolbarButton, Tooltip, useStyles2 } from '@grafana/ui'; +import { Button, Dropdown, Menu, ToolbarButton, Tooltip, useStyles2 } from '@grafana/ui'; import { downloadLogsModelAsTxt } from '../inspector/utils/download'; import { LogLabels } from '../logs/components/LogLabels'; import { MAX_CHARACTERS } from '../logs/components/LogRowMessage'; +import { logRowsToReadableJson } from '../logs/utils'; import { MetaInfoText, MetaItemProps } from './MetaInfoText'; @@ -31,6 +33,11 @@ export type Props = { clearDetectedFields: () => void; }; +enum DownloadFormat { + Text = 'text', + Json = 'json', +} + export const LogsMetaRow = React.memo( ({ meta, @@ -45,13 +52,27 @@ export const LogsMetaRow = React.memo( }: Props) => { const style = useStyles2(getStyles); - const downloadLogs = () => { + const downloadLogs = (format: DownloadFormat) => { reportInteraction('grafana_logs_download_logs_clicked', { app: CoreApp.Explore, - format: 'logs', + format, area: 'logs-meta-row', }); - downloadLogsModelAsTxt({ meta, rows: logRows }, 'Explore'); + + switch (format) { + case DownloadFormat.Text: + downloadLogsModelAsTxt({ meta, rows: logRows }, 'Explore'); + break; + case DownloadFormat.Json: + const jsonLogs = logRowsToReadableJson(logRows); + const blob = new Blob([JSON.stringify(jsonLogs)], { + type: 'application/json;charset=utf-8', + }); + + const fileName = `Explore-logs-${dateTimeFormat(new Date())}.json`; + saveAs(blob, fileName); + break; + } }; const logsMetaItem: Array = [...meta]; @@ -107,6 +128,12 @@ export const LogsMetaRow = React.memo( ), }); } + const downloadMenu = ( + + downloadLogs(DownloadFormat.Text)} /> + downloadLogs(DownloadFormat.Json)} /> + + ); return ( <> {logsMetaItem && ( @@ -119,9 +146,11 @@ export const LogsMetaRow = React.memo( }; })} /> - - Download logs - + + + Download + +
)} diff --git a/public/app/features/logs/components/logParser.ts b/public/app/features/logs/components/logParser.ts index d32db19ff51..22359e26080 100644 --- a/public/app/features/logs/components/logParser.ts +++ b/public/app/features/logs/components/logParser.ts @@ -27,7 +27,7 @@ export const getAllFields = memoizeOne( /** * creates fields from the dataframe-fields, adding data-links, when field.config.links exists */ -const getDataframeFields = memoizeOne( +export const getDataframeFields = memoizeOne( ( row: LogRowModel, getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array> diff --git a/public/app/features/logs/utils.test.ts b/public/app/features/logs/utils.test.ts index b6e3e9f290f..fed857ac22d 100644 --- a/public/app/features/logs/utils.test.ts +++ b/public/app/features/logs/utils.test.ts @@ -7,6 +7,7 @@ import { getLogLevelFromKey, sortLogsResult, checkLogsError, + logRowsToReadableJson, } from './utils'; describe('getLoglevel()', () => { @@ -205,3 +206,58 @@ describe('checkLogsError()', () => { expect(checkLogsError(log)).toStrictEqual({ hasError: true, errorMessage: 'Error Message' }); }); }); + +describe('logRowsToReadableJson', () => { + const testRow: LogRowModel = { + rowIndex: 1, + entryFieldIndex: 0, + dataFrame: new MutableDataFrame(), + entry: 'test entry', + hasAnsi: false, + hasUnescapedContent: false, + labels: { + foo: 'bar', + }, + logLevel: LogLevel.info, + raw: '', + timeEpochMs: 10, + timeEpochNs: '123456789', + timeFromNow: '', + timeLocal: '', + timeUtc: '', + uid: '2', + }; + const testDf = new MutableDataFrame(); + testDf.addField({ name: 'foo2', values: ['bar2'] }); + const testRow2: LogRowModel = { + rowIndex: 0, + entryFieldIndex: -1, + dataFrame: testDf, + entry: 'test entry', + hasAnsi: false, + hasUnescapedContent: false, + labels: { + foo: 'bar', + }, + logLevel: LogLevel.info, + raw: '', + timeEpochMs: 10, + timeEpochNs: '123456789', + timeFromNow: '', + timeLocal: '', + timeUtc: '', + uid: '2', + }; + + it('should format a single row', () => { + const result = logRowsToReadableJson([testRow]); + + expect(result).toEqual([{ line: 'test entry', timestamp: '123456789', fields: { foo: 'bar' } }]); + }); + + it('should format a df field row', () => { + const result = logRowsToReadableJson([testRow2]); + + expect(result).toEqual([{ line: 'test entry', timestamp: '123456789', fields: { foo: 'bar', foo2: 'bar2' } }]); + }); +}); diff --git a/public/app/features/logs/utils.ts b/public/app/features/logs/utils.ts index 298003d419c..66319a9931e 100644 --- a/public/app/features/logs/utils.ts +++ b/public/app/features/logs/utils.ts @@ -2,6 +2,8 @@ import { countBy, chain } from 'lodash'; import { LogLevel, LogRowModel, LogLabelStatsModel, LogsModel, LogsSortOrder } from '@grafana/data'; +import { getDataframeFields } from './components/logParser'; + /** * Returns the log level of a log line. * Parse the line for level words. If no level is found, it returns `LogLevel.unknown`. @@ -129,3 +131,21 @@ export const checkLogsError = (logRow: LogRowModel): { hasError: boolean; errorM export const escapeUnescapedString = (string: string) => string.replace(/\\r\\n|\\n|\\t|\\r/g, (match: string) => (match.slice(1) === 't' ? '\t' : '\n')); + +export function logRowsToReadableJson(logs: LogRowModel[]) { + return logs.map((log) => { + const fields = getDataframeFields(log).reduce>((acc, field) => { + acc[field.key] = field.value; + return acc; + }, {}); + + return { + line: log.entry, + timestamp: log.timeEpochNs, + fields: { + ...fields, + ...log.labels, + }, + }; + }); +} From 6fec8fda39b4343b65cd0be380328150e1bfb0aa Mon Sep 17 00:00:00 2001 From: Jo Date: Mon, 16 Jan 2023 16:37:04 +0000 Subject: [PATCH 22/65] AuthN: Clean errors in user/org sync (#61560) * clean errors in user/org sync * lower logging level for non 5xx errors --- pkg/models/context.go | 12 ++++-- .../authn/authnimpl/usersync/orgsync.go | 30 +++++++------- .../authn/authnimpl/usersync/usersync.go | 40 ++++++++++++++----- 3 files changed, 55 insertions(+), 27 deletions(-) diff --git a/pkg/models/context.go b/pkg/models/context.go index 432dee40e99..eaf1bbf2871 100644 --- a/pkg/models/context.go +++ b/pkg/models/context.go @@ -1,6 +1,7 @@ package models import ( + "net/http" "strings" "github.com/grafana/grafana/pkg/infra/log" @@ -60,16 +61,21 @@ func (ctx *ReqContext) JsonApiErr(status int, message string, err error) { if err != nil { resp["traceID"] = traceID - ctx.Logger.Error(message, "error", err, "traceID", traceID) + if status == http.StatusInternalServerError { + ctx.Logger.Error(message, "error", err, "traceID", traceID) + } else { + ctx.Logger.Warn(message, "error", err, "traceID", traceID) + } + if setting.Env != setting.Prod { resp["error"] = err.Error() } } switch status { - case 404: + case http.StatusNotFound: resp["message"] = "Not Found" - case 500: + case http.StatusInternalServerError: resp["message"] = "Internal Server Error" } diff --git a/pkg/services/authn/authnimpl/usersync/orgsync.go b/pkg/services/authn/authnimpl/usersync/orgsync.go index e51745f29e8..2c24e0438e0 100644 --- a/pkg/services/authn/authnimpl/usersync/orgsync.go +++ b/pkg/services/authn/authnimpl/usersync/orgsync.go @@ -3,10 +3,8 @@ package usersync import ( "context" "errors" - "fmt" "sort" - "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/authn" @@ -28,26 +26,27 @@ type OrgSync struct { func (s *OrgSync) SyncOrgUser(ctx context.Context, id *authn.Identity, _ *authn.Request) error { if !id.ClientParams.SyncUser { - s.log.Debug("Not syncing org user", "auth_module", id.AuthModule, "auth_id", id.AuthID) return nil } namespace, userID := id.NamespacedID() - if namespace != "user" && userID <= 0 { - return fmt.Errorf("invalid namespace %q for user ID %q", namespace, userID) + if namespace != "user" || userID <= 0 { + s.log.Warn("invalid namespace %q for user ID %q", namespace, userID) + return nil } - s.log.Debug("Syncing organization roles", "id", userID, "extOrgRoles", id.OrgRoles) + s.log.Debug("syncing organization roles", "id", userID, "extOrgRoles", id.OrgRoles) // don't sync org roles if none is specified if len(id.OrgRoles) == 0 { - s.log.Debug("Not syncing organization roles since external user doesn't have any") + s.log.Debug("not syncing organization roles since external user doesn't have any") return nil } orgsQuery := &org.GetUserOrgListQuery{UserID: userID} result, err := s.orgService.GetUserOrgList(ctx, orgsQuery) if err != nil { - return err + s.log.Error("failed to get user's organizations", "userId", userID, "error", err) + return nil } handledOrgIds := map[int64]bool{} @@ -64,7 +63,8 @@ func (s *OrgSync) SyncOrgUser(ctx context.Context, id *authn.Identity, _ *authn. // update role cmd := &org.UpdateOrgUserCommand{OrgID: orga.OrgID, UserID: userID, Role: extRole} if err := s.orgService.UpdateOrgUser(ctx, cmd); err != nil { - return err + s.log.Error("failed to update active org user", "userId", userID, "error", err) + return nil } } } @@ -81,7 +81,8 @@ func (s *OrgSync) SyncOrgUser(ctx context.Context, id *authn.Identity, _ *authn. cmd := &org.AddOrgUserCommand{UserID: userID, Role: orgRole, OrgID: orgId} err := s.orgService.AddOrgUser(ctx, cmd) if err != nil && !errors.Is(err, org.ErrOrgNotFound) { - return err + s.log.Error("failed to update active org user", "userId", userID, "error", err) + return nil } } @@ -92,16 +93,17 @@ func (s *OrgSync) SyncOrgUser(ctx context.Context, id *authn.Identity, _ *authn. cmd := &org.RemoveOrgUserCommand{OrgID: orgId, UserID: userID} if err := s.orgService.RemoveOrgUser(ctx, cmd); err != nil { if errors.Is(err, org.ErrLastOrgAdmin) { - logger.Error(err.Error(), "userId", cmd.UserID, "orgId", cmd.OrgID) + s.log.Error(err.Error(), "userId", cmd.UserID, "orgId", cmd.OrgID) continue } - return err + s.log.Error("failed to delete user org membership", "userId", userID, "error", err) + return nil } if err := s.accessControl.DeleteUserPermissions(ctx, orgId, cmd.UserID); err != nil { - logger.Error("failed to delete permissions for user", "error", err, "userID", cmd.UserID, "orgID", orgId) - return err + s.log.Error("failed to delete permissions for user", "error", err, "userID", cmd.UserID, "orgID", orgId) + return nil } } diff --git a/pkg/services/authn/authnimpl/usersync/usersync.go b/pkg/services/authn/authnimpl/usersync/usersync.go index 0736cc2891a..a292f6c2f2d 100644 --- a/pkg/services/authn/authnimpl/usersync/usersync.go +++ b/pkg/services/authn/authnimpl/usersync/usersync.go @@ -16,8 +16,12 @@ import ( ) var ( + errSyncUserForbidden = errutil.NewBase(errutil.StatusForbidden, + "user.sync.forbidden", errutil.WithPublicMessage("User sync forbidden")) + errSyncUserInternal = errutil.NewBase(errutil.StatusInternal, + "user.sync.forbidden", errutil.WithPublicMessage("User sync failed")) errUserProtection = errutil.NewBase(errutil.StatusForbidden, - "user.sync.protected role", errutil.WithPublicMessage("Unable to sync due to protected role")) + "user.sync.protectedrole", errutil.WithPublicMessage("Unable to sync due to protected role")) ) func ProvideUserSync(userService user.Service, @@ -49,14 +53,18 @@ func (s *UserSync) SyncUser(ctx context.Context, id *authn.Identity, _ *authn.Re // Does user exist in the database? usr, errUserInDB := s.UserInDB(ctx, &id.AuthModule, &id.AuthID, id.ClientParams.LookUpParams) if errUserInDB != nil && !errors.Is(errUserInDB, user.ErrUserNotFound) { - return errUserInDB + s.log.Error("error retrieving user", "error", errUserInDB, + "auth_module", id.AuthModule, "auth_id", id.AuthID, + "lookup_params", id.ClientParams.LookUpParams, + ) + return errSyncUserInternal.Errorf("unable to retrieve user") } if errors.Is(errUserInDB, user.ErrUserNotFound) { if !id.ClientParams.AllowSignUp { - s.log.Warn("Not allowing login, user not found in internal user database and allow signup = false", + s.log.Warn("not allowing login, user not found in internal user database and allow signup = false", "auth_module", id.AuthModule) - return login.ErrSignupNotAllowed + return errSyncUserForbidden.Errorf("%w", login.ErrSignupNotAllowed) } // quota check (FIXME: (jguer) this should be done in the user service) @@ -65,11 +73,11 @@ func (s *UserSync) SyncUser(ctx context.Context, id *authn.Identity, _ *authn.Re for _, srv := range []string{user.QuotaTargetSrv, org.QuotaTargetSrv} { limitReached, errLimit := s.quotaService.CheckQuotaReached(ctx, quota.TargetSrv(srv), nil) if errLimit != nil { - s.log.Warn("error getting user quota.", "error", errLimit) - return login.ErrGettingUserQuota + s.log.Error("error getting user quota", "error", errLimit) + return errSyncUserInternal.Errorf("%w", login.ErrGettingUserQuota) } if limitReached { - return login.ErrUsersQuotaReached + return errSyncUserForbidden.Errorf("%w", login.ErrUsersQuotaReached) } } @@ -77,7 +85,11 @@ func (s *UserSync) SyncUser(ctx context.Context, id *authn.Identity, _ *authn.Re var errCreate error usr, errCreate = s.createUser(ctx, id) if errCreate != nil { - return errCreate + s.log.Error("error creating user", "error", errCreate, + "auth_module", id.AuthModule, "auth_id", id.AuthID, + "id_login", id.Login, "id_email", id.Email, + ) + return errSyncUserInternal.Errorf("unable to create user") } } @@ -87,14 +99,22 @@ func (s *UserSync) SyncUser(ctx context.Context, id *authn.Identity, _ *authn.Re // update user if errUpdate := s.updateUserAttributes(ctx, usr, id); errUpdate != nil { - return errUpdate + s.log.Error("error creating user", "error", errUpdate, + "auth_module", id.AuthModule, "auth_id", id.AuthID, + "login", usr.Login, "email", usr.Email, + "id_login", id.Login, "id_email", id.Email, + ) + return errSyncUserInternal.Errorf("unable to update user") } syncUserToIdentity(usr, id) // persist latest auth info token if errAuthInfo := s.updateAuthInfo(ctx, id); errAuthInfo != nil { - return errAuthInfo + s.log.Error("error creating user", "error", errAuthInfo, + "auth_module", id.AuthModule, "auth_id", id.AuthID, + ) + return errSyncUserInternal.Errorf("unable to update auth info") } return nil From 7a026d90e912bfc14a1cddea292a647ace8c3ea8 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Mon, 16 Jan 2023 17:47:27 +0100 Subject: [PATCH 23/65] AzureMonitor: Set multiple resources for Logs (#61545) --- .../__mocks__/query.ts | 2 +- .../azure_log_analytics_datasource.test.ts | 24 +++++++++---------- .../azure_log_analytics_datasource.ts | 8 +++---- .../azure_monitor_datasource.test.ts | 2 +- .../LogsQueryEditor/LogsQueryEditor.tsx | 6 ++++- .../components/LogsQueryEditor/QueryField.tsx | 6 ++--- .../LogsQueryEditor/useMigrations.ts | 2 +- .../components/ResourcePicker/utils.test.ts | 2 +- .../components/ResourcePicker/utils.ts | 2 +- .../grafanaTemplateVariableFns.ts | 2 +- .../grafanaTemplateVariables.test.ts | 2 +- .../types/query.ts | 5 +++- .../utils/migrateAnnotation.test.ts | 3 ++- .../utils/migrateAnnotation.ts | 2 +- .../utils/migrateQuery.test.ts | 12 ++++++++++ .../utils/migrateQuery.ts | 16 +++++++++++++ 16 files changed, 66 insertions(+), 30 deletions(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query.ts index 12d14c1f449..4b8d43f11a8 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query.ts @@ -17,7 +17,7 @@ export default function createMockQuery(overrides?: Partial): '//change this example to create your own time series query\n //the table to query (e.g. Usage, Heartbeat, Perf)\n| where $__timeFilter(TimeGenerated) //this is a macro used to show the full chart’s time range, choose the datetime column here\n| summarize count() by , bin(TimeGenerated, $__interval) //change “group by column” to a column in your table, such as “Computer”. The $__interval macro is used to auto-select the time grain. Can also use 1h, 5m etc.\n| order by TimeGenerated asc', resultFormat: 'time_series', workspace: 'e3fe4fde-ad5e-4d60-9974-e2f3562ffdf2', - resource: 'test-resource', + resources: ['test-resource'], ...overrides?.azureLogAnalytics, }, diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts index 3d56a7be3b5..3b4a11b5bf3 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts @@ -1,5 +1,3 @@ -import { get, set } from 'lodash'; - import { toUtc } from '@grafana/data'; import { TemplateSrv } from 'app/features/templating/template_srv'; @@ -237,7 +235,7 @@ describe('AzureLogAnalyticsDatasource', () => { const ds = new AzureMonitorDatasource(ctx.instanceSettings, templateSrv); query.queryType = AzureQueryType.LogAnalytics; - query.azureLogAnalytics = { resource: `$${singleVariable.name}` }; + query.azureLogAnalytics = { resources: [`$${singleVariable.name}`] }; expect(ds.targetContainsTemplate(query)).toEqual(true); }); @@ -270,7 +268,7 @@ describe('AzureLogAnalyticsDatasource', () => { const query: AzureMonitorQuery = { refId: 'A', azureLogAnalytics: { - resource: '/sub/124/rg/cloud/vm/server', + resources: ['/sub/124/rg/cloud/vm/server'], query: 'perf | take 100', }, }; @@ -303,7 +301,7 @@ describe('AzureLogAnalyticsDatasource', () => { refId: 'A', hide: true, azureLogAnalytics: { - resource: '/sub/124/rg/cloud/vm/server', + resources: ['/sub/124/rg/cloud/vm/server'], query: 'perf | take 100', }, }; @@ -315,7 +313,7 @@ describe('AzureLogAnalyticsDatasource', () => { const query: AzureMonitorQuery = { refId: 'A', azureLogAnalytics: { - resource: '/sub/124/rg/cloud/vm/server', + resources: ['/sub/124/rg/cloud/vm/server'], }, }; @@ -352,9 +350,9 @@ describe('AzureLogAnalyticsDatasource', () => { templateSrv.init(Array.from(templateVariables.values()).map((item) => item.templateVariable)); const query = createMockQuery(); const azureLogAnalytics: { [index: string]: any } = {}; - for (const [path, templateVariable] of templateVariables.entries()) { - set(azureLogAnalytics, path, `$${templateVariable.variableName}`); - } + azureLogAnalytics.query = '$query'; + azureLogAnalytics.workspace = '$workspace'; + azureLogAnalytics.resources = ['$resource']; query.queryType = AzureQueryType.LogAnalytics; query.azureLogAnalytics = { ...query.azureLogAnalytics, @@ -362,9 +360,11 @@ describe('AzureLogAnalyticsDatasource', () => { }; const templatedQuery = ctx.ds.interpolateVariablesInQueries([query], {}); expect(templatedQuery[0]).toHaveProperty('datasource'); - for (const [path, templateVariable] of templateVariables.entries()) { - expect(get(templatedQuery[0].azureLogAnalytics, path)).toEqual(templateVariable.templateVariable.current.value); - } + expect(templatedQuery[0].azureLogAnalytics).toMatchObject({ + query: templateVariables.get('query')?.templateVariable.current.value, + workspace: templateVariables.get('workspace')?.templateVariable.current.value, + resources: [templateVariables.get('resource')?.templateVariable.current.value], + }); }); }); }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts index 0fb89e1060c..42984e611b8 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts @@ -66,7 +66,7 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend< return ( item.hide !== true && !!item.azureLogAnalytics?.query && - (!!item.azureLogAnalytics.resource || !!item.azureLogAnalytics.workspace) + (!!item.azureLogAnalytics.resources?.length || !!item.azureLogAnalytics.workspace) ); } @@ -124,10 +124,10 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend< } const templateSrv = getTemplateSrv(); - const resource = templateSrv.replace(item.resource, scopedVars); + const resources = item.resources?.map((r) => templateSrv.replace(r, scopedVars)); let workspace = templateSrv.replace(item.workspace, scopedVars); - if (!workspace && !resource && this.firstWorkspace) { + if (!workspace && !resources && this.firstWorkspace) { workspace = this.firstWorkspace; } @@ -140,7 +140,7 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend< azureLogAnalytics: { resultFormat: item.resultFormat, query, - resource, + resources, // Workspace was removed in Grafana 8, but remains for backwards compat workspace, diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts index 622bdfc88ea..98934049c87 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts @@ -924,7 +924,7 @@ describe('AzureMonitorDatasource', () => { const ds = new AzureMonitorDatasource(ctx.instanceSettings, templateSrv); query.queryType = AzureQueryType.AzureMonitor; - query.azureLogAnalytics = { resource: `$${singleVariable.name}` }; + query.azureLogAnalytics = { resources: [`$${singleVariable.name}`] }; expect(ds.targetContainsTemplate(query)).toEqual(false); }); }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx index e924cce236b..f6c612caffe 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx @@ -53,7 +53,11 @@ const LogsQueryEditor: React.FC = ({ ResourceRowType.Resource, ResourceRowType.Variable, ]} - resource={query.azureLogAnalytics?.resource ?? ''} + resource={ + query.azureLogAnalytics?.resources && query.azureLogAnalytics.resources.length + ? query.azureLogAnalytics.resources[0] + : '' + } queryType="logs" /> diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/QueryField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/QueryField.tsx index 46fceccb2d9..c0ba93b623d 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/QueryField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/QueryField.tsx @@ -33,12 +33,12 @@ const QueryField: React.FC = ({ query, datasource, o } useEffect(() => { - if (!query.azureLogAnalytics?.resource) { + if (!query.azureLogAnalytics?.resources || !query.azureLogAnalytics.resources.length) { return; } const promises = [ - datasource.azureLogAnalyticsDatasource.getKustoSchema(query.azureLogAnalytics.resource), + datasource.azureLogAnalyticsDatasource.getKustoSchema(query.azureLogAnalytics.resources[0]), getPromise(), ] as const; @@ -56,7 +56,7 @@ const QueryField: React.FC = ({ query, datasource, o worker?.setSchema(schema, 'https://help.kusto.windows.net', 'Samples'); }); }); - }, [datasource.azureLogAnalyticsDatasource, query.azureLogAnalytics?.resource]); + }, [datasource.azureLogAnalyticsDatasource, query.azureLogAnalytics?.resources]); const handleEditorMount = useCallback((editor: MonacoEditor, monaco: Monaco) => { monacoPromiseRef.current?.resolve?.({ editor, monaco }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/useMigrations.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/useMigrations.ts index 33ba224000e..9c5beb75421 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/useMigrations.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/useMigrations.ts @@ -9,7 +9,7 @@ async function migrateWorkspaceQueryToResourceQuery( query: AzureMonitorQuery, onChange: (newQuery: AzureMonitorQuery) => void ) { - if (query.azureLogAnalytics?.workspace !== undefined && !query.azureLogAnalytics.resource) { + if (query.azureLogAnalytics?.workspace !== undefined && !query.azureLogAnalytics.resources) { const isWorkspaceGUID = isGUIDish(query.azureLogAnalytics.workspace); let resource: string; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.test.ts index dbe224d5900..273272d61c4 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.test.ts @@ -166,7 +166,7 @@ describe('AzureMonitor ResourcePicker utils', () => { describe('setResource', () => { it('updates a resource with a resource URI for Log Analytics', () => { expect(setResource(createMockQuery(), '/subscription/sub')).toMatchObject({ - azureLogAnalytics: { resource: '/subscription/sub' }, + azureLogAnalytics: { resources: ['/subscription/sub'] }, }); }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.ts index 37900a6d151..105936d0fa7 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.ts @@ -139,7 +139,7 @@ export function setResource(query: AzureMonitorQuery, resource?: string | AzureM ...query, azureLogAnalytics: { ...query.azureLogAnalytics, - resource, + resources: [resource], }, }; } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/grafanaTemplateVariableFns.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/grafanaTemplateVariableFns.ts index 24b3894d3b3..0ed9de7dd36 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/grafanaTemplateVariableFns.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/grafanaTemplateVariableFns.ts @@ -246,7 +246,7 @@ const createLogAnalyticsTemplateVariableQuery = async ( queryType: AzureQueryType.LogAnalytics, azureLogAnalytics: { query: rawQuery, - resource, + resources: [resource], }, subscription: defaultSubscriptionId, }; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/grafanaTemplateVariables.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/grafanaTemplateVariables.test.ts index 33819e29751..07e78821810 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/grafanaTemplateVariables.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/grafanaTemplateVariables.test.ts @@ -219,7 +219,7 @@ describe('migrateStringQueriesToObjectQueries', () => { queryType: AzureQueryType.LogAnalytics, azureLogAnalytics: { query: 'some kind of kql query', - resource: '', + resources: [''], }, subscription: 'defaultSubscriptionId', }, diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/query.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/query.ts index 6cb1392bdd1..316091d5aa6 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/query.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/query.ts @@ -92,9 +92,12 @@ export interface AzureMetricQuery { export interface AzureLogsQuery { query?: string; resultFormat?: string; - resource?: string; + resources?: string[]; workspace?: string; + + /** @deprecated Use resources instead */ + resource?: string; } /** diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/migrateAnnotation.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/migrateAnnotation.test.ts index c55c9a4e985..bfd74252441 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/migrateAnnotation.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/migrateAnnotation.test.ts @@ -37,8 +37,9 @@ const NEW_ANNOTATION: AnnotationQuery = { queryType: AzureQueryType.LogAnalytics, azureLogAnalytics: { query: 'AzureActivity\r\n| where $__timeFilter() \r\n| project TimeGenerated, Text=OperationName', - resource: + resources: [ '/subscriptions/abc-123-def-456/resourcegroups/our-datasource/providers/microsoft.operationalinsights/workspaces/azureactivitylog', + ], }, }, }; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/migrateAnnotation.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/migrateAnnotation.ts index c630d34805f..31247ec7a78 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/migrateAnnotation.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/migrateAnnotation.ts @@ -20,7 +20,7 @@ export default function migrateAnnotation(annotation: AnnotationQuery { ); }); }); + + it('should migrate a sigle resource for Logs', () => { + const q = { + ...modernMetricsQuery, + azureLogAnalytics: { + ...modernMetricsQuery.azureLogAnalytics, + resource: 'foo', + }, + }; + const result = migrateQuery(q); + expect(result.azureLogAnalytics?.resources).toEqual(['foo']); + }); }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/migrateQuery.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/migrateQuery.ts index bf8a895e69b..69149cc24fe 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/migrateQuery.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/utils/migrateQuery.ts @@ -32,6 +32,10 @@ export default function migrateQuery(query: AzureMonitorQuery): AzureMonitorQuer workingQuery = migrateResourceGroupAndName(workingQuery); } + if (workingQuery.azureLogAnalytics?.resource) { + workingQuery = migrateLogsResource(workingQuery); + } + return workingQuery; } @@ -172,3 +176,15 @@ function migrateResourceGroupAndName(query: AzureMonitorQuery): AzureMonitorQuer return workingQuery; } + +function migrateLogsResource(query: AzureMonitorQuery): AzureMonitorQuery { + let workingQuery = query; + + if (workingQuery.azureLogAnalytics && workingQuery.azureLogAnalytics.resource) { + workingQuery.azureLogAnalytics.resources = [workingQuery.azureLogAnalytics.resource]; + + delete workingQuery.azureLogAnalytics.resource; + } + + return workingQuery; +} From 021eda7aad2e4d29d6c15ff1df578af9dfb394fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 16 Jan 2023 18:01:31 +0100 Subject: [PATCH 24/65] Scenes: Skip instance of check as it it does not work for apps (#61576) * Scenes: Skip instance of check as it it does not work for apps * remove logging --- public/app/features/templating/template_srv.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index a2454b56a35..d7d3ad49de3 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -9,7 +9,7 @@ import { TypedVariableModel, } from '@grafana/data'; import { getDataSourceSrv, setTemplateSrv, TemplateSrv as BaseTemplateSrv } from '@grafana/runtime'; -import { SceneObjectBase, sceneGraph, FormatRegistryID, formatRegistry, CustomFormatterFn } from '@grafana/scenes'; +import { sceneGraph, FormatRegistryID, formatRegistry, CustomFormatterFn } from '@grafana/scenes'; import { variableAdapters } from '../variables/adapters'; import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../variables/constants'; @@ -276,7 +276,7 @@ export class TemplateSrv implements BaseTemplateSrv { } replace(target?: string, scopedVars?: ScopedVars, format?: string | Function): string { - if (scopedVars && scopedVars.__sceneObject && scopedVars.__sceneObject.value instanceof SceneObjectBase) { + if (scopedVars && scopedVars.__sceneObject) { return sceneGraph.interpolate( scopedVars.__sceneObject.value, target, From f135c6cbf1c6477c9f42cc492a63d3fe82262f10 Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Mon, 16 Jan 2023 17:57:12 +0000 Subject: [PATCH 25/65] Cloudmonitor: Refactor query builder (#61410) * Reset filters when service is changed * Update to reset any query props * Reset query properties on metric change * Update tests * Refresh labels on panel time update * Review * Refactor VisualMetricsQueryEditor - Move any Metrics functionality to VisualMetricsQueryEditor - Update tests - Expose timeSrv from datasource - Update getLabels to make use of provided timeRange * Review --- .betterer.results | 3 - .../__mocks__/cloudMonitoringDatasource.ts | 2 + .../components/Metrics.test.tsx | 203 ------------ .../cloud-monitoring/components/Metrics.tsx | 197 ------------ .../VisualMetricQueryEditor.test.tsx | 286 +++++++++++++++++ .../components/VisualMetricQueryEditor.tsx | 300 ++++++++++++++---- .../cloud-monitoring/components/index.ts | 1 - .../datasource/cloud-monitoring/datasource.ts | 13 +- 8 files changed, 528 insertions(+), 477 deletions(-) delete mode 100644 public/app/plugins/datasource/cloud-monitoring/components/Metrics.test.tsx delete mode 100644 public/app/plugins/datasource/cloud-monitoring/components/Metrics.tsx create mode 100644 public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.test.tsx diff --git a/.betterer.results b/.betterer.results index 86ada2c5135..43be61f43d5 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5060,9 +5060,6 @@ exports[`better eslint`] = { "public/app/plugins/datasource/cloud-monitoring/components/MQLQueryEditor.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "public/app/plugins/datasource/cloud-monitoring/components/Metrics.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/plugins/datasource/cloud-monitoring/components/VariableQueryEditor.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/public/app/plugins/datasource/cloud-monitoring/__mocks__/cloudMonitoringDatasource.ts b/public/app/plugins/datasource/cloud-monitoring/__mocks__/cloudMonitoringDatasource.ts index 5ce6325ac66..87c377637bf 100644 --- a/public/app/plugins/datasource/cloud-monitoring/__mocks__/cloudMonitoringDatasource.ts +++ b/public/app/plugins/datasource/cloud-monitoring/__mocks__/cloudMonitoringDatasource.ts @@ -1,3 +1,4 @@ +import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { TemplateSrvMock } from 'app/features/templating/template_srv.mock'; @@ -15,6 +16,7 @@ export const createMockDatasource = (overrides?: Partial) => { templateSrv, getSLOServices: jest.fn().mockResolvedValue([]), migrateQuery: jest.fn().mockImplementation((query) => query), + timeSrv: getTimeSrv(), ...overrides, }; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Metrics.test.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Metrics.test.tsx deleted file mode 100644 index 74973ba7abe..00000000000 --- a/public/app/plugins/datasource/cloud-monitoring/components/Metrics.test.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import { render, screen, within } from '@testing-library/react'; -import React from 'react'; -import { openMenu, select } from 'react-select-event'; - -import { createMockDatasource } from '../__mocks__/cloudMonitoringDatasource'; -import { createMockMetricDescriptor } from '../__mocks__/cloudMonitoringMetricDescriptor'; -import { createMockTimeSeriesList } from '../__mocks__/cloudMonitoringQuery'; - -import { Metrics } from './Metrics'; - -describe('Metrics', () => { - it('renders metrics fields', async () => { - const onChange = jest.fn(); - const query = createMockTimeSeriesList(); - const datasource = createMockDatasource(); - - render( - - {() =>
} - - ); - - expect(await screen.findByLabelText('Service')).toBeInTheDocument(); - expect(await screen.findByLabelText('Metric name')).toBeInTheDocument(); - }); - - it('can select a service', async () => { - const onChange = jest.fn(); - const query = createMockTimeSeriesList(); - const datasource = createMockDatasource({ - getMetricTypes: jest.fn().mockResolvedValue([createMockMetricDescriptor()]), - }); - - render( - - {() =>
} - - ); - - const service = await screen.findByLabelText('Service'); - await openMenu(service); - await select(service, 'Srv', { container: document.body }); - expect(onChange).toBeCalledWith(expect.objectContaining({ service: 'service' })); - }); - - it('can select a metric name', async () => { - const onChange = jest.fn(); - const query = createMockTimeSeriesList(); - const datasource = createMockDatasource({ - getMetricTypes: jest.fn().mockResolvedValue([createMockMetricDescriptor()]), - }); - - render( - - {() =>
} - - ); - - const metricName = await screen.findByLabelText('Metric name'); - await openMenu(metricName); - await select(metricName, 'metricName', { container: document.body }); - expect(onChange).toBeCalledWith(expect.objectContaining({ type: 'type' })); - }); - - it('should render available metric options according to the selected service', async () => { - const onChange = jest.fn(); - const query = createMockTimeSeriesList(); - const datasource = createMockDatasource({ - getMetricTypes: jest.fn().mockResolvedValue([ - createMockMetricDescriptor({ - service: 'service_a', - serviceShortName: 'srv_a', - type: 'metric1', - description: 'description_metric1', - displayName: 'displayName_metric1', - }), - createMockMetricDescriptor({ - service: 'service_b', - serviceShortName: 'srv_b', - type: 'metric2', - description: 'description_metric2', - displayName: 'displayName_metric2', - }), - createMockMetricDescriptor({ - service: 'service_b', - serviceShortName: 'srv_b', - type: 'metric3', - description: 'description_metric3', - displayName: 'displayName_metric3', - }), - ]), - }); - - render( - - {() =>
} - - ); - - const metricName = await screen.findByLabelText('Metric name'); - await openMenu(metricName); - - const metricNameOptions = screen.getByLabelText('Select options menu'); - expect(within(metricNameOptions).getByText('description_metric1')).toBeInTheDocument(); - expect(within(metricNameOptions).getByText('displayName_metric1')).toBeInTheDocument(); - expect(within(metricNameOptions).queryByText('displayName_metric2')).not.toBeInTheDocument(); - expect(within(metricNameOptions).queryByText('description_metric2')).not.toBeInTheDocument(); - expect(within(metricNameOptions).queryByText('displayName_metric3')).not.toBeInTheDocument(); - expect(within(metricNameOptions).queryByText('description_metric3')).not.toBeInTheDocument(); - - await select(screen.getByLabelText('Service'), 'Srv B', { container: document.body }); - expect(within(metricNameOptions).queryByText('displayName_metric1')).not.toBeInTheDocument(); - expect(within(metricNameOptions).queryByText('description_metric1')).not.toBeInTheDocument(); - expect(within(metricNameOptions).getByText('displayName_metric2')).toBeInTheDocument(); - expect(within(metricNameOptions).getByText('description_metric2')).toBeInTheDocument(); - expect(within(metricNameOptions).getByText('displayName_metric3')).toBeInTheDocument(); - expect(within(metricNameOptions).getByText('description_metric3')).toBeInTheDocument(); - }); - - it('should have a distinct list of services', async () => { - const onChange = jest.fn(); - const datasource = createMockDatasource({ - getMetricTypes: jest.fn().mockResolvedValue([ - createMockMetricDescriptor({ - service: 'service_a', - serviceShortName: 'srv_a', - type: 'metric1', - description: 'description_metric1', - displayName: 'displayName_metric1', - }), - createMockMetricDescriptor({ - service: 'service_b', - serviceShortName: 'srv_b', - type: 'metric2', - description: 'description_metric2', - displayName: 'displayName_metric2', - }), - createMockMetricDescriptor({ - service: 'service_b', - serviceShortName: 'srv_b', - type: 'metric3', - description: 'description_metric3', - displayName: 'displayName_metric3', - }), - ]), - }); - const query = createMockTimeSeriesList(); - - render( - - {() =>
} - - ); - const service = await screen.findByLabelText('Service'); - await openMenu(service); - expect(screen.getAllByLabelText('Select option').length).toEqual(2); - }); -}); diff --git a/public/app/plugins/datasource/cloud-monitoring/components/Metrics.tsx b/public/app/plugins/datasource/cloud-monitoring/components/Metrics.tsx deleted file mode 100644 index 980c4ecb6ee..00000000000 --- a/public/app/plugins/datasource/cloud-monitoring/components/Metrics.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import { css } from '@emotion/css'; -import { startCase, uniqBy } from 'lodash'; -import React, { useCallback, useEffect, useState } from 'react'; - -import { GrafanaTheme2, SelectableValue } from '@grafana/data'; -import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/experimental'; -import { getSelectStyles, Select, useStyles2, useTheme2 } from '@grafana/ui'; - -import CloudMonitoringDatasource from '../datasource'; -import { MetricDescriptor, TimeSeriesList } from '../types'; - -import { Project } from './Project'; - -export interface Props { - refId: string; - onChange: (metricDescriptor: MetricDescriptor) => void; - templateVariableOptions: Array>; - datasource: CloudMonitoringDatasource; - projectName: string; - metricType: string; - query: TimeSeriesList; - children: (metricDescriptor?: MetricDescriptor) => JSX.Element; - onProjectChange: (query: TimeSeriesList) => void; -} - -export function Metrics(props: Props) { - const [metricDescriptors, setMetricDescriptors] = useState([]); - const [metricDescriptor, setMetricDescriptor] = useState(); - const [metrics, setMetrics] = useState>>([]); - const [services, setServices] = useState>>([]); - const [service, setService] = useState(''); - - const theme = useTheme2(); - const selectStyles = getSelectStyles(theme); - - const customStyle = useStyles2(getStyles); - - const { - onProjectChange, - query, - refId, - metricType, - templateVariableOptions, - projectName, - datasource, - onChange, - children, - } = props; - const { templateSrv } = datasource; - - const getSelectedMetricDescriptor = useCallback( - (metricDescriptors: MetricDescriptor[], metricType: string) => { - return metricDescriptors.find((md) => md.type === templateSrv.replace(metricType))!; - }, - [templateSrv] - ); - - useEffect(() => { - const loadMetricDescriptors = async () => { - if (projectName) { - const metricDescriptors = await datasource.getMetricTypes(projectName); - const services = getServicesList(metricDescriptors); - setMetricDescriptors(metricDescriptors); - setServices(services); - } - }; - loadMetricDescriptors(); - }, [datasource, projectName, customStyle, selectStyles.optionDescription]); - - useEffect(() => { - const getMetricsList = (metricDescriptors: MetricDescriptor[]) => { - const selectedMetricDescriptor = getSelectedMetricDescriptor(metricDescriptors, metricType); - if (!selectedMetricDescriptor) { - return []; - } - - const metricsByService = metricDescriptors - .filter((m) => m.service === selectedMetricDescriptor.service) - .map((m) => ({ - service: m.service, - value: m.type, - label: m.displayName, - component: function optionComponent() { - return ( -
-
{m.type}
-
{m.description}
-
- ); - }, - })); - return metricsByService; - }; - - const metrics = getMetricsList(metricDescriptors); - const service = metrics.length > 0 ? metrics[0].service : ''; - const metricDescriptor = getSelectedMetricDescriptor(metricDescriptors, metricType); - setMetricDescriptor(metricDescriptor); - setMetrics(metrics); - setService(service); - }, [metricDescriptors, getSelectedMetricDescriptor, metricType, customStyle, selectStyles.optionDescription]); - - const onServiceChange = ({ value: service }: any) => { - const metrics = metricDescriptors - .filter((m: MetricDescriptor) => m.service === templateSrv.replace(service)) - .map((m: MetricDescriptor) => ({ - service: m.service, - value: m.type, - label: m.displayName, - description: m.description, - })); - - if (metrics.length > 0 && !metrics.some((m) => m.value === templateSrv.replace(metricType))) { - onMetricTypeChange(metrics[0]); - setService(service); - setMetrics(metrics); - } else { - setService(service); - setMetrics(metrics); - } - }; - - const onMetricTypeChange = ({ value }: SelectableValue) => { - const metricDescriptor = getSelectedMetricDescriptor(metricDescriptors, value!); - setMetricDescriptor(metricDescriptor); - onChange({ ...metricDescriptor, type: value! }); - }; - - const getServicesList = (metricDescriptors: MetricDescriptor[]) => { - const services = metricDescriptors.map((m) => ({ - value: m.service, - label: startCase(m.serviceShortName), - })); - - return services.length > 0 ? uniqBy(services, (s) => s.value) : []; - }; - - return ( - <> - - - { - onProjectChange({ ...query, projectName }); - }} - /> - - - s.value === metricType)} - options={[ - { - label: 'Template Variables', - options: templateVariableOptions, - }, - ...metrics, - ]} - placeholder="Select Metric" - inputId={`${props.refId}-select-metric`} - /> - - - - - {children(metricDescriptor)} - - ); -} - -const getStyles = (theme: GrafanaTheme2) => css` - label: grafana-select-option-description; - font-weight: normal; - font-style: italic; - color: ${theme.colors.text.secondary}; -`; diff --git a/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.test.tsx b/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.test.tsx new file mode 100644 index 00000000000..b408714dc49 --- /dev/null +++ b/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.test.tsx @@ -0,0 +1,286 @@ +import { act, render, screen, waitFor, within } from '@testing-library/react'; +import React from 'react'; +import { openMenu, select } from 'react-select-event'; + +import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; +import { TemplateSrv } from 'app/features/templating/template_srv'; + +import { createMockDatasource } from '../__mocks__/cloudMonitoringDatasource'; +import { createMockMetricDescriptor } from '../__mocks__/cloudMonitoringMetricDescriptor'; +import { createMockTimeSeriesList } from '../__mocks__/cloudMonitoringQuery'; +import { MetricKind, PreprocessorType } from '../types'; + +import { defaultTimeSeriesList } from './MetricQueryEditor'; +import { VisualMetricQueryEditor } from './VisualMetricQueryEditor'; + +const defaultProps = { + refId: 'refId', + customMetaData: {}, + variableOptionGroup: { options: [] }, + aliasBy: '', + onChangeAliasBy: jest.fn(), +}; + +describe('VisualMetricQueryEditor', () => { + it('renders metrics fields', async () => { + const onChange = jest.fn(); + const query = createMockTimeSeriesList(); + const datasource = createMockDatasource(); + + render(); + + expect(await screen.findByLabelText('Service')).toBeInTheDocument(); + expect(await screen.findByLabelText('Metric name')).toBeInTheDocument(); + }); + + it('can select a service', async () => { + const onChange = jest.fn(); + const query = createMockTimeSeriesList(); + const mockMetricDescriptor = createMockMetricDescriptor(); + const datasource = createMockDatasource({ + getMetricTypes: jest.fn().mockResolvedValue([mockMetricDescriptor]), + getLabels: jest.fn().mockResolvedValue([]), + }); + + render(); + + const service = await screen.findByLabelText('Service'); + await openMenu(service); + await act(async () => { + await select(service, 'Srv', { container: document.body }); + expect(onChange).toBeCalledWith( + expect.objectContaining({ filters: ['metric.type', '=', mockMetricDescriptor.type] }) + ); + }); + }); + + it('can select a metric name', async () => { + const onChange = jest.fn(); + const query = createMockTimeSeriesList(); + const mockMetricDescriptor = createMockMetricDescriptor({ displayName: 'metricName_test', type: 'test_type' }); + const datasource = createMockDatasource({ + getMetricTypes: jest.fn().mockResolvedValue([createMockMetricDescriptor(), mockMetricDescriptor]), + getLabels: jest.fn().mockResolvedValue([]), + }); + + render(); + + const service = await screen.findByLabelText('Service'); + await openMenu(service); + await act(async () => { + await select(service, 'Srv', { container: document.body }); + }); + const metricName = await screen.findByLabelText('Metric name'); + await openMenu(metricName); + await waitFor(() => expect(document.body).toHaveTextContent('metricName_test')); + await act(async () => { + await select(metricName, 'metricName_test', { container: document.body }); + expect(onChange).toBeCalledWith( + expect.objectContaining({ filters: ['metric.type', '=', mockMetricDescriptor.type] }) + ); + }); + }); + + it('should render available metric options according to the selected service', async () => { + const onChange = jest.fn(); + const query = createMockTimeSeriesList(); + const datasource = createMockDatasource({ + getMetricTypes: jest.fn().mockResolvedValue([ + createMockMetricDescriptor({ + service: 'service_a', + serviceShortName: 'srv_a', + type: 'metric1', + description: 'description_metric1', + displayName: 'displayName_metric1', + }), + createMockMetricDescriptor({ + service: 'service_b', + serviceShortName: 'srv_b', + type: 'metric2', + description: 'description_metric2', + displayName: 'displayName_metric2', + }), + createMockMetricDescriptor({ + service: 'service_b', + serviceShortName: 'srv_b', + type: 'metric3', + description: 'description_metric3', + displayName: 'displayName_metric3', + }), + ]), + getLabels: jest.fn().mockResolvedValue([]), + }); + + render(); + + const service = await screen.findByLabelText('Service'); + await openMenu(service); + await act(async () => { + await select(service, 'Srv A', { container: document.body }); + }); + const metricName = await screen.findByLabelText('Metric name'); + await openMenu(metricName); + + const metricNameOptions = screen.getByLabelText('Select options menu'); + expect(within(metricNameOptions).getByText('description_metric1')).toBeInTheDocument(); + expect(within(metricNameOptions).getByText('displayName_metric1')).toBeInTheDocument(); + expect(within(metricNameOptions).queryByText('displayName_metric2')).not.toBeInTheDocument(); + expect(within(metricNameOptions).queryByText('description_metric2')).not.toBeInTheDocument(); + expect(within(metricNameOptions).queryByText('displayName_metric3')).not.toBeInTheDocument(); + expect(within(metricNameOptions).queryByText('description_metric3')).not.toBeInTheDocument(); + + await openMenu(service); + await act(async () => { + await select(service, 'Srv B', { container: document.body }); + }); + expect(within(metricNameOptions).queryByText('displayName_metric1')).not.toBeInTheDocument(); + expect(within(metricNameOptions).queryByText('description_metric1')).not.toBeInTheDocument(); + expect(within(metricNameOptions).getByText('displayName_metric2')).toBeInTheDocument(); + expect(within(metricNameOptions).getByText('description_metric2')).toBeInTheDocument(); + expect(within(metricNameOptions).getByText('displayName_metric3')).toBeInTheDocument(); + expect(within(metricNameOptions).getByText('description_metric3')).toBeInTheDocument(); + }); + + it('should have a distinct list of services', async () => { + const onChange = jest.fn(); + const datasource = createMockDatasource({ + getMetricTypes: jest.fn().mockResolvedValue([ + createMockMetricDescriptor({ + service: 'service_a', + serviceShortName: 'srv_a', + type: 'metric1', + description: 'description_metric1', + displayName: 'displayName_metric1', + }), + createMockMetricDescriptor({ + service: 'service_b', + serviceShortName: 'srv_b', + type: 'metric2', + description: 'description_metric2', + displayName: 'displayName_metric2', + }), + createMockMetricDescriptor({ + service: 'service_b', + serviceShortName: 'srv_b', + type: 'metric3', + description: 'description_metric3', + displayName: 'displayName_metric3', + }), + ]), + }); + const query = createMockTimeSeriesList(); + + render(); + const service = await screen.findByLabelText('Service'); + await openMenu(service); + expect(screen.getAllByLabelText('Select option').length).toEqual(2); + }); + + it('resets query to default when service changes', async () => { + const query = createMockTimeSeriesList({ filters: ['metric.test_label', '=', 'test', 'AND'] }); + const onChange = jest.fn(); + const datasource = createMockDatasource({ + getMetricTypes: jest + .fn() + .mockResolvedValue([ + createMockMetricDescriptor(), + createMockMetricDescriptor({ type: 'type2', service: 'service2', serviceShortName: 'srv2' }), + ]), + getLabels: jest.fn().mockResolvedValue([]), + }); + const defaultQuery = { ...query, ...defaultTimeSeriesList(datasource), filters: ['metric.type', '=', 'type2'] }; + + render(); + + expect(screen.getByText('metric.test_label')).toBeInTheDocument(); + const service = await screen.findByLabelText('Service'); + openMenu(service); + await select(service, 'Srv 2', { container: document.body }); + expect(onChange).toBeCalledWith(expect.objectContaining({ filters: ['metric.type', '=', 'type2'] })); + expect(query).toEqual(defaultQuery); + expect(screen.queryByText('metric.test_label')).not.toBeInTheDocument(); + }); + + it('resets query to defaults (except filters) when metric changes', async () => { + const groupBys = ['metric.test_groupby']; + const query = createMockTimeSeriesList({ + filters: ['metric.test_label', '=', 'test', 'AND', 'metric.type', '=', 'type'], + groupBys, + preprocessor: PreprocessorType.Delta, + }); + const onChange = jest.fn(); + const datasource = createMockDatasource({ + getMetricTypes: jest + .fn() + .mockResolvedValue([ + createMockMetricDescriptor(), + createMockMetricDescriptor({ type: 'type2', displayName: 'metricName2', metricKind: MetricKind.GAUGE }), + ]), + getLabels: jest.fn().mockResolvedValue({ 'metric.test_groupby': '' }), + templateSrv: new TemplateSrv(), + }); + const defaultQuery = { ...query, ...defaultTimeSeriesList(datasource), filters: query.filters }; + + render(); + expect(document.body).toHaveTextContent('metric.test_label'); + expect(await screen.findByText('Delta')).toBeInTheDocument(); + expect(await screen.findByText('metric.test_groupby')).toBeInTheDocument(); + const metric = await screen.findByLabelText('Metric name'); + openMenu(metric); + await select(metric, 'metricName2', { container: document.body }); + expect(onChange).toBeCalledWith( + expect.objectContaining({ filters: ['metric.test_label', '=', 'test', 'AND', 'metric.type', '=', 'type2'] }) + ); + expect(query).toEqual(defaultQuery); + expect(document.body).toHaveTextContent('metric.test_label'); + expect(await screen.queryByText('Delta')).not.toBeInTheDocument(); + expect(await screen.queryByText('metric.test_groupby')).not.toBeInTheDocument(); + }); + + it('updates labels on time range change', async () => { + const timeSrv = getTimeSrv(); + const query = createMockTimeSeriesList(); + const onChange = jest.fn(); + const datasource = createMockDatasource({ + getMetricTypes: jest.fn().mockResolvedValue([createMockMetricDescriptor()]), + getLabels: jest + .fn() + .mockResolvedValue( + timeSrv.time.from === 'now-6h' ? { 'metric.test_groupby': '' } : { 'metric.test_groupby_1': '' } + ), + templateSrv: new TemplateSrv(), + timeSrv, + }); + + const { rerender } = render( + + ); + + const service = await screen.findByLabelText('Service'); + await openMenu(service); + await act(async () => { + await select(service, 'Srv', { container: document.body }); + }); + const metricName = await screen.findByLabelText('Metric name'); + await openMenu(metricName); + await waitFor(() => expect(document.body).toHaveTextContent('metricName')); + await act(async () => { + await select(metricName, 'metricName', { container: document.body }); + }); + const groupBy = await screen.findByLabelText('Group by'); + await openMenu(groupBy); + await waitFor(() => expect(document.body).toHaveTextContent('metric.test_groupby')); + await act(async () => { + timeSrv.setTime({ from: 'now-12h', to: 'now' }); + const datasourceUpdated = createMockDatasource({ + timeSrv, + getLabels: jest.fn().mockResolvedValue({ 'metric.test_groupby_1': '' }), + }); + rerender( + + ); + await openMenu(groupBy); + await waitFor(() => expect(document.body).toHaveTextContent('metric.test_groupby_1')); + }); + }); +}); diff --git a/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.tsx b/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.tsx index ebaae3bf0aa..be1fe2a1b03 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/VisualMetricQueryEditor.tsx @@ -1,7 +1,10 @@ +import { css } from '@emotion/css'; +import { startCase, uniqBy } from 'lodash'; import React, { useCallback, useEffect, useState } from 'react'; -import { SelectableValue } from '@grafana/data'; -import { EditorRow } from '@grafana/experimental'; +import { GrafanaTheme2, SelectableValue, TimeRange } from '@grafana/data'; +import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/experimental'; +import { getSelectStyles, Select, useStyles2, useTheme2 } from '@grafana/ui'; import CloudMonitoringDatasource from '../datasource'; import { getAlignmentPickerData, getMetricType, setMetricType } from '../functions'; @@ -11,106 +14,263 @@ import { AliasBy } from './AliasBy'; import { Alignment } from './Alignment'; import { GroupBy } from './GroupBy'; import { LabelFilter } from './LabelFilter'; -import { Metrics } from './Metrics'; +import { defaultTimeSeriesList } from './MetricQueryEditor'; import { Preprocessor } from './Preprocessor'; +import { Project } from './Project'; export interface Props { refId: string; customMetaData: CustomMetaData; - variableOptionGroup: SelectableValue; onChange: (query: TimeSeriesList) => void; - query: TimeSeriesList; datasource: CloudMonitoringDatasource; + query: TimeSeriesList; + variableOptionGroup: SelectableValue; aliasBy?: string; onChangeAliasBy: (aliasBy: string) => void; } -function Editor({ +export function Editor({ refId, - query, - datasource, onChange, - customMetaData, + datasource, + query, variableOptionGroup, + customMetaData, aliasBy, onChangeAliasBy, }: React.PropsWithChildren) { const [labels, setLabels] = useState<{ [k: string]: any }>({}); + const [metricDescriptors, setMetricDescriptors] = useState([]); + const [metricDescriptor, setMetricDescriptor] = useState(); + const [metrics, setMetrics] = useState>>([]); + const [services, setServices] = useState>>([]); + const [service, setService] = useState(''); + const [timeRange, setTimeRange] = useState({ ...datasource.timeSrv.timeRange() }); + + const useTime = (time: TimeRange) => { + if (timeRange !== null && (timeRange.raw.from !== time.raw.from || timeRange.raw.to !== time.raw.to)) { + setTimeRange({ ...time }); + } + }; + + useTime(datasource.timeSrv.timeRange()); + + const theme = useTheme2(); + const selectStyles = getSelectStyles(theme); + + const customStyle = useStyles2(getStyles); + const { projectName, groupBys, crossSeriesReducer } = query; const metricType = getMetricType(query); + const { templateSrv } = datasource; + + const getSelectedMetricDescriptor = useCallback( + (metricDescriptors: MetricDescriptor[], metricType: string) => { + return metricDescriptors.find((md) => md.type === templateSrv.replace(metricType))!; + }, + [templateSrv] + ); useEffect(() => { if (projectName && metricType) { - datasource.getLabels(metricType, refId, projectName).then((labels) => setLabels(labels)); + datasource + .getLabels(metricType, refId, projectName, { groupBys, crossSeriesReducer }, timeRange) + .then((labels) => setLabels(labels)); } - }, [datasource, groupBys, metricType, projectName, refId, crossSeriesReducer]); + }, [datasource, groupBys, metricType, projectName, refId, crossSeriesReducer, timeRange]); - const onMetricTypeChange = useCallback( - ({ valueType, metricKind, type }: MetricDescriptor) => { - const preprocessor = - metricKind === MetricKind.GAUGE || valueType === ValueTypes.DISTRIBUTION - ? PreprocessorType.None - : PreprocessorType.Rate; - const { perSeriesAligner } = getAlignmentPickerData(valueType, metricKind, query.perSeriesAligner, preprocessor); - onChange({ - ...setMetricType( - { - ...query, - perSeriesAligner, + useEffect(() => { + const loadMetricDescriptors = async () => { + if (projectName) { + const metricDescriptors = await datasource.getMetricTypes(projectName); + const services = getServicesList(metricDescriptors); + setMetricDescriptors(metricDescriptors); + setServices(services); + } + }; + loadMetricDescriptors(); + }, [datasource, projectName, customStyle, selectStyles.optionDescription]); + + useEffect(() => { + const getMetricsList = (metricDescriptors: MetricDescriptor[]) => { + const selectedMetricDescriptor = getSelectedMetricDescriptor(metricDescriptors, metricType); + if (!selectedMetricDescriptor) { + return []; + } + + const metricsByService = metricDescriptors + .filter((m) => m.service === selectedMetricDescriptor.service) + .map((m) => ({ + service: m.service, + value: m.type, + label: m.displayName, + component: function optionComponent() { + return ( +
+
{m.type}
+
{m.description}
+
+ ); }, - type - ), - preprocessor, - }); - }, - [onChange, query] - ); + })); + return metricsByService; + }; + + const metrics = getMetricsList(metricDescriptors); + const service = metrics.length > 0 ? metrics[0].service : ''; + const metricDescriptor = getSelectedMetricDescriptor(metricDescriptors, metricType); + setMetricDescriptor(metricDescriptor); + setMetrics(metrics); + setService(service); + }, [metricDescriptors, getSelectedMetricDescriptor, metricType, customStyle, selectStyles.optionDescription]); + + const onServiceChange = ({ value: service }: SelectableValue) => { + const metrics = metricDescriptors + .filter((m: MetricDescriptor) => m.service === templateSrv.replace(service)) + .map((m: MetricDescriptor) => ({ + service: m.service, + value: m.type, + label: m.displayName, + description: m.description, + })); + // On service change reset all query values except the project name + query.filters = []; + + if (metrics.length > 0 && !metrics.some((m) => m.value === templateSrv.replace(metricType))) { + onMetricTypeChange(metrics[0]); + setService(service!); + setMetrics(metrics); + } else { + setService(service!); + setMetrics(metrics); + } + }; + + const getServicesList = (metricDescriptors: MetricDescriptor[]) => { + const services = metricDescriptors.map((m) => ({ + value: m.service, + label: startCase(m.serviceShortName), + })); + + return services.length > 0 ? uniqBy(services, (s) => s.value) : []; + }; + + const onMetricTypeChange = ({ value }: SelectableValue) => { + const metricDescriptor = getSelectedMetricDescriptor(metricDescriptors, value!); + setMetricDescriptor(metricDescriptor); + const { metricKind, valueType } = metricDescriptor; + const preprocessor = + metricKind === MetricKind.GAUGE || valueType === ValueTypes.DISTRIBUTION + ? PreprocessorType.None + : PreprocessorType.Rate; + const { perSeriesAligner } = getAlignmentPickerData(valueType, metricKind, query.perSeriesAligner, preprocessor); + + // On metric name change reset query to defaults except project name and filters + Object.assign(query, { + ...defaultTimeSeriesList(datasource), + projectName: query.projectName, + filters: query.filters, + }); + onChange({ + ...setMetricType( + { + ...query, + perSeriesAligner, + }, + value! + ), + preprocessor, + }); + }; return ( - - {(metric) => ( - <> - onChange({ ...query, filters })} - variableOptionGroup={variableOptionGroup} + <> + + + { + onChange({ ...query, projectName }); + }} /> - - - + s.value === metricType)} + options={[ + { + label: 'Template Variables', + options: variableOptionGroup.options, + }, + ...metrics, + ]} + placeholder="Select Metric" + inputId={`${refId}-select-metric`} /> - - - - )} - + + + + + <> + onChange({ ...query, filters })} + variableOptionGroup={variableOptionGroup} + /> + + + + + + + + ); } +const getStyles = (theme: GrafanaTheme2) => css` + label: grafana-select-option-description; + font-weight: normal; + font-style: italic; + color: ${theme.colors.text.secondary}; +`; + export const VisualMetricQueryEditor = React.memo(Editor); diff --git a/public/app/plugins/datasource/cloud-monitoring/components/index.ts b/public/app/plugins/datasource/cloud-monitoring/components/index.ts index 1651a77898c..734d8c47c22 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/index.ts +++ b/public/app/plugins/datasource/cloud-monitoring/components/index.ts @@ -1,5 +1,4 @@ export { Project } from './Project'; -export { Metrics } from './Metrics'; export { GroupBy } from './GroupBy'; export { Alignment } from './Alignment'; export { LabelFilter } from './LabelFilter'; diff --git a/public/app/plugins/datasource/cloud-monitoring/datasource.ts b/public/app/plugins/datasource/cloud-monitoring/datasource.ts index 2211c09942a..60df2221082 100644 --- a/public/app/plugins/datasource/cloud-monitoring/datasource.ts +++ b/public/app/plugins/datasource/cloud-monitoring/datasource.ts @@ -8,6 +8,7 @@ import { DataSourceInstanceSettings, ScopedVars, SelectableValue, + TimeRange, } from '@grafana/data'; import { DataSourceWithBackend, getBackendSrv, toDataQueryResponse, BackendSrv } from '@grafana/runtime'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; @@ -39,7 +40,7 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend< constructor( private instanceSettings: DataSourceInstanceSettings, public templateSrv: TemplateSrv = getTemplateSrv(), - private readonly timeSrv: TimeSrv = getTimeSrv() + readonly timeSrv: TimeSrv = getTimeSrv() ) { super(instanceSettings); this.authenticationType = instanceSettings.jsonData.authenticationType || 'jwt'; @@ -89,7 +90,13 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend< }; } - async getLabels(metricType: string, refId: string, projectName: string, aggregation?: Aggregation) { + async getLabels( + metricType: string, + refId: string, + projectName: string, + aggregation?: Aggregation, + timeRange?: TimeRange + ) { const options = { targets: [ { @@ -107,7 +114,7 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend< ), }, ], - range: this.timeSrv.timeRange(), + range: timeRange ?? this.timeSrv.timeRange(), }; const queries = options.targets; From 665f85cd7e901b5b817cd868cf214f4ff1a46e0a Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Mon, 16 Jan 2023 18:33:46 +0000 Subject: [PATCH 26/65] CommandPalette: Minor usability improvements (#61567) --- public/app/features/commandPalette/CommandPalette.tsx | 5 ++++- public/app/features/commandPalette/ResultItem.tsx | 9 --------- .../features/commandPalette/actions/dashboardActions.ts | 2 +- .../app/features/commandPalette/actions/staticActions.ts | 2 -- public/locales/de-DE/grafana.json | 3 +++ public/locales/en-US/grafana.json | 5 ++++- public/locales/es-ES/grafana.json | 3 +++ public/locales/fr-FR/grafana.json | 3 +++ public/locales/pseudo-LOCALE/grafana.json | 5 ++++- public/locales/zh-Hans/grafana.json | 3 +++ 10 files changed, 25 insertions(+), 15 deletions(-) diff --git a/public/app/features/commandPalette/CommandPalette.tsx b/public/app/features/commandPalette/CommandPalette.tsx index 1d5507eb799..b70709e443e 100644 --- a/public/app/features/commandPalette/CommandPalette.tsx +++ b/public/app/features/commandPalette/CommandPalette.tsx @@ -56,7 +56,10 @@ export const CommandPalette = () => {
- +
diff --git a/public/app/features/commandPalette/ResultItem.tsx b/public/app/features/commandPalette/ResultItem.tsx index 036d70dffb4..d82be2c2c89 100644 --- a/public/app/features/commandPalette/ResultItem.tsx +++ b/public/app/features/commandPalette/ResultItem.tsx @@ -59,15 +59,6 @@ export const ResultItem = React.forwardRef(
{action.subtitle && {action.subtitle}}
- {action.shortcut?.length ? ( -
- {action.shortcut.map((sc) => ( - - {sc} - - ))} -
- ) : null}
); } diff --git a/public/app/features/commandPalette/actions/dashboardActions.ts b/public/app/features/commandPalette/actions/dashboardActions.ts index e230a2730cd..8c8647293cd 100644 --- a/public/app/features/commandPalette/actions/dashboardActions.ts +++ b/public/app/features/commandPalette/actions/dashboardActions.ts @@ -36,7 +36,7 @@ export async function getRecentDashboardActions(): Promise { locationService.push(locationUtil.stripBaseFromUrl(url)); diff --git a/public/app/features/commandPalette/actions/staticActions.ts b/public/app/features/commandPalette/actions/staticActions.ts index 0eed8786fd2..835cec7be4f 100644 --- a/public/app/features/commandPalette/actions/staticActions.ts +++ b/public/app/features/commandPalette/actions/staticActions.ts @@ -55,7 +55,6 @@ export default (navBarTree: NavModelItem[]): CommandPaletteAction[] => { keywords: 'navigate', perform: () => locationService.push('?search=open'), section: t('command-palette.section.pages', 'Pages'), - shortcut: ['s', 'o'], priority: DEFAULT_PRIORITY, }, { @@ -63,7 +62,6 @@ export default (navBarTree: NavModelItem[]): CommandPaletteAction[] => { name: t('command-palette.action.change-theme', 'Change theme...'), keywords: 'interface color dark light', section: t('command-palette.section.preferences', 'Preferences'), - shortcut: ['c', 't'], priority: PREFERENCES_PRIORITY, }, { diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index f547f83fd63..1ea23009464 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -12,6 +12,9 @@ "light-theme": "", "search": "" }, + "search-box": { + "placeholder": "" + }, "section": { "actions": "", "dashboard-search-results": "", diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 4f6fdbc8c38..328381a74be 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -12,12 +12,15 @@ "light-theme": "Light", "search": "Search" }, + "search-box": { + "placeholder": "Search Grafana" + }, "section": { "actions": "Actions", "dashboard-search-results": "Dashboards", "pages": "Pages", "preferences": "Preferences", - "recent-dashboards": "Recently viewed dashboards" + "recent-dashboards": "Recent dashboards" } }, "common": { diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 5c805593302..3f8c566d9ba 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -12,6 +12,9 @@ "light-theme": "", "search": "" }, + "search-box": { + "placeholder": "" + }, "section": { "actions": "", "dashboard-search-results": "", diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 712fa1794fd..b01904456ba 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -12,6 +12,9 @@ "light-theme": "", "search": "" }, + "search-box": { + "placeholder": "" + }, "section": { "actions": "", "dashboard-search-results": "", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 21964cfc999..78c9ecf18bb 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -12,12 +12,15 @@ "light-theme": "Ŀįģĥŧ", "search": "Ŝęäřčĥ" }, + "search-box": { + "placeholder": "Ŝęäřčĥ Ğřäƒäʼnä" + }, "section": { "actions": "Åčŧįőʼnş", "dashboard-search-results": "Đäşĥþőäřđş", "pages": "Päģęş", "preferences": "Přęƒęřęʼnčęş", - "recent-dashboards": "Ŗęčęʼnŧľy vįęŵęđ đäşĥþőäřđş" + "recent-dashboards": "Ŗęčęʼnŧ đäşĥþőäřđş" } }, "common": { diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index a495b91748b..de1caaa8f02 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -12,6 +12,9 @@ "light-theme": "", "search": "" }, + "search-box": { + "placeholder": "" + }, "section": { "actions": "", "dashboard-search-results": "", From 0d70eb18ac2d92ded4084f808877c2d4cf202c8f Mon Sep 17 00:00:00 2001 From: Selene Date: Mon, 16 Jan 2023 21:59:43 +0100 Subject: [PATCH 27/65] Dashboards: Add org_id in dashboards query (#61542) * Add org_id in dashboards query * Update OrgID * Remove unused orgid --- pkg/services/dashboards/database/database.go | 3 +++ pkg/services/dashboards/models.go | 1 + 2 files changed, 4 insertions(+) diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index 0959e97643c..4ad536c683d 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -974,6 +974,9 @@ func (d *DashboardStore) GetDashboards(ctx context.Context, query *dashboards.Ge } else { session = sess.In("uid", query.DashboardUIDs) } + if query.OrgID > 0 { + session = sess.Where("org_id = ?", query.OrgID) + } err := session.Find(&dashboards) query.Result = dashboards diff --git a/pkg/services/dashboards/models.go b/pkg/services/dashboards/models.go index 417c9416a14..76ed476e483 100644 --- a/pkg/services/dashboards/models.go +++ b/pkg/services/dashboards/models.go @@ -256,6 +256,7 @@ type GetDashboardTagsQuery struct { type GetDashboardsQuery struct { DashboardIDs []int64 DashboardUIDs []string + OrgID int64 Result []*Dashboard } From 2324597d8d8b218847f3c190ceb505a5e10ebd3d Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Tue, 17 Jan 2023 09:11:45 +0100 Subject: [PATCH 28/65] AuthN: Perform login with authn.Service (#61466) * AuthN: Create password client wrapper and use that on in basic auth client * AuthN: fix basic auth client test * AuthN: Add tests for form authentication * API: Inject authn service * Login: If authnService feature flag is enabled use authn login * Login: Handle token creation errors --- pkg/api/http_server.go | 5 +- pkg/api/login.go | 30 ++++++++ pkg/api/login_test.go | 8 +- pkg/services/authn/authn.go | 1 + pkg/services/authn/authnimpl/service.go | 17 +++-- pkg/services/authn/clients/basic.go | 47 +----------- pkg/services/authn/clients/basic_test.go | 59 +++------------ pkg/services/authn/clients/constants.go | 1 - pkg/services/authn/clients/form.go | 42 +++++++++++ pkg/services/authn/clients/form_test.go | 48 ++++++++++++ pkg/services/authn/clients/ldap.go | 2 +- pkg/services/authn/clients/password.go | 67 +++++++++++++++++ pkg/services/authn/clients/password_test.go | 82 +++++++++++++++++++++ 13 files changed, 306 insertions(+), 103 deletions(-) create mode 100644 pkg/services/authn/clients/form.go create mode 100644 pkg/services/authn/clients/form_test.go create mode 100644 pkg/services/authn/clients/password.go create mode 100644 pkg/services/authn/clients/password_test.go diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index f218a72d893..15a75846c23 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware/csrf" "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/oauthtoken" "github.com/grafana/grafana/pkg/services/querylibrary" @@ -211,6 +212,7 @@ type HTTPServer struct { tagService tag.Service oauthTokenService oauthtoken.OAuthTokenService statsService stats.Service + authnService authn.Service } type ServerOptions struct { @@ -253,7 +255,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi accesscontrolService accesscontrol.Service, dashboardThumbsService thumbs.DashboardThumbService, navTreeService navtree.Service, annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService, queryLibraryHTTPService querylibrary.HTTPService, queryLibraryService querylibrary.Service, oauthTokenService oauthtoken.OAuthTokenService, - statsService stats.Service, + statsService stats.Service, authnService authn.Service, k8saccess k8saccess.K8SAccess, // required so that the router is registered ) (*HTTPServer, error) { web.Env = cfg.Env @@ -360,6 +362,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi QueryLibraryService: queryLibraryService, oauthTokenService: oauthTokenService, statsService: statsService, + authnService: authnService, } if hs.Listener != nil { hs.log.Debug("Using provided listener") diff --git a/pkg/api/login.go b/pkg/api/login.go index 1ed59213fbe..74d76373081 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -17,6 +17,8 @@ import ( "github.com/grafana/grafana/pkg/middleware/cookies" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/featuremgmt" loginService "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/user" @@ -167,6 +169,34 @@ func (hs *HTTPServer) LoginAPIPing(c *models.ReqContext) response.Response { } func (hs *HTTPServer) LoginPost(c *models.ReqContext) response.Response { + if hs.Features.IsEnabled(featuremgmt.FlagAuthnService) { + identity, err := hs.authnService.Login(c.Req.Context(), authn.ClientForm, &authn.Request{HTTPRequest: c.Req, Resp: c.Resp}) + if err != nil { + tokenErr := &auth.CreateTokenErr{} + if errors.As(err, &tokenErr) { + return response.Error(tokenErr.StatusCode, tokenErr.ExternalErr, tokenErr.InternalErr) + } + return response.Err(err) + } + + cookies.WriteSessionCookie(c, hs.Cfg, identity.SessionToken.UnhashedToken, hs.Cfg.LoginMaxLifetime) + result := map[string]interface{}{ + "message": "Logged in", + } + + if redirectTo := c.GetCookie("redirect_to"); len(redirectTo) > 0 { + if err := hs.ValidateRedirectTo(redirectTo); err == nil { + result["redirectUrl"] = redirectTo + } else { + c.Logger.Info("Ignored invalid redirect_to cookie value.", "url", redirectTo) + } + cookies.DeleteCookie(c.Resp, "redirect_to", hs.CookieOptionsFromCfg) + } + + metrics.MApiLoginPost.Inc() + return response.JSON(http.StatusOK, result) + } + cmd := dtos.LoginCommand{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad login data", err) diff --git a/pkg/api/login_test.go b/pkg/api/login_test.go index ea30addb451..9e09ba3deaa 100644 --- a/pkg/api/login_test.go +++ b/pkg/api/login_test.go @@ -12,9 +12,6 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" @@ -24,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth/authtest" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/hooks" "github.com/grafana/grafana/pkg/services/licensing" loginservice "github.com/grafana/grafana/pkg/services/login" @@ -33,6 +31,8 @@ import ( secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func fakeSetIndexViewData(t *testing.T) { @@ -324,6 +324,7 @@ func TestLoginPostRedirect(t *testing.T) { HooksService: &hooks.HooksService{}, License: &licensing.OSSLicensingService{}, AuthTokenService: authtest.NewFakeUserAuthTokenService(), + Features: featuremgmt.WithFeatures(), } hs.Cfg.CookieSecure = true @@ -603,6 +604,7 @@ func TestLoginPostRunLokingHook(t *testing.T) { Cfg: setting.NewCfg(), License: &licensing.OSSLicensingService{}, AuthTokenService: authtest.NewFakeUserAuthTokenService(), + Features: featuremgmt.WithFeatures(), HooksService: hookService, } diff --git a/pkg/services/authn/authn.go b/pkg/services/authn/authn.go index a6394b77308..5537e7e8998 100644 --- a/pkg/services/authn/authn.go +++ b/pkg/services/authn/authn.go @@ -24,6 +24,7 @@ const ( ClientJWT = "auth.client.jwt" ClientRender = "auth.client.render" ClientSession = "auth.client.session" + ClientForm = "auth.client.form" ) const ( diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index 9a874c33c41..ae93c60aa65 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -65,18 +65,23 @@ func ProvideService( } var passwordClients []authn.PasswordClient - if !s.cfg.DisableLogin { passwordClients = append(passwordClients, clients.ProvideGrafana(userService)) } - if s.cfg.LDAPEnabled { passwordClients = append(passwordClients, clients.ProvideLDAP(cfg)) } - // only configure basic auth client if it is enabled, and we have at least one password client enabled - if s.cfg.BasicAuthEnabled && len(passwordClients) > 0 { - s.clients[authn.ClientBasic] = clients.ProvideBasic(loginAttempts, passwordClients...) + // if we have password clients configure check if basic auth or form auth is enabled + if len(passwordClients) > 0 { + passwordClient := clients.ProvidePassword(loginAttempts, passwordClients...) + if s.cfg.BasicAuthEnabled { + s.clients[authn.ClientBasic] = clients.ProvideBasic(passwordClient) + } + // FIXME (kalleep): Remove the global variable and stick it into cfg + if !setting.DisableLoginForm { + s.clients[authn.ClientForm] = clients.ProvideForm(passwordClient) + } } if s.cfg.JWTAuthEnabled { @@ -128,6 +133,8 @@ func (s *Service) Authenticate(ctx context.Context, client string, r *authn.Requ return nil, true, err } + // FIXME (kalleep): Handle disabled identities + for _, hook := range s.postAuthHooks { if err := hook(ctx, identity, r); err != nil { s.log.FromContext(ctx).Warn("post auth hook failed", "error", err, "id", identity) diff --git a/pkg/services/authn/clients/basic.go b/pkg/services/authn/clients/basic.go index e3fcf608908..4c8c0c529cf 100644 --- a/pkg/services/authn/clients/basic.go +++ b/pkg/services/authn/clients/basic.go @@ -2,30 +2,25 @@ package clients import ( "context" - "errors" "strings" "github.com/grafana/grafana/pkg/services/authn" - "github.com/grafana/grafana/pkg/services/loginattempt" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/errutil" - "github.com/grafana/grafana/pkg/web" ) var ( errDecodingBasicAuthHeader = errutil.NewBase(errutil.StatusBadRequest, "basic-auth.invalid-header", errutil.WithPublicMessage("Invalid Basic Auth Header")) - errBasicAuthCredentials = errutil.NewBase(errutil.StatusUnauthorized, "basic-auth.invalid-credentials", errutil.WithPublicMessage("Invalid username or password")) ) var _ authn.Client = new(Basic) -func ProvideBasic(loginAttempts loginattempt.Service, clients ...authn.PasswordClient) *Basic { - return &Basic{clients, loginAttempts} +func ProvideBasic(client authn.PasswordClient) *Basic { + return &Basic{client} } type Basic struct { - clients []authn.PasswordClient - loginAttempts loginattempt.Service + client authn.PasswordClient } func (c *Basic) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { @@ -34,44 +29,10 @@ func (c *Basic) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden return nil, errDecodingBasicAuthHeader.Errorf("failed to decode basic auth header: %w", err) } - r.SetMeta(authn.MetaKeyUsername, username) - - ok, err := c.loginAttempts.Validate(ctx, username) - if err != nil { - return nil, err - } - if !ok { - return nil, errBasicAuthCredentials.Errorf("too many consecutive incorrect login attempts for user - login for user temporarily blocked") - } - - if len(password) == 0 { - return nil, errBasicAuthCredentials.Errorf("no password provided") - } - - for _, pwClient := range c.clients { - identity, err := pwClient.AuthenticatePassword(ctx, r, username, password) - if err != nil { - if errors.Is(err, errIdentityNotFound) { - // continue to next password client if identity could not be found - continue - } - if errors.Is(err, errInvalidPassword) { - // only add login attempt if identity was found but the provided password was invalid - _ = c.loginAttempts.Add(ctx, username, web.RemoteAddr(r.HTTPRequest)) - } - return nil, errBasicAuthCredentials.Errorf("failed to authenticate identity: %w", err) - } - - return identity, nil - } - - return nil, errBasicAuthCredentials.Errorf("failed to authenticate identity using basic auth") + return c.client.AuthenticatePassword(ctx, r, username, password) } func (c *Basic) Test(ctx context.Context, r *authn.Request) bool { - if len(c.clients) == 0 { - return false - } return looksLikeBasicAuthRequest(r) } diff --git a/pkg/services/authn/clients/basic_test.go b/pkg/services/authn/clients/basic_test.go index 1f61ade0dc7..81b974ee20d 100644 --- a/pkg/services/authn/clients/basic_test.go +++ b/pkg/services/authn/clients/basic_test.go @@ -7,7 +7,6 @@ import ( "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn/authntest" - "github.com/grafana/grafana/pkg/services/loginattempt/loginattempttest" "github.com/stretchr/testify/assert" ) @@ -15,8 +14,7 @@ func TestBasic_Authenticate(t *testing.T) { type TestCase struct { desc string req *authn.Request - blockLogin bool - clients []authn.PasswordClient + client authn.PasswordClient expectedErr error expectedIdentity *authn.Identity } @@ -25,40 +23,19 @@ func TestBasic_Authenticate(t *testing.T) { { desc: "should success when password client return identity", req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{authorizationHeaderName: {encodeBasicAuth("user", "password")}}}}, - clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: "user:1"}}}, + client: authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: "user:1"}}, expectedIdentity: &authn.Identity{ID: "user:1"}, }, { - desc: "should success when found in second client", - req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{authorizationHeaderName: {encodeBasicAuth("user", "password")}}}}, - clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedErr: errIdentityNotFound}, authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: "user:2"}}}, - expectedIdentity: &authn.Identity{ID: "user:2"}, - }, - { - desc: "should fail for empty password", - req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{authorizationHeaderName: {encodeBasicAuth("user", "")}}}}, - expectedErr: errBasicAuthCredentials, - }, - { - desc: "should if login is blocked by to many attempts", - req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{authorizationHeaderName: {encodeBasicAuth("user", "")}}}}, - blockLogin: true, - expectedErr: errBasicAuthCredentials, - }, - { - desc: "should fail when not found in any clients", - req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{authorizationHeaderName: {encodeBasicAuth("user", "password")}}}}, - clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedErr: errIdentityNotFound}, authntest.FakePasswordClient{ExpectedErr: errIdentityNotFound}}, - expectedErr: errBasicAuthCredentials, + desc: "should fail when basic auth header could not be decoded", + req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{authorizationHeaderName: {}}}}, + expectedErr: errDecodingBasicAuthHeader, }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - c := ProvideBasic( - loginattempttest.FakeLoginAttemptService{ExpectedValid: !tt.blockLogin}, - tt.clients..., - ) + c := ProvideBasic(tt.client) identity, err := c.Authenticate(context.Background(), tt.req) if tt.expectedErr != nil { @@ -74,10 +51,9 @@ func TestBasic_Authenticate(t *testing.T) { func TestBasic_Test(t *testing.T) { type TestCase struct { - desc string - req *authn.Request - noClients bool - expected bool + desc string + req *authn.Request + expected bool } tests := []TestCase{ @@ -92,18 +68,6 @@ func TestBasic_Test(t *testing.T) { }, expected: true, }, - { - desc: "should fail when no password client is configured", - req: &authn.Request{ - HTTPRequest: &http.Request{ - Header: map[string][]string{ - authorizationHeaderName: {encodeBasicAuth("user", "password")}, - }, - }, - }, - noClients: true, - expected: false, - }, { desc: "should fail when no http request is passed", req: &authn.Request{}, @@ -124,10 +88,7 @@ func TestBasic_Test(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - c := ProvideBasic(loginattempttest.FakeLoginAttemptService{}, authntest.FakePasswordClient{}) - if tt.noClients { - c.clients = nil - } + c := ProvideBasic(authntest.FakePasswordClient{}) assert.Equal(t, tt.expected, c.Test(context.Background(), tt.req)) }) } diff --git a/pkg/services/authn/clients/constants.go b/pkg/services/authn/clients/constants.go index d0c2f477f31..0d8734c61fa 100644 --- a/pkg/services/authn/clients/constants.go +++ b/pkg/services/authn/clients/constants.go @@ -10,5 +10,4 @@ const ( var ( errIdentityNotFound = errutil.NewBase(errutil.StatusNotFound, "identity.not-found") - errInvalidPassword = errutil.NewBase(errutil.StatusBadRequest, "identity.invalid-password", errutil.WithPublicMessage("Invalid password or username")) ) diff --git a/pkg/services/authn/clients/form.go b/pkg/services/authn/clients/form.go new file mode 100644 index 00000000000..f8e4de8bdb0 --- /dev/null +++ b/pkg/services/authn/clients/form.go @@ -0,0 +1,42 @@ +package clients + +import ( + "context" + + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/util/errutil" + "github.com/grafana/grafana/pkg/web" +) + +var ( + errBadForm = errutil.NewBase(errutil.StatusBadRequest, "form-auth.invalid", errutil.WithPublicMessage("bad login data")) +) + +var _ authn.Client = new(Form) + +func ProvideForm(client authn.PasswordClient) *Form { + return &Form{client} +} + +type Form struct { + client authn.PasswordClient +} + +type loginForm struct { + Username string `json:"user" binding:"Required"` + Password string `json:"password" binding:"Required"` +} + +func (f *Form) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { + form := loginForm{} + if err := web.Bind(r.HTTPRequest, &form); err != nil { + return nil, errBadForm.Errorf("failed to parse request: %w", err) + } + return f.client.AuthenticatePassword(ctx, r, form.Username, form.Password) +} + +func (f *Form) Test(ctx context.Context, r *authn.Request) bool { + // FIXME: How should we detect this?? + // Maybe create client test interface and not all clients has to implement this?? + return true +} diff --git a/pkg/services/authn/clients/form_test.go b/pkg/services/authn/clients/form_test.go new file mode 100644 index 00000000000..6c3c0b30f9e --- /dev/null +++ b/pkg/services/authn/clients/form_test.go @@ -0,0 +1,48 @@ +package clients + +import ( + "context" + "io" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/authn/authntest" +) + +func TestForm_Authenticate(t *testing.T) { + type testCase struct { + desc string + req *authn.Request + expectedErr error + } + + tests := []testCase{ + { + desc: "should success on valid request", + req: &authn.Request{HTTPRequest: &http.Request{ + Header: map[string][]string{"Content-Type": {"application/json"}}, + Body: io.NopCloser(strings.NewReader(`{"user": "test", "password": "test"}`)), + }}, + }, + { + desc: "should return error for bad request", + req: &authn.Request{HTTPRequest: &http.Request{ + Header: map[string][]string{"Content-Type": {"application/json"}}, + Body: io.NopCloser(strings.NewReader(`{}`)), + }}, + expectedErr: errBadForm, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + c := ProvideForm(&authntest.FakePasswordClient{}) + _, err := c.Authenticate(context.Background(), tt.req) + assert.ErrorIs(t, err, tt.expectedErr) + }) + } +} diff --git a/pkg/services/authn/clients/ldap.go b/pkg/services/authn/clients/ldap.go index 364be4bb65b..0a4a9bfbf8a 100644 --- a/pkg/services/authn/clients/ldap.go +++ b/pkg/services/authn/clients/ldap.go @@ -28,6 +28,7 @@ func (c *LDAP) AuthenticatePassword(ctx context.Context, r *authn.Request, usern }) if errors.Is(err, multildap.ErrCouldNotFindUser) { + // FIXME: disable user in grafana if not found return nil, errIdentityNotFound.Errorf("no user found: %w", err) } @@ -35,7 +36,6 @@ func (c *LDAP) AuthenticatePassword(ctx context.Context, r *authn.Request, usern r.SetMeta(authn.MetaKeyAuthModule, "ldap") if errors.Is(err, multildap.ErrInvalidCredentials) { - // FIXME: disable user in grafana if not found return nil, errInvalidPassword.Errorf("invalid password: %w", err) } diff --git a/pkg/services/authn/clients/password.go b/pkg/services/authn/clients/password.go new file mode 100644 index 00000000000..50e76b4c35f --- /dev/null +++ b/pkg/services/authn/clients/password.go @@ -0,0 +1,67 @@ +package clients + +import ( + "context" + "errors" + + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/loginattempt" + "github.com/grafana/grafana/pkg/util/errutil" + "github.com/grafana/grafana/pkg/web" +) + +var ( + errEmptyPassword = errutil.NewBase(errutil.StatusBadRequest, "password-auth.empty", errutil.WithPublicMessage("Invalid username or password")) + errPasswordAuthFailed = errutil.NewBase(errutil.StatusBadRequest, "password-auth.failed", errutil.WithPublicMessage("Invalid username or password")) + errInvalidPassword = errutil.NewBase(errutil.StatusBadRequest, "password-auth.invalid", errutil.WithPublicMessage("Invalid password or username")) + errLoginAttemptBlocked = errutil.NewBase(errutil.StatusUnauthorized, "login-attempt.blocked", errutil.WithPublicMessage("Invalid username or password")) +) + +var _ authn.PasswordClient = new(Password) + +func ProvidePassword(loginAttempts loginattempt.Service, clients ...authn.PasswordClient) *Password { + return &Password{loginAttempts, clients} +} + +type Password struct { + loginAttempts loginattempt.Service + clients []authn.PasswordClient +} + +func (c *Password) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) { + r.SetMeta(authn.MetaKeyUsername, username) + + ok, err := c.loginAttempts.Validate(ctx, username) + if err != nil { + return nil, err + } + if !ok { + return nil, errLoginAttemptBlocked.Errorf("too many consecutive incorrect login attempts for user - login for user temporarily blocked") + } + + if len(password) == 0 { + return nil, errEmptyPassword.Errorf("no password provided") + } + + var clientErr error + for _, pwClient := range c.clients { + var identity *authn.Identity + identity, clientErr = pwClient.AuthenticatePassword(ctx, r, username, password) + // for invalid password or if the identity is not found by a client continue to next one + if errors.Is(clientErr, errInvalidPassword) || errors.Is(clientErr, errIdentityNotFound) { + continue + } + + if clientErr != nil { + return nil, errPasswordAuthFailed.Errorf("failed to authenticate identity: %w", clientErr) + } + + return identity, nil + } + + if errors.Is(clientErr, errInvalidPassword) { + _ = c.loginAttempts.Add(ctx, username, web.RemoteAddr(r.HTTPRequest)) + } + + return nil, errPasswordAuthFailed.Errorf("failed to authenticate identity: %w", clientErr) +} diff --git a/pkg/services/authn/clients/password_test.go b/pkg/services/authn/clients/password_test.go new file mode 100644 index 00000000000..e064265e90a --- /dev/null +++ b/pkg/services/authn/clients/password_test.go @@ -0,0 +1,82 @@ +package clients + +import ( + "context" + "testing" + + "github.com/grafana/grafana/pkg/services/loginattempt/loginattempttest" + "github.com/stretchr/testify/assert" + + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/authn/authntest" +) + +func TestPassword_AuthenticatePassword(t *testing.T) { + type TestCase struct { + desc string + username string + password string + req *authn.Request + blockLogin bool + clients []authn.PasswordClient + expectedErr error + expectedIdentity *authn.Identity + } + + tests := []TestCase{ + { + desc: "should success when password client return identity", + username: "test", + password: "test", + req: &authn.Request{}, + clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: "user:1"}}}, + expectedIdentity: &authn.Identity{ID: "user:1"}, + }, + { + desc: "should success when found in second client", + username: "test", + password: "test", + req: &authn.Request{}, + clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedErr: errIdentityNotFound}, authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: "user:2"}}}, + expectedIdentity: &authn.Identity{ID: "user:2"}, + }, + { + desc: "should fail for empty password", + username: "test", + password: "", + req: &authn.Request{}, + expectedErr: errEmptyPassword, + }, + { + desc: "should if login is blocked by to many attempts", + username: "test", + password: "test", + req: &authn.Request{}, + blockLogin: true, + expectedErr: errLoginAttemptBlocked, + }, + { + desc: "should fail when not found in any clients", + username: "test", + password: "test", + req: &authn.Request{}, + clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedErr: errIdentityNotFound}, authntest.FakePasswordClient{ExpectedErr: errIdentityNotFound}}, + expectedErr: errPasswordAuthFailed, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + c := ProvidePassword(loginattempttest.FakeLoginAttemptService{ExpectedValid: !tt.blockLogin}, tt.clients...) + + identity, err := c.AuthenticatePassword(context.Background(), tt.req, tt.username, tt.password) + if tt.expectedErr != nil { + assert.ErrorIs(t, err, tt.expectedErr) + assert.Nil(t, identity) + } else { + assert.NoError(t, err) + assert.EqualValues(t, *tt.expectedIdentity, *identity) + } + }) + } +} From 54fd1b634feb72ca7da5791a975372bc1fc07d23 Mon Sep 17 00:00:00 2001 From: Gareth Dawson Date: Tue, 17 Jan 2023 08:45:23 +0000 Subject: [PATCH 29/65] Loki: Show configured log line limit (#61291) * feat: show configured line limit * test: correct failing test * refactor: update variable names * fix: ui renders the wrong value given a falsy value * refactor: use nullish coalescing operator --- .../loki/components/LokiQueryEditor.tsx | 8 +++++++- .../components/LokiQueryEditorByApp.test.tsx | 1 + .../LokiQueryBuilderOptions.test.tsx | 1 + .../components/LokiQueryBuilderOptions.tsx | 18 ++++++++++++------ 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx b/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx index b3b8563591f..85a9bd97902 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx @@ -165,7 +165,13 @@ export const LokiQueryEditor = React.memo((props) => { showExplain={explain} /> )} - + ); diff --git a/public/app/plugins/datasource/loki/components/LokiQueryEditorByApp.test.tsx b/public/app/plugins/datasource/loki/components/LokiQueryEditorByApp.test.tsx index 2f64b8faef9..c7530a2cddd 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryEditorByApp.test.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryEditorByApp.test.tsx @@ -20,6 +20,7 @@ function setup(app: CoreApp): RenderResult { }, getQueryHints: () => [], getDataSamples: () => [], + maxLines: 20, } as unknown as LokiDatasource; return render( diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.test.tsx index 511bc2d7374..3f0c9e3eb59 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.test.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.test.tsx @@ -46,6 +46,7 @@ function setup(queryOverrides: Partial = {}) { }, onRunQuery: jest.fn(), onChange: jest.fn(), + maxLines: 20, }; const { container } = render(); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx index 7e6e46596b5..cc46e9631ce 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx @@ -14,10 +14,11 @@ export interface Props { query: LokiQuery; onChange: (update: LokiQuery) => void; onRunQuery: () => void; + maxLines: number; app?: CoreApp; } -export const LokiQueryBuilderOptions = React.memo(({ app, query, onChange, onRunQuery }) => { +export const LokiQueryBuilderOptions = React.memo(({ app, query, onChange, onRunQuery, maxLines }) => { const onQueryTypeChange = (value: LokiQueryType) => { onChange({ ...query, queryType: value }); onRunQuery(); @@ -50,7 +51,7 @@ export const LokiQueryBuilderOptions = React.memo(({ app, query, onChange return ( - + (({ app, query, onChange (({ app, query, onChange ); }); -function getCollapsedInfo(query: LokiQuery, queryType: LokiQueryType, showMaxLines: boolean): string[] { +function getCollapsedInfo( + query: LokiQuery, + queryType: LokiQueryType, + showMaxLines: boolean, + maxLines: number +): string[] { const queryTypeLabel = queryTypeOptions.find((x) => x.value === queryType); const resolutionLabel = RESOLUTION_OPTIONS.find((x) => x.value === (query.resolution ?? 1)); @@ -109,8 +115,8 @@ function getCollapsedInfo(query: LokiQuery, queryType: LokiQueryType, showMaxLin items.push(`Type: ${queryTypeLabel?.label}`); - if (showMaxLines && query.maxLines) { - items.push(`Line limit: ${query.maxLines}`); + if (showMaxLines) { + items.push(`Line limit: ${query.maxLines ?? maxLines}`); } return items; From c1d3b596438b3967fec2373be6918633eef7d66d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 17 Jan 2023 09:52:14 +0100 Subject: [PATCH 30/65] PanelChrome: Simplify props and code a bit (#61595) * PanelChrome: Simplify props and code a bit * Don't set a default empty array for leftItems so we can know user has set it * fix 'this' is undefined * add storybook examples of statusMessage; remove SB examples of loadingState === Error; simplify loadingState checks in PanelChrome; Co-authored-by: polinaboneva --- .../PanelChrome/PanelChrome.story.tsx | 40 ++++++------- .../components/PanelChrome/PanelChrome.tsx | 59 ++++++++----------- .../dashboard/dashgrid/PanelStateWrapper.tsx | 13 ++-- 3 files changed, 50 insertions(+), 62 deletions(-) diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx index d6f4b3d0493..994756175e4 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx @@ -93,34 +93,33 @@ export const Examples = () => { - {renderPanel('Error status', { + {renderPanel('Has statusMessage', { title: 'Default title', - status: { - message: 'Error text', - onClick: action('ErrorIndicator: onClick fired'), - }, + statusMessage: 'Error text', + statusMessageOnClick: action('ErrorIndicator: onClick fired'), })} - {renderPanel('No padding, error loadingState', { + {renderPanel('No padding, has statusMessage', { padding: 'none', title: 'Default title', + statusMessage: 'Error text', + statusMessageOnClick: action('ErrorIndicator: onClick fired'), + })} + {renderPanel('No title, loadingState is Error, no statusMessage', { loadingState: LoadingState.Error, })} - {renderPanel('No title, error loadingState', { - loadingState: LoadingState.Error, - })} - {renderPanel('Streaming loadingState', { + {renderPanel('loadingState is Streaming', { title: 'Default title', loadingState: LoadingState.Streaming, })} - {renderPanel('Loading loadingState', { + {renderPanel('loadingState is Loading', { title: 'Default title', loadingState: LoadingState.Loading, })} {renderPanel('Default panel: no non-required props')} - {renderPanel('No padding, no title', { + {renderPanel('No padding', { padding: 'none', })} {renderPanel('Very long title', { @@ -137,28 +136,27 @@ export const Examples = () => { {renderPanel('Error status, menu', { title: 'Default title', menu, - status: { - message: 'Error text', - onClick: action('ErrorIndicator: onClick fired'), - }, + statusMessage: 'Error text', + statusMessageOnClick: action('ErrorIndicator: onClick fired'), })} - {renderPanel('No padding, error loadingState, menu', { + {renderPanel('No padding; has statusMessage, menu', { padding: 'none', title: 'Default title', menu, - loadingState: LoadingState.Error, + statusMessage: 'Error text', + statusMessageOnClick: action('ErrorIndicator: onClick fired'), })} - {renderPanel('No title, error loadingState, menu', { + {renderPanel('No title, loadingState is Error, no statusMessage, menu', { menu, loadingState: LoadingState.Error, })} - {renderPanel('Streaming loadingState, menu', { + {renderPanel('loadingState is Streaming, menu', { title: 'Default title', menu, loadingState: LoadingState.Streaming, })} - {renderPanel('Loading loadingState, menu', { + {renderPanel('loadingState is Loading, menu', { title: 'Default title', menu, loadingState: LoadingState.Loading, diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx index d2f2b3716a6..e91d3297633 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx @@ -1,6 +1,5 @@ import { css, cx } from '@emotion/css'; -import { isEmpty } from 'lodash'; -import React, { CSSProperties, ReactNode, ReactElement } from 'react'; +import React, { CSSProperties, ReactElement, ReactNode } from 'react'; import { GrafanaTheme2, LoadingState } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; @@ -15,11 +14,6 @@ import { Tooltip } from '../Tooltip'; import { PanelDescription } from './PanelDescription'; import { PanelStatus } from './PanelStatus'; -interface Status { - message?: string; - onClick?: (e: React.SyntheticEvent) => void; -} - /** * @internal */ @@ -36,7 +30,14 @@ export interface PanelChromeProps { // dragClass?: string; hoverHeader?: boolean; loadingState?: LoadingState; - status?: Status; + /** + * Used to display status message (used for panel errors currently) + */ + statusMessage?: string; + /** + * 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 * which will serve the same purpose @@ -65,8 +66,9 @@ export function PanelChrome({ // dragClass, hoverHeader = false, loadingState, - status, - leftItems = [], + statusMessage, + statusMessageOnClick, + leftItems, }: PanelChromeProps) { const theme = useTheme2(); const styles = useStyles2(getStyles); @@ -82,7 +84,7 @@ export function PanelChrome({ titleItems.length > 0 || description !== '' || loadingState === LoadingState.Streaming || - leftItems.length > 0); + (leftItems?.length ?? 0) > 0); const headerHeight = getHeaderHeight(theme, hasHeader); const { contentStyle, innerWidth, innerHeight } = getContentStyle(padding, theme, width, headerHeight, height); @@ -97,29 +99,12 @@ export function PanelChrome({ }; const containerStyles: CSSProperties = { width, height }; - - const isUsingDeprecatedLeftItems = isEmpty(status) && !loadingState; - const showLoading = loadingState === LoadingState.Loading && !isUsingDeprecatedLeftItems; - const showStreaming = loadingState === LoadingState.Streaming && !isUsingDeprecatedLeftItems; - - const renderStatus = () => { - const showError = loadingState === LoadingState.Error || status?.message; - if (!isUsingDeprecatedLeftItems && showError) { - return ( -
- -
- ); - } else { - return null; - } - }; - const ariaLabel = title ? selectors.components.Panels.Panel.containerByTitle(title) : 'Panel'; + return (
- {showLoading ? : null} + {loadingState === LoadingState.Loading ? : null}
@@ -131,13 +116,13 @@ export function PanelChrome({ - {titleItems && ( + {titleItems.length > 0 && (
{titleItems.map((item) => item)}
)} - {showStreaming && ( + {loadingState === LoadingState.Streaming && (
@@ -159,11 +144,16 @@ export function PanelChrome({ )} - {isUsingDeprecatedLeftItems &&
{itemsRenderer(leftItems, (item) => item)}
} + {leftItems &&
{itemsRenderer(leftItems, (item) => item)}
}
- {renderStatus()} + {statusMessage && ( +
+ +
+ )}
+
{children(innerWidth, innerHeight)}
@@ -180,6 +170,7 @@ const getHeaderHeight = (theme: GrafanaTheme2, hasHeader: boolean) => { if (hasHeader) { return theme.spacing.gridSize * theme.components.panel.headerHeight; } + return 0; }; diff --git a/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx b/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx index 0a0c3df1643..b7e2a8daeda 100644 --- a/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx +++ b/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx @@ -592,10 +592,11 @@ export class PanelStateWrapper extends PureComponent { e.stopPropagation(); locationService.partial({ inspect: this.props.panel.id, inspectTab: tab }); }; - onOpenErrorInspect(e: React.SyntheticEvent, tab: string) { + + onOpenErrorInspect = (e: React.SyntheticEvent) => { e.stopPropagation(); - locationService.partial({ inspect: this.props.panel.id, inspectTab: tab }); - } + locationService.partial({ inspect: this.props.panel.id, inspectTab: InspectTab.Error }); + }; render() { const { dashboard, panel, isViewing, isEditing, width, height, plugin } = this.props; @@ -648,10 +649,8 @@ export class PanelStateWrapper extends PureComponent { height={height} title={title} loadingState={data.state} - status={{ - message: errorMessage, - onClick: (e: React.SyntheticEvent) => this.onOpenErrorInspect(e, InspectTab.Error), - }} + statusMessage={errorMessage} + statusMessageOnClick={this.onOpenErrorInspect} description={!!panel.description ? this.onShowPanelDescription : undefined} titleItems={titleItems} menu={menu} From b44b6fc5c6ca032988f4a17b2e2932f3b89f32b7 Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Tue, 17 Jan 2023 10:07:46 +0100 Subject: [PATCH 31/65] AuthN: Add auth proxy client (#61555) * AuthN: set up boilerplate for proxy client * AuthN: Implement Test for proxy client * AuthN: parse accept list in constructor * AuthN: add proxy client interface * AuthN: handle error * AuthN: Implement the proxy client interface for ldap * AuthN: change reciever name * AuthN: add grafana as a proxy client * AuthN: for error returned * AuthN: add tests for grafana proxy auth * AuthN: swap order of grafan and ldap auth * AuthN: Parse additional proxy headers in proxy client and pass down --- pkg/services/authn/authn.go | 5 + pkg/services/authn/authnimpl/service.go | 23 ++- pkg/services/authn/authntest/mock.go | 13 ++ pkg/services/authn/clients/grafana.go | 71 +++++++- pkg/services/authn/clients/grafana_test.go | 111 ++++++++++- pkg/services/authn/clients/ldap.go | 71 +++++--- pkg/services/authn/clients/ldap_test.go | 73 +++++++- pkg/services/authn/clients/proxy.go | 147 +++++++++++++++ pkg/services/authn/clients/proxy_test.go | 172 ++++++++++++++++++ .../contexthandler/auth_proxy_test.go | 3 +- pkg/services/contexthandler/contexthandler.go | 22 +++ 11 files changed, 678 insertions(+), 33 deletions(-) create mode 100644 pkg/services/authn/clients/proxy.go create mode 100644 pkg/services/authn/clients/proxy_test.go diff --git a/pkg/services/authn/authn.go b/pkg/services/authn/authn.go index 5537e7e8998..ce6e9f20a47 100644 --- a/pkg/services/authn/authn.go +++ b/pkg/services/authn/authn.go @@ -25,6 +25,7 @@ const ( ClientRender = "auth.client.render" ClientSession = "auth.client.session" ClientForm = "auth.client.form" + ClientProxy = "auth.client.proxy" ) const ( @@ -72,6 +73,10 @@ type PasswordClient interface { AuthenticatePassword(ctx context.Context, r *Request, username, password string) (*Identity, error) } +type ProxyClient interface { + AuthenticateProxy(ctx context.Context, r *Request, username string, additional map[string]string) (*Identity, error) +} + type Request struct { // OrgID will be populated by authn.Service OrgID int64 diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index ae93c60aa65..63f76c9e80b 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -64,12 +64,18 @@ func ProvideService( s.clients[authn.ClientAnonymous] = clients.ProvideAnonymous(cfg, orgService) } + var proxyClients []authn.ProxyClient var passwordClients []authn.PasswordClient - if !s.cfg.DisableLogin { - passwordClients = append(passwordClients, clients.ProvideGrafana(userService)) - } if s.cfg.LDAPEnabled { - passwordClients = append(passwordClients, clients.ProvideLDAP(cfg)) + ldap := clients.ProvideLDAP(cfg) + proxyClients = append(proxyClients, ldap) + passwordClients = append(passwordClients, ldap) + } + + if !s.cfg.DisableLogin { + grafana := clients.ProvideGrafana(cfg, userService) + proxyClients = append(proxyClients, grafana) + passwordClients = append(passwordClients, grafana) } // if we have password clients configure check if basic auth or form auth is enabled @@ -84,6 +90,15 @@ func ProvideService( } } + if s.cfg.AuthProxyEnabled && len(proxyClients) > 0 { + proxy, err := clients.ProvideProxy(cfg, proxyClients...) + if err != nil { + s.log.Error("failed to configure auth proxy", "err", err) + } else { + s.clients[authn.ClientProxy] = proxy + } + } + if s.cfg.JWTAuthEnabled { s.clients[authn.ClientJWT] = clients.ProvideJWT(jwtService, cfg) } diff --git a/pkg/services/authn/authntest/mock.go b/pkg/services/authn/authntest/mock.go index a3e35ec6770..7bb7f1c92df 100644 --- a/pkg/services/authn/authntest/mock.go +++ b/pkg/services/authn/authntest/mock.go @@ -26,3 +26,16 @@ func (m MockClient) Test(ctx context.Context, r *authn.Request) bool { } return false } + +var _ authn.ProxyClient = new(MockProxyClient) + +type MockProxyClient struct { + AuthenticateProxyFunc func(ctx context.Context, r *authn.Request, username string, additional map[string]string) (*authn.Identity, error) +} + +func (m MockProxyClient) AuthenticateProxy(ctx context.Context, r *authn.Request, username string, additional map[string]string) (*authn.Identity, error) { + if m.AuthenticateProxyFunc != nil { + return m.AuthenticateProxyFunc(ctx, r, username, additional) + } + return nil, nil +} diff --git a/pkg/services/authn/clients/grafana.go b/pkg/services/authn/clients/grafana.go index a14be60f163..ca830973627 100644 --- a/pkg/services/authn/clients/grafana.go +++ b/pkg/services/authn/clients/grafana.go @@ -4,23 +4,88 @@ import ( "context" "crypto/subtle" "errors" + "net/mail" "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) +var _ authn.ProxyClient = new(Grafana) var _ authn.PasswordClient = new(Grafana) -func ProvideGrafana(userService user.Service) *Grafana { - return &Grafana{userService} +func ProvideGrafana(cfg *setting.Cfg, userService user.Service) *Grafana { + return &Grafana{cfg, userService} } type Grafana struct { + cfg *setting.Cfg userService user.Service } -func (c Grafana) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) { +func (c *Grafana) AuthenticateProxy(ctx context.Context, r *authn.Request, username string, additional map[string]string) (*authn.Identity, error) { + identity := &authn.Identity{ + AuthModule: login.AuthProxyAuthModule, + AuthID: username, + ClientParams: authn.ClientParams{ + SyncUser: true, + SyncTeamMembers: true, + AllowSignUp: c.cfg.AuthProxyAutoSignUp, + }, + } + + switch c.cfg.AuthProxyHeaderProperty { + case "username": + identity.Login = username + addr, err := mail.ParseAddress(username) + if err == nil { + identity.Email = addr.Address + } + case "email": + identity.Login = username + identity.Email = username + default: + return nil, errInvalidProxyHeader.Errorf("invalid auth proxy header property, expected username or email but got: %s", c.cfg.AuthProxyHeaderProperty) + } + + if v, ok := additional[proxyFieldName]; ok { + identity.Name = v + } + + if v, ok := additional[proxyFieldEmail]; ok { + identity.Email = v + } + + if v, ok := additional[proxyFieldLogin]; ok { + identity.Login = v + } + + if v, ok := additional[proxyFieldRole]; ok { + role := org.RoleType(v) + if role.IsValid() { + orgID := int64(1) + if c.cfg.AutoAssignOrg && c.cfg.AutoAssignOrgId > 0 { + orgID = int64(c.cfg.AutoAssignOrgId) + } + identity.OrgID = orgID + identity.OrgRoles = map[int64]org.RoleType{orgID: role} + } + } + + if v, ok := additional[proxyFieldGroups]; ok { + identity.Groups = util.SplitString(v) + } + + identity.ClientParams.LookUpParams.Email = &identity.Email + identity.ClientParams.LookUpParams.Login = &identity.Login + + return identity, nil +} + +func (c *Grafana) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) { usr, err := c.userService.GetByLogin(ctx, &user.GetUserByLoginQuery{LoginOrEmail: username}) if err != nil { if errors.Is(err, user.ErrUserNotFound) { diff --git a/pkg/services/authn/clients/grafana_test.go b/pkg/services/authn/clients/grafana_test.go index 4d17bfa8795..918888f5a23 100644 --- a/pkg/services/authn/clients/grafana_test.go +++ b/pkg/services/authn/clients/grafana_test.go @@ -2,16 +2,125 @@ package clients import ( "context" + "net/http" "testing" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" "github.com/stretchr/testify/assert" ) +func TestGrafana_AuthenticateProxy(t *testing.T) { + type testCase struct { + desc string + req *authn.Request + username string + proxyProperty string + additional map[string]string + expectedErr error + expectedIdentity *authn.Identity + } + + tests := []testCase{ + { + desc: "expect valid identity", + username: "test", + req: &authn.Request{HTTPRequest: &http.Request{}}, + proxyProperty: "username", + additional: map[string]string{ + proxyFieldName: "name", + proxyFieldRole: "Viewer", + proxyFieldGroups: "grp1,grp2", + proxyFieldEmail: "email@email.com", + }, + expectedIdentity: &authn.Identity{ + OrgID: 1, + OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, + Login: "test", + Name: "name", + Email: "email@email.com", + AuthModule: "authproxy", + AuthID: "test", + Groups: []string{"grp1", "grp2"}, + ClientParams: authn.ClientParams{ + SyncUser: true, + SyncTeamMembers: true, + AllowSignUp: true, + LookUpParams: models.UserLookupParams{ + Email: strPtr("email@email.com"), + Login: strPtr("test"), + }, + }, + }, + }, + { + desc: "should set email as both email and login when configured proxy auth header property is email", + username: "test@test.com", + req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{}}}, + additional: map[string]string{}, + expectedIdentity: &authn.Identity{ + Login: "test@test.com", + Email: "test@test.com", + AuthModule: "authproxy", + AuthID: "test@test.com", + ClientParams: authn.ClientParams{ + SyncUser: true, + SyncTeamMembers: true, + AllowSignUp: true, + LookUpParams: models.UserLookupParams{ + Email: strPtr("test@test.com"), + Login: strPtr("test@test.com"), + }, + }, + }, + proxyProperty: "email", + }, + { + desc: "should return error on invalid auth proxy header property", + req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{}}}, + proxyProperty: "other", + expectedErr: errInvalidProxyHeader, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + cfg := setting.NewCfg() + cfg.AuthProxyAutoSignUp = true + cfg.AuthProxyHeaderProperty = tt.proxyProperty + c := ProvideGrafana(cfg, usertest.NewUserServiceFake()) + + identity, err := c.AuthenticateProxy(context.Background(), tt.req, tt.username, tt.additional) + assert.ErrorIs(t, err, tt.expectedErr) + if tt.expectedIdentity != nil { + assert.Equal(t, tt.expectedIdentity.OrgID, identity.OrgID) + assert.Equal(t, tt.expectedIdentity.Login, identity.Login) + assert.Equal(t, tt.expectedIdentity.Name, identity.Name) + assert.Equal(t, tt.expectedIdentity.Email, identity.Email) + assert.Equal(t, tt.expectedIdentity.AuthID, identity.AuthID) + assert.Equal(t, tt.expectedIdentity.AuthModule, identity.AuthModule) + assert.Equal(t, tt.expectedIdentity.Groups, identity.Groups) + + assert.Equal(t, tt.expectedIdentity.ClientParams.SyncUser, identity.ClientParams.SyncUser) + assert.Equal(t, tt.expectedIdentity.ClientParams.AllowSignUp, identity.ClientParams.AllowSignUp) + assert.Equal(t, tt.expectedIdentity.ClientParams.SyncTeamMembers, identity.ClientParams.SyncTeamMembers) + assert.Equal(t, tt.expectedIdentity.ClientParams.EnableDisabledUsers, identity.ClientParams.EnableDisabledUsers) + + assert.EqualValues(t, tt.expectedIdentity.ClientParams.LookUpParams.Email, identity.ClientParams.LookUpParams.Email) + assert.EqualValues(t, tt.expectedIdentity.ClientParams.LookUpParams.Login, identity.ClientParams.LookUpParams.Login) + assert.EqualValues(t, tt.expectedIdentity.ClientParams.LookUpParams.UserID, identity.ClientParams.LookUpParams.UserID) + } else { + assert.Nil(t, tt.expectedIdentity) + } + }) + } +} + func TestGrafana_AuthenticatePassword(t *testing.T) { type testCase struct { desc string @@ -60,7 +169,7 @@ func TestGrafana_AuthenticatePassword(t *testing.T) { userService.ExpectedError = user.ErrUserNotFound } - c := ProvideGrafana(userService) + c := ProvideGrafana(setting.NewCfg(), userService) identity, err := c.AuthenticatePassword(context.Background(), &authn.Request{OrgID: 1}, tt.username, tt.password) assert.ErrorIs(t, err, tt.expectedErr) assert.EqualValues(t, tt.expectedIdentity, identity) diff --git a/pkg/services/authn/clients/ldap.go b/pkg/services/authn/clients/ldap.go index 0a4a9bfbf8a..0e8f14a5be0 100644 --- a/pkg/services/authn/clients/ldap.go +++ b/pkg/services/authn/clients/ldap.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/setting" ) +var _ authn.ProxyClient = new(LDAP) var _ authn.PasswordClient = new(LDAP) func ProvideLDAP(cfg *setting.Cfg) *LDAP { @@ -21,6 +22,19 @@ type LDAP struct { service ldapService } +func (c *LDAP) AuthenticateProxy(ctx context.Context, r *authn.Request, username string, _ map[string]string) (*authn.Identity, error) { + info, err := c.service.User(username) + if errors.Is(err, multildap.ErrDidNotFindUser) { + return nil, errIdentityNotFound.Errorf("no user found: %w", err) + } + + if err != nil { + return nil, err + } + + return identityFromLDAPInfo(r.OrgID, info, c.cfg.LDAPAllowSignup), nil +} + func (c *LDAP) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) { info, err := c.service.Login(&models.LoginUserQuery{ Username: username, @@ -43,31 +57,12 @@ func (c *LDAP) AuthenticatePassword(ctx context.Context, r *authn.Request, usern return nil, err } - return &authn.Identity{ - OrgID: r.OrgID, - OrgRoles: info.OrgRoles, - Login: info.Login, - Name: info.Name, - Email: info.Email, - IsGrafanaAdmin: info.IsGrafanaAdmin, - AuthModule: info.AuthModule, - AuthID: info.AuthId, - Groups: info.Groups, - ClientParams: authn.ClientParams{ - SyncUser: true, - SyncTeamMembers: true, - AllowSignUp: c.cfg.LDAPAllowSignup, - EnableDisabledUsers: true, - LookUpParams: models.UserLookupParams{ - Login: &info.Login, - Email: &info.Email, - }, - }, - }, nil + return identityFromLDAPInfo(r.OrgID, info, c.cfg.LDAPAllowSignup), nil } type ldapService interface { Login(query *models.LoginUserQuery) (*models.ExternalUserInfo, error) + User(username string) (*models.ExternalUserInfo, error) } // FIXME: remove the implementation if we convert ldap to an actual service @@ -83,3 +78,37 @@ func (s *ldapServiceImpl) Login(query *models.LoginUserQuery) (*models.ExternalU return multildap.New(cfg.Servers).Login(query) } + +func (s *ldapServiceImpl) User(username string) (*models.ExternalUserInfo, error) { + cfg, err := multildap.GetConfig(s.cfg) + if err != nil { + return nil, err + } + + user, _, err := multildap.New(cfg.Servers).User(username) + return user, err +} + +func identityFromLDAPInfo(orgID int64, info *models.ExternalUserInfo, allowSignup bool) *authn.Identity { + return &authn.Identity{ + OrgID: orgID, + OrgRoles: info.OrgRoles, + Login: info.Login, + Name: info.Name, + Email: info.Email, + IsGrafanaAdmin: info.IsGrafanaAdmin, + AuthModule: info.AuthModule, + AuthID: info.AuthId, + Groups: info.Groups, + ClientParams: authn.ClientParams{ + SyncUser: true, + SyncTeamMembers: true, + AllowSignUp: allowSignup, + EnableDisabledUsers: true, + LookUpParams: models.UserLookupParams{ + Login: &info.Login, + Email: &info.Email, + }, + }, + } +} diff --git a/pkg/services/authn/clients/ldap_test.go b/pkg/services/authn/clients/ldap_test.go index 91971225519..e59516f2cf1 100644 --- a/pkg/services/authn/clients/ldap_test.go +++ b/pkg/services/authn/clients/ldap_test.go @@ -8,11 +8,74 @@ import ( "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/multildap" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" ) +func TestLDAP_AuthenticateProxy(t *testing.T) { + type testCase struct { + desc string + username string + expectedLDAPErr error + expectedLDAPInfo *models.ExternalUserInfo + expectedErr error + expectedIdentity *authn.Identity + } + + tests := []testCase{ + { + desc: "should return valid identity when found by ldap service", + username: "test", + expectedLDAPInfo: &models.ExternalUserInfo{ + AuthModule: login.LDAPAuthModule, + AuthId: "123", + Email: "test@test.com", + Login: "test", + Name: "test test", + Groups: []string{"1", "2"}, + OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, + }, + expectedIdentity: &authn.Identity{ + OrgID: 1, + OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, + Login: "test", + Name: "test test", + Email: "test@test.com", + AuthModule: login.LDAPAuthModule, + AuthID: "123", + Groups: []string{"1", "2"}, + ClientParams: authn.ClientParams{ + SyncUser: true, + SyncTeamMembers: true, + AllowSignUp: false, + EnableDisabledUsers: true, + LookUpParams: models.UserLookupParams{ + Email: strPtr("test@test.com"), + Login: strPtr("test"), + }, + }, + }, + }, + { + desc: "should return error when user is not found", + username: "test", + expectedLDAPErr: multildap.ErrDidNotFindUser, + expectedErr: errIdentityNotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + c := &LDAP{cfg: setting.NewCfg(), service: fakeLDAPService{ExpectedInfo: tt.expectedLDAPInfo, ExpectedErr: tt.expectedLDAPErr}} + identity, err := c.AuthenticateProxy(context.Background(), &authn.Request{OrgID: 1}, tt.username, nil) + assert.ErrorIs(t, err, tt.expectedErr) + assert.EqualValues(t, tt.expectedIdentity, identity) + }) + } +} + func TestLDAP_AuthenticatePassword(t *testing.T) { type testCase struct { desc string @@ -20,7 +83,7 @@ func TestLDAP_AuthenticatePassword(t *testing.T) { password string expectedErr error expectedLDAPErr error - expectedInfo *models.ExternalUserInfo + expectedLDAPInfo *models.ExternalUserInfo expectedIdentity *authn.Identity } @@ -29,7 +92,7 @@ func TestLDAP_AuthenticatePassword(t *testing.T) { desc: "should successfully authenticate with correct username and password", username: "test", password: "test123", - expectedInfo: &models.ExternalUserInfo{ + expectedLDAPInfo: &models.ExternalUserInfo{ AuthModule: login.LDAPAuthModule, AuthId: "123", Email: "test@test.com", @@ -77,7 +140,7 @@ func TestLDAP_AuthenticatePassword(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - c := &LDAP{cfg: setting.NewCfg(), service: fakeLDAPService{ExpectedInfo: tt.expectedInfo, ExpectedErr: tt.expectedLDAPErr}} + c := &LDAP{cfg: setting.NewCfg(), service: fakeLDAPService{ExpectedInfo: tt.expectedLDAPInfo, ExpectedErr: tt.expectedLDAPErr}} identity, err := c.AuthenticatePassword(context.Background(), &authn.Request{OrgID: 1}, tt.username, tt.password) assert.ErrorIs(t, err, tt.expectedErr) @@ -100,3 +163,7 @@ type fakeLDAPService struct { func (f fakeLDAPService) Login(query *models.LoginUserQuery) (*models.ExternalUserInfo, error) { return f.ExpectedInfo, f.ExpectedErr } + +func (f fakeLDAPService) User(username string) (*models.ExternalUserInfo, error) { + return f.ExpectedInfo, f.ExpectedErr +} diff --git a/pkg/services/authn/clients/proxy.go b/pkg/services/authn/clients/proxy.go new file mode 100644 index 00000000000..1f8a7d0ad69 --- /dev/null +++ b/pkg/services/authn/clients/proxy.go @@ -0,0 +1,147 @@ +package clients + +import ( + "context" + "fmt" + "net" + "path" + "strings" + + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" + "github.com/grafana/grafana/pkg/util/errutil" +) + +const ( + proxyFieldName = "Name" + proxyFieldEmail = "Email" + proxyFieldLogin = "Login" + proxyFieldRole = "Role" + proxyFieldGroups = "Groups" +) + +var proxyFields = [...]string{proxyFieldName, proxyFieldEmail, proxyFieldLogin, proxyFieldRole, proxyFieldGroups} + +var ( + errNotAcceptedIP = errutil.NewBase(errutil.StatusUnauthorized, "auth-proxy.invalid-ip") + errEmptyProxyHeader = errutil.NewBase(errutil.StatusUnauthorized, "auth-proxy.empty-header") + errInvalidProxyHeader = errutil.NewBase(errutil.StatusInternal, "auth-proxy.invalid-proxy-header") +) + +var _ authn.Client = new(Proxy) + +func ProvideProxy(cfg *setting.Cfg, clients ...authn.ProxyClient) (*Proxy, error) { + list, err := parseAcceptList(cfg.AuthProxyWhitelist) + if err != nil { + return nil, err + } + return &Proxy{cfg, clients, list}, nil +} + +type Proxy struct { + cfg *setting.Cfg + clients []authn.ProxyClient + acceptedIPs []*net.IPNet +} + +func (c *Proxy) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { + if !c.isAllowedIP(r) { + return nil, errNotAcceptedIP.Errorf("request ip is not in the configured accept list") + } + + username := getProxyHeader(r, c.cfg.AuthProxyHeaderName, c.cfg.AuthProxyHeadersEncoded) + if len(username) == 0 { + return nil, errEmptyProxyHeader.Errorf("no username provided in auth proxy header") + } + + additional := getAdditionalProxyHeaders(r, c.cfg) + + // FIXME: add cache to prevent sync on every request + + var clientErr error + for _, proxyClient := range c.clients { + var identity *authn.Identity + identity, clientErr = proxyClient.AuthenticateProxy(ctx, r, username, additional) + if identity != nil { + return identity, nil + } + } + + return nil, clientErr +} + +func (c *Proxy) Test(ctx context.Context, r *authn.Request) bool { + return len(getProxyHeader(r, c.cfg.AuthProxyHeaderName, c.cfg.AuthProxyHeadersEncoded)) != 0 +} + +func (c *Proxy) isAllowedIP(r *authn.Request) bool { + if len(c.acceptedIPs) == 0 { + return true + } + + host, _, err := net.SplitHostPort(r.HTTPRequest.RemoteAddr) + if err != nil { + return false + } + + ip := net.ParseIP(host) + for _, v := range c.acceptedIPs { + if v.Contains(ip) { + return true + } + } + + return false +} + +func parseAcceptList(s string) ([]*net.IPNet, error) { + if len(strings.TrimSpace(s)) == 0 { + return nil, nil + } + addresses := strings.Split(s, ",") + list := make([]*net.IPNet, 0, len(addresses)) + for _, addr := range addresses { + result, err := coerceProxyAddress(addr) + if err != nil { + return nil, err + } + list = append(list, result) + } + return list, nil +} + +// coerceProxyAddress gets network of the presented CIDR notation +func coerceProxyAddress(proxyAddr string) (*net.IPNet, error) { + proxyAddr = strings.TrimSpace(proxyAddr) + if !strings.Contains(proxyAddr, "/") { + proxyAddr = path.Join(proxyAddr, "32") + } + + _, network, err := net.ParseCIDR(proxyAddr) + if err != nil { + return nil, fmt.Errorf("could not parse the network: %w", err) + } + return network, nil +} + +func getProxyHeader(r *authn.Request, headerName string, encoded bool) string { + if r.HTTPRequest == nil { + return "" + } + v := r.HTTPRequest.Header.Get(headerName) + if encoded { + v = util.DecodeQuotedPrintable(v) + } + return v +} + +func getAdditionalProxyHeaders(r *authn.Request, cfg *setting.Cfg) map[string]string { + additional := make(map[string]string, len(proxyFields)) + for _, k := range proxyFields { + if v := getProxyHeader(r, cfg.AuthProxyHeaders[k], cfg.AuthProxyHeadersEncoded); v != "" { + additional[k] = v + } + } + return additional +} diff --git a/pkg/services/authn/clients/proxy_test.go b/pkg/services/authn/clients/proxy_test.go new file mode 100644 index 00000000000..62a72e32ff8 --- /dev/null +++ b/pkg/services/authn/clients/proxy_test.go @@ -0,0 +1,172 @@ +package clients + +import ( + "context" + "net/http" + "testing" + + "github.com/grafana/grafana/pkg/services/authn/authntest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/setting" +) + +func TestProxy_Authenticate(t *testing.T) { + type testCase struct { + desc string + req *authn.Request + ips string + proxyHeader string + proxyHeaders map[string]string + expectedErr error + expectedUsername string + expectedAdditional map[string]string + } + + tests := []testCase{ + { + desc: "should authenticate using passed in proxy client", + ips: "127.0.0.1", + req: &authn.Request{ + HTTPRequest: &http.Request{ + Header: map[string][]string{ + "X-Username": {"username"}, + "X-Name": {"name"}, + "X-Email": {"email"}, + "X-Login": {"login"}, + "X-Role": {"Viewer"}, + "X-Group": {"grp1,grp2"}, + }, + RemoteAddr: "127.0.0.1:333", + }, + }, + proxyHeader: "X-Username", + proxyHeaders: map[string]string{ + proxyFieldName: "X-Name", + proxyFieldEmail: "X-Email", + proxyFieldLogin: "X-Login", + proxyFieldRole: "X-Role", + proxyFieldGroups: "X-Group", + }, + expectedUsername: "username", + expectedAdditional: map[string]string{ + proxyFieldName: "name", + proxyFieldEmail: "email", + proxyFieldLogin: "login", + proxyFieldRole: "Viewer", + proxyFieldGroups: "grp1,grp2", + }, + }, + { + desc: "should fail when proxy header is empty", + req: &authn.Request{ + HTTPRequest: &http.Request{Header: map[string][]string{ + "X-Username": {""}, + "X-Name": {"name"}, + "X-Email": {"email"}, + "X-Login": {"login"}, + "X-Role": {"Viewer"}, + "X-Group": {"grp1,grp2"}, + }}, + }, + proxyHeader: "X-Username", + proxyHeaders: map[string]string{ + proxyFieldName: "X-Name", + proxyFieldEmail: "X-Email", + proxyFieldLogin: "X-Login", + proxyFieldRole: "X-Role", + proxyFieldGroups: "X-Group", + }, + expectedErr: errEmptyProxyHeader, + }, + { + desc: "should fail when caller ip is not in accept list", + req: &authn.Request{ + HTTPRequest: &http.Request{ + Header: map[string][]string{}, + RemoteAddr: "127.0.0.2:333", + }, + }, + ips: "127.0.0.1", + expectedErr: errNotAcceptedIP, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + cfg := setting.NewCfg() + cfg.AuthProxyHeaderName = "X-Username" + cfg.AuthProxyHeaders = tt.proxyHeaders + cfg.AuthProxyWhitelist = tt.ips + + calledUsername := "" + var calledAdditional map[string]string + + proxyClient := authntest.MockProxyClient{AuthenticateProxyFunc: func(ctx context.Context, r *authn.Request, username string, additional map[string]string) (*authn.Identity, error) { + calledUsername = username + calledAdditional = additional + return nil, nil + }} + c, err := ProvideProxy(cfg, proxyClient) + require.NoError(t, err) + + _, err = c.Authenticate(context.Background(), tt.req) + assert.ErrorIs(t, err, tt.expectedErr) + assert.Equal(t, tt.expectedUsername, calledUsername) + assert.EqualValues(t, tt.expectedAdditional, calledAdditional) + }) + } +} + +func TestProxy_Test(t *testing.T) { + type testCase struct { + desc string + req *authn.Request + expectedOK bool + } + + tests := []testCase{ + { + desc: "should return true when proxy header exists", + req: &authn.Request{ + HTTPRequest: &http.Request{ + Header: map[string][]string{"Proxy-Header": {"some value"}}, + }, + }, + expectedOK: true, + }, + { + desc: "should return false when proxy header exists but has no value", + req: &authn.Request{ + HTTPRequest: &http.Request{ + Header: map[string][]string{"Proxy-Header": {""}}, + }, + }, + expectedOK: false, + }, + { + desc: "should return false when no proxy header is set on request", + req: &authn.Request{ + HTTPRequest: &http.Request{Header: map[string][]string{}}, + }, + expectedOK: false, + }, + { + desc: "should return false when no http request is present", + req: &authn.Request{}, + expectedOK: false, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + cfg := setting.NewCfg() + cfg.AuthProxyHeaderName = "Proxy-Header" + + c, _ := ProvideProxy(cfg, nil) + assert.Equal(t, tt.expectedOK, c.Test(context.Background(), tt.req)) + }) + } +} diff --git a/pkg/services/contexthandler/auth_proxy_test.go b/pkg/services/contexthandler/auth_proxy_test.go index 72981b2e32b..38b9f3e27dd 100644 --- a/pkg/services/contexthandler/auth_proxy_test.go +++ b/pkg/services/contexthandler/auth_proxy_test.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/services/auth/authtest" "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/grafana/grafana/pkg/services/contexthandler/authproxy" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/login/loginservice" "github.com/grafana/grafana/pkg/services/org/orgtest" "github.com/grafana/grafana/pkg/services/rendering" @@ -106,7 +107,7 @@ func getContextHandler(t *testing.T) *ContextHandler { return ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, sqlStore, tracer, authProxy, loginService, nil, authenticator, - &userService, orgService, nil, nil, &authntest.FakeService{}) + &userService, orgService, nil, featuremgmt.WithFeatures(), &authntest.FakeService{}) } type fakeAuthenticator struct{} diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index aad500e1451..4888c6cee86 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -714,6 +714,28 @@ func (h *ContextHandler) handleError(ctx *models.ReqContext, err error, statusCo } func (h *ContextHandler) initContextWithAuthProxy(reqContext *models.ReqContext, orgID int64) bool { + if h.features.IsEnabled(featuremgmt.FlagAuthnService) { + identity, ok, err := h.authnService.Authenticate(reqContext.Req.Context(), authn.ClientProxy, &authn.Request{HTTPRequest: reqContext.Req, Resp: reqContext.Resp}) + if !ok { + return false + } + + if err != nil { + writeErr(reqContext, err) + } + + ctx := WithAuthHTTPHeader(reqContext.Req.Context(), h.Cfg.AuthProxyHeaderName) + for _, header := range h.Cfg.AuthProxyHeaders { + if header != "" { + ctx = WithAuthHTTPHeader(ctx, header) + } + } + + *reqContext.Req = *reqContext.Req.WithContext(ctx) + reqContext.IsSignedIn = true + reqContext.SignedInUser = identity.SignedInUser() + return true + } username := reqContext.Req.Header.Get(h.Cfg.AuthProxyHeaderName) logger := log.New("auth.proxy") From b5383b7d05c9c1f5593147a13f270cb45b18eb30 Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Tue, 17 Jan 2023 11:30:30 +0200 Subject: [PATCH 32/65] Barchart: Fix erroneous tooltip value (#61455) Fix Barchart tooltip value issue --- public/app/plugins/panel/barchart/BarChartPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/barchart/BarChartPanel.tsx b/public/app/plugins/panel/barchart/BarChartPanel.tsx index afd30c625e8..f94abde5c03 100644 --- a/public/app/plugins/panel/barchart/BarChartPanel.tsx +++ b/public/app/plugins/panel/barchart/BarChartPanel.tsx @@ -355,7 +355,7 @@ export const BarChartPanel: React.FunctionComponent = ({ offset={{ x: TOOLTIP_OFFSET, y: TOOLTIP_OFFSET }} allowPointerEvents={isToolTipOpen.current} > - {renderTooltip(info.aligned, focusedSeriesIdx, focusedPointIdx)} + {renderTooltip(info.viz[0], focusedSeriesIdx, focusedPointIdx)} )} From f44bc0dd6b0a80143fb27ed0f712d28c87b77a6a Mon Sep 17 00:00:00 2001 From: Ieva Date: Tue, 17 Jan 2023 09:58:40 +0000 Subject: [PATCH 33/65] RBAC: use scope reduction for user permission listing (#61583) use scope reduction for user permission listing --- .../accesscontrol/accesscontrol_test.go | 21 +++++++++++++++++++ pkg/services/accesscontrol/api/api.go | 4 ++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/pkg/services/accesscontrol/accesscontrol_test.go b/pkg/services/accesscontrol/accesscontrol_test.go index 9d20738441a..6d8afd673da 100644 --- a/pkg/services/accesscontrol/accesscontrol_test.go +++ b/pkg/services/accesscontrol/accesscontrol_test.go @@ -42,6 +42,17 @@ func TestReduce(t *testing.T) { "teams:write": {"teams:id:1"}, }, }, + { + name: "specific permissions with repeated scope", + ps: []Permission{ + {Action: "teams:read", Scope: "teams:id:1"}, + {Action: "teams:read", Scope: "teams:id:2"}, + {Action: "teams:read", Scope: "teams:id:1"}, + }, + want: map[string][]string{ + "teams:read": {"teams:id:1", "teams:id:2"}, + }, + }, { name: "wildcard permission", ps: []Permission{ @@ -88,6 +99,16 @@ func TestReduce(t *testing.T) { "dashboards:read": {"*"}, }, }, + { + name: "non-wilcard scopes with * in them", + ps: []Permission{ + {Action: "dashboards:read", Scope: "dashboards:uid:123"}, + {Action: "dashboards:read", Scope: "dashboards:uid:1*"}, + }, + want: map[string][]string{ + "dashboards:read": {"dashboards:uid:123", "dashboards:uid:1*"}, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/services/accesscontrol/api/api.go b/pkg/services/accesscontrol/api/api.go index eadfaf0a028..a3d279e103f 100644 --- a/pkg/services/accesscontrol/api/api.go +++ b/pkg/services/accesscontrol/api/api.go @@ -91,7 +91,7 @@ func (api *AccessControlAPI) searchUsersPermissions(c *models.ReqContext) respon permsByAction := map[int64]map[string][]string{} for userID, userPerms := range permissions { - permsByAction[userID] = ac.GroupScopesByAction(userPerms) + permsByAction[userID] = ac.Reduce(userPerms) } return response.JSON(http.StatusOK, permsByAction) @@ -121,5 +121,5 @@ func (api *AccessControlAPI) searchUserPermissions(c *models.ReqContext) respons response.Error(http.StatusInternalServerError, "could not search user permissions", err) } - return response.JSON(http.StatusOK, ac.GroupScopesByAction(permissions)) + return response.JSON(http.StatusOK, ac.Reduce(permissions)) } From 9a8c798bae63da7c04eb8c79b5777e1580c4cfec Mon Sep 17 00:00:00 2001 From: Ieva Date: Tue, 17 Jan 2023 10:03:31 +0000 Subject: [PATCH 34/65] Docs: add missing plugin roles, actions and scope (#61182) * add missing plugin roles, actions and scope * Update docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md Co-authored-by: Alyssa Wada <101596687+alyssawada@users.noreply.github.com> * Update docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md Co-authored-by: Gabriel MABILLE * Apply suggestions from code review Co-authored-by: Gabriel MABILLE Co-authored-by: Alyssa Wada <101596687+alyssawada@users.noreply.github.com> Co-authored-by: Gabriel MABILLE --- .../custom-role-actions-scopes/index.md | 3 +++ .../rbac-fixed-basic-role-definitions/index.md | 14 ++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md b/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md index c8c148d3adc..33c22c8a913 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/custom-role-actions-scopes/index.md @@ -95,6 +95,8 @@ The following list contains role-based access control actions. | `orgs:read` | `orgs:*`
`orgs:id:*` | Read one or more organizations. | | `orgs:write` | `orgs:*`
`orgs:id:*` | Update one or more organizations. | | `plugins.app:access` | `plugins:*`
`plugins:id:*` | Access one or more application plugins (still enforcing the organization role) | +| `plugins:install` | n/a | Install and uninstall plugins. | +| `plugins:write` | `plugins:*`
`plugins:id:*` | Edit settings for one or more plugins. | | `provisioning:reload` | `provisioners:*` | Reload provisioning files. To find the exact scope for specific provisioner, see [Scope definitions]({{< relref "#scope-definitions" >}}). | | `reports:create` | n/a | Create reports. | | `reports:write` | `reports:*`
`reports:id:*` | Update reports. | @@ -196,6 +198,7 @@ The following list contains role-based access control scopes. | `orgs:*`
`orgs:id:*` | Restrict an action to a set of organizations. For example, `orgs:*` matches any organization and `orgs:id:1` matches the organization whose ID is `1`. | | `permissions:type:delegate` | The scope is only applicable for roles associated with the Access Control itself and indicates that you can delegate your permissions only, or a subset of it, by creating a new role or making an assignment. | | `permissions:type:escalate` | The scope is required to trigger the reset of basic roles permissions. It indicates that users might acquire additional permissions they did not previously have. | +| `plugins:*`
`plugins:id:*` | Restrict an action to a set of plugins. For example, `plugins:id:grafana-oncall-app` matches Grafana OnCall plugin, and `plugins:*` matches all plugins. | | `provisioners:*` | Restrict an action to a set of provisioners. For example, `provisioners:*` matches any provisioner, and `provisioners:accesscontrol` matches the role-based access control [provisioner]({{< relref "./rbac-grafana-provisioning/" >}}). | | `reports:*`
`reports:id:*` | Restrict an action to a set of reports. For example, `reports:*` matches any report and `reports:id:1` matches the report whose ID is `1`. | | `roles:*`
`roles:uid:*` | Restrict an action to a set of roles. For example, `roles:*` matches any role and `roles:uid:randomuid` matches only the role whose UID is `randomuid`. | diff --git a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md index 675d13a5853..907ac6a92f8 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md @@ -17,12 +17,12 @@ The following tables list permissions associated with basic and fixed roles. ## Basic role assignments -| Basic role | Associated fixed roles | Description | -| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -| Grafana Admin | `fixed:roles:reader`
`fixed:roles:writer`
`fixed:users:reader`
`fixed:users:writer`
`fixed:org.users:reader`
`fixed:org.users:writer`
`fixed:ldap:reader`
`fixed:ldap:writer`
`fixed:stats:reader`
`fixed:settings:reader`
`fixed:settings:writer`
`fixed:provisioning:writer`
`fixed:organization:reader`
`fixed:organization:maintainer`
`fixed:licensing:reader`
`fixed:licensing:writer`
`fixed:datasources.caching:reader`
`fixed:datasources.caching:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader` | Default [Grafana server administrator]({{< relref "../#grafana-server-administrators" >}}) assignments. | -| Admin | `fixed:reports:reader`
`fixed:reports:writer`
`fixed:datasources:reader`
`fixed:datasources:writer`
`fixed:organization:writer`
`fixed:datasources.permissions:reader`
`fixed:datasources.permissions:writer`
`fixed:teams:writer`
`fixed:dashboards:reader`
`fixed:dashboards:writer`
`fixed:dashboards.permissions:reader`
`fixed:dashboards.permissions:writer`
`fixed:folders:reader`
`fixed:folders:writer`
`fixed:folders.permissions:reader`
`fixed:folders.permissions:writer`
`fixed:alerting:writer`
`fixed:apikeys:reader`
`fixed:apikeys:writer`
`fixed:alerting.provisioning:writer`
`fixed:datasources.caching:reader`
`fixed:datasources.caching:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader` | Default [Grafana organization administrator]({{< relref "../#organization-users-and-permissions" >}}) assignments. | -| Editor | `fixed:datasources:explorer`
`fixed:dashboards:creator`
`fixed:folders:creator`
`fixed:annotations:writer`
`fixed:teams:creator` if the `editors_can_admin` configuration flag is enabled
`fixed:alerting:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader` | Default [Editor]({{< relref "../#organization-users-and-permissions" >}}) assignments. | -| Viewer | `fixed:datasources:id:reader`
`fixed:organization:reader`
`fixed:annotations:reader`
`fixed:annotations.dashboard:writer`
`fixed:alerting:reader`
`fixed:plugins.app:reader`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader` | Default [Viewer]({{< relref "../#organization-users-and-permissions" >}}) assignments. | +| Basic role | Associated fixed roles | Description | +| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| Grafana Admin | `fixed:roles:reader`
`fixed:roles:writer`
`fixed:users:reader`
`fixed:users:writer`
`fixed:org.users:reader`
`fixed:org.users:writer`
`fixed:ldap:reader`
`fixed:ldap:writer`
`fixed:stats:reader`
`fixed:settings:reader`
`fixed:settings:writer`
`fixed:provisioning:writer`
`fixed:organization:reader`
`fixed:organization:maintainer`
`fixed:licensing:reader`
`fixed:licensing:writer`
`fixed:datasources.caching:reader`
`fixed:datasources.caching:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader`
`fixed:plugins:maintainer` | Default [Grafana server administrator]({{< relref "../#grafana-server-administrators" >}}) assignments. | +| Admin | `fixed:reports:reader`
`fixed:reports:writer`
`fixed:datasources:reader`
`fixed:datasources:writer`
`fixed:organization:writer`
`fixed:datasources.permissions:reader`
`fixed:datasources.permissions:writer`
`fixed:teams:writer`
`fixed:dashboards:reader`
`fixed:dashboards:writer`
`fixed:dashboards.permissions:reader`
`fixed:dashboards.permissions:writer`
`fixed:folders:reader`
`fixed:folders:writer`
`fixed:folders.permissions:reader`
`fixed:folders.permissions:writer`
`fixed:alerting:writer`
`fixed:apikeys:reader`
`fixed:apikeys:writer`
`fixed:alerting.provisioning:writer`
`fixed:datasources.caching:reader`
`fixed:datasources.caching:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader`
`fixed:plugins:writer` | Default [Grafana organization administrator]({{< relref "../#organization-users-and-permissions" >}}) assignments. | +| Editor | `fixed:datasources:explorer`
`fixed:dashboards:creator`
`fixed:folders:creator`
`fixed:annotations:writer`
`fixed:teams:creator` if the `editors_can_admin` configuration flag is enabled
`fixed:alerting:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader` | Default [Editor]({{< relref "../#organization-users-and-permissions" >}}) assignments. | +| Viewer | `fixed:datasources:id:reader`
`fixed:organization:reader`
`fixed:annotations:reader`
`fixed:annotations.dashboard:writer`
`fixed:alerting:reader`
`fixed:plugins.app:reader`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader` | Default [Viewer]({{< relref "../#organization-users-and-permissions" >}}) assignments. | ## Fixed role definitions @@ -72,6 +72,8 @@ The following tables list permissions associated with basic and fixed roles. | `fixed:organization:reader` | `orgs:read`
`orgs.quotas:read` | Read an organization and its quotas. | | `fixed:organization:writer` | All permissions from `fixed:organization:reader` and
`orgs:write`
`orgs.preferences:read`
`orgs.preferences:write` | Read an organization, its quotas, or its preferences. Update organization properties, or its preferences. | | `fixed:plugins.app:reader` | `plugins.app:access` | Access application plugins (still enforcing the organization role). | +| `fixed:plugins:maintainer` | `plugins:install` | Install and uninstall plugins. | +| `fixed:plugins:writer` | `plugins:write` | Enable and disable plugins and edit plugins' settings. | | `fixed:provisioning:writer` | `provisioning:reload` | Reload provisioning. | | `fixed:reports:reader` | `reports:read`
`reports:send`
`reports.settings:read` | Read all reports and shared report settings. | | `fixed:reports:writer` | All permissions from `fixed:reports:reader` and
`reports:create`
`reports:write`
`reports:delete`
`reports.settings:write` | Create, read, update, or delete all reports and shared report settings. | From 04b5e6ed9e71148957b7d8563c3509e1b1722e74 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Tue, 17 Jan 2023 11:16:54 +0100 Subject: [PATCH 35/65] Alerting: Fix group select not being filled by selected folder when creating alert from panel (#61577) Add fetchRulerRulesIfNotFetchedYet fetching when results are an empty object --- public/app/features/alerting/unified/state/actions.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts index 6daa4946ba1..c9efd56ddab 100644 --- a/public/app/features/alerting/unified/state/actions.ts +++ b/public/app/features/alerting/unified/state/actions.ts @@ -258,7 +258,8 @@ export function fetchRulerRulesIfNotFetchedYet(rulesSourceName: string): ThunkRe return (dispatch, getStore) => { const { rulerRules } = getStore().unifiedAlerting; const resp = rulerRules[rulesSourceName]; - if (!resp?.result && !(resp && isRulerNotSupportedResponse(resp)) && !resp?.loading) { + const emptyResults = isEmpty(resp?.result); + if (emptyResults && !(resp && isRulerNotSupportedResponse(resp)) && !resp?.loading) { dispatch(fetchRulerRulesAction({ rulesSourceName })); } }; From cd363cb03773d5296f1038ddf8162e48c9885fa0 Mon Sep 17 00:00:00 2001 From: Ieva Date: Tue, 17 Jan 2023 10:33:01 +0000 Subject: [PATCH 36/65] RBAC: remove access control mock from org quota tests (#61574) * remove ac mock from org quota tests * fix incorrect expected status code and swap tests to make setup easier * remove empty line --- pkg/api/org_test.go | 18 ---- pkg/api/quota_test.go | 201 ++++++++++++++++++++++++++---------------- 2 files changed, 123 insertions(+), 96 deletions(-) diff --git a/pkg/api/org_test.go b/pkg/api/org_test.go index 84ae9726370..26ecdbfcea9 100644 --- a/pkg/api/org_test.go +++ b/pkg/api/org_test.go @@ -1,8 +1,6 @@ package api import ( - "context" - "fmt" "net/http" "strings" "testing" @@ -14,7 +12,6 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgtest" - "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" @@ -622,18 +619,3 @@ func TestAPIEndpoint_GetOrg_RBAC(t *testing.T) { }) } } - -// setupOrgsDBForAccessControlTests creates orgs up until orgID and fake user as member of org -func setupOrgsDBForAccessControlTests(t *testing.T, db *sqlstore.SQLStore, c accessControlScenarioContext, orgID int64) { - t.Helper() - setInitCtxSignedInViewer(c.initCtx) - u := *c.initCtx.SignedInUser - u.OrgID = orgID - c.userService.(*usertest.FakeUserService).ExpectedSignedInUser = &u - - // Create `orgsCount` orgs - for i := 1; i <= int(orgID); i++ { - _, err := c.hs.orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: fmt.Sprintf("TestOrg%v", i), UserID: 0}) - require.NoError(t, err) - } -} diff --git a/pkg/api/quota_test.go b/pkg/api/quota_test.go index 36e128f9124..16564c90f0f 100644 --- a/pkg/api/quota_test.go +++ b/pkg/api/quota_test.go @@ -7,9 +7,14 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/web/webtest" ) var ( @@ -28,57 +33,60 @@ var testOrgQuota = setting.OrgQuota{ AlertRule: 10, } -// setupDBAndSettingsForAccessControlQuotaTests stores users and create two orgs -func setupDBAndSettingsForAccessControlQuotaTests(t *testing.T, sc accessControlScenarioContext) { - t.Helper() - - // Create two orgs with the context user - setupOrgsDBForAccessControlTests(t, sc.db, sc, 2) -} - func TestAPIEndpoint_GetCurrentOrgQuotas_LegacyAccessControl(t *testing.T) { cfg := setting.NewCfg() cfg.Quota.Enabled = true cfg.RBACEnabled = false - sc := setupHTTPServerWithCfg(t, true, cfg) - setInitCtxSignedInViewer(sc.initCtx) - - setupDBAndSettingsForAccessControlQuotaTests(t, sc) - - t.Run("Viewer can view CurrentOrgQuotas", func(t *testing.T) { - response := callAPI(sc.server, http.MethodGet, getCurrentOrgQuotasURL, nil, t) - assert.Equal(t, http.StatusOK, response.Code) + server := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.Cfg = cfg }) - sc.initCtx.IsSignedIn = false t.Run("Unsigned user cannot view CurrentOrgQuotas", func(t *testing.T) { - response := callAPI(sc.server, http.MethodGet, getCurrentOrgQuotasURL, nil, t) - assert.Equal(t, http.StatusUnauthorized, response.Code) + req := server.NewGetRequest(getCurrentOrgQuotasURL) + res, err := server.Send(req) + require.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, res.StatusCode) + require.NoError(t, res.Body.Close()) + }) + t.Run("Viewer can view CurrentOrgQuotas", func(t *testing.T) { + req := webtest.RequestWithSignedInUser(server.NewGetRequest(getCurrentOrgQuotasURL), &user.SignedInUser{OrgID: 1, OrgRole: org.RoleViewer}) + res, err := server.Send(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + require.NoError(t, res.Body.Close()) }) } func TestAPIEndpoint_GetCurrentOrgQuotas_AccessControl(t *testing.T) { cfg := setting.NewCfg() cfg.Quota.Enabled = true - sc := setupHTTPServerWithCfg(t, true, cfg) - setInitCtxSignedInViewer(sc.initCtx) - - setupDBAndSettingsForAccessControlQuotaTests(t, sc) + server := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.Cfg = cfg + }) t.Run("AccessControl allows viewing CurrentOrgQuotas with correct permissions", func(t *testing.T) { - setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasRead}}, sc.initCtx.OrgID) - response := callAPI(sc.server, http.MethodGet, getCurrentOrgQuotasURL, nil, t) - assert.Equal(t, http.StatusOK, response.Code) + req := webtest.RequestWithSignedInUser(server.NewGetRequest(getCurrentOrgQuotasURL), userWithPermissions(1, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasRead}})) + res, err := server.Send(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + require.NoError(t, res.Body.Close()) }) t.Run("AccessControl prevents viewing CurrentOrgQuotas with correct permissions in another org", func(t *testing.T) { - setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasRead}}, 2) - response := callAPI(sc.server, http.MethodGet, getCurrentOrgQuotasURL, nil, t) - assert.Equal(t, http.StatusForbidden, response.Code) + // Set permissions in org 2, but set current org to org 1 + user := userWithPermissions(2, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasRead}}) + user.OrgID = 1 + req := webtest.RequestWithSignedInUser(server.NewGetRequest(getCurrentOrgQuotasURL), user) + res, err := server.Send(req) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + require.NoError(t, res.Body.Close()) }) t.Run("AccessControl prevents viewing CurrentOrgQuotas with incorrect permissions", func(t *testing.T) { - setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: "orgs:invalid"}}, sc.initCtx.OrgID) - response := callAPI(sc.server, http.MethodGet, getCurrentOrgQuotasURL, nil, t) - assert.Equal(t, http.StatusForbidden, response.Code) + req := webtest.RequestWithSignedInUser(server.NewGetRequest(getCurrentOrgQuotasURL), userWithPermissions(1, []accesscontrol.Permission{{Action: "orgs:invalid"}})) + res, err := server.Send(req) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + require.NoError(t, res.Body.Close()) }) } @@ -86,46 +94,60 @@ func TestAPIEndpoint_GetOrgQuotas_LegacyAccessControl(t *testing.T) { cfg := setting.NewCfg() cfg.Quota.Enabled = true cfg.RBACEnabled = false - sc := setupHTTPServerWithCfg(t, true, cfg) - setInitCtxSignedInViewer(sc.initCtx) + server := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.Cfg = cfg + }) - setupDBAndSettingsForAccessControlQuotaTests(t, sc) - t.Run("Viewer cannot view another org quotas", func(t *testing.T) { - response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsQuotasURL, 2), nil, t) - assert.Equal(t, http.StatusForbidden, response.Code) + req := webtest.RequestWithSignedInUser(server.NewGetRequest(fmt.Sprintf(getOrgsQuotasURL, 2)), &user.SignedInUser{OrgID: 1, OrgRole: org.RoleViewer}) + res, err := server.Send(req) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + require.NoError(t, res.Body.Close()) }) - sc.initCtx.SignedInUser.IsGrafanaAdmin = true t.Run("Grafana admin viewer can view another org quotas", func(t *testing.T) { - response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsQuotasURL, 2), nil, t) - assert.Equal(t, http.StatusOK, response.Code) + req := webtest.RequestWithSignedInUser(server.NewGetRequest(fmt.Sprintf(getOrgsQuotasURL, 2)), &user.SignedInUser{OrgID: 1, OrgRole: org.RoleViewer, IsGrafanaAdmin: true}) + res, err := server.Send(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + require.NoError(t, res.Body.Close()) }) } func TestAPIEndpoint_GetOrgQuotas_AccessControl(t *testing.T) { cfg := setting.NewCfg() cfg.Quota.Enabled = true - sc := setupHTTPServerWithCfg(t, true, cfg) - setupDBAndSettingsForAccessControlQuotaTests(t, sc) + server := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.Cfg = cfg + hs.userService = &usertest.FakeUserService{ + ExpectedSignedInUser: &user.SignedInUser{OrgID: 2}, + } + }) t.Run("AccessControl allows viewing another org quotas with correct permissions", func(t *testing.T) { - setInitCtxSignedInViewer(sc.initCtx) - setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasRead}}, 2) - response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsQuotasURL, 2), nil, t) - assert.Equal(t, http.StatusOK, response.Code) + req := webtest.RequestWithSignedInUser(server.NewGetRequest(fmt.Sprintf(getOrgsQuotasURL, 2)), userWithPermissions(2, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasRead}})) + res, err := server.Send(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + require.NoError(t, res.Body.Close()) }) t.Run("AccessControl prevents viewing another org quotas with correct permissions in another org", func(t *testing.T) { - setInitCtxSignedInViewer(sc.initCtx) - setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasRead}}, 1) - response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsQuotasURL, 2), nil, t) - assert.Equal(t, http.StatusForbidden, response.Code) + // Set correct permissions in org 1 and empty permissions in org 2 + user := userWithPermissions(1, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasRead}}) + user.Permissions[2] = map[string][]string{} + req := webtest.RequestWithSignedInUser(server.NewGetRequest(fmt.Sprintf(getOrgsQuotasURL, 2)), user) + res, err := server.Send(req) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + require.NoError(t, res.Body.Close()) }) t.Run("AccessControl prevents viewing another org quotas with incorrect permissions", func(t *testing.T) { - setInitCtxSignedInViewer(sc.initCtx) - setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: "orgs:invalid"}}, 2) - response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsQuotasURL, 2), nil, t) - assert.Equal(t, http.StatusForbidden, response.Code) + req := webtest.RequestWithSignedInUser(server.NewGetRequest(fmt.Sprintf(getOrgsQuotasURL, 2)), userWithPermissions(2, []accesscontrol.Permission{{Action: "orgs:invalid"}})) + res, err := server.Send(req) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + require.NoError(t, res.Body.Close()) }) } @@ -133,22 +155,33 @@ func TestAPIEndpoint_PutOrgQuotas_LegacyAccessControl(t *testing.T) { cfg := setting.NewCfg() cfg.Quota.Enabled = true cfg.RBACEnabled = false - sc := setupHTTPServerWithCfg(t, true, cfg) - setInitCtxSignedInViewer(sc.initCtx) - - setupDBAndSettingsForAccessControlQuotaTests(t, sc) + server := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.Cfg = cfg + }) input := strings.NewReader(testUpdateOrgQuotaCmd) t.Run("Viewer cannot update another org quotas", func(t *testing.T) { - response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input, t) - assert.Equal(t, http.StatusForbidden, response.Code) + req := webtest.RequestWithSignedInUser(server.NewRequest(http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input), &user.SignedInUser{ + OrgID: 1, + OrgRole: org.RoleViewer, + }) + response, err := server.SendJSON(req) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, response.StatusCode) + require.NoError(t, response.Body.Close()) }) - sc.initCtx.SignedInUser.IsGrafanaAdmin = true input = strings.NewReader(testUpdateOrgQuotaCmd) t.Run("Grafana admin viewer can update another org quotas", func(t *testing.T) { - response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input, t) - assert.Equal(t, http.StatusOK, response.Code) + req := webtest.RequestWithSignedInUser(server.NewRequest(http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input), &user.SignedInUser{ + OrgID: 1, + OrgRole: org.RoleViewer, + IsGrafanaAdmin: true, + }) + response, err := server.SendJSON(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, response.StatusCode) + require.NoError(t, response.Body.Close()) }) } @@ -166,30 +199,42 @@ func TestAPIEndpoint_PutOrgQuotas_AccessControl(t *testing.T) { Org: 5, }, } - sc := setupHTTPServerWithCfg(t, true, cfg) - setupDBAndSettingsForAccessControlQuotaTests(t, sc) + server := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.Cfg = cfg + hs.userService = &usertest.FakeUserService{ + ExpectedSignedInUser: &user.SignedInUser{OrgID: 2}, + } + }) input := strings.NewReader(testUpdateOrgQuotaCmd) t.Run("AccessControl allows updating another org quotas with correct permissions", func(t *testing.T) { - setInitCtxSignedInViewer(sc.initCtx) - setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasWrite}}, 2) - response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input, t) - assert.Equal(t, http.StatusOK, response.Code) + user := userWithPermissions(2, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasWrite}}) + user.OrgID = 1 + req := webtest.RequestWithSignedInUser(server.NewRequest(http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input), user) + response, err := server.SendJSON(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, response.StatusCode) + require.NoError(t, response.Body.Close()) }) input = strings.NewReader(testUpdateOrgQuotaCmd) t.Run("AccessControl prevents updating another org quotas with correct permissions in another org", func(t *testing.T) { - setInitCtxSignedInViewer(sc.initCtx) - setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasWrite}}, 1) - response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input, t) - assert.Equal(t, http.StatusForbidden, response.Code) + user := userWithPermissions(1, []accesscontrol.Permission{{Action: accesscontrol.ActionOrgsQuotasWrite}}) + user.Permissions[2] = map[string][]string{} + req := webtest.RequestWithSignedInUser(server.NewRequest(http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input), user) + response, err := server.SendJSON(req) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, response.StatusCode) + require.NoError(t, response.Body.Close()) }) input = strings.NewReader(testUpdateOrgQuotaCmd) t.Run("AccessControl prevents updating another org quotas with incorrect permissions", func(t *testing.T) { - setInitCtxSignedInViewer(sc.initCtx) - setAccessControlPermissions(sc.acmock, []accesscontrol.Permission{{Action: "orgs:invalid"}}, 2) - response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input, t) - assert.Equal(t, http.StatusForbidden, response.Code) + user := userWithPermissions(2, []accesscontrol.Permission{{Action: "orgs:invalid"}}) + req := webtest.RequestWithSignedInUser(server.NewRequest(http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input), user) + response, err := server.SendJSON(req) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, response.StatusCode) + require.NoError(t, response.Body.Close()) }) } From be72e570cf7e951e593b744d29d8ffc248fed228 Mon Sep 17 00:00:00 2001 From: Alex Moreno Date: Tue, 17 Jan 2023 11:37:56 +0100 Subject: [PATCH 37/65] Fix documentation for notification policy object_matchers (#61602) * Fix documentation * Fix grammar * Update docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md Co-authored-by: George Robinson Co-authored-by: George Robinson --- .../provision-alerting-resources/file-provisioning/index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md b/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md index b3046262c9b..4719728582f 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md @@ -524,9 +524,9 @@ policies: - severity =~ "warning|critical" # a list of grafana-like matchers that an alert rule has to fulfill to match the node object_matchers: - - alertname = CPUUsage - - service_id-X = serviceX - - severity =~ "warning|critical" + - ['alertname', '=', 'CPUUsage'] + - ['service_id-X', '=', 'serviceX'] + - ['severity', '=~', 'warning|critical'] # Times when the route should be muted. These must match the name of a # mute time interval. # Additionally, the root node cannot have any mute times. From 9e097c531d62c99e9d5f4731dae5efd13a1f3dc6 Mon Sep 17 00:00:00 2001 From: Jo Date: Tue, 17 Jan 2023 10:39:59 +0000 Subject: [PATCH 38/65] SupportBundles: Build tars in memory (#61581) * build tar in memory * match tag --- pkg/services/supportbundles/interface.go | 2 +- .../supportbundles/supportbundlesimpl/api.go | 23 +---- .../supportbundlesimpl/service.go | 8 -- .../supportbundlesimpl/service_bundle.go | 93 ++++++------------- .../supportbundlesimpl/store.go | 8 +- 5 files changed, 41 insertions(+), 93 deletions(-) diff --git a/pkg/services/supportbundles/interface.go b/pkg/services/supportbundles/interface.go index 3a46ab3e1ea..66ed4a1e867 100644 --- a/pkg/services/supportbundles/interface.go +++ b/pkg/services/supportbundles/interface.go @@ -23,10 +23,10 @@ func (s State) String() string { type Bundle struct { UID string `json:"uid"` State State `json:"state"` - FilePath string `json:"filePath"` Creator string `json:"creator"` CreatedAt int64 `json:"createdAt"` ExpiresAt int64 `json:"expiresAt"` + TarBytes []byte `json:"tarBytes,omitempty"` } type CollectorFunc func(context.Context) (*SupportItem, error) diff --git a/pkg/services/supportbundles/supportbundlesimpl/api.go b/pkg/services/supportbundles/supportbundlesimpl/api.go index 9c48dde29d0..aaeab726e3a 100644 --- a/pkg/services/supportbundles/supportbundlesimpl/api.go +++ b/pkg/services/supportbundles/supportbundlesimpl/api.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "net/http" - "os" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" @@ -72,32 +71,20 @@ func (s *Service) handleCreate(ctx *models.ReqContext) response.Response { return response.JSON(http.StatusCreated, data) } -func (s *Service) handleDownload(ctx *models.ReqContext) { +func (s *Service) handleDownload(ctx *models.ReqContext) response.Response { uid := web.Params(ctx.Req)[":uid"] bundle, err := s.get(ctx.Req.Context(), uid) if err != nil { - ctx.Redirect("/admin/support-bundles") - return + return response.Redirect("/admin/support-bundles") } if bundle.State != supportbundles.StateComplete { - ctx.Redirect("/admin/support-bundles") - return - } - - if bundle.FilePath == "" { - ctx.Redirect("/admin/support-bundles") - return - } - - if _, err := os.Stat(bundle.FilePath); err != nil { - ctx.Redirect("/admin/support-bundles") - return + return response.Redirect("/admin/support-bundles") } ctx.Resp.Header().Set("Content-Type", "application/tar+gzip") - ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%d.tar.gz", bundle.CreatedAt)) - http.ServeFile(ctx.Resp, ctx.Req, bundle.FilePath) + ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.tar.gz", uid)) + return response.CreateNormalResponse(ctx.Resp.Header(), bundle.TarBytes, http.StatusOK) } func (s *Service) handleRemove(ctx *models.ReqContext) response.Response { diff --git a/pkg/services/supportbundles/supportbundlesimpl/service.go b/pkg/services/supportbundles/supportbundlesimpl/service.go index b9264fdda33..599e82b492c 100644 --- a/pkg/services/supportbundles/supportbundlesimpl/service.go +++ b/pkg/services/supportbundles/supportbundlesimpl/service.go @@ -3,8 +3,6 @@ package supportbundlesimpl import ( "context" "fmt" - "os" - "path/filepath" "time" "github.com/grafana/grafana/pkg/api/routing" @@ -151,12 +149,6 @@ func (s *Service) remove(ctx context.Context, uid string) error { return fmt.Errorf("could not remove a support bundle with uid %s as it is still being created", uid) } - if bundle.FilePath != "" { - if err := os.RemoveAll(filepath.Dir(bundle.FilePath)); err != nil { - return fmt.Errorf("could not remove directory for support bundle %s: %w", uid, err) - } - } - // Remove the KV store entry return s.store.Remove(ctx, uid) } diff --git a/pkg/services/supportbundles/supportbundlesimpl/service_bundle.go b/pkg/services/supportbundles/supportbundlesimpl/service_bundle.go index 2af15e44fec..6a798a0c051 100644 --- a/pkg/services/supportbundles/supportbundlesimpl/service_bundle.go +++ b/pkg/services/supportbundles/supportbundlesimpl/service_bundle.go @@ -6,11 +6,10 @@ import ( "compress/gzip" "context" "errors" - "fmt" "io" - "os" "path/filepath" "runtime/debug" + "time" "github.com/grafana/grafana/pkg/services/supportbundles" ) @@ -18,8 +17,8 @@ import ( var ErrCollectorPanicked = errors.New("collector panicked") type bundleResult struct { - path string - err error + tarBytes []byte + err error } func (s *Service) startBundleWork(ctx context.Context, collectors []string, uid string) { @@ -33,47 +32,43 @@ func (s *Service) startBundleWork(ctx context.Context, collectors []string, uid } }() - sbFilePath, err := s.bundle(ctx, collectors, uid) + bundleBytes, err := s.bundle(ctx, collectors, uid) if err != nil { result <- bundleResult{err: err} } - result <- bundleResult{ - path: sbFilePath, - } + result <- bundleResult{tarBytes: bundleBytes} close(result) }() select { case <-ctx.Done(): s.log.Warn("Context cancelled while collecting support bundle") - if err := s.store.Update(ctx, uid, supportbundles.StateTimeout, ""); err != nil { + if err := s.store.Update(ctx, uid, supportbundles.StateTimeout, nil); err != nil { s.log.Error("failed to update bundle after timeout") } return case r := <-result: if r.err != nil { - if err := s.store.Update(ctx, uid, supportbundles.StateError, ""); err != nil { + s.log.Error("failed to make bundle", "error", r.err, "uid", uid) + if err := s.store.Update(ctx, uid, supportbundles.StateError, nil); err != nil { s.log.Error("failed to update bundle after error") } return } - if err := s.store.Update(ctx, uid, supportbundles.StateComplete, r.path); err != nil { + if err := s.store.Update(ctx, uid, supportbundles.StateComplete, r.tarBytes); err != nil { s.log.Error("failed to update bundle after completion") } return } } -func (s *Service) bundle(ctx context.Context, collectors []string, uid string) (string, error) { +func (s *Service) bundle(ctx context.Context, collectors []string, uid string) ([]byte, error) { lookup := make(map[string]bool, len(collectors)) for _, c := range collectors { lookup[c] = true } - sbDir, err := os.MkdirTemp("", "") - if err != nil { - return "", err - } + files := map[string][]byte{} for _, collector := range s.collectors { if !lookup[collector.UID] && !collector.IncludedByDefault { @@ -86,70 +81,42 @@ func (s *Service) bundle(ctx context.Context, collectors []string, uid string) ( // write item to file if item != nil { - if err := os.WriteFile(filepath.Join(sbDir, item.Filename), item.FileBytes, 0600); err != nil { - s.log.Warn("Failed to collect support bundle item", "error", err) - } + files[item.Filename] = item.FileBytes } } // create tar.gz file var buf bytes.Buffer - errCompress := compress(sbDir, &buf) + errCompress := compress(files, &buf) if errCompress != nil { - return "", errCompress + return nil, errCompress } - finalFilePath := filepath.Join(sbDir, fmt.Sprintf("%s.tar.gz", uid)) - - // Ignore gosec G304 as this function is only used internally. - //nolint:gosec - fileToWrite, err := os.OpenFile(finalFilePath, os.O_CREATE|os.O_RDWR, 0600) - if err != nil { - return "", err - } - if _, err := io.Copy(fileToWrite, &buf); err != nil { - return "", err - } - - return finalFilePath, nil + return buf.Bytes(), nil } -func compress(src string, buf io.Writer) error { +func compress(files map[string][]byte, buf io.Writer) error { // tar > gzip > buf zr := gzip.NewWriter(buf) tw := tar.NewWriter(zr) - // walk through every file in the folder - err := filepath.Walk(src, func(file string, fi os.FileInfo, err error) error { - // if not a dir, write file content - if !fi.IsDir() { - // generate tar header - header, err := tar.FileInfoHeader(fi, file) - if err != nil { - return err - } + for name, data := range files { + header := &tar.Header{ + Name: name, + ModTime: time.Now(), + Mode: int64(0o644), + Size: int64(len(data)), + } - header.Name = filepath.ToSlash("/bundle/" + header.Name) + header.Name = filepath.ToSlash("/bundle/" + header.Name) + // write header + if err := tw.WriteHeader(header); err != nil { + return err + } - // write header - if err := tw.WriteHeader(header); err != nil { - return err - } - - // Ignore gosec G304 as this function is only used internally. - //nolint:gosec - data, err := os.Open(file) - if err != nil { - return err - } - if _, err := io.Copy(tw, data); err != nil { - return err - } + if _, err := io.Copy(tw, bytes.NewReader(data)); err != nil { + return err } - return nil - }) - if err != nil { - return err } // produce tar diff --git a/pkg/services/supportbundles/supportbundlesimpl/store.go b/pkg/services/supportbundles/supportbundlesimpl/store.go index 01a3fe1becc..d0f8df74094 100644 --- a/pkg/services/supportbundles/supportbundlesimpl/store.go +++ b/pkg/services/supportbundles/supportbundlesimpl/store.go @@ -31,7 +31,7 @@ type bundleStore interface { Get(ctx context.Context, uid string) (*supportbundles.Bundle, error) List() ([]supportbundles.Bundle, error) Remove(ctx context.Context, uid string) error - Update(ctx context.Context, uid string, state supportbundles.State, filePath string) error + Update(ctx context.Context, uid string, state supportbundles.State, tarBytes []byte) error } func (s *store) Create(ctx context.Context, usr *user.SignedInUser) (*supportbundles.Bundle, error) { @@ -54,14 +54,14 @@ func (s *store) Create(ctx context.Context, usr *user.SignedInUser) (*supportbun return &bundle, nil } -func (s *store) Update(ctx context.Context, uid string, state supportbundles.State, filePath string) error { +func (s *store) Update(ctx context.Context, uid string, state supportbundles.State, tarBytes []byte) error { bundle, err := s.Get(ctx, uid) if err != nil { return err } bundle.State = state - bundle.FilePath = filePath + bundle.TarBytes = tarBytes return s.set(ctx, bundle) } @@ -108,6 +108,8 @@ func (s *store) List() ([]supportbundles.Bundle, error) { if err := json.NewDecoder(strings.NewReader(s)).Decode(&b); err != nil { return nil, err } + + b.TarBytes = nil res = append(res, b) } } From 68f1bfa4713410a24078bdce0ffa13e08add8c92 Mon Sep 17 00:00:00 2001 From: Selene Date: Tue, 17 Jan 2023 11:58:08 +0100 Subject: [PATCH 39/65] Chore: Update ast to dst (#61469) * Update ast to dst * Sort imports * Update thema * Update pkg/codegen/util_go.go Co-authored-by: sam boyer * Move DecoderCompactor into ApplyFuncs * Remove unnecessary file * Use dst decorator * Downgrade parca-dev library Co-authored-by: sam boyer --- go.mod | 19 +++--- go.sum | 31 +++++---- pkg/codegen/astmanip_test.go | 10 +-- pkg/codegen/jenny_gotypes.go | 28 +------- pkg/codegen/util_go.go | 65 ++++++++++--------- pkg/plugins/codegen/util_go.go | 9 +-- pkg/plugins/plugindef/gen.go | 8 +-- .../plugindef/plugindef_bindings_gen.go | 1 - pkg/plugins/plugindef/plugindef_types_gen.go | 3 + 9 files changed, 82 insertions(+), 92 deletions(-) diff --git a/go.mod b/go.mod index 12483f6bfa8..da974b8e47b 100644 --- a/go.mod +++ b/go.mod @@ -64,7 +64,6 @@ require ( github.com/grafana/grafana-aws-sdk v0.11.0 github.com/grafana/grafana-azure-sdk-go v1.5.1 github.com/grafana/grafana-plugin-sdk-go v0.147.0 - github.com/grafana/thema v0.0.0-20221222001256-d3a8fa941a6b github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/hashicorp/go-hclog v1.2.0 github.com/hashicorp/go-plugin v1.4.3 @@ -78,7 +77,7 @@ require ( github.com/linkedin/goavro/v2 v2.10.0 github.com/m3db/prometheus_remote_client_golang v0.4.4 github.com/magefile/mage v1.14.0 - github.com/mattn/go-isatty v0.0.14 + github.com/mattn/go-isatty v0.0.16 github.com/mattn/go-sqlite3 v1.14.16 github.com/matttproud/golang_protobuf_extensions v1.0.4 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f @@ -127,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 // indirect + xorm.io/builder v0.3.6 xorm.io/core v0.7.3 xorm.io/xorm v0.8.2 ) @@ -253,7 +252,7 @@ require ( github.com/bufbuild/connect-go v1.4.1 github.com/dlmiddlecote/sqlstats v1.0.2 github.com/drone/drone-cli v1.6.1 - github.com/getkin/kin-openapi v0.103.0 + github.com/getkin/kin-openapi v0.107.0 github.com/golang-migrate/migrate/v4 v4.7.0 github.com/google/go-github/v45 v45.2.0 github.com/grafana/codejen v0.0.3 @@ -274,8 +273,9 @@ require ( require ( github.com/dave/dst v0.27.2 - github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f + github.com/grafana/thema v0.0.0-20230113164405-e0e8893756cf github.com/parca-dev/parca v0.12.1 + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f k8s.io/apimachinery v0.25.3 ) @@ -309,11 +309,12 @@ require ( github.com/hashicorp/memberlist v0.5.0 // indirect github.com/invopop/yaml v0.1.0 // indirect github.com/kr/text v0.2.0 // indirect - github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-ieproxy v0.0.3 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect @@ -378,13 +379,13 @@ require ( github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect github.com/klauspost/compress v1.15.13 // indirect github.com/kylelemons/godebug v1.1.0 // indirect - github.com/labstack/echo/v4 v4.9.0 // indirect - github.com/labstack/gommon v0.3.1 // indirect + github.com/labstack/echo/v4 v4.9.1 // indirect + github.com/labstack/gommon v0.4.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/pierrec/lz4/v4 v4.1.15 // indirect - github.com/valyala/fasttemplate v1.2.1 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect github.com/wk8/go-ordered-map v1.0.0 github.com/xanzy/ssh-agent v0.3.0 // indirect github.com/xlab/treeprint v1.1.0 // indirect diff --git a/go.sum b/go.sum index 10e99e1a63a..fd311161d03 100644 --- a/go.sum +++ b/go.sum @@ -874,8 +874,8 @@ github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/getkin/kin-openapi v0.94.0/go.mod h1:LWZfzOd7PRy8GJ1dJ6mCU6tNdSfOwRac1BUPam4aw6Q= -github.com/getkin/kin-openapi v0.103.0 h1:F5wAtaQvPWxKCAYZ69LgHAThgu16p4u41VQtbn1U8LA= -github.com/getkin/kin-openapi v0.103.0/go.mod h1:w4lRPHiyOdwGbOkLIyk+P0qCwlu7TXPCHD/64nSXzgE= +github.com/getkin/kin-openapi v0.107.0 h1:bxhL6QArW7BXQj8NjXfIJQy680NsMKd25nwhvpCXchg= +github.com/getkin/kin-openapi v0.107.0/go.mod h1:9Dhr+FasATJZjS4iOLvB0hkaxgYdulrNYm2e9epLWOo= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/getsentry/sentry-go v0.13.0 h1:20dgTiUSfxRB/EhMPtxcL9ZEbM1ZdR+W/7f7NWD+xWo= github.com/getsentry/sentry-go v0.13.0/go.mod h1:EOsfu5ZdvKPfeHYV6pTVQnsjfp30+XA7//UooKNumH0= @@ -1383,10 +1383,6 @@ 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-20230112102204-5545cdf0edc9 h1:rMfagCACaYtBvj6bbJq5zVuNR6ASeKUTUOgr/DM+QrU= -github.com/grafana/alerting v0.0.0-20230112102204-5545cdf0edc9/go.mod h1:w5jcmHYPAfju/QcI69SwFs0+qMcm8jvW6fsqSc2tJtc= -github.com/grafana/alerting v0.0.0-20230113002715-e287d43f4b57 h1:VyzefGsa17SXPzqCvnI8cczbPfuVNUMXiPJNVNy/PdA= -github.com/grafana/alerting v0.0.0-20230113002715-e287d43f4b57/go.mod h1:w5jcmHYPAfju/QcI69SwFs0+qMcm8jvW6fsqSc2tJtc= github.com/grafana/alerting v0.0.0-20230113220341-b9b3de89bca2 h1:XkzN0dBir+oso3XjGOV2HLGSCjyyYHLL5AuUqI/LIXY= github.com/grafana/alerting v0.0.0-20230113220341-b9b3de89bca2/go.mod h1:w5jcmHYPAfju/QcI69SwFs0+qMcm8jvW6fsqSc2tJtc= github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw= @@ -1412,8 +1408,8 @@ github.com/grafana/prometheus-alertmanager v0.25.1-0.20230109182643-002d8ba820f6 github.com/grafana/prometheus-alertmanager v0.25.1-0.20230109182643-002d8ba820f6/go.mod h1:MnBfDPXJqXmmfPwQlCLvVUdqfnvrAw+hSPtDeaaFwj4= github.com/grafana/saml v0.4.9-0.20220727151557-61cd9c9353fc h1:1PY8n+rXuBNr3r1JQhoytWDCpc+pq+BibxV0SZv+Cr4= github.com/grafana/saml v0.4.9-0.20220727151557-61cd9c9353fc/go.mod h1:9Zh6dWPtB3MSzTRt8fIFH60Z351QQ+s7hCU3J/tTlA4= -github.com/grafana/thema v0.0.0-20221222001256-d3a8fa941a6b h1:LBjw2NaMbTdx/etBbyZO0n0L2Lqj5PCDJAGwKk9TCII= -github.com/grafana/thema v0.0.0-20221222001256-d3a8fa941a6b/go.mod h1:8wmENx19kzNI+5De7889AVRJ3T4OnOb5I1bSJLcj1t4= +github.com/grafana/thema v0.0.0-20230113164405-e0e8893756cf h1:jl1iWUlJt5GJWaafOqtaEp46MDFBCni7bmv75VGFkq0= +github.com/grafana/thema v0.0.0-20230113164405-e0e8893756cf/go.mod h1:5j2nf4xmWhKr+1vyGouML8eJ8xERS5Jw/lhjs0eyz78= github.com/grafana/xorm v0.8.3-0.20220614223926-2fcda7565af6 h1:I9dh1MXGX0wGyxdV/Sl7+ugnki4Dfsy8lv2s5Yf887o= github.com/grafana/xorm v0.8.3-0.20220614223926-2fcda7565af6/go.mod h1:ZkJLEYLoVyg7amJK/5r779bHyzs2AU8f8VMiP6BM7uY= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= @@ -1765,11 +1761,12 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg= github.com/labstack/echo/v4 v4.7.2/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks= -github.com/labstack/echo/v4 v4.9.0 h1:wPOF1CE6gvt/kmbMR4dGzWvHMPT+sAEUJOwOTtvITVY= -github.com/labstack/echo/v4 v4.9.0/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks= +github.com/labstack/echo/v4 v4.9.1 h1:GliPYSpzGKlyOhqIbG8nmHBo3i1saKWFOgh41AN3b+Y= +github.com/labstack/echo/v4 v4.9.1/go.mod h1:Pop5HLc+xoc4qhTZ1ip6C0RtP7Z+4VzRLWZZFKqbbjo= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= -github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o= github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= +github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/lann/builder v0.0.0-20150808151131-f22ce00fd939/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= @@ -1849,8 +1846,9 @@ github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= github.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= github.com/mattn/go-ieproxy v0.0.0-20191113090002-7c0f6868bffe/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= @@ -1866,8 +1864,9 @@ github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2y github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= @@ -1971,6 +1970,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= @@ -2518,8 +2519,9 @@ github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/X github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= -github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/vectordotdev/go-datemath v0.1.1-0.20220323213446-f3954d0b18ae h1:oyiy3uBj1F4O3AaFh7hUGBrJjAssJhKyAbwxtkslxqo= github.com/vectordotdev/go-datemath v0.1.1-0.20220323213446-f3954d0b18ae/go.mod h1:PnwzbSst7KD3vpBzzlntZU5gjVa455Uqa5QPiKSYJzQ= github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= @@ -3194,6 +3196,7 @@ golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= diff --git a/pkg/codegen/astmanip_test.go b/pkg/codegen/astmanip_test.go index e09e1071b16..bdfd0a4eef2 100644 --- a/pkg/codegen/astmanip_test.go +++ b/pkg/codegen/astmanip_test.go @@ -2,13 +2,13 @@ package codegen import ( "bytes" - "go/format" "go/parser" "go/token" "testing" + "github.com/dave/dst/decorator" + "github.com/dave/dst/dstutil" "github.com/matryer/is" - "golang.org/x/tools/go/ast/astutil" ) func TestPrefixDropper(t *testing.T) { @@ -276,15 +276,15 @@ type Thing struct { } is := is.New(t) fset := token.NewFileSet() - inf, err := parser.ParseFile(fset, "input.go", item.in, parser.ParseComments) + inf, err := decorator.ParseFile(fset, "input.go", item.in, parser.ParseComments) if err != nil { t.Fatal(err) } drop := PrefixDropper("Foo") - astutil.Apply(inf, drop, nil) + dstutil.Apply(inf, drop, nil) buf := new(bytes.Buffer) - err = format.Node(buf, fset, inf) + err = decorator.Fprint(buf, inf) if err != nil { t.Fatal(err) } diff --git a/pkg/codegen/jenny_gotypes.go b/pkg/codegen/jenny_gotypes.go index 7f4ad8ecae6..972dd0eb818 100644 --- a/pkg/codegen/jenny_gotypes.go +++ b/pkg/codegen/jenny_gotypes.go @@ -1,19 +1,15 @@ package codegen import ( - "bytes" - - "github.com/dave/dst/decorator" "github.com/dave/dst/dstutil" "github.com/grafana/codejen" "github.com/grafana/thema/encoding/gocode" - "golang.org/x/tools/go/ast/astutil" ) // GoTypesJenny creates a [OneToOne] that produces Go types for the provided // [thema.Schema]. type GoTypesJenny struct { - ApplyFuncs []astutil.ApplyFunc + ApplyFuncs []dstutil.ApplyFunc } func (j GoTypesJenny) JennyName() string { @@ -25,32 +21,12 @@ func (j GoTypesJenny) Generate(sfg SchemaForGen) (*codejen.File, error) { b, err := gocode.GenerateTypesOpenAPI(sfg.Schema, &gocode.TypeConfigOpenAPI{ // TODO will need to account for sanitizing e.g. dashes here at some point PackageName: sfg.Schema.Lineage().Name(), - ApplyFuncs: append(j.ApplyFuncs, PrefixDropper(sfg.Name)), + ApplyFuncs: append(j.ApplyFuncs, PrefixDropper(sfg.Name), DecoderCompactor()), }) if err != nil { return nil, err } - // TODO switch to dst completely in thema so this can be made an ApplyFuncs element - fb, err := decorator.Parse(b) - if err != nil { - return nil, err - } - dstutil.Apply(fb, DecoderCompactor(), nil) - buf := new(bytes.Buffer) - err = decorator.Fprint(buf, fb) - if err != nil { - return nil, err - } - b, err = postprocessGoFile(genGoFile{ - path: "", - walker: nil, - in: buf.Bytes(), - }) - if err != nil { - return nil, err - } - return codejen.NewFile(sfg.Schema.Lineage().Name()+"_types_gen.go", b, j), nil } diff --git a/pkg/codegen/util_go.go b/pkg/codegen/util_go.go index cdd464e2914..70604f035d5 100644 --- a/pkg/codegen/util_go.go +++ b/pkg/codegen/util_go.go @@ -3,7 +3,6 @@ package codegen import ( "bytes" "fmt" - "go/ast" "go/format" "go/parser" "go/token" @@ -13,14 +12,14 @@ import ( "strings" "github.com/dave/dst" + "github.com/dave/dst/decorator" "github.com/dave/dst/dstutil" - "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/imports" ) type genGoFile struct { path string - walker astutil.ApplyFunc + walker dstutil.ApplyFunc in []byte } @@ -28,13 +27,13 @@ func postprocessGoFile(cfg genGoFile) ([]byte, error) { fname := filepath.Base(cfg.path) buf := new(bytes.Buffer) fset := token.NewFileSet() - gf, err := parser.ParseFile(fset, fname, string(cfg.in), parser.ParseComments) + gf, err := decorator.ParseFile(fset, fname, string(cfg.in), parser.ParseComments) if err != nil { return nil, fmt.Errorf("error parsing generated file: %w", err) } if cfg.walker != nil { - astutil.Apply(gf, cfg.walker, nil) + dstutil.Apply(gf, cfg.walker, nil) err = format.Node(buf, fset, gf) if err != nil { @@ -77,10 +76,10 @@ type prefixmod struct { rxpsuff *regexp.Regexp } -// PrefixDropper returns an astutil.ApplyFunc that removes the provided prefix +// PrefixDropper returns a dstutil.ApplyFunc that removes the provided prefix // string when it appears as a leading sequence in type names, var names, and // comments in a generated Go file. -func PrefixDropper(prefix string) astutil.ApplyFunc { +func PrefixDropper(prefix string) dstutil.ApplyFunc { return (&prefixmod{ prefix: prefix, rxpsuff: regexp.MustCompile(fmt.Sprintf(`%s([a-zA-Z_]+)`, prefix)), @@ -88,13 +87,13 @@ func PrefixDropper(prefix string) astutil.ApplyFunc { }).applyfunc } -// PrefixReplacer returns an astutil.ApplyFunc that removes the provided prefix +// PrefixReplacer returns a dstutil.ApplyFunc that removes the provided prefix // string when it appears as a leading sequence in type names, var names, and // comments in a generated Go file. // // When an exact match for prefix is found, the provided replace string // is substituted. -func PrefixReplacer(prefix, replace string) astutil.ApplyFunc { +func PrefixReplacer(prefix, replace string) dstutil.ApplyFunc { return (&prefixmod{ prefix: prefix, replace: replace, @@ -103,62 +102,70 @@ func PrefixReplacer(prefix, replace string) astutil.ApplyFunc { }).applyfunc } -func depoint(e ast.Expr) ast.Expr { - if star, is := e.(*ast.StarExpr); is { +func depoint(e dst.Expr) dst.Expr { + if star, is := e.(*dst.StarExpr); is { return star.X } return e } -func (d prefixmod) applyfunc(c *astutil.Cursor) bool { +func (d prefixmod) applyfunc(c *dstutil.Cursor) bool { n := c.Node() switch x := n.(type) { - case *ast.ValueSpec: + case *dst.ValueSpec: d.handleExpr(x.Type) for _, id := range x.Names { d.do(id) } - case *ast.TypeSpec: + case *dst.TypeSpec: // Always do typespecs d.do(x.Name) - case *ast.Field: + case *dst.Field: // Don't rename struct fields. We just want to rename type declarations, and // field value specifications that reference those types. d.handleExpr(x.Type) - - case *ast.CommentGroup: - for _, c := range x.List { - c.Text = d.rxpsuff.ReplaceAllString(c.Text, "$1") - if d.replace != "" { - c.Text = d.rxp.ReplaceAllString(c.Text, d.replace+"$1") + case *dst.File: + for _, decl := range x.Decls { + comments := decl.Decorations().Start.All() + decl.Decorations().Start.Clear() + // For any reason, sometimes it retrieves the comment duplicated 🤷 + commentMap := make(map[string]bool) + for _, c := range comments { + if _, ok := commentMap[c]; !ok { + commentMap[c] = true + decl.Decorations().Start.Append(d.rxpsuff.ReplaceAllString(c, "$1")) + if d.replace != "" { + decl.Decorations().Start.Append(d.rxp.ReplaceAllString(c, d.replace+"$1")) + } + } } } } return true } -func (d prefixmod) handleExpr(e ast.Expr) { +func (d prefixmod) handleExpr(e dst.Expr) { // Deref a StarExpr, if there is one expr := depoint(e) switch x := expr.(type) { - case *ast.Ident: + case *dst.Ident: d.do(x) - case *ast.ArrayType: - if id, is := depoint(x.Elt).(*ast.Ident); is { + case *dst.ArrayType: + if id, is := depoint(x.Elt).(*dst.Ident); is { d.do(id) } - case *ast.MapType: - if id, is := depoint(x.Key).(*ast.Ident); is { + case *dst.MapType: + if id, is := depoint(x.Key).(*dst.Ident); is { d.do(id) } - if id, is := depoint(x.Value).(*ast.Ident); is { + if id, is := depoint(x.Value).(*dst.Ident); is { d.do(id) } } } -func (d prefixmod) do(n *ast.Ident) { +func (d prefixmod) do(n *dst.Ident) { if n.Name != d.prefix { n.Name = strings.TrimPrefix(n.Name, d.prefix) } else if d.replace != "" { diff --git a/pkg/plugins/codegen/util_go.go b/pkg/plugins/codegen/util_go.go index 9033e29b968..a9caf75ea2a 100644 --- a/pkg/plugins/codegen/util_go.go +++ b/pkg/plugins/codegen/util_go.go @@ -10,13 +10,14 @@ import ( "path/filepath" "strings" - "golang.org/x/tools/go/ast/astutil" + "github.com/dave/dst/decorator" + "github.com/dave/dst/dstutil" "golang.org/x/tools/imports" ) type genGoFile struct { path string - walker astutil.ApplyFunc + walker dstutil.ApplyFunc in []byte } @@ -24,13 +25,13 @@ func postprocessGoFile(cfg genGoFile) ([]byte, error) { fname := filepath.Base(cfg.path) buf := new(bytes.Buffer) fset := token.NewFileSet() - gf, err := parser.ParseFile(fset, fname, string(cfg.in), parser.ParseComments) + gf, err := decorator.ParseFile(fset, fname, string(cfg.in), parser.ParseComments) if err != nil { return nil, fmt.Errorf("error parsing generated file: %w", err) } if cfg.walker != nil { - astutil.Apply(gf, cfg.walker, nil) + dstutil.Apply(gf, cfg.walker, nil) err = format.Node(buf, fset, gf) if err != nil { diff --git a/pkg/plugins/plugindef/gen.go b/pkg/plugins/plugindef/gen.go index 1c1c8e4595a..d3a7333282b 100644 --- a/pkg/plugins/plugindef/gen.go +++ b/pkg/plugins/plugindef/gen.go @@ -8,19 +8,19 @@ import ( "context" "encoding/json" "fmt" - "go/ast" "os" "path/filepath" "strings" "cuelang.org/go/cue/cuecontext" + "github.com/dave/dst" + "github.com/dave/dst/dstutil" "github.com/grafana/codejen" "github.com/grafana/grafana/pkg/codegen" "github.com/grafana/grafana/pkg/cuectx" "github.com/grafana/thema" "github.com/grafana/thema/encoding/gocode" "github.com/grafana/thema/encoding/jsonschema" - "golang.org/x/tools/go/ast/astutil" ) var dirPlugindef = filepath.Join("pkg", "plugins", "plugindef") @@ -62,7 +62,7 @@ func (j *jennytypego) JennyName() string { } func (j *jennytypego) Generate(lin thema.Lineage) (*codejen.File, error) { - f, err := codegen.GoTypesJenny{ApplyFuncs: []astutil.ApplyFunc{ + f, err := codegen.GoTypesJenny{ApplyFuncs: []dstutil.ApplyFunc{ codegen.PrefixReplacer("Plugindef", "PluginDef"), }}.Generate(codegen.SchemaForGen{ Name: "PluginDef", @@ -85,7 +85,7 @@ func (j *jennybindgo) JennyName() string { func (j *jennybindgo) Generate(lin thema.Lineage) (*codejen.File, error) { b, err := gocode.GenerateLineageBinding(lin, &gocode.BindingConfig{ TitleName: "PluginDef", - Assignee: ast.NewIdent("*PluginDef"), + Assignee: dst.NewIdent("*PluginDef"), PrivateFactory: true, }) if err != nil { diff --git a/pkg/plugins/plugindef/plugindef_bindings_gen.go b/pkg/plugins/plugindef/plugindef_bindings_gen.go index 1a9aae120a6..3438f6896a3 100644 --- a/pkg/plugins/plugindef/plugindef_bindings_gen.go +++ b/pkg/plugins/plugindef/plugindef_bindings_gen.go @@ -46,7 +46,6 @@ func doLineage(rt *thema.Runtime, opts ...thema.BindOption) (thema.ConvergentLin } return tsch.ConvergentLineage(), nil } - func baseLineage(rt *thema.Runtime, opts ...thema.BindOption) (thema.Lineage, error) { // First, we must get the bytes of the .cue file(s) in which the "plugindef" lineage // is declared, and load them into a diff --git a/pkg/plugins/plugindef/plugindef_types_gen.go b/pkg/plugins/plugindef/plugindef_types_gen.go index 471dcaae99b..2c26d1d9510 100644 --- a/pkg/plugins/plugindef/plugindef_types_gen.go +++ b/pkg/plugins/plugindef/plugindef_types_gen.go @@ -67,6 +67,7 @@ const ( ) // Defines values for Category. +// Defines values for PlugindefCategory. const ( CategoryCloud Category = "cloud" @@ -86,6 +87,7 @@ const ( ) // Defines values for Type. +// Defines values for PlugindefType. const ( TypeApp Type = "app" @@ -358,6 +360,7 @@ type URLParam struct { Name string `json:"name"` } +// Plugindef defines model for plugindef. // PluginDef defines model for plugindef. type PluginDef struct { // For data source plugins, if the plugin supports alerting. From e7271ee3f1a591556775b55f451f8b5840a13fa6 Mon Sep 17 00:00:00 2001 From: Vicky Lee <36230812+vickyyyyyyy@users.noreply.github.com> Date: Tue, 17 Jan 2023 11:16:18 +0000 Subject: [PATCH 40/65] chore: update CODEOWNERS for Cloud Provider Plugins to Partner Plugins renaming (#61575) * chore: update CODEOWNERS for Cloud Provider Plugins to Partner Plugins renaming * chore: update CODEOWNERS for Cloud Provider Plugins to Partner Plugins renaming * trigger build --- .github/CODEOWNERS | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c6c16aa07f6..d87feb8b0a3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -167,7 +167,7 @@ WORKFLOW.md @torkelo /devenv/dev-dashboards-without-uid/ @grafana/dashboards-squad /devenv/dev-dashboards/ @grafana/dashboards-squad /devenv/docker/blocks/alert_webhook_listener/ @grafana/alerting-squad-backend -/devenv/docker/blocks/clickhouse/ @grafana/enterprise-plugins +/devenv/docker/blocks/clickhouse/ @grafana/partner-plugins /devenv/docker/blocks/collectd/ @grafana/observability-metrics /devenv/docker/blocks/grafana/ @grafana/grafana-as-code /devenv/docker/blocks/graphite/ @grafana/observability-metrics @@ -228,10 +228,10 @@ WORKFLOW.md @torkelo /Makefile @grafana/grafana-release-eng /scripts/build/ @grafana/grafana-release-eng -# Cloud Datasources backend code +# OSS Plugin Partnerships backend code /pkg/tsdb/cloudwatch/ @grafana/aws-plugins -/pkg/tsdb/azuremonitor/ @grafana/cloud-provider-plugins -/pkg/tsdb/cloudmonitoring/ @grafana/cloud-provider-plugins +/pkg/tsdb/azuremonitor/ @grafana/partner-plugins +/pkg/tsdb/cloudmonitoring/ @grafana/partner-plugins # Observability backend code /pkg/tsdb/prometheus/ @grafana/observability-metrics @@ -293,7 +293,7 @@ WORKFLOW.md @torkelo /public/locales/ @grafana/user-essentials /public/app/core/internationalization/ @grafana/user-essentials /e2e/ @grafana/user-essentials -/e2e/cloud-plugins-suite/ @grafana/cloud-provider-plugins +/e2e/cloud-plugins-suite/ @grafana/partner-plugins /packages/ @grafana/user-essentials @grafana/plugins-platform-frontend @grafana/grafana-bi-squad /packages/grafana-e2e-selectors/ @grafana/user-essentials /packages/grafana-e2e/ @grafana/user-essentials @@ -487,7 +487,7 @@ lerna.json @grafana/frontend-ops /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/grafana-azure-monitor-datasource/ @grafana/cloud-provider-plugins +/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 /public/app/plugins/datasource/jaeger/ @grafana/observability-traces-and-profiling @@ -498,7 +498,7 @@ lerna.json @grafana/frontend-ops /public/app/plugins/datasource/opentsdb/ @grafana/backend-platform /public/app/plugins/datasource/postgres/ @grafana/grafana-bi-squad /public/app/plugins/datasource/prometheus/ @grafana/observability-metrics -/public/app/plugins/datasource/cloud-monitoring/ @grafana/cloud-provider-plugins +/public/app/plugins/datasource/cloud-monitoring/ @grafana/partner-plugins /public/app/plugins/datasource/zipkin/ @grafana/observability-traces-and-profiling /public/app/plugins/datasource/tempo/ @grafana/observability-traces-and-profiling /public/app/plugins/datasource/phlare/ @grafana/observability-traces-and-profiling @@ -565,7 +565,7 @@ embed.go @grafana/grafana-as-code /.github/workflows/backport.yml @grafana/grafana-release-eng /.github/workflows/bump-version.yml @grafana/grafana-release-eng /.github/workflows/close-milestone.yml @grafana/grafana-release-eng -/.github/workflows/cloud-data-sources-code-coverage.yml @grafana/cloud-provider-plugins +/.github/workflows/cloud-data-sources-code-coverage.yml @grafana/partner-plugins /.github/workflows/codeowners-validator.yml @tolzhabayev /.github/workflows/codeql-analysis.yml @DanCech /.github/workflows/commands.yml @torkelo From d2a5b9b28920e7a7d1931aebcaa8818620159d83 Mon Sep 17 00:00:00 2001 From: Andre Pereira Date: Tue, 17 Jan 2023 11:42:56 +0000 Subject: [PATCH 41/65] Tempo: Trace View - Search bar to absolute position instead of sticky (#61324) * Trace View: Search bar to absolute position instead of sticky * Fix search bar position to the inside of the trace view panel --- .../TracePageHeader/TracePageSearchBar.tsx | 5 ++- .../explore/TraceView/TraceViewContainer.tsx | 35 +++++++++---------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx b/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx index 84a4a3c6c82..b8fa71464eb 100644 --- a/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx +++ b/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx @@ -29,12 +29,11 @@ export const getStyles = (theme: GrafanaTheme2) => { TracePageSearchBar: css` label: TracePageSearchBar; float: right; - position: sticky; - top: 8px; + position: absolute; + top: 0; right: 0; z-index: ${theme.zIndex.navbarFixed}; background: ${theme.colors.background.primary}; - margin-top: 8px; margin-bottom: -48px; padding: 8px; margin-right: 2px; diff --git a/public/app/features/explore/TraceView/TraceViewContainer.tsx b/public/app/features/explore/TraceView/TraceViewContainer.tsx index 6bdadacea77..e5bbdce5b2e 100644 --- a/public/app/features/explore/TraceView/TraceViewContainer.tsx +++ b/public/app/features/explore/TraceView/TraceViewContainer.tsx @@ -35,7 +35,7 @@ export function TraceViewContainer(props: Props) { } return ( - <> + - - - - - + + ); } From e6dee8a723d56864f3f26d214a2e71b8cf0b9fa5 Mon Sep 17 00:00:00 2001 From: Denis Limarev Date: Tue, 17 Jan 2023 17:50:17 +0600 Subject: [PATCH 42/65] Perfomance: Preallocate slices (#61580) --- pkg/services/ngalert/notifier/alertmanager.go | 2 +- pkg/services/ngalert/notifier/receivers.go | 2 +- pkg/services/ngalert/state/manager.go | 4 ++-- pkg/services/ngalert/store/deltas.go | 5 ++++- pkg/services/pluginsettings/service/service.go | 2 +- pkg/services/provisioning/values/values.go | 4 ++-- .../commands/generate_datasources/generate.go | 2 +- pkg/services/publicdashboards/service/query.go | 2 +- pkg/services/searchV2/allowed_actions.go | 2 +- pkg/services/searchV2/index.go | 3 +-- pkg/services/secrets/kvstore/plugin.go | 4 ++-- .../sqlstore/migrations/accesscontrol/action_migrator.go | 3 ++- .../sqlstore/migrations/accesscontrol/team_membership.go | 2 +- 13 files changed, 20 insertions(+), 17 deletions(-) diff --git a/pkg/services/ngalert/notifier/alertmanager.go b/pkg/services/ngalert/notifier/alertmanager.go index 97fcfe5695f..543f792dd24 100644 --- a/pkg/services/ngalert/notifier/alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager.go @@ -403,7 +403,7 @@ func (am *Alertmanager) buildIntegrationsMap(receivers []*apimodels.PostableApiR // buildReceiverIntegrations builds a list of integration notifiers off of a receiver config. func (am *Alertmanager) buildReceiverIntegrations(receiver *apimodels.PostableApiReceiver, tmpl *alerting.Template) ([]*alerting.Integration, error) { - var integrations []*alerting.Integration + integrations := make([]*alerting.Integration, 0, len(receiver.GrafanaManagedReceivers)) for i, r := range receiver.GrafanaManagedReceivers { n, err := am.buildReceiverIntegration(r, tmpl) if err != nil { diff --git a/pkg/services/ngalert/notifier/receivers.go b/pkg/services/ngalert/notifier/receivers.go index 3c49a292353..2897f9799b9 100644 --- a/pkg/services/ngalert/notifier/receivers.go +++ b/pkg/services/ngalert/notifier/receivers.go @@ -130,7 +130,7 @@ func (am *Alertmanager) GetReceivers(ctx context.Context) []apimodels.Receiver { am.reloadConfigMtx.RLock() defer am.reloadConfigMtx.RUnlock() - var apiReceivers []apimodels.Receiver + apiReceivers := make([]apimodels.Receiver, 0, len(am.Base.GetReceivers())) for _, rcv := range am.Base.GetReceivers() { // Build integrations slice for each receiver. integrations := make([]*models.Integration, 0, len(rcv.Integrations())) diff --git a/pkg/services/ngalert/state/manager.go b/pkg/services/ngalert/state/manager.go index d937a7328a5..41b06c37f41 100644 --- a/pkg/services/ngalert/state/manager.go +++ b/pkg/services/ngalert/state/manager.go @@ -184,7 +184,7 @@ func (st *Manager) ResetStateByRuleUID(ctx context.Context, ruleKey ngModels.Ale func (st *Manager) ProcessEvalResults(ctx context.Context, evaluatedAt time.Time, alertRule *ngModels.AlertRule, results eval.Results, extraLabels data.Labels) []StateTransition { logger := st.log.FromContext(ctx) logger.Debug("State manager processing evaluation results", "resultCount", len(results)) - var states []StateTransition + states := make([]StateTransition, 0, len(results)) for _, result := range results { s := st.setNextState(ctx, alertRule, result, extraLabels, logger) @@ -383,10 +383,10 @@ func (st *Manager) deleteStaleStatesFromCache(ctx context.Context, logger log.Lo // TODO: We will need to change this when we support images without screenshots as each series will have a different image var resolvedImage *ngModels.Image - var resolvedStates []StateTransition staleStates := st.cache.deleteRuleStates(alertRule.GetKey(), func(s *State) bool { return stateIsStale(evaluatedAt, s.LastEvaluationTime, alertRule.IntervalSeconds) }) + resolvedStates := make([]StateTransition, 0, len(staleStates)) for _, s := range staleStates { logger.Info("Detected stale state entry", "cacheID", s.CacheID, "state", s.State, "reason", s.StateReason) diff --git a/pkg/services/ngalert/store/deltas.go b/pkg/services/ngalert/store/deltas.go index a3f589a8cc7..bfb4a8daa15 100644 --- a/pkg/services/ngalert/store/deltas.go +++ b/pkg/services/ngalert/store/deltas.go @@ -58,7 +58,9 @@ func CalculateChanges(ctx context.Context, ruleReader RuleReader, groupKey model existingGroupRulesUIDs[r.UID] = r } - var toAdd, toDelete []*models.AlertRule + //nolint:prealloc // difficult logic + var toAdd []*models.AlertRule + //nolint:prealloc // difficult logic var toUpdate []RuleDelta loadedRulesByUID := map[string]*models.AlertRule{} // auxiliary cache to avoid unnecessary queries if there are multiple moves from the same group for _, r := range submittedRules { @@ -110,6 +112,7 @@ func CalculateChanges(ctx context.Context, ruleReader RuleReader, groupKey model continue } + toDelete := make([]*models.AlertRule, 0, len(existingGroupRulesUIDs)) for _, rule := range existingGroupRulesUIDs { toDelete = append(toDelete, rule) } diff --git a/pkg/services/pluginsettings/service/service.go b/pkg/services/pluginsettings/service/service.go index d1534398aae..8ec61036d16 100644 --- a/pkg/services/pluginsettings/service/service.go +++ b/pkg/services/pluginsettings/service/service.go @@ -49,7 +49,7 @@ func (s *Service) GetPluginSettings(ctx context.Context, args *pluginsettings.Ge return nil, err } - var result []*pluginsettings.InfoDTO + result := make([]*pluginsettings.InfoDTO, 0, len(ps)) for _, p := range ps { result = append(result, &pluginsettings.InfoDTO{ OrgID: p.OrgID, diff --git a/pkg/services/provisioning/values/values.go b/pkg/services/provisioning/values/values.go index 20c0de27b9a..b384317f9d4 100644 --- a/pkg/services/provisioning/values/values.go +++ b/pkg/services/provisioning/values/values.go @@ -255,8 +255,8 @@ func transformInterface(i interface{}) (interface{}, interface{}, error) { } func transformSlice(i []interface{}) (interface{}, interface{}, error) { - var transformedSlice []interface{} - var rawSlice []interface{} + transformedSlice := make([]interface{}, 0, len(i)) + rawSlice := make([]interface{}, 0, len(i)) for _, val := range i { transformed, raw, err := transformInterface(val) if err != nil { diff --git a/pkg/services/publicdashboards/commands/generate_datasources/generate.go b/pkg/services/publicdashboards/commands/generate_datasources/generate.go index d36852860e2..9a98454badc 100644 --- a/pkg/services/publicdashboards/commands/generate_datasources/generate.go +++ b/pkg/services/publicdashboards/commands/generate_datasources/generate.go @@ -56,7 +56,7 @@ func getDatasourcePluginSlugs(baseUrl string) ([]string, error) { if err != nil { return nil, err } - var slugs []string + slugs := make([]string, 0, len(res.Items)) for _, meta := range res.Items { slugs = append(slugs, meta.Slug) } diff --git a/pkg/services/publicdashboards/service/query.go b/pkg/services/publicdashboards/service/query.go index c6f1f1c92f8..bb10954a64f 100644 --- a/pkg/services/publicdashboards/service/query.go +++ b/pkg/services/publicdashboards/service/query.go @@ -88,7 +88,7 @@ func (pd *PublicDashboardServiceImpl) FindAnnotations(ctx context.Context, reqDT } } - var results []models.AnnotationEvent + results := make([]models.AnnotationEvent, 0, len(uniqueEvents)) for _, result := range uniqueEvents { results = append(results, result) } diff --git a/pkg/services/searchV2/allowed_actions.go b/pkg/services/searchV2/allowed_actions.go index d6519c9e6b0..2e9dd13907e 100644 --- a/pkg/services/searchV2/allowed_actions.go +++ b/pkg/services/searchV2/allowed_actions.go @@ -100,7 +100,7 @@ func (s *StandardSearchService) createAllowedActions(ctx context.Context, orgId dsActionsByUid = make(map[string][]string) } - var out [][]allowedActions + out := make([][]allowedActions, 0, len(references)) for _, ref := range references { var actions []allowedActions diff --git a/pkg/services/searchV2/index.go b/pkg/services/searchV2/index.go index 15632368411..0e92a96993e 100644 --- a/pkg/services/searchV2/index.go +++ b/pkg/services/searchV2/index.go @@ -776,13 +776,12 @@ func (i *searchIndex) updateDashboard(ctx context.Context, orgID int64, index *o return err } - var actualPanelIDs []string - if location != "" { location += "/" } location += dash.uid panelDocs := getDashboardPanelDocs(dash, location) + actualPanelIDs := make([]string, 0, len(panelDocs)) for _, panelDoc := range panelDocs { actualPanelIDs = append(actualPanelIDs, string(panelDoc.ID().Term())) batch.Update(panelDoc.ID(), panelDoc) diff --git a/pkg/services/secrets/kvstore/plugin.go b/pkg/services/secrets/kvstore/plugin.go index 51f0ba02927..d04578bc47d 100644 --- a/pkg/services/secrets/kvstore/plugin.go +++ b/pkg/services/secrets/kvstore/plugin.go @@ -190,7 +190,7 @@ func (kv *SecretsKVStorePlugin) WithFallbackEnabled(fn func() error) error { } func parseKeys(keys []*smp.Key) []Key { - var newKeys []Key + newKeys := make([]Key, 0, len(keys)) for _, k := range keys { newKey := Key{OrgId: k.OrgId, Namespace: k.Namespace, Type: k.Type} @@ -201,7 +201,7 @@ func parseKeys(keys []*smp.Key) []Key { } func parseItems(items []*smp.Item) []Item { - var newItems []Item + newItems := make([]Item, 0, len(items)) for _, i := range items { newItem := Item{OrgId: &i.Key.OrgId, Namespace: &i.Key.Namespace, Type: &i.Key.Type, Value: i.Value} diff --git a/pkg/services/sqlstore/migrations/accesscontrol/action_migrator.go b/pkg/services/sqlstore/migrations/accesscontrol/action_migrator.go index 7e2266163a4..47de5f8fa30 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/action_migrator.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/action_migrator.go @@ -54,7 +54,8 @@ func (m *actionNameMigrator) migrateActionNames() error { "alert.rules:update": accesscontrol.ActionAlertingRuleUpdate, } - var oldActionNames, newActionNames []interface{} + oldActionNames := make([]interface{}, 0, len(actionNameMapping)) + newActionNames := make([]interface{}, 0, len(actionNameMapping)) for oldName, newName := range actionNameMapping { oldActionNames = append(oldActionNames, oldName) newActionNames = append(newActionNames, newName) diff --git a/pkg/services/sqlstore/migrations/accesscontrol/team_membership.go b/pkg/services/sqlstore/migrations/accesscontrol/team_membership.go index a0be96f92a9..c9e9f29c207 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/team_membership.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/team_membership.go @@ -47,7 +47,7 @@ func (p *teamPermissionMigrator) setRolePermissions(roleID int64, permissions [] } // Then insert new permissions - var newPermissions []accesscontrol.Permission + newPermissions := make([]accesscontrol.Permission, 0, len(permissions)) now := time.Now() for _, permission := range permissions { permission.RoleID = roleID From ac25913f53448c43f5405313949ab98af2f63a4f Mon Sep 17 00:00:00 2001 From: Hamas Shafiq Date: Tue, 17 Jan 2023 12:35:36 +0000 Subject: [PATCH 43/65] Chore: Convert `TimelineViewingLayer.test.tsx` to RTL (#61056) --- .betterer.results | 3 - .../TimelineViewingLayer.test.tsx | 235 ++++-------------- 2 files changed, 42 insertions(+), 196 deletions(-) diff --git a/.betterer.results b/.betterer.results index 43be61f43d5..2a58c91dbb0 100644 --- a/.betterer.results +++ b/.betterer.results @@ -11,9 +11,6 @@ exports[`no enzyme tests`] = { "packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/index.test.tsx:3266788928": [ [14, 56, 13, "RegExp match", "2409514259"] ], - "packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.test.tsx:3450948735": [ - [15, 31, 13, "RegExp match", "2409514259"] - ], "packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.test.tsx:3891071965": [ [13, 42, 13, "RegExp match", "2409514259"] ] diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.test.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.test.tsx index f90c2c1fd52..654ac191b18 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.test.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.test.tsx @@ -12,34 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { cx } from '@emotion/css'; -import { mount, ReactWrapper } from 'enzyme'; +import { render, screen } from '@testing-library/react'; import React from 'react'; -import DraggableManager, { DraggingUpdate } from 'src/utils/DraggableManager'; import { ViewRangeTime } from '../types'; -import TimelineViewingLayer, { getStyles, TimelineViewingLayerProps } from './TimelineViewingLayer'; - -function mapFromSubRange(viewStart: number, viewEnd: number, value: number) { - return viewStart + value * (viewEnd - viewStart); -} - -// Remove the prefix generated by cx (css-), i.e. change the string from something like: -// 'css-7g92us-TimelineViewingLayerDragged-Timeline...' -// to: -// 'TimelineViewingLayerDragged-Timeline...' -function removeCxPrefix(cxString: string) { - return cxString.split('-').slice(2).join('-'); -} +import TimelineViewingLayer, { TimelineViewingLayerProps } from './TimelineViewingLayer'; describe('', () => { - let wrapper: ReactWrapper; - let instance: TimelineViewingLayer; - const viewStart = 0.25; const viewEnd = 0.9; - const props = { + + let props: TimelineViewingLayerProps = { boundsInvalidator: Math.random(), updateNextViewRangeTime: jest.fn(), updateViewRangeTime: jest.fn(), @@ -48,184 +32,49 @@ describe('', () => { }, }; - beforeEach(() => { - props.updateNextViewRangeTime.mockReset(); - props.updateViewRangeTime.mockReset(); - wrapper = mount(); - instance = wrapper.instance(); - }); - it('renders without exploding', () => { - expect(wrapper).toBeDefined(); - expect(wrapper.find('[data-testid="TimelineViewingLayer"]').length).toBe(1); - }); - - it('sets _root to the root DOM node', () => { - expect(instance._root).toBeDefined(); - expect(wrapper.find('[data-testid="TimelineViewingLayer"]').getDOMNode()).toBe(instance._root); - }); - - describe('uses DraggableManager', () => { - it('initializes the DraggableManager', () => { - const dm = instance._draggerReframe; - expect(dm).toBeDefined(); - expect(dm._onMouseMove).toBe(instance._handleReframeMouseMove); - expect(dm._onMouseLeave).toBe(instance._handleReframeMouseLeave); - expect(dm._onDragStart).toBe(instance._handleReframeDragUpdate); - expect(dm._onDragMove).toBe(instance._handleReframeDragUpdate); - expect(dm._onDragEnd).toBe(instance._handleReframeDragEnd); - }); - - it('provides the DraggableManager handlers as callbacks', () => { - const { handleMouseDown, handleMouseLeave, handleMouseMove } = instance._draggerReframe; - const rootWrapper = wrapper.find('[data-testid="TimelineViewingLayer"]'); - expect(rootWrapper.prop('onMouseDown')).toBe(handleMouseDown); - expect(rootWrapper.prop('onMouseLeave')).toBe(handleMouseLeave); - expect(rootWrapper.prop('onMouseMove')).toBe(handleMouseMove); - }); - - it('returns the dragging bounds from _getDraggingBounds()', () => { - const left = 10; - const width = 100; - instance._root!.getBoundingClientRect = () => ({ left, width } as DOMRect); - expect(instance._getDraggingBounds()).toEqual({ width, clientXLeft: left }); - }); - - it('updates viewRange.time.cursor via _draggerReframe._onMouseMove', () => { - const value = 0.5; - const cursor = mapFromSubRange(viewStart, viewEnd, value); - instance._draggerReframe._onMouseMove!({ value } as DraggingUpdate); - expect(props.updateNextViewRangeTime.mock.calls).toEqual([[{ cursor }]]); - }); - - it('resets viewRange.time.cursor via _draggerReframe._onMouseLeave', () => { - instance._draggerReframe._onMouseLeave!(null as unknown as DraggingUpdate); - expect(props.updateNextViewRangeTime.mock.calls).toEqual([[{ cursor: undefined }]]); - }); - - it('handles drag start via _draggerReframe._onDragStart', () => { - const value = 0.5; - const shift = mapFromSubRange(viewStart, viewEnd, value); - const update = { reframe: { shift, anchor: shift } }; - instance._draggerReframe._onDragStart!({ value } as DraggingUpdate); - expect(props.updateNextViewRangeTime.mock.calls).toEqual([[update]]); - }); - - it('handles drag move via _draggerReframe._onDragMove', () => { - const anchor = 0.25; - const viewRangeTime = { ...props.viewRangeTime, reframe: { anchor, shift: Math.random() } } as ViewRangeTime; - const value = 0.5; - const shift = mapFromSubRange(viewStart, viewEnd, value); - // make sure `anchor` is already present on the props - wrapper.setProps({ viewRangeTime }); - expect(wrapper.prop('viewRangeTime').reframe?.anchor).toBe(anchor); - // the next update should integrate `value` and use the existing anchor - instance._draggerReframe._onDragStart!({ value } as DraggingUpdate); - const update = { reframe: { anchor, shift } }; - expect(props.updateNextViewRangeTime.mock.calls).toEqual([[update]]); - }); - - it('handles drag end via _draggerReframe._onDragEnd', () => { - const manager = { resetBounds: jest.fn() } as unknown as DraggableManager; - const value = 0.5; - const shift = mapFromSubRange(viewStart, viewEnd, value); - const anchor = 0.25; - const viewRangeTime = { ...props.viewRangeTime, reframe: { anchor, shift: Math.random() } } as ViewRangeTime; - wrapper.setProps({ viewRangeTime }); - instance._draggerReframe._onDragEnd!({ manager, value } as DraggingUpdate); - expect(jest.mocked(manager.resetBounds).mock.calls).toEqual([[]]); - expect(props.updateViewRangeTime.mock.calls).toEqual([[anchor, shift, 'timeline-header']]); - }); + render(); + expect(screen.getByTestId('TimelineViewingLayer')).toBeTruthy(); }); describe('render()', () => { it('renders nothing without a nextViewRangeTime', () => { - expect(wrapper.find('div').length).toBe(1); - }); - - it('renders the cursor when it is the only non-current value set', () => { - const cursor = viewStart + 0.5 * (viewEnd - viewStart); - const baseViewRangeTime = { ...props.viewRangeTime, cursor }; - wrapper.setProps({ viewRangeTime: baseViewRangeTime }); - // cursor is rendered when solo - expect(wrapper.find('[data-testid="TimelineViewingLayer--cursorGuide"]').length).toBe(1); - // cursor is skipped when shiftStart, shiftEnd, or reframe are present - let viewRangeTime: ViewRangeTime = { - ...baseViewRangeTime, - shiftStart: cursor, - shiftEnd: cursor, - reframe: { anchor: cursor, shift: cursor }, - }; - wrapper.setProps({ viewRangeTime }); - expect(wrapper.find('[data-testid="TimelineViewingLayer--cursorGuide"]').length).toBe(0); - viewRangeTime = { ...baseViewRangeTime, shiftEnd: cursor }; - wrapper.setProps({ viewRangeTime }); - expect(wrapper.find('[data-testid="TimelineViewingLayer--cursorGuide"]').length).toBe(0); - viewRangeTime = { ...baseViewRangeTime, reframe: { anchor: cursor, shift: cursor } }; - wrapper.setProps({ viewRangeTime }); - expect(wrapper.find('[data-testid="TimelineViewingLayer--cursorGuide"]').length).toBe(0); - }); - - it('renders the reframe dragging', () => { - const viewRangeTime = { ...props.viewRangeTime, reframe: { anchor: viewStart, shift: viewEnd } }; - wrapper.setProps({ viewRangeTime }); - const styles = getStyles(); - expect( - wrapper - .find('[data-testid="Dragged"]') - .prop('className')! - .indexOf( - removeCxPrefix( - cx( - styles.TimelineViewingLayerDragged, - styles.TimelineViewingLayerDraggedDraggingLeft, - styles.TimelineViewingLayerDraggedDraggingRight, - styles.TimelineViewingLayerDraggedReframeDrag - ) - ) - ) >= 0 - ).toBe(true); - }); - - it('renders the shiftStart dragging', () => { - const viewRangeTime = { ...props.viewRangeTime, shiftStart: viewEnd }; - wrapper.setProps({ viewRangeTime }); - const styles = getStyles(); - expect( - wrapper - .find('[data-testid="Dragged"]') - .prop('className')! - .indexOf( - removeCxPrefix( - cx( - styles.TimelineViewingLayerDragged, - styles.TimelineViewingLayerDraggedDraggingLeft, - styles.TimelineViewingLayerDraggedDraggingRight, - styles.TimelineViewingLayerDraggedShiftDrag - ) - ) - ) >= 0 - ).toBe(true); - }); - - it('renders the shiftEnd dragging', () => { - const viewRangeTime = { ...props.viewRangeTime, shiftEnd: viewStart }; - wrapper.setProps({ viewRangeTime }); - const styles = getStyles(); - expect( - wrapper - .find('[data-testid="Dragged"]') - .prop('className')! - .indexOf( - removeCxPrefix( - cx( - styles.TimelineViewingLayerDragged, - styles.TimelineViewingLayerDraggedDraggingLeft, - styles.TimelineViewingLayerDraggedShiftDrag - ) - ) - ) >= 0 - ).toBe(true); + render(); + expect(screen.queryByTestId('TimelineViewingLayer--cursorGuide')).not.toBeInTheDocument(); }); }); + + it('renders the cursor when it is the only non-current value set', () => { + const cursor = viewStart + 0.5 * (viewEnd - viewStart); + const baseViewRangeTime = { ...props.viewRangeTime, cursor }; + props = { ...props, viewRangeTime: baseViewRangeTime }; + render(); + expect(screen.queryByTestId('TimelineViewingLayer--cursorGuide')).toBeInTheDocument(); + }); + + it('does not render the cursor when shiftStart, shiftEnd, or reframe are present', () => { + const cursor = viewStart + 0.5 * (viewEnd - viewStart); + const baseViewRangeTime = { ...props.viewRangeTime, cursor }; + + let viewRangeTime: ViewRangeTime = { + ...baseViewRangeTime, + shiftStart: cursor, + shiftEnd: cursor, + reframe: { anchor: cursor, shift: cursor }, + }; + + props = { ...props, viewRangeTime }; + render(); + expect(screen.queryByTestId('TimelineViewingLayer--cursorGuide')).not.toBeInTheDocument(); + + viewRangeTime = { ...baseViewRangeTime, shiftEnd: cursor }; + props = { ...props, viewRangeTime }; + render(); + expect(screen.queryByTestId('TimelineViewingLayer--cursorGuide')).not.toBeInTheDocument(); + + viewRangeTime = { ...baseViewRangeTime, reframe: { anchor: cursor, shift: cursor } }; + props = { ...props, viewRangeTime }; + render(); + expect(screen.queryByTestId('TimelineViewingLayer--cursorGuide')).not.toBeInTheDocument(); + }); }); From 766fa4e7d552e0a172ba1fd22989c4a1ae2ad834 Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Tue, 17 Jan 2023 13:50:58 +0100 Subject: [PATCH 44/65] AuthN: Add last seen sync hooks for user and api keys (#61571) * AUthN: Add last seen sync hooks for user / service account and move api key last seen to own hook * ContextHandler: only run sync for last seen if auth.Service is not enabled --- pkg/services/authn/authnimpl/service.go | 2 + .../usersync/apikey_last_seen_sync.go | 38 ++++++++++++++ .../authnimpl/usersync/user_last_seen_sync.go | 50 +++++++++++++++++++ pkg/services/authn/clients/api_key.go | 11 ---- pkg/services/contexthandler/contexthandler.go | 13 +++-- 5 files changed, 98 insertions(+), 16 deletions(-) create mode 100644 pkg/services/authn/authnimpl/usersync/apikey_last_seen_sync.go create mode 100644 pkg/services/authn/authnimpl/usersync/user_last_seen_sync.go diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index 63f76c9e80b..01c30af77d5 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -108,6 +108,8 @@ func ProvideService( orgUserSyncService := sync.ProvideOrgSync(userService, orgService, accessControlService) s.RegisterPostAuthHook(userSyncService.SyncUser) s.RegisterPostAuthHook(orgUserSyncService.SyncOrgUser) + s.RegisterPostAuthHook(sync.ProvideUserLastSeenSync(userService).SyncLastSeen) + s.RegisterPostAuthHook(sync.ProvideAPIKeyLastSeenSync(apikeyService).SyncLastSeen) return s } diff --git a/pkg/services/authn/authnimpl/usersync/apikey_last_seen_sync.go b/pkg/services/authn/authnimpl/usersync/apikey_last_seen_sync.go new file mode 100644 index 00000000000..293fc3e88bf --- /dev/null +++ b/pkg/services/authn/authnimpl/usersync/apikey_last_seen_sync.go @@ -0,0 +1,38 @@ +package usersync + +import ( + "context" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/apikey" + "github.com/grafana/grafana/pkg/services/authn" +) + +func ProvideAPIKeyLastSeenSync(service apikey.Service) *APIKeyLastSeenSync { + return &APIKeyLastSeenSync{log.New("apikeylastseen.sync"), service} +} + +type APIKeyLastSeenSync struct { + log log.Logger + service apikey.Service +} + +func (s *APIKeyLastSeenSync) SyncLastSeen(ctx context.Context, identity *authn.Identity, _ *authn.Request) error { + namespace, id := identity.NamespacedID() + if namespace != authn.NamespaceAPIKey { + return nil + } + + go func(apikeyID int64) { + defer func() { + if err := recover(); err != nil { + s.log.Error("panic during user last seen sync", "err", err) + } + }() + if err := s.service.UpdateAPIKeyLastUsedDate(context.Background(), apikeyID); err != nil { + s.log.Warn("failed to update last use date for api key", "id", apikeyID) + } + }(id) + + return nil +} diff --git a/pkg/services/authn/authnimpl/usersync/user_last_seen_sync.go b/pkg/services/authn/authnimpl/usersync/user_last_seen_sync.go new file mode 100644 index 00000000000..881cd18e333 --- /dev/null +++ b/pkg/services/authn/authnimpl/usersync/user_last_seen_sync.go @@ -0,0 +1,50 @@ +package usersync + +import ( + "context" + "time" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/user" +) + +func ProvideUserLastSeenSync(service user.Service) *UserLastSeenSync { + return &UserLastSeenSync{log.New("userlastseen.sync"), service} +} + +type UserLastSeenSync struct { + log log.Logger + service user.Service +} + +func (s *UserLastSeenSync) SyncLastSeen(ctx context.Context, identity *authn.Identity, _ *authn.Request) error { + namespace, id := identity.NamespacedID() + + if namespace != authn.NamespaceUser && namespace != authn.NamespaceServiceAccount { + // skip sync + return nil + } + + if !shouldUpdateLastSeen(identity.LastSeenAt) { + return nil + } + + go func(userID int64) { + defer func() { + if err := recover(); err != nil { + s.log.Error("panic during user last seen sync", "err", err) + } + }() + + if err := s.service.UpdateLastSeenAt(context.Background(), &user.UpdateUserLastSeenAtCommand{UserID: userID}); err != nil { + s.log.Error("failed to update last_seen_at", "err", err, "userId", userID) + } + }(id) + + return nil +} + +func shouldUpdateLastSeen(t time.Time) bool { + return time.Since(t) > time.Minute*5 +} diff --git a/pkg/services/authn/clients/api_key.go b/pkg/services/authn/clients/api_key.go index ab684a5672d..958cdf7fb63 100644 --- a/pkg/services/authn/clients/api_key.go +++ b/pkg/services/authn/clients/api_key.go @@ -56,17 +56,6 @@ func (s *APIKey) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ide return nil, errAPIKeyRevoked.Errorf("Api key is revoked") } - go func(id int64) { - defer func() { - if err := recover(); err != nil { - s.log.Error("api key authentication panic", "err", err) - } - }() - if err := s.apiKeyService.UpdateAPIKeyLastUsedDate(context.Background(), id); err != nil { - s.log.Warn("failed to update last use date for api key", "id", id) - } - }(apiKey.Id) - // if the api key don't belong to a service account construct the identity and return it if apiKey.ServiceAccountId == nil || *apiKey.ServiceAccountId < 1 { return &authn.Identity{ diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index 4888c6cee86..1f84d4484f3 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -180,11 +180,14 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler { {Num: reqContext.UserID}}, ) - // update last seen every 5min - if reqContext.ShouldUpdateLastSeenAt() { - reqContext.Logger.Debug("Updating last user_seen_at", "user_id", reqContext.UserID) - if err := h.userService.UpdateLastSeenAt(mContext.Req.Context(), &user.UpdateUserLastSeenAtCommand{UserID: reqContext.UserID}); err != nil { - reqContext.Logger.Error("Failed to update last_seen_at", "error", err) + // when using authn service this is implemented as a post auth hook + if !h.features.IsEnabled(featuremgmt.FlagAuthnService) { + // update last seen every 5min + if reqContext.ShouldUpdateLastSeenAt() { + reqContext.Logger.Debug("Updating last user_seen_at", "user_id", reqContext.UserID) + if err := h.userService.UpdateLastSeenAt(mContext.Req.Context(), &user.UpdateUserLastSeenAtCommand{UserID: reqContext.UserID}); err != nil { + reqContext.Logger.Error("Failed to update last_seen_at", "error", err) + } } } From 004705a10b2b95eb03ad47cf44e257fc5b89f093 Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Tue, 17 Jan 2023 14:05:58 +0100 Subject: [PATCH 45/65] Table Panel: Fix image of image cell overflowing table cell when a data link is added. (#59392) * TablePanel: fix image of image cell overflowing table cell when a data link is added * Fix image cell and gauge panel in respective places instead --- .../src/components/DataLinks/DataLinksContextMenu.tsx | 2 +- packages/grafana-ui/src/components/Table/ImageCell.tsx | 2 +- public/app/plugins/panel/gauge/GaugePanel.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx index 992ba6de058..f692a2ff568 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx @@ -62,7 +62,7 @@ export const DataLinksContextMenu = ({ children, links, style }: DataLinksContex onClick={linkModel.onClick} target={linkModel.target} title={linkModel.title} - style={{ ...style, overflow: 'hidden', display: 'flex', flexGrow: 1 }} + style={{ ...style, overflow: 'hidden', display: 'flex' }} aria-label={selectors.components.DataLinksContextMenu.singleLink} > {children({})} diff --git a/packages/grafana-ui/src/components/Table/ImageCell.tsx b/packages/grafana-ui/src/components/Table/ImageCell.tsx index 6961be2fe22..c1e8b2ee2f1 100644 --- a/packages/grafana-ui/src/components/Table/ImageCell.tsx +++ b/packages/grafana-ui/src/components/Table/ImageCell.tsx @@ -17,7 +17,7 @@ export const ImageCell: FC = (props) => {
{!hasLinks && } {hasLinks && ( - getCellLinks(field, row) || []}> + getCellLinks(field, row) || []}> {(api) => { return (
diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index e5766087ef2..bbe3094e5cf 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -41,7 +41,7 @@ export class GaugePanel extends PureComponent> { if (hasLinks && getLinks) { return ( - + {(api) => { return this.renderComponent(valueProps, api); }} From 1b86a496227ee18ae312e715c3040f8b6ae1e3a9 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Tue, 17 Jan 2023 14:11:11 +0100 Subject: [PATCH 46/65] AzureMonitor: Adapt ResourcePicker and Advanced components to multiple resources (#61605) --- .../ResourceField/ResourceField.tsx | 5 +- .../ResourcePicker/Advanced.test.tsx | 21 +- .../components/ResourcePicker/Advanced.tsx | 219 ++++++++++-------- .../ResourcePicker/ResourcePicker.test.tsx | 82 +++++-- .../ResourcePicker/ResourcePicker.tsx | 51 ++-- .../components/ResourcePicker/utils.test.ts | 94 +++++++- .../components/ResourcePicker/utils.ts | 23 +- .../resourcePicker/resourcePickerData.test.ts | 57 +++++ .../resourcePicker/resourcePickerData.ts | 38 ++- 9 files changed, 429 insertions(+), 161 deletions(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourceField/ResourceField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourceField/ResourceField.tsx index 18e5c3f800d..4098d27b9ac 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourceField/ResourceField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourceField/ResourceField.tsx @@ -63,8 +63,9 @@ const ResourceField: React.FC> > resources && handleApply(resources[0])} onCancel={closePicker} selectableEntryTypes={selectableEntryTypes} queryType={queryType} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/Advanced.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/Advanced.test.tsx index e9efb20c08d..f4f55d70b0d 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/Advanced.test.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/Advanced.test.tsx @@ -7,29 +7,38 @@ import Advanced from './Advanced'; describe('AzureMonitor ResourcePicker', () => { it('should set a parameter as an object', async () => { const onChange = jest.fn(); - const { rerender } = render(); + const { rerender } = render(); const advancedSection = screen.getByText('Advanced'); advancedSection.click(); const subsInput = await screen.findByLabelText('Subscription'); await userEvent.type(subsInput, 'd'); - expect(onChange).toHaveBeenCalledWith({ subscription: 'd' }); + expect(onChange).toHaveBeenCalledWith([{ subscription: 'd' }]); - rerender(); + rerender(); expect(screen.getByLabelText('Subscription').outerHTML).toMatch('value="def-123"'); }); it('should set a parameter as uri', async () => { const onChange = jest.fn(); - const { rerender } = render(); + const { rerender } = render(); const advancedSection = screen.getByText('Advanced'); advancedSection.click(); const subsInput = await screen.findByLabelText('Resource URI'); await userEvent.type(subsInput, '/'); - expect(onChange).toHaveBeenCalledWith('/'); + expect(onChange).toHaveBeenCalledWith(['/']); - rerender(); + rerender(); expect(screen.getByLabelText('Resource URI').outerHTML).toMatch('value="/subscriptions/sub"'); }); + + it('should render multiple resources', async () => { + render(); + const advancedSection = screen.getByText('Advanced'); + advancedSection.click(); + + expect(screen.getByDisplayValue('/subscriptions/sub1')).toBeInTheDocument(); + expect(screen.getByDisplayValue('/subscriptions/sub2')).toBeInTheDocument(); + }); }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/Advanced.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/Advanced.tsx index 2a6958a50b7..15c20289dd7 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/Advanced.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/Advanced.tsx @@ -7,12 +7,18 @@ import { AzureMetricResource } from '../../types'; import { Space } from '../Space'; interface ResourcePickerProps { - resource: T; - onChange: (resource: T) => void; + resources: T[]; + onChange: (resources: T[]) => void; } -const Advanced = ({ resource, onChange }: ResourcePickerProps) => { - const [isAdvancedOpen, setIsAdvancedOpen] = useState(!!resource && JSON.stringify(resource).includes('$')); +const Advanced = ({ resources, onChange }: ResourcePickerProps) => { + const [isAdvancedOpen, setIsAdvancedOpen] = useState(!!resources.length && JSON.stringify(resources).includes('$')); + + const onResourceChange = (resource: string | AzureMetricResource, index: number) => { + const newResources = [...resources]; + newResources[index] = resource; + onChange(newResources); + }; return (
@@ -22,104 +28,115 @@ const Advanced = ({ resource, onChange }: ResourcePickerProps setIsAdvancedOpen(!isAdvancedOpen)} > - {typeof resource === 'string' ? ( - <> - {' '} - - onChange(event.currentTarget.value)} - placeholder="ex: /subscriptions/$subId" - /> - - ) : ( - <> - - onChange({ ...resource, subscription: event.currentTarget.value })} - placeholder="aaaaaaaa-bbbb-cccc-dddd-eeeeeeee" - /> - - - onChange({ ...resource, resourceGroup: event.currentTarget.value })} - placeholder="resource-group" - /> - - - onChange({ ...resource, metricNamespace: event.currentTarget.value })} - placeholder="Microsoft.Insights/metricNamespaces" - /> - - - onChange({ ...resource, resourceName: event.currentTarget.value })} - placeholder="name" - /> - - - )} + + onResourceChange({ ...resource, subscription: event.currentTarget.value }, index) + } + placeholder="aaaaaaaa-bbbb-cccc-dddd-eeeeeeee" + /> + + + + onResourceChange({ ...resource, resourceGroup: event.currentTarget.value }, index) + } + placeholder="resource-group" + /> + + + + onResourceChange({ ...resource, metricNamespace: event.currentTarget.value }, index) + } + placeholder="Microsoft.Insights/metricNamespaces" + /> + + + + onResourceChange({ ...resource, resourceName: event.currentTarget.value }, index) + } + placeholder="name" + /> + + + )} +
+ ))}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.test.tsx index 4e2922b4568..6537802765d 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.test.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.test.tsx @@ -17,6 +17,15 @@ import { ResourceRowType } from './types'; import ResourcePicker from '.'; +jest.mock('@grafana/runtime', () => ({ + ...(jest.requireActual('@grafana/runtime') as unknown as object), + getTemplateSrv: () => ({ + replace: (val: string) => { + return val; + }, + }), +})); + const noResourceURI = ''; const singleSubscriptionSelectionURI = '/subscriptions/def-456'; const singleResourceGroupSelectionURI = '/subscriptions/def-456/resourceGroups/dev-3'; @@ -50,7 +59,7 @@ const queryType: ResourcePickerQueryType = 'logs'; const defaultProps = { templateVariables: [], - resource: noResourceURI, + resources: [noResourceURI], resourcePickerData: createMockResourcePickerData(), onCancel: noop, onApply: noop, @@ -68,7 +77,7 @@ describe('AzureMonitor ResourcePicker', () => { window.HTMLElement.prototype.scrollIntoView = jest.fn(); }); it('should pre-load subscriptions when there is no existing selection', async () => { - render(); + render(); const subscriptionCheckbox = await screen.findByLabelText('Primary Subscription'); expect(subscriptionCheckbox).toBeInTheDocument(); expect(subscriptionCheckbox).not.toBeChecked(); @@ -77,7 +86,7 @@ describe('AzureMonitor ResourcePicker', () => { }); it('should show a subscription as selected if there is one saved', async () => { - render(); + render(); const subscriptionCheckboxes = await screen.findAllByLabelText('Dev Subscription'); expect(subscriptionCheckboxes.length).toBe(2); expect(subscriptionCheckboxes[0]).toBeChecked(); @@ -85,7 +94,7 @@ describe('AzureMonitor ResourcePicker', () => { }); it('should show a resourceGroup as selected if there is one saved', async () => { - render(); + render(); const resourceGroupCheckboxes = await screen.findAllByLabelText('A Great Resource Group'); expect(resourceGroupCheckboxes.length).toBe(2); expect(resourceGroupCheckboxes[0]).toBeChecked(); @@ -93,7 +102,7 @@ describe('AzureMonitor ResourcePicker', () => { }); it('should show scroll down to a resource and mark it as selected if there is one saved', async () => { - render(); + render(); const resourceCheckboxes = await screen.findAllByLabelText('db-server'); expect(resourceCheckboxes.length).toBe(2); expect(resourceCheckboxes[0]).toBeChecked(); @@ -101,7 +110,7 @@ describe('AzureMonitor ResourcePicker', () => { }); it('opens the selected nested resources', async () => { - render(); + render(); const collapseSubscriptionBtn = await screen.findByLabelText('Collapse Dev Subscription'); expect(collapseSubscriptionBtn).toBeInTheDocument(); const collapseResourceGroupBtn = await screen.findByLabelText('Collapse A Great Resource Group'); @@ -109,7 +118,7 @@ describe('AzureMonitor ResourcePicker', () => { }); it('scrolls down to the selected resource', async () => { - render(); + render(); await screen.findByLabelText('Collapse A Great Resource Group'); expect(window.HTMLElement.prototype.scrollIntoView).toBeCalledTimes(1); }); @@ -133,12 +142,40 @@ describe('AzureMonitor ResourcePicker', () => { const applyButton = screen.getByRole('button', { name: 'Apply' }); applyButton.click(); expect(onApply).toBeCalledTimes(1); - expect(onApply).toBeCalledWith('/subscriptions/def-123'); + expect(onApply).toBeCalledWith(['/subscriptions/def-123']); + }); + + it('should call onApply removing an element', async () => { + const onApply = jest.fn(); + render(); + const subscriptionCheckbox = await screen.findAllByLabelText('Primary Subscription'); + expect(subscriptionCheckbox).toHaveLength(2); + expect(subscriptionCheckbox.at(0)).toBeChecked(); + subscriptionCheckbox.at(0)?.click(); + const applyButton = screen.getByRole('button', { name: 'Apply' }); + applyButton.click(); + expect(onApply).toBeCalledTimes(1); + expect(onApply).toBeCalledWith([]); + }); + + it('should call onApply removing an element ignoring the case', async () => { + const onApply = jest.fn(); + render( + + ); + const subscriptionCheckbox = await screen.findAllByLabelText('A Great Resource Group'); + expect(subscriptionCheckbox).toHaveLength(2); + expect(subscriptionCheckbox.at(0)).toBeChecked(); + subscriptionCheckbox.at(0)?.click(); + const applyButton = screen.getByRole('button', { name: 'Apply' }); + applyButton.click(); + expect(onApply).toBeCalledTimes(1); + expect(onApply).toBeCalledWith([]); }); it('should call onApply with a new subscription when a user clicks on the checkbox in the row', async () => { const onApply = jest.fn(); - render(); + render(); const subscriptionCheckbox = await screen.findByLabelText('Primary Subscription'); expect(subscriptionCheckbox).toBeInTheDocument(); expect(subscriptionCheckbox).not.toBeChecked(); @@ -146,7 +183,20 @@ describe('AzureMonitor ResourcePicker', () => { const applyButton = screen.getByRole('button', { name: 'Apply' }); applyButton.click(); expect(onApply).toBeCalledTimes(1); - expect(onApply).toBeCalledWith({ subscription: 'def-123' }); + expect(onApply).toBeCalledWith([{ subscription: 'def-123' }]); + }); + + it('should call onApply removing a resource element', async () => { + const onApply = jest.fn(); + render(); + const subscriptionCheckbox = await screen.findAllByLabelText('Primary Subscription'); + expect(subscriptionCheckbox).toHaveLength(2); + expect(subscriptionCheckbox.at(0)).toBeChecked(); + subscriptionCheckbox.at(0)?.click(); + const applyButton = screen.getByRole('button', { name: 'Apply' }); + applyButton.click(); + expect(onApply).toBeCalledTimes(1); + expect(onApply).toBeCalledWith([]); }); it('should call onApply with a new subscription uri when a user types it in the selection box', async () => { @@ -166,12 +216,12 @@ describe('AzureMonitor ResourcePicker', () => { applyButton.click(); expect(onApply).toBeCalledTimes(1); - expect(onApply).toBeCalledWith('/subscriptions/def-123'); + expect(onApply).toBeCalledWith(['/subscriptions/def-123']); }); it('should call onApply with a new subscription when a user types it in the selection box', async () => { const onApply = jest.fn(); - render(); + render(); const subscriptionCheckbox = await screen.findByLabelText('Primary Subscription'); expect(subscriptionCheckbox).toBeInTheDocument(); expect(subscriptionCheckbox).not.toBeChecked(); @@ -186,11 +236,11 @@ describe('AzureMonitor ResourcePicker', () => { applyButton.click(); expect(onApply).toBeCalledTimes(1); - expect(onApply).toBeCalledWith({ subscription: 'def-123' }); + expect(onApply).toBeCalledWith([{ subscription: 'def-123' }]); }); it('should show unselect a subscription if the value is manually edited', async () => { - render(); + render(); const subscriptionCheckboxes = await screen.findAllByLabelText('Dev Subscription'); expect(subscriptionCheckboxes.length).toBe(2); expect(subscriptionCheckboxes[0]).toBeChecked(); @@ -264,7 +314,7 @@ describe('AzureMonitor ResourcePicker', () => { }); it('resets result when the user clears their search', async () => { - render(); + render(); const subscriptionCheckboxBeforeSearch = await screen.findByLabelText('Primary Subscription'); expect(subscriptionCheckboxBeforeSearch).toBeInTheDocument(); @@ -295,7 +345,7 @@ describe('AzureMonitor ResourcePicker', () => { {...defaultProps} queryType={'metrics'} resourcePickerData={resourcePickerData} - resource={noResourceURI} + resources={[noResourceURI]} /> ); const subscriptionExpand = await screen.findByLabelText('Expand Primary Subscription'); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.tsx index 5a45f394632..aa9a5ffbb61 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.tsx @@ -15,21 +15,21 @@ import NestedRow from './NestedRow'; import Search from './Search'; import getStyles from './styles'; import { ResourceRow, ResourceRowGroup, ResourceRowType } from './types'; -import { findRow, parseResourceDetails, resourceToString } from './utils'; +import { findRows, parseMultipleResourceDetails, resourcesToStrings, matchURI, resourceToString } from './utils'; interface ResourcePickerProps { resourcePickerData: ResourcePickerData; - resource: T; + resources: T[]; selectableEntryTypes: ResourceRowType[]; queryType: ResourcePickerQueryType; - onApply: (resource?: T) => void; + onApply: (resources: T[]) => void; onCancel: () => void; } const ResourcePicker = ({ resourcePickerData, - resource, + resources, onApply, onCancel, selectableEntryTypes, @@ -40,14 +40,14 @@ const ResourcePicker = ({ const [isLoading, setIsLoading] = useState(false); const [rows, setRows] = useState([]); const [selectedRows, setSelectedRows] = useState([]); - const [internalSelected, setInternalSelected] = useState(resource); + const [internalSelected, setInternalSelected] = useState(resources); const [errorMessage, setErrorMessage] = useState(undefined); const [shouldShowLimitFlag, setShouldShowLimitFlag] = useState(false); // Sync the resourceURI prop to internal state useEffect(() => { - setInternalSelected(resource); - }, [resource]); + setInternalSelected(resources); + }, [resources]); const loadInitialData = useCallback(async () => { if (!isLoading) { @@ -55,7 +55,7 @@ const ResourcePicker = ({ setIsLoading(true); const resources = await resourcePickerData.fetchInitialRows( queryType, - parseResourceDetails(internalSelected ?? {}) + parseMultipleResourceDetails(internalSelected ?? {}) ); setRows(resources); } catch (error) { @@ -75,14 +75,9 @@ const ResourcePicker = ({ setSelectedRows([]); } - const found = internalSelected && findRow(rows, resourceToString(internalSelected)); - if (found) { - return setSelectedRows([ - { - ...found, - children: undefined, - }, - ]); + const found = internalSelected && findRows(rows, resourcesToStrings(internalSelected)); + if (found && found.length) { + return setSelectedRows(found); } return setSelectedRows([]); }, [internalSelected, rows]); @@ -109,19 +104,29 @@ const ResourcePicker = ({ [resourcePickerData, rows, queryType] ); - const resourceIsString = typeof resource === 'string'; + const resourceIsString = resources?.length && typeof resources[0] === 'string'; const handleSelectionChanged = useCallback( (row: ResourceRow, isSelected: boolean) => { - isSelected - ? setInternalSelected(resourceIsString ? row.uri : parseResourceDetails(row.uri, row.location)) - : setInternalSelected(resourceIsString ? '' : {}); + if (isSelected) { + const newRes = resourceIsString ? row.uri : parseMultipleResourceDetails([row.uri], row.location)[0]; + const newSelected = (internalSelected ? internalSelected.concat(newRes) : [newRes]).filter((r) => { + // avoid setting empty resources + return typeof r === 'string' ? r !== '' : r.subscription; + }); + setInternalSelected(newSelected); + } else { + const newInternalSelected = internalSelected?.filter((r) => { + return !matchURI(resourceToString(r), row.uri); + }); + setInternalSelected(newInternalSelected); + } }, - [resourceIsString] + [resourceIsString, internalSelected, setInternalSelected] ); const handleApply = useCallback(() => { if (internalSelected) { - onApply(resourceIsString ? internalSelected : parseResourceDetails(internalSelected)); + onApply(resourceIsString ? internalSelected : parseMultipleResourceDetails(internalSelected)); } }, [resourceIsString, internalSelected, onApply]); @@ -230,7 +235,7 @@ const ResourcePicker = ({ )} - setInternalSelected(r)} /> + setInternalSelected(r)} />
+
+ o.value === options.jsonData.defaultEditor)} + onChange={onChangeHandler('defaultEditor', options, onOptionsChange)} + width={20} + disabled={options.readOnly} + /> + } + tooltip={`Set default editor option (builder/code) for all users of this datasource. If no option was selected, the default editor will be the "builder". If they switch to other option rather than the specified with this setting on the panel we always show the selected editor for that user.`} + /> +
+ new PrometheusDatasource( { id: 1, uid: '', @@ -57,14 +79,17 @@ const defaultProps = { name: 'prom-test', access: 'proxy', url: '', - jsonData: {}, - meta: {} as any, + jsonData: jsonDataOverrides, + meta: defaultMeta, readOnly: false, }, undefined, undefined, new EmptyLanguageProviderMock() as unknown as PromQlLanguageProvider - ), + ); + +const defaultProps = { + datasource: getDefaultDatasource(), query: defaultQuery, onRunQuery: () => {}, onChange: () => {}, @@ -77,6 +102,16 @@ describe('PromQueryEditorSelector', () => { expectCodeEditor(); }); + it('shows code editor if no expr and nothing else since defaultEditor is code', async () => { + renderWithDatasourceDefaultEditorMode(QueryEditorMode.Code); + expectCodeEditor(); + }); + + it('shows builder if no expr and nothing else since defaultEditor is builder', async () => { + renderWithDatasourceDefaultEditorMode(QueryEditorMode.Builder); + expectBuilder(); + }); + it('shows code editor when code mode is set', async () => { renderWithMode(QueryEditorMode.Code); expectCodeEditor(); @@ -168,6 +203,22 @@ function renderWithMode(mode: QueryEditorMode) { return renderWithProps({ editorMode: mode } as any); } +function renderWithDatasourceDefaultEditorMode(mode: QueryEditorMode) { + const props = { + ...defaultProps, + datasource: getDefaultDatasource({ + defaultEditor: mode, + }), + query: { + refId: 'B', + expr: '', + }, + onRunQuery: () => {}, + onChange: () => {}, + }; + render(); +} + function renderWithProps(overrides?: Partial, componentProps: Partial = {}) { const query = defaultsDeep(overrides ?? {}, cloneDeep(defaultQuery)); const onChange = jest.fn(); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx index e943754df7a..768621924c0 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx @@ -34,12 +34,18 @@ export const INTERVAL_FACTOR_OPTIONS: Array> = map([1, 2 type Props = PromQueryEditorProps; export const PromQueryEditorSelector = React.memo((props) => { - const { onChange, onRunQuery, data, app } = props; + const { + onChange, + onRunQuery, + data, + app, + datasource: { defaultEditor }, + } = props; const [parseModalOpen, setParseModalOpen] = useState(false); const [dataIsStale, setDataIsStale] = useState(false); const { flag: explain, setFlag: setExplain } = useFlag(promQueryEditorExplainKey); - const query = getQueryWithDefaults(props.query, app); + const query = getQueryWithDefaults(props.query, app, defaultEditor); // This should be filled in from the defaults by now. const editorMode = query.editorMode!; diff --git a/public/app/plugins/datasource/prometheus/querybuilder/state.test.ts b/public/app/plugins/datasource/prometheus/querybuilder/state.test.ts index fb8ded74555..952c55929e7 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/state.test.ts +++ b/public/app/plugins/datasource/prometheus/querybuilder/state.test.ts @@ -49,4 +49,14 @@ describe('getQueryWithDefaults(', () => { QueryEditorMode.Code ); }); + + it('should return default editor mode when it is provided', () => { + expect(getQueryWithDefaults({ refId: 'A' } as PromQuery, CoreApp.Dashboard, QueryEditorMode.Code)).toEqual({ + editorMode: 'code', + expr: '', + legendFormat: '__auto', + range: true, + refId: 'A', + }); + }); }); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/state.ts b/public/app/plugins/datasource/prometheus/querybuilder/state.ts index 748b8fb814f..0ffbc346b85 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/state.ts +++ b/public/app/plugins/datasource/prometheus/querybuilder/state.ts @@ -16,7 +16,7 @@ export function changeEditorMode(query: PromQuery, editorMode: QueryEditorMode, onChange({ ...query, editorMode }); } -function getDefaultEditorMode(expr: string) { +function getDefaultEditorMode(expr: string, defaultEditor: QueryEditorMode = QueryEditorMode.Builder): QueryEditorMode { // If we already have an expression default to code view if (expr != null && expr !== '') { return QueryEditorMode.Code; @@ -28,18 +28,22 @@ function getDefaultEditorMode(expr: string) { case QueryEditorMode.Code: return value; default: - return QueryEditorMode.Builder; + return defaultEditor; } } /** * Returns query with defaults, and boolean true/false depending on change was required */ -export function getQueryWithDefaults(query: PromQuery, app: CoreApp | undefined): PromQuery { +export function getQueryWithDefaults( + query: PromQuery, + app: CoreApp | undefined, + defaultEditor?: QueryEditorMode +): PromQuery { let result = query; if (!query.editorMode) { - result = { ...query, editorMode: getDefaultEditorMode(query.expr) }; + result = { ...query, editorMode: getDefaultEditorMode(query.expr, defaultEditor) }; } if (query.expr == null) { diff --git a/public/app/plugins/datasource/prometheus/types.ts b/public/app/plugins/datasource/prometheus/types.ts index 5181a8fe6eb..3fbede5a34c 100644 --- a/public/app/plugins/datasource/prometheus/types.ts +++ b/public/app/plugins/datasource/prometheus/types.ts @@ -35,6 +35,7 @@ export interface PromOptions extends DataSourceJsonData { prometheusType?: PromApplication; prometheusVersion?: string; enableSecureSocksProxy?: boolean; + defaultEditor?: QueryEditorMode; } export type ExemplarTraceIdDestination = { @@ -116,6 +117,7 @@ export type PromValue = [number, any]; export interface PromMetric { __name__?: string; + [index: string]: any; } From 4076933e6657b58732ad93395af04239b4ce69de Mon Sep 17 00:00:00 2001 From: Joey Tawadrous <90795735+joey-grafana@users.noreply.github.com> Date: Tue, 17 Jan 2023 14:43:28 +0000 Subject: [PATCH 51/65] Traces: extra feature tracking (#61348) * grafana_traces_service_graph_size * grafana_traces_trace_view_find_next_prev_clicked * grafana_traces_trace_view_scroll_to_top_clicked * grafana_traces_cheatsheet_clicked --- .../src/TracePageHeader/TracePageSearchBar.tsx | 13 +++++++++++++ .../TraceTimelineViewer/VirtualizedTraceView.tsx | 8 +++++++- .../src/TraceTimelineViewer/index.tsx | 1 + public/app/features/explore/NodeGraphContainer.tsx | 2 +- .../explore/TraceView/TraceViewContainer.tsx | 2 ++ public/app/plugins/datasource/tempo/CheatSheet.tsx | 6 ++++++ public/app/plugins/datasource/tempo/datasource.ts | 10 ++++++++++ public/app/plugins/panel/traces/TracesPanel.tsx | 2 ++ 8 files changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx b/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx index b8fa71464eb..186e0d2b074 100644 --- a/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx +++ b/packages/jaeger-ui-components/src/TracePageHeader/TracePageSearchBar.tsx @@ -17,6 +17,7 @@ import cx from 'classnames'; import React, { memo, Dispatch, SetStateAction } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; +import { reportInteraction } from '@grafana/runtime'; import { Button, useStyles2 } from '@grafana/ui'; import UiFindInput from '../common/UiFindInput'; @@ -77,6 +78,7 @@ export type TracePageSearchBarProps = { focusedSpanIdForSearch: string; setSearchBarSuffix: Dispatch>; setFocusedSpanIdForSearch: Dispatch>; + datasourceType: string; }; export default memo(function TracePageSearchBar(props: TracePageSearchBarProps) { @@ -89,6 +91,7 @@ export default memo(function TracePageSearchBar(props: TracePageSearchBarProps) focusedSpanIdForSearch, setSearchBarSuffix, setFocusedSpanIdForSearch, + datasourceType, } = props; const styles = useStyles2(getStyles); @@ -112,6 +115,11 @@ export default memo(function TracePageSearchBar(props: TracePageSearchBarProps) }; const nextResult = () => { + reportInteraction('grafana_traces_trace_view_find_next_prev_clicked', { + datasourceType: datasourceType, + direction: 'next', + }); + const spanMatches = Array.from(spanFindMatches!); const prevMatchedIndex = spanMatches.indexOf(focusedSpanIdForSearch) ? spanMatches.indexOf(focusedSpanIdForSearch) @@ -130,6 +138,11 @@ export default memo(function TracePageSearchBar(props: TracePageSearchBarProps) }; const prevResult = () => { + reportInteraction('grafana_traces_trace_view_find_next_prev_clicked', { + datasourceType: datasourceType, + direction: 'prev', + }); + const spanMatches = Array.from(spanFindMatches!); const prevMatchedIndex = spanMatches.indexOf(focusedSpanIdForSearch) ? spanMatches.indexOf(focusedSpanIdForSearch) diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx index f36bcbc03dc..b789298dae8 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx @@ -19,6 +19,7 @@ import * as React from 'react'; import { createRef, RefObject } from 'react'; import { GrafanaTheme2, LinkModel, TimeZone } from '@grafana/data'; +import { reportInteraction } from '@grafana/runtime'; import { stylesFactory, withTheme2, ToolbarButton } from '@grafana/ui'; import { Accessors } from '../ScrollManager'; @@ -536,8 +537,13 @@ export class UnthemedVirtualizedTraceView extends React.Component { - const { topOfViewRef } = this.props; + const { topOfViewRef, datasourceType, trace } = this.props; topOfViewRef?.current?.scrollIntoView({ behavior: 'smooth' }); + reportInteraction('grafana_traces_trace_view_scroll_to_top_clicked', { + datasourceType: datasourceType, + numServices: trace.services.length, + numSpans: trace.spans.length, + }); }; render() { diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/index.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/index.tsx index fcae2626799..c27231564e4 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/index.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/index.tsx @@ -216,6 +216,7 @@ export class UnthemedTraceTimelineViewer extends React.PureComponent
); diff --git a/public/app/features/explore/NodeGraphContainer.tsx b/public/app/features/explore/NodeGraphContainer.tsx index 0a0821bfd60..b15870dd27f 100644 --- a/public/app/features/explore/NodeGraphContainer.tsx +++ b/public/app/features/explore/NodeGraphContainer.tsx @@ -59,7 +59,7 @@ export function UnconnectedNodeGraphContainer(props: Props) { toggleOpen(); reportInteraction('grafana_traces_node_graph_panel_clicked', { datasourceType: datasourceType, - expanded: !open, + isExpanded: !open, }); }; diff --git a/public/app/features/explore/TraceView/TraceViewContainer.tsx b/public/app/features/explore/TraceView/TraceViewContainer.tsx index e5bbdce5b2e..1d908a0ed64 100644 --- a/public/app/features/explore/TraceView/TraceViewContainer.tsx +++ b/public/app/features/explore/TraceView/TraceViewContainer.tsx @@ -29,6 +29,7 @@ export function TraceViewContainer(props: Props) { const datasource = useSelector( (state: StoreState) => state.explore[props.exploreId!]?.datasourceInstance ?? undefined ); + const datasourceType = datasource ? datasource?.type : 'unknown'; if (!traceProp) { return null; @@ -45,6 +46,7 @@ export function TraceViewContainer(props: Props) { setSearchBarSuffix={setSearchBarSuffix} focusedSpanIdForSearch={focusedSpanIdForSearch} setFocusedSpanIdForSearch={setFocusedSpanIdForSearch} + datasourceType={datasourceType} />

Tempo Cheat Sheet

diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index b29b51fedba..4bad7bb23ec 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -471,6 +471,16 @@ function serviceMapQuery(request: DataQueryRequest, datasourceUid: s } const { nodes, edges } = mapPromMetricsToServiceMap(responses, request.range); + if (nodes.fields.length > 0 && edges.fields.length > 0) { + const nodeLength = nodes.fields[0].values.length; + const edgeLength = edges.fields[0].values.length; + + reportInteraction('grafana_traces_service_graph_size', { + datasourceType: 'tempo', + nodeLength, + edgeLength, + }); + } // No handling of multiple targets assume just one. NodeGraph does not support it anyway, but still should be // fixed at some point. diff --git a/public/app/plugins/panel/traces/TracesPanel.tsx b/public/app/plugins/panel/traces/TracesPanel.tsx index 5643354cd85..b39b8b1aee1 100644 --- a/public/app/plugins/panel/traces/TracesPanel.tsx +++ b/public/app/plugins/panel/traces/TracesPanel.tsx @@ -27,6 +27,7 @@ export const TracesPanel: React.FunctionComponent = ({ data }) => { return await getDataSourceSrv().get(data.request?.targets[0].datasource?.uid); }); const scrollElement = document.getElementsByClassName(styles.wrapper)[0]; + const datasourceType = dataSource && dataSource.value ? dataSource.value.type : 'unknown'; if (!data || !data.series.length || !traceProp) { return ( @@ -49,6 +50,7 @@ export const TracesPanel: React.FunctionComponent = ({ data }) => { setSearchBarSuffix={setSearchBarSuffix} focusedSpanIdForSearch={focusedSpanIdForSearch} setFocusedSpanIdForSearch={setFocusedSpanIdForSearch} + datasourceType={datasourceType} /> ) : null} From bb7410aa09ae1b89f87ca693a79959d8010c14f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Fern=C3=A1ndez?= Date: Tue, 17 Jan 2023 16:35:54 +0100 Subject: [PATCH 52/65] Search: Fix empty folder message showing when by starred dashboards (#61610) --- .../page/components/SearchView.test.tsx | 40 +++++++++++++++++++ .../search/page/components/SearchView.tsx | 2 +- .../search/state/SearchStateManager.ts | 4 ++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/public/app/features/search/page/components/SearchView.test.tsx b/public/app/features/search/page/components/SearchView.test.tsx index cb3f230c736..8c594887eca 100644 --- a/public/app/features/search/page/components/SearchView.test.tsx +++ b/public/app/features/search/page/components/SearchView.test.tsx @@ -105,6 +105,46 @@ describe('SearchView', () => { expect(screen.getByRole('button', { name: 'Clear search and filters' })).toBeInTheDocument(); }); + it('shows an empty state if no starred dashboard returned', async () => { + jest.spyOn(getGrafanaSearcher(), 'search').mockResolvedValue({ + ...mockSearchResult, + totalRows: 0, + view: new DataFrameView({ fields: [], length: 0 }), + }); + + setup(undefined, { starred: true }); + + await waitFor(() => expect(screen.queryByText('No results found for your query.')).toBeInTheDocument()); + expect(screen.getByRole('button', { name: 'Clear search and filters' })).toBeInTheDocument(); + }); + + it('shows empty folder cta for empty folder', async () => { + jest.spyOn(getGrafanaSearcher(), 'search').mockResolvedValue({ + ...mockSearchResult, + totalRows: 0, + view: new DataFrameView({ fields: [], length: 0 }), + }); + + setup( + { + folderDTO: { + id: 1, + uid: 'abc', + title: 'morning coffee', + url: '/morningcoffee', + version: 1, + canSave: true, + canEdit: true, + canAdmin: true, + canDelete: true, + }, + }, + undefined + ); + + await waitFor(() => expect(screen.queryByText("This folder doesn't have any dashboards yet")).toBeInTheDocument()); + }); + describe('include panels', () => { it('should be enabled when layout is list', async () => { config.featureToggles.panelTitleSearch = true; diff --git a/public/app/features/search/page/components/SearchView.tsx b/public/app/features/search/page/components/SearchView.tsx index 5a03908f1df..dbde3e3ad22 100644 --- a/public/app/features/search/page/components/SearchView.tsx +++ b/public/app/features/search/page/components/SearchView.tsx @@ -159,7 +159,7 @@ export const SearchView = ({ showManage, folderDTO, hidePseudoFolders, keyboardE ); }; - if (folderDTO && !state.loading && !state.result?.totalRows && !state.query.length) { + if (folderDTO && !state.loading && !state.result?.totalRows && !stateManager.hasSearchFilters()) { return ( { store.set(SEARCH_PANELS_LOCAL_STORAGE_KEY, includePanels); }; + hasSearchFilters() { + return this.state.query || this.state.tag.length || this.state.starred; + } + getSearchQuery() { const q: SearchQuery = { query: this.state.query, From 8620909006d189fae414d43b4c7d98397b34071a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 17 Jan 2023 17:08:23 +0100 Subject: [PATCH 53/65] Table: Fixes broken link styles after recent cell options PR (#61582) * Table: Fixes broken link styles after recent cell options PR * Share migration and fix bar gauge as well * Remove unused import * Review fixes * Fixed test Co-authored-by: Kyle Cunningham --- .betterer.results | 3 +- .../grafana-schema/src/schema/mudball.cue | 30 +++++-- .../grafana-schema/src/schema/mudball.gen.ts | 35 +++++++-- .../src/components/Table/BarGaugeCell.tsx | 26 +------ .../src/components/Table/DefaultCell.tsx | 51 ++++-------- .../grafana-ui/src/components/Table/utils.tsx | 68 ++++++++++++++++ .../dashboard/state/DashboardMigrator.ts | 56 ++----------- .../panel/table/TableCellOptionEditor.tsx | 78 ++++++------------- 8 files changed, 168 insertions(+), 179 deletions(-) diff --git a/.betterer.results b/.betterer.results index 7c76001a715..f5efc89e878 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1488,7 +1488,8 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "11"], [0, 0, 0, "Unexpected any. Specify a different type.", "12"], [0, 0, 0, "Unexpected any. Specify a different type.", "13"], - [0, 0, 0, "Unexpected any. Specify a different type.", "14"] + [0, 0, 0, "Unexpected any. Specify a different type.", "14"], + [0, 0, 0, "Do not use any type assertions.", "15"] ], "packages/grafana-ui/src/components/Tags/Tag.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] diff --git a/packages/grafana-schema/src/schema/mudball.cue b/packages/grafana-schema/src/schema/mudball.cue index ee7aaff82cd..c1098d0204e 100644 --- a/packages/grafana-schema/src/schema/mudball.cue +++ b/packages/grafana-schema/src/schema/mudball.cue @@ -206,7 +206,6 @@ FieldTextAlignment: "auto" | "left" | "right" | "center" @cuetsy(kind="type") // The color-background-solid, gradient-gauge, and lcd-gauge // modes are deprecated in favor of new cell subOptions TableCellDisplayMode: "auto" | "color-text" | "color-background" | "color-background-solid" | "gradient-gauge" | "lcd-gauge" | "json-view" | "basic" | "image" | "gauge" @cuetsy(kind="enum",memberNames="Auto|ColorText|ColorBackground|ColorBackgroundSolid|GradientGauge|LcdGauge|JSONView|BasicGauge|Image|Gauge") -// : TableCellDisplayMode @cuetsy(kind="enum") // Display mode to the "Colored Background" display // mode for table cells. Either displays a solid color (basic mode) @@ -260,26 +259,41 @@ VizLegendOptions: { // for the bar gauge component of Grafana UI BarGaugeDisplayMode: "basic" | "lcd" | "gradient" @cuetsy(kind="enum") -// Interface for table cell types that have no additional options. +// Auto mode table cell options TableAutoCellOptions: { - type: TableCellDisplayMode + type: TableCellDisplayMode & "auto" } @cuetsy(kind="interface") -// Allows for the table cell gauge display type to set the gauge mode. +// Colored text cell options +TableColorTextCellOptions: { + type: TableCellDisplayMode & "color-text" +} @cuetsy(kind="interface") + +// Json view cell options +TableJsonViewCellOptions: { + type: TableCellDisplayMode & "json-view" +} @cuetsy(kind="interface") + +// Json view cell options +TableImageCellOptions: { + type: TableCellDisplayMode & "image" +} @cuetsy(kind="interface") + +// Gauge cell options TableBarGaugeCellOptions: { type: TableCellDisplayMode & "gauge" - mode: BarGaugeDisplayMode + mode?: BarGaugeDisplayMode } @cuetsy(kind="interface") -// Allows for the background display mode to be set for the color background cell. +// Colored background cell options TableColoredBackgroundCellOptions: { type: TableCellDisplayMode & "color-background" - mode: TableCellBackgroundDisplayMode + mode?: TableCellBackgroundDisplayMode } @cuetsy(kind="interface") // Table cell options. Each cell has a display mode // and other potential options for that display. -TableCellOptions: TableAutoCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions @cuetsy(kind="type") +TableCellOptions: TableAutoCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TableJsonViewCellOptions @cuetsy(kind="type") // Field options for each field within a table (e.g 10, "The String", 64.20, etc.) // Generally defines alignment, filtering capabilties, display options, etc. diff --git a/packages/grafana-schema/src/schema/mudball.gen.ts b/packages/grafana-schema/src/schema/mudball.gen.ts index a279dae6967..3c2b02ed86b 100644 --- a/packages/grafana-schema/src/schema/mudball.gen.ts +++ b/packages/grafana-schema/src/schema/mudball.gen.ts @@ -489,25 +489,46 @@ export enum BarGaugeDisplayMode { } /** - * Interface for table cell types that have no additional options. + * Auto mode table cell options */ export interface TableAutoCellOptions { - type: TableCellDisplayMode; + type: TableCellDisplayMode.Auto; } /** - * Allows for the table cell gauge display type to set the gauge mode. + * Colored text cell options + */ +export interface TableColorTextCellOptions { + type: TableCellDisplayMode.ColorText; +} + +/** + * Json view cell options + */ +export interface TableJsonViewCellOptions { + type: TableCellDisplayMode.JSONView; +} + +/** + * Json view cell options + */ +export interface TableImageCellOptions { + type: TableCellDisplayMode.Image; +} + +/** + * Gauge cell options */ export interface TableBarGaugeCellOptions { - mode: BarGaugeDisplayMode; + mode?: BarGaugeDisplayMode; type: TableCellDisplayMode.Gauge; } /** - * Allows for the background display mode to be set for the color background cell. + * Colored background cell options */ export interface TableColoredBackgroundCellOptions { - mode: TableCellBackgroundDisplayMode; + mode?: TableCellBackgroundDisplayMode; type: TableCellDisplayMode.ColorBackground; } @@ -515,7 +536,7 @@ export interface TableColoredBackgroundCellOptions { * Table cell options. Each cell has a display mode * and other potential options for that display. */ -export type TableCellOptions = (TableAutoCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions); +export type TableCellOptions = (TableAutoCellOptions | TableBarGaugeCellOptions | TableColoredBackgroundCellOptions | TableColorTextCellOptions | TableImageCellOptions | TableJsonViewCellOptions); /** * Field options for each field within a table (e.g 10, "The String", 64.20, etc.) diff --git a/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx b/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx index cdb831d81ab..3bbb3b2761a 100644 --- a/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx +++ b/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx @@ -8,6 +8,7 @@ import { BarGauge } from '../BarGauge/BarGauge'; import { DataLinksContextMenu, DataLinksContextMenuApi } from '../DataLinks/DataLinksContextMenu'; import { TableCellProps, TableCellDisplayMode } from './types'; +import { getCellOptions } from './utils'; const defaultScale: ThresholdsConfig = { mode: ThresholdsMode.Absolute, @@ -39,28 +40,9 @@ export const BarGaugeCell: FC = (props) => { // Set default display mode let barGaugeMode: BarGaugeDisplayMode = BarGaugeDisplayMode.Gradient; - // Support deprecated settings - const usingDeprecatedSettings = field.config.custom.displayMode !== undefined; - - // If we're using the old settings format we read the displayMode directly from - // the cell options - if (usingDeprecatedSettings) { - if ( - (field.config.custom && field.config.custom.cellOptions.displayMode === TableCellDisplayMode.Gauge) || - (field.config.custom && field.config.custom.cellOptions.displayMode === BarGaugeDisplayMode.Lcd) - ) { - barGaugeMode = BarGaugeDisplayMode.Lcd; - } else if ( - (field.config.custom && field.config.custom.cellOptions.displayMode === TableCellDisplayMode.Gauge) || - (field.config.custom && field.config.custom.cellOptions.displayMode === BarGaugeDisplayMode.Basic) - ) { - barGaugeMode = BarGaugeDisplayMode.Basic; - } - } - // Otherwise in the case of sub-options we read specifically from the sub-options - // object in order to get the display mode - else { - barGaugeMode = field.config.custom.cellOptions.mode; + const cellOptions = getCellOptions(field); + if (cellOptions.type === TableCellDisplayMode.Gauge) { + barGaugeMode = cellOptions.mode ?? BarGaugeDisplayMode.Gradient; } const getLinks = () => { diff --git a/packages/grafana-ui/src/components/Table/DefaultCell.tsx b/packages/grafana-ui/src/components/Table/DefaultCell.tsx index 1e2c0bcb953..026cfb16e8a 100644 --- a/packages/grafana-ui/src/components/Table/DefaultCell.tsx +++ b/packages/grafana-ui/src/components/Table/DefaultCell.tsx @@ -2,8 +2,8 @@ import { cx } from '@emotion/css'; import React, { FC, ReactElement } from 'react'; import tinycolor from 'tinycolor2'; -import { DisplayValue, Field, formattedValueToString } from '@grafana/data'; -import { TableCellBackgroundDisplayMode } from '@grafana/schema'; +import { DisplayValue, formattedValueToString } from '@grafana/data'; +import { TableCellBackgroundDisplayMode, TableCellOptions } from '@grafana/schema'; import { getCellLinks, getTextColorForAlphaBackground } from '../../utils'; import { DataLinksContextMenu } from '../DataLinks/DataLinksContextMenu'; @@ -11,6 +11,7 @@ import { DataLinksContextMenu } from '../DataLinks/DataLinksContextMenu'; import { CellActions } from './CellActions'; import { TableStyles } from './styles'; import { TableCellDisplayMode, TableCellProps, TableFieldOptions } from './types'; +import { getCellOptions } from './utils'; export const DefaultCell: FC = (props) => { const { field, cell, tableStyles, row, cellProps } = props; @@ -27,7 +28,8 @@ export const DefaultCell: FC = (props) => { const showFilters = field.config.filterable; const showActions = (showFilters && cell.value !== undefined) || inspectEnabled; - const cellStyle = getCellStyle(tableStyles, field, displayValue, inspectEnabled); + const cellOptions = getCellOptions(field); + const cellStyle = getCellStyle(tableStyles, cellOptions, displayValue, inspectEnabled); const hasLinks = Boolean(getCellLinks(field, row)?.length); return ( @@ -38,7 +40,7 @@ export const DefaultCell: FC = (props) => { getCellLinks(field, row) || []}> {(api) => { return ( -
+
{value}
); @@ -53,49 +55,24 @@ export const DefaultCell: FC = (props) => { function getCellStyle( tableStyles: TableStyles, - field: Field, + cellOptions: TableCellOptions, displayValue: DisplayValue, disableOverflowOnHover = false ) { // How much to darken elements depends upon if we're in dark mode const darkeningFactor = tableStyles.theme.isDark ? 1 : -0.7; - // See if we're using deprecated settings - const usingDeprecatedSettings = field.config.custom?.displayMode !== undefined; - // Setup color variables let textColor: string | undefined = undefined; let bgColor: string | undefined = undefined; - // Set colors using deprecated settings format - if (usingDeprecatedSettings) { - if (field.config.custom?.displayMode === TableCellDisplayMode.ColorText) { - textColor = displayValue.color; - } else if (field.config.custom?.displayMode === TableCellDisplayMode.ColorBackground) { + if (cellOptions.type === TableCellDisplayMode.ColorText) { + textColor = displayValue.color; + } else if (cellOptions.type === TableCellDisplayMode.ColorBackground) { + if (cellOptions.mode === TableCellBackgroundDisplayMode.Basic) { textColor = getTextColorForAlphaBackground(displayValue.color!, tableStyles.theme.isDark); bgColor = tinycolor(displayValue.color).toRgbString(); - } else if ( - field.config.custom?.displayMode === TableCellDisplayMode.ColorBackground && - field.config.custom?.backgroundDisplayMode === TableCellBackgroundDisplayMode.Gradient - ) { - const bgColor2 = tinycolor(displayValue.color) - .darken(10 * darkeningFactor) - .spin(5); - textColor = getTextColorForAlphaBackground(displayValue.color!, tableStyles.theme.isDark); - bgColor = `linear-gradient(120deg, ${bgColor2.toRgbString()}, ${displayValue.color})`; - } - } - // Set colors using updated sub-options format - else { - const cellDisplayMode = field.config.custom?.cellOptions?.mode; - const cellDisplayType = field.config.custom?.cellOptions?.type; - - if (cellDisplayType === TableCellDisplayMode.ColorText) { - textColor = displayValue.color; - } else if (cellDisplayMode === TableCellBackgroundDisplayMode.Basic) { - textColor = getTextColorForAlphaBackground(displayValue.color!, tableStyles.theme.isDark); - bgColor = tinycolor(displayValue.color).toRgbString(); - } else if (cellDisplayMode === TableCellBackgroundDisplayMode.Gradient) { + } else if (cellOptions.mode === TableCellBackgroundDisplayMode.Gradient) { const bgColor2 = tinycolor(displayValue.color) .darken(10 * darkeningFactor) .spin(5); @@ -113,8 +90,8 @@ function getCellStyle( return disableOverflowOnHover ? tableStyles.cellContainerNoOverflow : tableStyles.cellContainer; } -function getLinkStyle(tableStyles: TableStyles, field: Field, targetClassName: string | undefined) { - if (field.config.custom?.displayMode === TableCellDisplayMode.Auto) { +function getLinkStyle(tableStyles: TableStyles, cellOptions: TableCellOptions, targetClassName: string | undefined) { + if (cellOptions.type === TableCellDisplayMode.Auto) { return cx(tableStyles.cellLink, targetClassName); } diff --git a/packages/grafana-ui/src/components/Table/utils.tsx b/packages/grafana-ui/src/components/Table/utils.tsx index edaa526e0b1..77b68af4b1b 100644 --- a/packages/grafana-ui/src/components/Table/utils.tsx +++ b/packages/grafana-ui/src/components/Table/utils.tsx @@ -16,6 +16,12 @@ import { GrafanaTheme2, ArrayVector, } from '@grafana/data'; +import { + BarGaugeDisplayMode, + TableAutoCellOptions, + TableCellBackgroundDisplayMode, + TableCellOptions, +} from '@grafana/schema'; import { BarGaugeCell } from './BarGaugeCell'; import { DefaultCell } from './DefaultCell'; @@ -344,3 +350,65 @@ export function createFooterCalculationValues(rows: Row[]): any[number] { return values; } + +const defaultCellOptions: TableAutoCellOptions = { type: TableCellDisplayMode.Auto }; + +export function getCellOptions(field: Field): TableCellOptions { + if (field.config.custom?.displayMode) { + return migrateTableDisplayModeToCellOptions(field.config.custom?.displayMode); + } + + if (!field.config.custom?.cellOptions) { + return defaultCellOptions; + } + + return (field.config.custom as TableFieldOptions).cellOptions; +} + +/** + * Migrates table cell display mode to new object format. + * + * @param displayMode The display mode of the cell + * @returns TableCellOptions object in the correct format + * relative to the old display mode. + */ +export function migrateTableDisplayModeToCellOptions(displayMode: TableCellDisplayMode): TableCellOptions { + switch (displayMode) { + // In the case of the gauge we move to a different option + case 'basic': + case 'gradient-gauge': + case 'lcd-gauge': + let gaugeMode = BarGaugeDisplayMode.Basic; + + if (displayMode === 'gradient-gauge') { + gaugeMode = BarGaugeDisplayMode.Gradient; + } else if (displayMode === 'lcd-gauge') { + gaugeMode = BarGaugeDisplayMode.Lcd; + } + + return { + type: TableCellDisplayMode.Gauge, + mode: gaugeMode, + }; + // Also true in the case of the color background + case 'color-background': + case 'color-background-solid': + let mode = TableCellBackgroundDisplayMode.Basic; + + // Set the new mode field, somewhat confusingly the + // color-background mode is for gradient display + if (displayMode === 'color-background') { + mode = TableCellBackgroundDisplayMode.Gradient; + } + + return { + type: TableCellDisplayMode.ColorBackground, + mode: mode, + }; + default: + return { + // @ts-ignore + type: displayMode, + }; + } +} diff --git a/public/app/features/dashboard/state/DashboardMigrator.ts b/public/app/features/dashboard/state/DashboardMigrator.ts index fb6073d83e7..51deeecd53c 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.ts @@ -26,8 +26,8 @@ import { import { labelsToFieldsTransformer } from '@grafana/data/src/transformations/transformers/labelsToFields'; import { mergeTransformer } from '@grafana/data/src/transformations/transformers/merge'; import { getDataSourceSrv, setDataSourceSrv } from '@grafana/runtime'; -import { BarGaugeDisplayMode, TableCellBackgroundDisplayMode, TableCellOptions } from '@grafana/schema'; -import { AxisPlacement, GraphFieldConfig, TableCellDisplayMode } from '@grafana/ui'; +import { AxisPlacement, GraphFieldConfig } from '@grafana/ui'; +import { migrateTableDisplayModeToCellOptions } from '@grafana/ui/src/components/Table/utils'; import { getAllOptionEditors, getAllStandardFieldConfigs } from 'app/core/components/OptionsUI/registry'; import { config } from 'app/core/config'; import { @@ -818,7 +818,7 @@ export class DashboardMigrator { // Update field configuration if (displayMode !== undefined) { // Migrate any options for the panel - panel.fieldConfig.defaults.custom.cellOptions = migrateTableCellConfig(displayMode); + panel.fieldConfig.defaults.custom.cellOptions = migrateTableDisplayModeToCellOptions(displayMode); // Delete the legacy field delete panel.fieldConfig.defaults.custom.displayMode; @@ -831,7 +831,8 @@ export class DashboardMigrator { if (panel.fieldConfig.overrides[i].properties[j].id === 'custom.displayMode') { panel.fieldConfig.overrides[i].properties[j].id = 'custom.cellOptions'; - panel.fieldConfig.overrides[i].properties[j].value = migrateTableCellConfig(overrideDisplayMode); + panel.fieldConfig.overrides[i].properties[j].value = + migrateTableDisplayModeToCellOptions(overrideDisplayMode); } } } @@ -1364,50 +1365,3 @@ function ensureXAxisVisibility(panel: PanelModel) { return panel; } - -/** - * Migrates table cell display mode to new object format. - * - * @param displayMode The display mode of the cell - * @returns TableCellOptions object in the correct format - * relative to the old display mode. - */ -function migrateTableCellConfig(displayMode: TableCellDisplayMode): TableCellOptions { - switch (displayMode) { - // In the case of the gauge we move to a different option - case 'basic': - case 'gradient-gauge': - case 'lcd-gauge': - let gaugeMode = BarGaugeDisplayMode.Basic; - - if (displayMode === 'gradient-gauge') { - gaugeMode = BarGaugeDisplayMode.Gradient; - } else if (displayMode === 'lcd-gauge') { - gaugeMode = BarGaugeDisplayMode.Lcd; - } - - return { - type: TableCellDisplayMode.Gauge, - mode: gaugeMode, - }; - // Also true in the case of the color background - case 'color-background': - case 'color-background-solid': - let mode = TableCellBackgroundDisplayMode.Basic; - - // Set the new mode field, somewhat confusingly the - // color-background mode is for gradient display - if (displayMode === 'color-background') { - mode = TableCellBackgroundDisplayMode.Gradient; - } - - return { - type: TableCellDisplayMode.ColorBackground, - mode: mode, - }; - default: - return { - type: displayMode, - }; - } -} diff --git a/public/app/plugins/panel/table/TableCellOptionEditor.tsx b/public/app/plugins/panel/table/TableCellOptionEditor.tsx index 02b21973316..c1bf6854791 100644 --- a/public/app/plugins/panel/table/TableCellOptionEditor.tsx +++ b/public/app/plugins/panel/table/TableCellOptionEditor.tsx @@ -1,22 +1,13 @@ import { merge } from 'lodash'; -import React, { ReactNode, useState } from 'react'; +import React, { useState } from 'react'; import { SelectableValue } from '@grafana/data'; import { TableCellOptions } from '@grafana/schema'; -import { Field, HorizontalGroup, Select, TableCellDisplayMode } from '@grafana/ui'; +import { Field, Select, TableCellDisplayMode } from '@grafana/ui'; import { BarGaugeCellOptionsEditor } from './cells/BarGaugeCellOptionsEditor'; import { ColorBackgroundCellOptionsEditor } from './cells/ColorBackgroundCellOptionsEditor'; -const cellDisplayModeOptions = [ - { value: TableCellDisplayMode.Auto, label: 'Auto' }, - { value: TableCellDisplayMode.ColorText, label: 'Colored text' }, - { value: TableCellDisplayMode.ColorBackground, label: 'Colored background' }, - { value: TableCellDisplayMode.Gauge, label: 'Gauge' }, - { value: TableCellDisplayMode.JSONView, label: 'JSON View' }, - { value: TableCellDisplayMode.Image, label: 'Image' }, -]; - // The props that any cell type editor are expected // to handle. In this case the generic type should // be a discriminated interface of TableCellOptions @@ -25,29 +16,6 @@ export interface TableCellEditorProps { onChange: (value: T) => void; } -// Maps display modes to editor components -interface ComponentMap { - [key: string]: Function; -} - -// Maps cell type to options for caching -interface SettingMap { - [key: string]: TableCellOptions; -} - -/* - Map of display modes to editor components - Additional cell types can be placed here - --- - A cell editor is expected to be a functional - component that accepts options and displays - them in a form. -*/ -const displayModeComponentMap: ComponentMap = { - [TableCellDisplayMode.Gauge]: BarGaugeCellOptionsEditor, - [TableCellDisplayMode.ColorBackground]: ColorBackgroundCellOptionsEditor, -}; - interface Props { value: TableCellOptions; onChange: (v: TableCellOptions) => void; @@ -55,25 +23,21 @@ interface Props { export const TableCellOptionEditor = ({ value, onChange }: Props) => { const cellType = value.type; - let editor: ReactNode | null = null; - let [settingCache, setSettingCache] = useState({}); + const currentMode = cellDisplayModeOptions.find((o) => o.value!.type === cellType)!; + + let [settingCache, setSettingCache] = useState>({}); // Update display mode on change - const onCellTypeChange = (v: SelectableValue) => { + const onCellTypeChange = (v: SelectableValue) => { if (v.value !== undefined) { // Set the new type of cell starting // with default settings - value = { - type: v.value, - }; + value = v.value; // When changing cell type see if there were previously stored // settings and merge those with the changed value - if (settingCache[v.value] !== undefined && Object.keys(settingCache[v.value]).length > 1) { - settingCache[v.value] = merge(value, settingCache[v.value]); - setSettingCache(settingCache); - onChange(settingCache[v.value]); - return; + if (settingCache[value.type] !== undefined && Object.keys(settingCache[value.type]).length > 1) { + value = merge(value, settingCache[value.type]); } onChange(value); @@ -88,19 +52,27 @@ export const TableCellOptionEditor = ({ value, onChange }: Props) => { onChange(settingCache[value.type]); }; - // Setup specific cell editor - if (cellType !== undefined && displayModeComponentMap[cellType] !== undefined) { - let Comp: Function = displayModeComponentMap[cellType]; - editor = ; - } - // Setup and inject editor return ( <> - - {editor} + {cellType === TableCellDisplayMode.Gauge && ( + + )} + {cellType === TableCellDisplayMode.ColorBackground && ( + + )} ); }; + +const cellDisplayModeOptions: Array> = [ + { value: { type: TableCellDisplayMode.Auto }, label: 'Auto' }, + { value: { type: TableCellDisplayMode.ColorText }, label: 'Colored text' }, + { value: { type: TableCellDisplayMode.ColorBackground }, label: 'Colored background' }, + { value: { type: TableCellDisplayMode.Gauge }, label: 'Gauge' }, + { value: { type: TableCellDisplayMode.JSONView }, label: 'JSON View' }, + { value: { type: TableCellDisplayMode.Image }, label: 'Image' }, +]; From daf171820c0c32102602492fbd434fc506d6fc82 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Tue, 17 Jan 2023 16:34:40 +0000 Subject: [PATCH 54/65] CommandPalette: Improve section header styling (#61584) * Add border between command palette sections * make breadcrumbs secondary text color * Improve comments --- .../commandPalette/CommandPalette.tsx | 52 ++++++++++++++----- .../features/commandPalette/ResultItem.tsx | 3 +- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/public/app/features/commandPalette/CommandPalette.tsx b/public/app/features/commandPalette/CommandPalette.tsx index b70709e443e..7ee00813631 100644 --- a/public/app/features/commandPalette/CommandPalette.tsx +++ b/public/app/features/commandPalette/CommandPalette.tsx @@ -1,4 +1,4 @@ -import { css } from '@emotion/css'; +import { css, cx } from '@emotion/css'; import { useDialog } from '@react-aria/dialog'; import { FocusScope } from '@react-aria/focus'; import { useOverlay } from '@react-aria/overlays'; @@ -83,24 +83,33 @@ const RenderResults = ({ dashboardResults }: RenderResultsProps) => { () => dashboardResults.map((dashboard) => new ActionImpl(dashboard, { store: {} })), [dashboardResults] ); + const items = useMemo( () => (dashboardResultItems.length > 0 ? [...results, dashboardsSectionTitle, ...dashboardResultItems] : results), [results, dashboardsSectionTitle, dashboardResultItems] ); 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 renderedItem = typeof item === 'string' ? ( -
{item}
+
+
{item}
+
) : ( - ) - } - /> -
+ ); + + return isLastItem ?
{renderedItem}
: renderedItem; + }} + /> ); }; @@ -137,15 +146,30 @@ const getSearchStyles = (theme: GrafanaTheme2) => ({ border: 'none', background: theme.colors.background.canvas, color: theme.colors.text.primary, - borderBottom: `1px solid ${theme.colors.border.weak}`, + borderBottom: `1px solid ${theme.colors.border.medium}`, }), + + // Virtual list measures margin incorrectly, so we need to split padding before/after border + // over and inner and outer element sectionHeader: css({ - padding: theme.spacing(1, 2), + paddingTop: theme.spacing(2), fontSize: theme.typography.h6.fontSize, fontWeight: theme.typography.body.fontWeight, color: theme.colors.text.secondary, }), - resultsContainer: css({ - padding: theme.spacing(2, 0), + 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({ + paddingBottom: theme.spacing(1), }), }); diff --git a/public/app/features/commandPalette/ResultItem.tsx b/public/app/features/commandPalette/ResultItem.tsx index d82be2c2c89..fc4858fa00e 100644 --- a/public/app/features/commandPalette/ResultItem.tsx +++ b/public/app/features/commandPalette/ResultItem.tsx @@ -78,6 +78,7 @@ const getResultItemStyles = (theme: GrafanaTheme2, isActive: boolean) => { alightItems: 'center', justifyContent: 'space-between', cursor: 'pointer', + position: 'relative', '&:before': { display: isActive ? 'block' : 'none', content: '" "', @@ -107,8 +108,8 @@ const getResultItemStyles = (theme: GrafanaTheme2, isActive: boolean) => { fontSize: theme.typography.fontSize, }), breadcrumbAncestor: css({ - opacity: 0.5, marginRight: theme.spacing(1), + color: theme.colors.text.secondary, }), subtitleText: css({ fontSize: theme.typography.fontSize - 2, From 4f1bdc0607be5d0a37ce54e6041398a8a6f69056 Mon Sep 17 00:00:00 2001 From: Alexander Weaver Date: Tue, 17 Jan 2023 10:39:16 -0600 Subject: [PATCH 55/65] Alerting: Skip flaky test in TestIntegrationUpdateAlertRules (#61627) * Skip flaky test * Add comment --- pkg/services/ngalert/store/alert_rule_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/services/ngalert/store/alert_rule_test.go b/pkg/services/ngalert/store/alert_rule_test.go index 1471109ba4a..0b0cc72ade2 100644 --- a/pkg/services/ngalert/store/alert_rule_test.go +++ b/pkg/services/ngalert/store/alert_rule_test.go @@ -52,6 +52,7 @@ func TestIntegrationUpdateAlertRules(t *testing.T) { }) t.Run("should fail due to optimistic locking if version does not match", func(t *testing.T) { + t.Skip() // This test intermittently fails. rule := createRule(t, store) rule.Version-- // simulate version discrepancy From 354342ab26a97197a5425bfcdd22ac6838c47cb4 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 17 Jan 2023 17:50:14 +0100 Subject: [PATCH 56/65] Support Bundles: Improve creating bundle UX (#61611) * Support Bundles: Improve creating bundle UX * Refactor create bundle page * Fix typo * Don't show loading indicaror after deleting bundle --- public/app/core/reducers/root.ts | 2 + .../support-bundles/SupportBundles.tsx | 82 ++++++++++------ .../support-bundles/SupportBundlesCreate.tsx | 97 +++++++++---------- .../features/support-bundles/state/actions.ts | 72 ++++++++++++++ .../support-bundles/state/reducers.ts | 60 ++++++++++++ public/app/routes/routes.tsx | 3 +- public/app/types/index.ts | 1 + public/app/types/supportBundles.ts | 30 ++++++ 8 files changed, 263 insertions(+), 84 deletions(-) create mode 100644 public/app/features/support-bundles/state/actions.ts create mode 100644 public/app/features/support-bundles/state/reducers.ts create mode 100644 public/app/types/supportBundles.ts diff --git a/public/app/core/reducers/root.ts b/public/app/core/reducers/root.ts index b4203991570..07e871a703e 100644 --- a/public/app/core/reducers/root.ts +++ b/public/app/core/reducers/root.ts @@ -17,6 +17,7 @@ import panelsReducers from 'app/features/panel/state/reducers'; import { reducer as pluginsReducer } from 'app/features/plugins/admin/state/reducer'; import userReducers from 'app/features/profile/state/reducers'; import serviceAccountsReducer from 'app/features/serviceaccounts/state/reducers'; +import supportBundlesReducer from 'app/features/support-bundles/state/reducers'; import teamsReducers from 'app/features/teams/state/reducers'; import usersReducers from 'app/features/users/state/reducers'; import templatingReducers from 'app/features/variables/state/keyedVariablesReducer'; @@ -43,6 +44,7 @@ const rootReducers = { ...panelEditorReducers, ...panelsReducers, ...templatingReducers, + ...supportBundlesReducer, plugins: pluginsReducer, [alertingApi.reducerPath]: alertingApi.reducer, [publicDashboardApi.reducerPath]: publicDashboardApi.reducer, diff --git a/public/app/features/support-bundles/SupportBundles.tsx b/public/app/features/support-bundles/SupportBundles.tsx index 0e75a1c27a6..a2a6fa98f72 100644 --- a/public/app/features/support-bundles/SupportBundles.tsx +++ b/public/app/features/support-bundles/SupportBundles.tsx @@ -1,10 +1,13 @@ import React, { useEffect } from 'react'; -import { useAsyncFn } from 'react-use'; +import { connect, ConnectedProps } from 'react-redux'; import { dateTimeFormat } from '@grafana/data'; -import { config, getBackendSrv } from '@grafana/runtime'; -import { LinkButton } from '@grafana/ui'; +import { config } from '@grafana/runtime'; +import { LinkButton, Spinner, IconButton } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; +import { StoreState } from 'app/types'; + +import { loadBundles, removeBundle, checkBundles } from './state/actions'; const subTitle = ( @@ -13,39 +16,48 @@ const subTitle = ( ); -const newButton = ( +const NewBundleButton = ( New support bundle ); -type SupportBundleState = 'complete' | 'error' | 'timeout' | 'pending'; - -interface SupportBundle { - uid: string; - state: SupportBundleState; - creator: string; - createdAt: number; - expiresAt: number; -} - -const getBundles = () => { - return getBackendSrv().get('/api/support-bundles'); +const mapStateToProps = (state: StoreState) => { + return { + supportBundles: state.supportBundles.supportBundles, + isLoading: state.supportBundles.isLoading, + }; }; -function SupportBundles() { - const [bundlesState, fetchBundles] = useAsyncFn(getBundles, []); +const mapDispatchToProps = { + loadBundles, + removeBundle, + checkBundles, +}; + +const connector = connect(mapStateToProps, mapDispatchToProps); + +type Props = ConnectedProps; + +const SupportBundlesUnconnected = ({ supportBundles, isLoading, loadBundles, removeBundle, checkBundles }: Props) => { + const isPending = supportBundles.some((b) => b.state === 'pending'); useEffect(() => { - fetchBundles(); - }, [fetchBundles]); + loadBundles(); + }, [loadBundles]); - const actions = config.featureToggles.topnav ? newButton : undefined; + useEffect(() => { + if (isPending) { + checkBundles(); + } + }); + + const actions = config.featureToggles.topnav ? NewBundleButton : undefined; return ( - - {!config.featureToggles.topnav && newButton} + + {!config.featureToggles.topnav && NewBundleButton}
@@ -53,25 +65,31 @@ function SupportBundles() { + - {bundlesState?.value?.map((b) => ( - - - - + {supportBundles?.map((bundle) => ( + + + + + + ))} @@ -79,6 +97,6 @@ function SupportBundles() { ); -} +}; -export default SupportBundles; +export default connector(SupportBundlesUnconnected); diff --git a/public/app/features/support-bundles/SupportBundlesCreate.tsx b/public/app/features/support-bundles/SupportBundlesCreate.tsx index 3ecbc3212f6..d8070c1c096 100644 --- a/public/app/features/support-bundles/SupportBundlesCreate.tsx +++ b/public/app/features/support-bundles/SupportBundlesCreate.tsx @@ -1,29 +1,11 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { useAsyncFn } from 'react-use'; +import React, { useEffect } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; -import { getBackendSrv, locationService } from '@grafana/runtime'; -import { Form, Button, Field, Checkbox } from '@grafana/ui'; +import { Form, Button, Field, Checkbox, LinkButton, HorizontalGroup, Alert } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; +import { StoreState } from 'app/types'; -// move to types -export interface SupportBundleCreateRequest { - collectors: string[]; -} - -export interface SupportBundleCollector { - uid: string; - displayName: string; - description: string; - includedByDefault: boolean; - default: boolean; -} - -export interface Props {} - -const createSupportBundle = async (data: SupportBundleCreateRequest) => { - const result = await getBackendSrv().post('/api/support-bundles', data); - return result; -}; +import { loadSupportBundleCollectors, createSupportBundle } from './state/actions'; const subTitle = ( @@ -31,50 +13,60 @@ const subTitle = ( ); -export const SupportBundlesCreate = ({}: Props): JSX.Element => { - const onSubmit = useCallback(async (data) => { - try { - const selectedLabelsArray = Object.keys(data).filter((key) => data[key]); - const response = await createSupportBundle({ collectors: selectedLabelsArray }); - console.info(response); - } catch (e) { - console.error(e); - } +const mapStateToProps = (state: StoreState) => { + return { + collectors: state.supportBundles.supportBundleCollectors, + isLoading: state.supportBundles.createBundlePageLoading, + loadCollectorsError: state.supportBundles.loadBundlesError, + createBundleError: state.supportBundles.createBundleError, + }; +}; - locationService.push('/admin/support-bundles'); - }, []); +const mapDispatchToProps = { + loadSupportBundleCollectors, + createSupportBundle, +}; - const [components, setComponents] = useState([]); - // populate components from the backend - const populateComponents = async () => { - return await getBackendSrv().get('/api/support-bundles/collectors'); +const connector = connect(mapStateToProps, mapDispatchToProps); + +type Props = ConnectedProps; + +export const SupportBundlesCreateUnconnected = ({ + collectors, + isLoading, + loadCollectorsError, + createBundleError, + loadSupportBundleCollectors, + createSupportBundle, +}: Props): JSX.Element => { + const onSubmit = (data: Record) => { + const selectedLabelsArray = Object.keys(data).filter((key) => data[key]); + createSupportBundle({ collectors: selectedLabelsArray }); }; - const [state, fetchComponents] = useAsyncFn(populateComponents); useEffect(() => { - fetchComponents().then((res) => { - setComponents(res); - }); - }, [fetchComponents]); + loadSupportBundleCollectors(); + }, [loadSupportBundleCollectors]); // turn components into a uuid -> enabled map - const values: Record = components.reduce((acc, curr) => { + const values: Record = collectors.reduce((acc, curr) => { return { ...acc, [curr.uid]: curr.default }; }, {}); return ( - +

Create support bundle

- {state.error &&

{state.error}

} - {!!components.length && ( + {loadCollectorsError && } + {createBundleError && } + {!!collectors.length && (
{({ register, errors }) => { return ( <> - {components.map((component) => { + {collectors.map((component) => { return ( { ); })} - + + + + Cancel + + ); }} @@ -99,4 +96,4 @@ export const SupportBundlesCreate = ({}: Props): JSX.Element => { ); }; -export default SupportBundlesCreate; +export default connector(SupportBundlesCreateUnconnected); diff --git a/public/app/features/support-bundles/state/actions.ts b/public/app/features/support-bundles/state/actions.ts new file mode 100644 index 00000000000..6deb633c207 --- /dev/null +++ b/public/app/features/support-bundles/state/actions.ts @@ -0,0 +1,72 @@ +import { throttle } from 'lodash'; + +import { getBackendSrv, locationService } from '@grafana/runtime'; +import { SupportBundle, SupportBundleCollector, SupportBundleCreateRequest, ThunkResult } from 'app/types'; + +import { + collectorsFetchBegin, + collectorsFetchEnd, + fetchBegin, + fetchEnd, + setCreateBundleError, + setLoadBundleError, + supportBundleCollectorsLoaded, + supportBundlesLoaded, +} from './reducers'; + +export function loadBundles(skipPageRefresh = false): ThunkResult { + return async (dispatch) => { + try { + if (!skipPageRefresh) { + dispatch(fetchBegin()); + } + const result = await getBackendSrv().get('/api/support-bundles'); + dispatch(supportBundlesLoaded(result)); + } finally { + dispatch(fetchEnd()); + } + }; +} + +const checkBundlesStatusThrottled = throttle(async (dispatch) => { + const result = await getBackendSrv().get('/api/support-bundles'); + dispatch(supportBundlesLoaded(result)); +}, 1000); + +export function checkBundles(): ThunkResult { + return async (dispatch) => { + dispatch(checkBundlesStatusThrottled); + }; +} + +export function removeBundle(uid: string): ThunkResult { + return async (dispatch) => { + await getBackendSrv().delete(`/api/support-bundles/${uid}`); + dispatch(loadBundles(true)); + }; +} + +export function loadSupportBundleCollectors(): ThunkResult { + return async (dispatch) => { + try { + dispatch(collectorsFetchBegin()); + const result = await getBackendSrv().get('/api/support-bundles/collectors'); + dispatch(supportBundleCollectorsLoaded(result)); + } catch (err) { + dispatch(setLoadBundleError('Error loading support bundles data collectors')); + } finally { + dispatch(collectorsFetchEnd()); + } + }; +} + +export function createSupportBundle(data: SupportBundleCreateRequest): ThunkResult { + return async (dispatch) => { + try { + await getBackendSrv().post('/api/support-bundles', data); + locationService.push('/admin/support-bundles'); + } catch (err) { + dispatch(setCreateBundleError('Error creating support bundle')); + } + }; +} diff --git a/public/app/features/support-bundles/state/reducers.ts b/public/app/features/support-bundles/state/reducers.ts new file mode 100644 index 00000000000..21e32402ec0 --- /dev/null +++ b/public/app/features/support-bundles/state/reducers.ts @@ -0,0 +1,60 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +import { SupportBundle, SupportBundleCollector, SupportBundlesState } from 'app/types'; + +export const initialState: SupportBundlesState = { + supportBundles: [], + isLoading: false, + supportBundleCollectors: [], + createBundlePageLoading: false, + loadBundlesError: '', + createBundleError: '', +}; + +const supportBundlesSlice = createSlice({ + name: 'supportBundles', + initialState, + reducers: { + supportBundlesLoaded: (state, action: PayloadAction): SupportBundlesState => { + return { ...state, supportBundles: action.payload, isLoading: false }; + }, + fetchBegin: (state): SupportBundlesState => { + return { ...state, isLoading: true }; + }, + fetchEnd: (state): SupportBundlesState => { + return { ...state, isLoading: false }; + }, + collectorsFetchBegin: (state): SupportBundlesState => { + return { ...state, createBundlePageLoading: true }; + }, + collectorsFetchEnd: (state): SupportBundlesState => { + return { ...state, createBundlePageLoading: false }; + }, + supportBundleCollectorsLoaded: (state, action: PayloadAction): SupportBundlesState => { + return { ...state, supportBundleCollectors: action.payload, createBundlePageLoading: false }; + }, + setLoadBundleError: (state, action: PayloadAction): SupportBundlesState => { + return { ...state, loadBundlesError: action.payload, supportBundleCollectors: [] }; + }, + setCreateBundleError: (state, action: PayloadAction): SupportBundlesState => { + return { ...state, createBundleError: action.payload }; + }, + }, +}); + +export const { + supportBundlesLoaded, + fetchBegin, + fetchEnd, + supportBundleCollectorsLoaded, + collectorsFetchBegin, + collectorsFetchEnd, + setLoadBundleError, + setCreateBundleError, +} = supportBundlesSlice.actions; + +export const supportBundlesReducer = supportBundlesSlice.reducer; + +export default { + supportBundles: supportBundlesReducer, +}; diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index f2895ca0785..7db846c12bb 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -575,8 +575,7 @@ export function getSupportBundleRoutes(cfg = config): RouteDescriptor[] { { path: '/admin/support-bundles/create', component: SafeDynamicImport( - () => - import(/* webpackChunkName: "ServiceAccountCreatePage" */ 'app/features/support-bundles/SupportBundlesCreate') + () => import(/* webpackChunkName: "SupportBundlesCreate" */ 'app/features/support-bundles/SupportBundlesCreate') ), }, ]; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 3ffade53998..ec74cb8539e 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -18,6 +18,7 @@ export * from './appEvent'; export * from './query'; export * from './preferences'; export * from './accessControl'; +export * from './supportBundles'; import * as CoreEvents from './events'; export { CoreEvents }; diff --git a/public/app/types/supportBundles.ts b/public/app/types/supportBundles.ts new file mode 100644 index 00000000000..5e895cd17aa --- /dev/null +++ b/public/app/types/supportBundles.ts @@ -0,0 +1,30 @@ +type SupportBundleState = 'complete' | 'error' | 'timeout' | 'pending'; + +export interface SupportBundle { + uid: string; + state: SupportBundleState; + creator: string; + createdAt: number; + expiresAt: number; +} + +export interface SupportBundlesState { + supportBundles: SupportBundle[]; + isLoading: boolean; + createBundlePageLoading: boolean; + supportBundleCollectors: SupportBundleCollector[]; + loadBundlesError: string; + createBundleError: string; +} + +export interface SupportBundleCollector { + uid: string; + displayName: string; + description: string; + includedByDefault: boolean; + default: boolean; +} + +export interface SupportBundleCreateRequest { + collectors: string[]; +} From 70f2b01525db4da06f14cf8eab3a497ff705037e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 17 Jan 2023 18:02:46 +0100 Subject: [PATCH 57/65] Scenes: Cleanups and simplify (#61579) * Remove use of Scene / Embedded scene * Use DashboardScene * Update scenes package * Updated scenes * Updated DashboardScene * Updates * Updates --- .../app/features/scenes/SceneEmbeddedPage.tsx | 5 +- public/app/features/scenes/SceneListPage.tsx | 12 +--- public/app/features/scenes/ScenePage.tsx | 16 ++++- .../features/scenes/components/Scene.test.tsx | 16 ----- .../app/features/scenes/components/Scene.tsx | 59 ------------------- .../scenes/dashboard/DashboardScene.tsx | 40 ++++++++++--- .../scenes/dashboard/DashboardsLoader.test.ts | 24 ++++---- .../scenes/dashboard/DashboardsLoader.ts | 10 ++-- public/app/features/scenes/scenes/demo.tsx | 19 +++--- public/app/features/scenes/scenes/grid.tsx | 19 ++---- .../scenes/scenes/gridMultiTimeRange.tsx | 19 ++---- .../features/scenes/scenes/gridMultiple.tsx | 19 ++---- .../scenes/scenes/gridWithMultipleData.tsx | 19 ++---- .../features/scenes/scenes/gridWithRow.tsx | 19 ++---- .../features/scenes/scenes/gridWithRows.tsx | 9 ++- public/app/features/scenes/scenes/index.tsx | 18 +++--- public/app/features/scenes/scenes/nested.tsx | 19 ++---- .../scenes/scenes/queryVariableDemo.tsx | 16 ++--- .../features/scenes/scenes/sceneWithRows.tsx | 19 ++---- .../scenes/scenes/transformations.tsx | 21 ++----- .../features/scenes/scenes/variablesDemo.tsx | 26 +++----- yarn.lock | 6 +- 22 files changed, 141 insertions(+), 289 deletions(-) delete mode 100644 public/app/features/scenes/components/Scene.test.tsx delete mode 100644 public/app/features/scenes/components/Scene.tsx diff --git a/public/app/features/scenes/SceneEmbeddedPage.tsx b/public/app/features/scenes/SceneEmbeddedPage.tsx index 5728ffa7c0c..7b7b2c06500 100644 --- a/public/app/features/scenes/SceneEmbeddedPage.tsx +++ b/public/app/features/scenes/SceneEmbeddedPage.tsx @@ -10,15 +10,16 @@ import { getSceneByTitle } from './scenes'; export interface Props extends GrafanaRouteComponentProps<{ name: string }> {} export const SceneEmbeddedPage = (props: Props) => { - const scene = getSceneByTitle(props.match.params.name, false); + const scene = getSceneByTitle(props.match.params.name); if (!scene) { return

Scene not found

; } const pageNav: NavModelItem = { - text: scene.state.title, + text: 'Embedded Scene', }; + return ( diff --git a/public/app/features/scenes/SceneListPage.tsx b/public/app/features/scenes/SceneListPage.tsx index 6518394f78d..33078f91313 100644 --- a/public/app/features/scenes/SceneListPage.tsx +++ b/public/app/features/scenes/SceneListPage.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { useAsync } from 'react-use'; import { Stack } from '@grafana/experimental'; -import { Card, LinkButton } from '@grafana/ui'; +import { Card } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; // Types @@ -26,16 +26,8 @@ export const SceneListPage = ({}: Props) => {
Test scenes
{scenes.map((scene) => ( - + {scene.title} - - - Open as standalone scene - - - Open as embedded scene - - ))} diff --git a/public/app/features/scenes/ScenePage.tsx b/public/app/features/scenes/ScenePage.tsx index 92587896aee..b755d11491b 100644 --- a/public/app/features/scenes/ScenePage.tsx +++ b/public/app/features/scenes/ScenePage.tsx @@ -1,5 +1,5 @@ // Libraries -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; @@ -8,12 +8,24 @@ import { getSceneByTitle } from './scenes'; export interface Props extends GrafanaRouteComponentProps<{ name: string }> {} export const ScenePage = (props: Props) => { - const scene = getSceneByTitle(props.match.params.name, true); + const scene = getSceneByTitle(props.match.params.name); + const [isInitialized, setInitialized] = useState(false); + + useEffect(() => { + if (scene && !isInitialized) { + scene.initUrlSync(); + setInitialized(true); + } + }, [isInitialized, scene]); if (!scene) { return

Scene not found

; } + if (!isInitialized) { + return null; + } + return ; }; diff --git a/public/app/features/scenes/components/Scene.test.tsx b/public/app/features/scenes/components/Scene.test.tsx deleted file mode 100644 index b6138fca279..00000000000 --- a/public/app/features/scenes/components/Scene.test.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { SceneFlexLayout } from '@grafana/scenes'; - -import { Scene } from './Scene'; - -describe('Scene', () => { - it('Simple scene', () => { - const scene = new Scene({ - title: 'Hello', - body: new SceneFlexLayout({ - children: [], - }), - }); - - expect(scene.state.title).toBe('Hello'); - }); -}); diff --git a/public/app/features/scenes/components/Scene.tsx b/public/app/features/scenes/components/Scene.tsx deleted file mode 100644 index 18857af0438..00000000000 --- a/public/app/features/scenes/components/Scene.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; - -import { PageLayoutType } from '@grafana/data'; -import { config } from '@grafana/runtime'; -import { SceneObjectBase, SceneComponentProps, SceneState, UrlSyncManager } from '@grafana/scenes'; -import { PageToolbar, ToolbarButton } from '@grafana/ui'; -import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; -import { Page } from 'app/core/components/Page/Page'; - -export class Scene extends SceneObjectBase { - public static Component = SceneRenderer; - private urlSyncManager?: UrlSyncManager; - - public activate() { - super.activate(); - this.urlSyncManager = new UrlSyncManager(this); - this.urlSyncManager.initSync(); - } - - public deactivate() { - super.deactivate(); - this.urlSyncManager!.cleanUp(); - } -} - -function SceneRenderer({ model }: SceneComponentProps) { - const { title, body, actions = [], isEditing, $editor, subMenu } = model.useState(); - - const toolbarActions = (actions ?? []).map((action) => ); - - if ($editor) { - toolbarActions.push( - model.setState({ isEditing: !model.state.isEditing })} - /> - ); - } - - const pageToolbar = config.featureToggles.topnav ? ( - - ) : ( - {toolbarActions} - ); - - return ( - -
- {subMenu && } -
- - {$editor && <$editor.Component model={$editor} isEditing={isEditing} />} -
-
-
- ); -} diff --git a/public/app/features/scenes/dashboard/DashboardScene.tsx b/public/app/features/scenes/dashboard/DashboardScene.tsx index dc88bf95a05..5990086af42 100644 --- a/public/app/features/scenes/dashboard/DashboardScene.tsx +++ b/public/app/features/scenes/dashboard/DashboardScene.tsx @@ -1,25 +1,25 @@ +import { css } from '@emotion/css'; import React from 'react'; -import { PageLayoutType } from '@grafana/data'; +import { GrafanaTheme2, PageLayoutType } from '@grafana/data'; import { config, locationService } from '@grafana/runtime'; import { UrlSyncManager, SceneObjectBase, SceneComponentProps, - SceneLayout, SceneObject, SceneObjectStatePlain, } from '@grafana/scenes'; -import { PageToolbar, ToolbarButton } from '@grafana/ui'; +import { PageToolbar, ToolbarButton, useStyles2 } from '@grafana/ui'; import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; import { Page } from 'app/core/components/Page/Page'; interface DashboardSceneState extends SceneObjectStatePlain { title: string; - uid: string; - body: SceneLayout; + uid?: string; + body: SceneObject; actions?: SceneObject[]; - subMenu?: SceneObject; + controls?: SceneObject[]; } export class DashboardScene extends SceneObjectBase { @@ -48,7 +48,8 @@ export class DashboardScene extends SceneObjectBase { } function DashboardSceneRenderer({ model }: SceneComponentProps) { - const { title, body, actions = [], uid, subMenu } = model.useState(); + const { title, body, actions = [], uid, controls } = model.useState(); + const styles = useStyles2(getStyles); const toolbarActions = (actions ?? []).map((action) => ); @@ -63,10 +64,31 @@ function DashboardSceneRenderer({ model }: SceneComponentProps) return ( - {subMenu && } -
+ {controls && ( +
+ {controls.map((control) => ( + + ))} +
+ )} +
); } + +function getStyles(theme: GrafanaTheme2) { + return { + body: css({ + flexGrow: 1, + display: 'flex', + gap: '8px', + }), + controls: css({ + display: 'flex', + gap: theme.spacing(1), + alignItems: 'center', + }), + }; +} diff --git a/public/app/features/scenes/dashboard/DashboardsLoader.test.ts b/public/app/features/scenes/dashboard/DashboardsLoader.test.ts index 63e672ace35..8684e5d2b3a 100644 --- a/public/app/features/scenes/dashboard/DashboardsLoader.test.ts +++ b/public/app/features/scenes/dashboard/DashboardsLoader.test.ts @@ -135,7 +135,7 @@ describe('DashboardLoader', () => { expect(scene.state.uid).toBe('test-uid'); expect(scene.state?.$timeRange?.state.value.raw).toEqual(dash.time); expect(scene.state?.$variables?.state.variables).toHaveLength(1); - expect(scene.state.subMenu).toBeDefined(); + expect(scene.state.controls).toBeDefined(); }); }); @@ -162,9 +162,10 @@ describe('DashboardLoader', () => { const oldModel = new DashboardModel(dashboard); const scene = createDashboardSceneFromDashboardModel(oldModel); + const body = scene.state.body as SceneGridLayout; - expect(scene.state.body.state.children).toHaveLength(1); - const rowScene = scene.state.body.state.children[0] as SceneGridRow; + expect(body.state.children).toHaveLength(1); + const rowScene = body.state.children[0] as SceneGridRow; expect(rowScene).toBeInstanceOf(SceneGridRow); expect(rowScene.state.title).toEqual(row.title); expect(rowScene.state.placement?.y).toEqual(row.gridPos!.y); @@ -226,16 +227,17 @@ describe('DashboardLoader', () => { const oldModel = new DashboardModel(dashboard); const scene = createDashboardSceneFromDashboardModel(oldModel); + const body = scene.state.body as SceneGridLayout; - expect(scene.state.body.state.children).toHaveLength(3); - expect(scene.state.body).toBeInstanceOf(SceneGridLayout); + expect(body.state.children).toHaveLength(3); + expect(body).toBeInstanceOf(SceneGridLayout); // Panel out of row - expect(scene.state.body.state.children[0]).toBeInstanceOf(VizPanel); - const panelOutOfRowVizPanel = scene.state.body.state.children[0] as VizPanel; + expect(body.state.children[0]).toBeInstanceOf(VizPanel); + const panelOutOfRowVizPanel = body.state.children[0] as VizPanel; expect(panelOutOfRowVizPanel.state.title).toBe(panelOutOfRow.title); // Row with panel - expect(scene.state.body.state.children[1]).toBeInstanceOf(SceneGridRow); - const rowWithPanelsScene = scene.state.body.state.children[1] as SceneGridRow; + expect(body.state.children[1]).toBeInstanceOf(SceneGridRow); + const rowWithPanelsScene = body.state.children[1] as SceneGridRow; expect(rowWithPanelsScene.state.title).toBe(rowWithPanel.title); expect(rowWithPanelsScene.state.children).toHaveLength(1); // Panel within row @@ -243,8 +245,8 @@ describe('DashboardLoader', () => { const panelInRowVizPanel = rowWithPanelsScene.state.children[0] as VizPanel; expect(panelInRowVizPanel.state.title).toBe(panelInRow.title); // Empty row - expect(scene.state.body.state.children[2]).toBeInstanceOf(SceneGridRow); - const emptyRowScene = scene.state.body.state.children[2] as SceneGridRow; + expect(body.state.children[2]).toBeInstanceOf(SceneGridRow); + const emptyRowScene = body.state.children[2] as SceneGridRow; expect(emptyRowScene.state.title).toBe(emptyRow.title); expect(emptyRowScene.state.children).toHaveLength(0); }); diff --git a/public/app/features/scenes/dashboard/DashboardsLoader.ts b/public/app/features/scenes/dashboard/DashboardsLoader.ts index d2bcd4de2a7..bd19241d56e 100644 --- a/public/app/features/scenes/dashboard/DashboardsLoader.ts +++ b/public/app/features/scenes/dashboard/DashboardsLoader.ts @@ -13,7 +13,6 @@ import { SceneTimeRange, SceneObject, SceneQueryRunner, - SceneSubMenu, SceneVariableSet, VariableValueSelectors, SceneVariable, @@ -150,7 +149,6 @@ export function createSceneObjectsForPanels(oldPanels: PanelModel[]): SceneObjec } export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel) { - let subMenu: SceneSubMenu | undefined = undefined; let variables: SceneVariableSet | undefined = undefined; if (oldModel.templating.list.length) { @@ -166,9 +164,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel) // TODO: Remove filter // Added temporarily to allow skipping non-compatible variables .filter((v): v is SceneVariable => Boolean(v)); - subMenu = new SceneSubMenu({ - children: [new VariableValueSelectors({})], - }); + variables = new SceneVariableSet({ variables: variableObjects, }); @@ -183,7 +179,9 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel) $timeRange: new SceneTimeRange(oldModel.time), actions: [new SceneTimePicker({})], $variables: variables, - subMenu, + ...(variables && { + controls: [new VariableValueSelectors({})], + }), }); } diff --git a/public/app/features/scenes/scenes/demo.tsx b/public/app/features/scenes/scenes/demo.tsx index bf63f63c019..489e3088a23 100644 --- a/public/app/features/scenes/scenes/demo.tsx +++ b/public/app/features/scenes/scenes/demo.tsx @@ -6,18 +6,17 @@ import { VizPanel, SceneCanvasText, SceneToolbarInput, - EmbeddedScene, SceneDataNode, } from '@grafana/scenes'; import { panelBuilders } from '../builders/panelBuilders'; -import { Scene } from '../components/Scene'; +import { DashboardScene } from '../dashboard/DashboardScene'; import { SceneEditManager } from '../editor/SceneEditManager'; import { getQueryRunnerWithRandomWalkQuery } from './queries'; -export function getFlexLayoutTest(standalone: boolean): Scene | EmbeddedScene { - const state = { +export function getFlexLayoutTest(): DashboardScene { + return new DashboardScene({ title: 'Flex layout test', body: new SceneFlexLayout({ direction: 'row', @@ -63,19 +62,17 @@ export function getFlexLayoutTest(standalone: boolean): Scene | EmbeddedScene { $timeRange: new SceneTimeRange(), $data: getQueryRunnerWithRandomWalkQuery(), actions: [new SceneTimePicker({})], - }; - - return standalone ? new Scene(state) : new EmbeddedScene(state); + }); } -export function getScenePanelRepeaterTest(standalone: boolean): Scene | EmbeddedScene { +export function getScenePanelRepeaterTest(): DashboardScene { const queryRunner = getQueryRunnerWithRandomWalkQuery({ seriesCount: 2, alias: '__server_names', scenarioId: 'random_walk', }); - const state = { + return new DashboardScene({ title: 'Panel repeater test', body: new SceneByFrameRepeater({ body: new SceneFlexLayout({ @@ -133,7 +130,5 @@ export function getScenePanelRepeaterTest(standalone: boolean): Scene | Embedded }), new SceneTimePicker({}), ], - }; - - return standalone ? new Scene(state) : new EmbeddedScene(state); + }); } diff --git a/public/app/features/scenes/scenes/grid.tsx b/public/app/features/scenes/scenes/grid.tsx index 6cbfce18580..4b7e85e9aa3 100644 --- a/public/app/features/scenes/scenes/grid.tsx +++ b/public/app/features/scenes/scenes/grid.tsx @@ -1,19 +1,12 @@ -import { - VizPanel, - SceneTimePicker, - SceneFlexLayout, - SceneGridLayout, - SceneTimeRange, - EmbeddedScene, -} from '@grafana/scenes'; +import { VizPanel, SceneTimePicker, SceneFlexLayout, SceneGridLayout, SceneTimeRange } from '@grafana/scenes'; -import { Scene } from '../components/Scene'; +import { DashboardScene } from '../dashboard/DashboardScene'; import { SceneEditManager } from '../editor/SceneEditManager'; import { getQueryRunnerWithRandomWalkQuery } from './queries'; -export function getGridLayoutTest(standalone: boolean): Scene | EmbeddedScene { - const state = { +export function getGridLayoutTest(): DashboardScene { + return new DashboardScene({ title: 'Grid layout test', body: new SceneGridLayout({ children: [ @@ -58,7 +51,5 @@ export function getGridLayoutTest(standalone: boolean): Scene | EmbeddedScene { $timeRange: new SceneTimeRange(), $data: getQueryRunnerWithRandomWalkQuery(), actions: [new SceneTimePicker({})], - }; - - return standalone ? new Scene(state) : new EmbeddedScene(state); + }); } diff --git a/public/app/features/scenes/scenes/gridMultiTimeRange.tsx b/public/app/features/scenes/scenes/gridMultiTimeRange.tsx index dc856427bb2..052f5ee9903 100644 --- a/public/app/features/scenes/scenes/gridMultiTimeRange.tsx +++ b/public/app/features/scenes/scenes/gridMultiTimeRange.tsx @@ -1,25 +1,18 @@ -import { - VizPanel, - SceneGridRow, - SceneTimePicker, - SceneGridLayout, - SceneTimeRange, - EmbeddedScene, -} from '@grafana/scenes'; +import { VizPanel, SceneGridRow, SceneTimePicker, SceneGridLayout, SceneTimeRange } from '@grafana/scenes'; -import { Scene } from '../components/Scene'; +import { DashboardScene } from '../dashboard/DashboardScene'; import { SceneEditManager } from '../editor/SceneEditManager'; import { getQueryRunnerWithRandomWalkQuery } from './queries'; -export function getGridWithMultipleTimeRanges(standalone: boolean): Scene | EmbeddedScene { +export function getGridWithMultipleTimeRanges(): DashboardScene { const globalTimeRange = new SceneTimeRange(); const row1TimeRange = new SceneTimeRange({ from: 'now-1y', to: 'now', }); - const state = { + return new DashboardScene({ title: 'Grid with rows and different queries and time ranges', body: new SceneGridLayout({ children: [ @@ -66,7 +59,5 @@ export function getGridWithMultipleTimeRanges(standalone: boolean): Scene | Embe $timeRange: globalTimeRange, $data: getQueryRunnerWithRandomWalkQuery(), actions: [new SceneTimePicker({})], - }; - - return standalone ? new Scene(state) : new EmbeddedScene(state); + }); } diff --git a/public/app/features/scenes/scenes/gridMultiple.tsx b/public/app/features/scenes/scenes/gridMultiple.tsx index bfd49bc6e00..76cb257b203 100644 --- a/public/app/features/scenes/scenes/gridMultiple.tsx +++ b/public/app/features/scenes/scenes/gridMultiple.tsx @@ -1,19 +1,12 @@ -import { - VizPanel, - SceneTimePicker, - SceneFlexLayout, - SceneGridLayout, - SceneTimeRange, - EmbeddedScene, -} from '@grafana/scenes'; +import { VizPanel, SceneTimePicker, SceneFlexLayout, SceneGridLayout, SceneTimeRange } from '@grafana/scenes'; -import { Scene } from '../components/Scene'; +import { DashboardScene } from '../dashboard/DashboardScene'; import { SceneEditManager } from '../editor/SceneEditManager'; import { getQueryRunnerWithRandomWalkQuery } from './queries'; -export function getMultipleGridLayoutTest(standalone: boolean): Scene | EmbeddedScene { - const state = { +export function getMultipleGridLayoutTest(): DashboardScene { + return new DashboardScene({ title: 'Multiple grid layouts test', body: new SceneFlexLayout({ children: [ @@ -98,7 +91,5 @@ export function getMultipleGridLayoutTest(standalone: boolean): Scene | Embedded $timeRange: new SceneTimeRange(), $data: getQueryRunnerWithRandomWalkQuery(), actions: [new SceneTimePicker({})], - }; - - return standalone ? new Scene(state) : new EmbeddedScene(state); + }); } diff --git a/public/app/features/scenes/scenes/gridWithMultipleData.tsx b/public/app/features/scenes/scenes/gridWithMultipleData.tsx index 683d118932b..051c539c61a 100644 --- a/public/app/features/scenes/scenes/gridWithMultipleData.tsx +++ b/public/app/features/scenes/scenes/gridWithMultipleData.tsx @@ -1,19 +1,12 @@ -import { - VizPanel, - SceneGridRow, - SceneTimePicker, - SceneGridLayout, - SceneTimeRange, - EmbeddedScene, -} from '@grafana/scenes'; +import { VizPanel, SceneGridRow, SceneTimePicker, SceneGridLayout, SceneTimeRange } from '@grafana/scenes'; -import { Scene } from '../components/Scene'; +import { DashboardScene } from '../dashboard/DashboardScene'; import { SceneEditManager } from '../editor/SceneEditManager'; import { getQueryRunnerWithRandomWalkQuery } from './queries'; -export function getGridWithMultipleData(standalone: boolean): Scene | EmbeddedScene { - const state = { +export function getGridWithMultipleData(): DashboardScene { + return new DashboardScene({ title: 'Grid with rows and different queries', body: new SceneGridLayout({ children: [ @@ -93,7 +86,5 @@ export function getGridWithMultipleData(standalone: boolean): Scene | EmbeddedSc $timeRange: new SceneTimeRange(), $data: getQueryRunnerWithRandomWalkQuery(), actions: [new SceneTimePicker({})], - }; - - return standalone ? new Scene(state) : new EmbeddedScene(state); + }); } diff --git a/public/app/features/scenes/scenes/gridWithRow.tsx b/public/app/features/scenes/scenes/gridWithRow.tsx index d3739040722..8f3c7f09500 100644 --- a/public/app/features/scenes/scenes/gridWithRow.tsx +++ b/public/app/features/scenes/scenes/gridWithRow.tsx @@ -1,19 +1,12 @@ -import { - VizPanel, - SceneGridLayout, - SceneGridRow, - SceneTimePicker, - SceneTimeRange, - EmbeddedScene, -} from '@grafana/scenes'; +import { VizPanel, SceneGridLayout, SceneGridRow, SceneTimePicker, SceneTimeRange } from '@grafana/scenes'; -import { Scene } from '../components/Scene'; +import { DashboardScene } from '../dashboard/DashboardScene'; import { SceneEditManager } from '../editor/SceneEditManager'; import { getQueryRunnerWithRandomWalkQuery } from './queries'; -export function getGridWithRowLayoutTest(standalone: boolean): Scene | EmbeddedScene { - const state = { +export function getGridWithRowLayoutTest(): DashboardScene { + return new DashboardScene({ title: 'Grid with row layout test', body: new SceneGridLayout({ children: [ @@ -76,7 +69,5 @@ export function getGridWithRowLayoutTest(standalone: boolean): Scene | EmbeddedS $timeRange: new SceneTimeRange(), $data: getQueryRunnerWithRandomWalkQuery(), actions: [new SceneTimePicker({})], - }; - - return standalone ? new Scene(state) : new EmbeddedScene(state); + }); } diff --git a/public/app/features/scenes/scenes/gridWithRows.tsx b/public/app/features/scenes/scenes/gridWithRows.tsx index c3bde62bb1f..ad1c871d35d 100644 --- a/public/app/features/scenes/scenes/gridWithRows.tsx +++ b/public/app/features/scenes/scenes/gridWithRows.tsx @@ -7,12 +7,11 @@ import { SceneTimeRange, } from '@grafana/scenes'; -import { Scene } from '../components/Scene'; -import { SceneEditManager } from '../editor/SceneEditManager'; +import { DashboardScene } from '../dashboard/DashboardScene'; import { getQueryRunnerWithRandomWalkQuery } from './queries'; -export function getGridWithRowsTest(): Scene { +export function getGridWithRowsTest(): DashboardScene { const panel = new VizPanel({ pluginId: 'timeseries', title: 'Fill height', @@ -77,12 +76,12 @@ export function getGridWithRowsTest(): Scene { }), ], }); - const scene = new Scene({ + + const scene = new DashboardScene({ title: 'Grid rows test', body: new SceneGridLayout({ children: [cell1, cell2, row1, row2], }), - $editor: new SceneEditManager({}), $timeRange: new SceneTimeRange(), $data: getQueryRunnerWithRandomWalkQuery(), actions: [new SceneTimePicker({})], diff --git a/public/app/features/scenes/scenes/index.tsx b/public/app/features/scenes/scenes/index.tsx index bdb94daebe4..f398ff1a2ae 100644 --- a/public/app/features/scenes/scenes/index.tsx +++ b/public/app/features/scenes/scenes/index.tsx @@ -1,6 +1,4 @@ -import { EmbeddedScene, SceneObjectBase, SceneState } from '@grafana/scenes'; - -import { Scene } from '../components/Scene'; +import { DashboardScene } from '../dashboard/DashboardScene'; import { getFlexLayoutTest, getScenePanelRepeaterTest } from './demo'; import { getGridLayoutTest } from './grid'; @@ -16,7 +14,7 @@ import { getVariablesDemo, getVariablesDemoWithAll } from './variablesDemo'; interface SceneDef { title: string; - getScene: (standalone: boolean) => Scene | EmbeddedScene; + getScene: () => DashboardScene; } export function getScenes(): SceneDef[] { return [ @@ -36,20 +34,18 @@ export function getScenes(): SceneDef[] { ]; } -const cache: Record }> = {}; +const cache: Record = {}; -export function getSceneByTitle(title: string, standalone = true) { +export function getSceneByTitle(title: string) { if (cache[title]) { - if (cache[title].standalone === standalone) { - return cache[title].scene; - } + return cache[title]; } const scene = getScenes().find((x) => x.title === title); if (scene) { - cache[title] = { scene: scene.getScene(standalone), standalone }; + cache[title] = scene.getScene(); } - return cache[title].scene; + return cache[title]; } diff --git a/public/app/features/scenes/scenes/nested.tsx b/public/app/features/scenes/scenes/nested.tsx index 9243a1c1abc..7ac0056e3e8 100644 --- a/public/app/features/scenes/scenes/nested.tsx +++ b/public/app/features/scenes/scenes/nested.tsx @@ -1,18 +1,11 @@ -import { - VizPanel, - NestedScene, - SceneTimePicker, - SceneFlexLayout, - SceneTimeRange, - EmbeddedScene, -} from '@grafana/scenes'; +import { VizPanel, NestedScene, SceneTimePicker, SceneFlexLayout, SceneTimeRange } from '@grafana/scenes'; -import { Scene } from '../components/Scene'; +import { DashboardScene } from '../dashboard/DashboardScene'; import { getQueryRunnerWithRandomWalkQuery } from './queries'; -export function getNestedScene(standalone: boolean): Scene | EmbeddedScene { - const state = { +export function getNestedScene(): DashboardScene { + return new DashboardScene({ title: 'Nested Scene demo', body: new SceneFlexLayout({ direction: 'column', @@ -28,9 +21,7 @@ export function getNestedScene(standalone: boolean): Scene | EmbeddedScene { $timeRange: new SceneTimeRange(), $data: getQueryRunnerWithRandomWalkQuery(), actions: [new SceneTimePicker({})], - }; - - return standalone ? new Scene(state) : new EmbeddedScene(state); + }); } export function getInnerScene(title: string) { diff --git a/public/app/features/scenes/scenes/queryVariableDemo.tsx b/public/app/features/scenes/scenes/queryVariableDemo.tsx index 4eb211d8d59..69fd91ded75 100644 --- a/public/app/features/scenes/scenes/queryVariableDemo.tsx +++ b/public/app/features/scenes/scenes/queryVariableDemo.tsx @@ -1,7 +1,6 @@ import { VariableRefresh } from '@grafana/data'; import { SceneCanvasText, - SceneSubMenu, SceneTimePicker, SceneFlexLayout, SceneTimeRange, @@ -10,13 +9,12 @@ import { CustomVariable, DataSourceVariable, QueryVariable, - EmbeddedScene, } from '@grafana/scenes'; -import { Scene } from '../components/Scene'; +import { DashboardScene } from '../dashboard/DashboardScene'; -export function getQueryVariableDemo(standalone: boolean): Scene | EmbeddedScene { - const state = { +export function getQueryVariableDemo(): DashboardScene { + return new DashboardScene({ title: 'Query variable', $variables: new SceneVariableSet({ variables: [ @@ -65,10 +63,6 @@ export function getQueryVariableDemo(standalone: boolean): Scene | EmbeddedScene }), $timeRange: new SceneTimeRange(), actions: [new SceneTimePicker({})], - subMenu: new SceneSubMenu({ - children: [new VariableValueSelectors({})], - }), - }; - - return standalone ? new Scene(state) : new EmbeddedScene(state); + controls: [new VariableValueSelectors({})], + }); } diff --git a/public/app/features/scenes/scenes/sceneWithRows.tsx b/public/app/features/scenes/scenes/sceneWithRows.tsx index c7d5d7f4f1f..51e4352129b 100644 --- a/public/app/features/scenes/scenes/sceneWithRows.tsx +++ b/public/app/features/scenes/scenes/sceneWithRows.tsx @@ -1,19 +1,12 @@ -import { - VizPanel, - NestedScene, - SceneTimePicker, - SceneFlexLayout, - SceneTimeRange, - EmbeddedScene, -} from '@grafana/scenes'; +import { VizPanel, NestedScene, SceneTimePicker, SceneFlexLayout, SceneTimeRange } from '@grafana/scenes'; -import { Scene } from '../components/Scene'; +import { DashboardScene } from '../dashboard/DashboardScene'; import { SceneEditManager } from '../editor/SceneEditManager'; import { getQueryRunnerWithRandomWalkQuery } from './queries'; -export function getSceneWithRows(standalone: boolean): Scene | EmbeddedScene { - const state = { +export function getSceneWithRows(): DashboardScene { + return new DashboardScene({ title: 'Scene with rows', body: new SceneFlexLayout({ direction: 'column', @@ -60,7 +53,5 @@ export function getSceneWithRows(standalone: boolean): Scene | EmbeddedScene { $timeRange: new SceneTimeRange(), $data: getQueryRunnerWithRandomWalkQuery(), actions: [new SceneTimePicker({})], - }; - - return standalone ? new Scene(state) : new EmbeddedScene(state); + }); } diff --git a/public/app/features/scenes/scenes/transformations.tsx b/public/app/features/scenes/scenes/transformations.tsx index 739f88c820f..d8c5a08f335 100644 --- a/public/app/features/scenes/scenes/transformations.tsx +++ b/public/app/features/scenes/scenes/transformations.tsx @@ -1,19 +1,11 @@ -import { - SceneTimePicker, - SceneFlexLayout, - VizPanel, - SceneDataTransformer, - SceneTimeRange, - EmbeddedScene, -} from '@grafana/scenes'; +import { SceneTimePicker, SceneFlexLayout, VizPanel, SceneDataTransformer, SceneTimeRange } from '@grafana/scenes'; -import { Scene } from '../components/Scene'; -import { SceneEditManager } from '../editor/SceneEditManager'; +import { DashboardScene } from '../dashboard/DashboardScene'; import { getQueryRunnerWithRandomWalkQuery } from './queries'; -export function getTransformationsDemo(standalone: boolean): Scene | EmbeddedScene { - const state = { +export function getTransformationsDemo(): DashboardScene { + return new DashboardScene({ title: 'Transformations demo', body: new SceneFlexLayout({ direction: 'row', @@ -63,11 +55,8 @@ export function getTransformationsDemo(standalone: boolean): Scene | EmbeddedSce }), ], }), - $editor: new SceneEditManager({}), $timeRange: new SceneTimeRange(), $data: getQueryRunnerWithRandomWalkQuery(), actions: [new SceneTimePicker({})], - }; - - return standalone ? new Scene(state) : new EmbeddedScene(state); + }); } diff --git a/public/app/features/scenes/scenes/variablesDemo.tsx b/public/app/features/scenes/scenes/variablesDemo.tsx index 14547e4302d..aae86008f13 100644 --- a/public/app/features/scenes/scenes/variablesDemo.tsx +++ b/public/app/features/scenes/scenes/variablesDemo.tsx @@ -1,7 +1,6 @@ import { VizPanel, SceneCanvasText, - SceneSubMenu, SceneTimePicker, SceneFlexLayout, SceneTimeRange, @@ -10,15 +9,14 @@ import { CustomVariable, DataSourceVariable, TestVariable, - EmbeddedScene, } from '@grafana/scenes'; -import { Scene } from '../components/Scene'; +import { DashboardScene } from '../dashboard/DashboardScene'; import { getQueryRunnerWithRandomWalkQuery } from './queries'; -export function getVariablesDemo(standalone: boolean): Scene | EmbeddedScene { - const state = { +export function getVariablesDemo(): DashboardScene { + return new DashboardScene({ title: 'Variables', $variables: new SceneVariableSet({ variables: [ @@ -82,16 +80,12 @@ export function getVariablesDemo(standalone: boolean): Scene | EmbeddedScene { }), $timeRange: new SceneTimeRange(), actions: [new SceneTimePicker({})], - subMenu: new SceneSubMenu({ - children: [new VariableValueSelectors({})], - }), - }; - - return standalone ? new Scene(state) : new EmbeddedScene(state); + controls: [new VariableValueSelectors({})], + }); } -export function getVariablesDemoWithAll(): Scene { - const scene = new Scene({ +export function getVariablesDemoWithAll(): DashboardScene { + return new DashboardScene({ title: 'Variables with All values', $variables: new SceneVariableSet({ variables: [ @@ -153,10 +147,6 @@ export function getVariablesDemoWithAll(): Scene { }), $timeRange: new SceneTimeRange(), actions: [new SceneTimePicker({})], - subMenu: new SceneSubMenu({ - children: [new VariableValueSelectors({})], - }), + controls: [new VariableValueSelectors({})], }); - - return scene; } diff --git a/yarn.lock b/yarn.lock index 0fcfb12c7a8..cc831b0a19d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5121,8 +5121,8 @@ __metadata: linkType: soft "@grafana/scenes@npm:latest": - version: 0.0.5 - resolution: "@grafana/scenes@npm:0.0.5" + version: 0.0.8 + resolution: "@grafana/scenes@npm:0.0.8" dependencies: "@grafana/e2e-selectors": canary "@grafana/experimental": 1.0.1 @@ -5130,7 +5130,7 @@ __metadata: react-use: 17.4.0 react-virtualized-auto-sizer: 1.0.7 uuid: ^9.0.0 - checksum: 7388aaccb0788801f94734126d1abab71df3f81131eec41c967b0c462223a13fcfbc5a65ce7a45bf6761032d8efd1330cb85ac8e86136cba9e4dd244f41b1054 + checksum: 4c76ca9850906b98fc9131b48ea6a664f84a0f8ac5f7d87543d73f8ca8214c33483fc4454447e46d67096826f7cb32f3db70461323e65aefc241c4169c88fd98 languageName: node linkType: hard From 6ed7c7751607506755eeab619ed718cc81fcc3f0 Mon Sep 17 00:00:00 2001 From: Kristina Date: Tue, 17 Jan 2023 11:12:37 -0600 Subject: [PATCH 58/65] Explore: Add correlation variables for interpolation (#61008) * Add correlation variables for interpolation * Change to only look at relevant field * Add links ignoring correlation check, add tests * Add variables for all fields in dataframe, add tests for it, simplify URL expect --- .../app/features/explore/utils/links.test.ts | 113 +++++++++++++++++- public/app/features/explore/utils/links.ts | 26 +++- 2 files changed, 131 insertions(+), 8 deletions(-) diff --git a/public/app/features/explore/utils/links.test.ts b/public/app/features/explore/utils/links.test.ts index 23c2f7be0b3..1c60871ed87 100644 --- a/public/app/features/explore/utils/links.test.ts +++ b/public/app/features/explore/utils/links.test.ts @@ -133,7 +133,7 @@ describe('getFieldLinksForExplore', () => { expect(links).toHaveLength(0); }); - it('returns internal links when target contains defined template variables', () => { + it('returns internal links when target contains __data template variables', () => { const { field, range, dataFrame } = setup({ title: '', url: '', @@ -145,6 +145,100 @@ describe('getFieldLinksForExplore', () => { }); const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range, dataFrame }); expect(links).toHaveLength(1); + expect(links[0].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo"}],"panelsState":{}}' + )}` + ); + }); + + it('returns internal links when target contains targetField template variable', () => { + const { field, range, dataFrame } = setup({ + title: '', + url: '', + internal: { + query: { query: 'query_1-${__targetField}' }, + datasourceUid: 'uid_1', + datasourceName: 'test_ds', + }, + }); + const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range, dataFrame }); + expect(links).toHaveLength(1); + expect(links[0].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo"}],"panelsState":{}}' + )}` + ); + }); + + it('returns internal links when target contains field name template variable', () => { + // field cannot be hyphenated, change field name to non-hyphenated + const noHyphenLink = { + title: '', + url: '', + internal: { + query: { query: 'query_1-${fluxDimensions}' }, + datasourceUid: 'uid_1', + datasourceName: 'test_ds', + }, + }; + const { field, range, dataFrame } = setup(noHyphenLink, true, { + name: 'fluxDimensions', + type: FieldType.string, + values: new ArrayVector([ROW_WITH_TEXT_VALUE.value, ROW_WITH_NULL_VALUE.value]), + config: { + links: [noHyphenLink], + }, + }); + const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range, dataFrame }); + expect(links).toHaveLength(1); + expect(links[0].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo"}],"panelsState":{}}' + )}` + ); + }); + + it('returns internal links when target contains other field name template variables', () => { + // field cannot be hyphenated, change field name to non-hyphenated + const noHyphenLink = { + title: '', + url: '', + internal: { + query: { query: 'query_1-${fluxDimensions}-${fluxDimension2}' }, + datasourceUid: 'uid_1', + datasourceName: 'test_ds', + }, + }; + const { field, range, dataFrame } = setup( + noHyphenLink, + true, + { + name: 'fluxDimensions', + type: FieldType.string, + values: new ArrayVector([ROW_WITH_TEXT_VALUE.value, ROW_WITH_NULL_VALUE.value]), + config: { + links: [noHyphenLink], + }, + }, + [ + { + name: 'fluxDimension2', + type: FieldType.string, + values: new ArrayVector(['foo2', ROW_WITH_NULL_VALUE.value]), + config: { + links: [noHyphenLink], + }, + }, + ] + ); + const links = getFieldLinksForExplore({ field, rowIndex: ROW_WITH_TEXT_VALUE.index, range, dataFrame }); + expect(links).toHaveLength(1); + expect(links[0].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo-foo2"}],"panelsState":{}}' + )}` + ); }); it('returns no internal links when target contains empty template variables', () => { @@ -165,7 +259,12 @@ describe('getFieldLinksForExplore', () => { const ROW_WITH_TEXT_VALUE = { value: 'foo', index: 0 }; const ROW_WITH_NULL_VALUE = { value: null, index: 1 }; -function setup(link: DataLink, hasAccess = true) { +function setup( + link: DataLink, + hasAccess = true, + fieldOverride?: Field, + dataFrameOtherFieldOverride?: Field[] +) { setLinkSrv({ getDataLinkUIModel(link: DataLink, replaceVariables: InterpolateFunction | undefined, origin: any): LinkModel { return { @@ -196,8 +295,14 @@ function setup(link: DataLink, hasAccess = true) { }, }; + let fieldsArr = [fieldOverride || field]; + + if (dataFrameOtherFieldOverride) { + fieldsArr = [...fieldsArr, ...dataFrameOtherFieldOverride]; + } + const dataFrame: DataFrame = toDataFrame({ - fields: [field], + fields: fieldsArr, }); const range: TimeRange = { @@ -209,5 +314,5 @@ function setup(link: DataLink, hasAccess = true) { }, }; - return { range, field, dataFrame }; + return { range, field: fieldOverride || field, dataFrame }; } diff --git a/public/app/features/explore/utils/links.ts b/public/app/features/explore/utils/links.ts index ad0f27341e0..a8e507a0b59 100644 --- a/public/app/features/explore/utils/links.ts +++ b/public/app/features/explore/utils/links.ts @@ -11,6 +11,7 @@ import { getFieldDisplayValuesProxy, SplitOpen, DataLink, + DisplayValue, } from '@grafana/data'; import { getTemplateSrv } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; @@ -72,19 +73,36 @@ export const getFieldLinksForExplore = (options: { text: 'Raw value', }; + let fieldDisplayValuesProxy: Record | undefined = undefined; + // If we have a dataFrame we can allow referencing other columns and their values in the interpolation. if (dataFrame) { + fieldDisplayValuesProxy = getFieldDisplayValuesProxy({ + frame: dataFrame, + rowIndex, + }); + scopedVars['__data'] = { value: { name: dataFrame.name, refId: dataFrame.refId, - fields: getFieldDisplayValuesProxy({ - frame: dataFrame, - rowIndex, - }), + fields: fieldDisplayValuesProxy, }, text: 'Data', }; + + dataFrame.fields.forEach((f) => { + if (fieldDisplayValuesProxy && fieldDisplayValuesProxy[f.name]) { + scopedVars[f.name] = { + value: fieldDisplayValuesProxy[f.name], + }; + } + }); + + // add this for convenience + scopedVars['__targetField'] = { + value: fieldDisplayValuesProxy[field.name], + }; } if (field.config.links) { From 3fe81b3c3f809e1b24e110ccf50618124c975ad4 Mon Sep 17 00:00:00 2001 From: owensmallwood Date: Tue, 17 Jan 2023 12:18:12 -0600 Subject: [PATCH 59/65] PublicDashboards: Add share column to public dashboards table (#61102) Adds share column to public dashboards table --- pkg/services/sqlstore/migrations/dashboard_public_mig.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/services/sqlstore/migrations/dashboard_public_mig.go b/pkg/services/sqlstore/migrations/dashboard_public_mig.go index f3e932f45ce..252d95e0aaa 100644 --- a/pkg/services/sqlstore/migrations/dashboard_public_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_public_mig.go @@ -84,4 +84,12 @@ func addPublicDashboardMigration(mg *Migrator) { mg.AddMigration("delete orphaned public dashboards", NewRawSQLMigration( "DELETE FROM dashboard_public WHERE dashboard_uid NOT IN (SELECT uid FROM dashboard)")) + + mg.AddMigration("add share column", NewAddColumnMigration(dashboardPublicCfgV2, &Column{ + Name: "share", + Type: DB_NVarchar, + Length: 64, + Nullable: false, + Default: "'public'", + })) } From 23e05373a77fb184a00af8a61a8e4f8bff669064 Mon Sep 17 00:00:00 2001 From: Matthew Jacobson Date: Tue, 17 Jan 2023 14:09:46 -0500 Subject: [PATCH 60/65] Alerting: Fix flaky TestIntegrationUpdateAlertRules (#61641) Prevents random OrgID=0 in test alert generation causing invalid alert rule. --- pkg/services/ngalert/models/testing.go | 2 +- pkg/services/ngalert/store/alert_rule_test.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/services/ngalert/models/testing.go b/pkg/services/ngalert/models/testing.go index 29d538e8f99..82b0ab7d443 100644 --- a/pkg/services/ngalert/models/testing.go +++ b/pkg/services/ngalert/models/testing.go @@ -60,7 +60,7 @@ func AlertRuleGen(mutators ...AlertRuleMutator) func() *AlertRule { rule := &AlertRule{ ID: rand.Int63n(1500), - OrgID: rand.Int63n(1500), + OrgID: rand.Int63n(1500) + 1, // Prevent OrgID=0 as this does not pass alert rule validation. Title: "TEST-ALERT-" + util.GenerateShortUID(), Condition: "A", Data: []AlertQuery{GenerateAlertQuery()}, diff --git a/pkg/services/ngalert/store/alert_rule_test.go b/pkg/services/ngalert/store/alert_rule_test.go index 0b0cc72ade2..1471109ba4a 100644 --- a/pkg/services/ngalert/store/alert_rule_test.go +++ b/pkg/services/ngalert/store/alert_rule_test.go @@ -52,7 +52,6 @@ func TestIntegrationUpdateAlertRules(t *testing.T) { }) t.Run("should fail due to optimistic locking if version does not match", func(t *testing.T) { - t.Skip() // This test intermittently fails. rule := createRule(t, store) rule.Version-- // simulate version discrepancy From 8c826cd78550252cc53ccfc96050e0484de9a819 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 17 Jan 2023 20:27:53 +0100 Subject: [PATCH 61/65] Cloudwatch: Cleanup resource api (#61465) Co-authored-by: Isabella Siu --- .../__mocks__/CloudWatchDataSource.ts | 12 ++-- .../__mocks__/{API.ts => ResourcesAPI.ts} | 6 +- .../completion/CompletionItemProvider.ts | 20 +++--- .../components/AnnotationQueryEditor.test.tsx | 20 ++++-- .../cloudwatch/components/CloudWatchLink.tsx | 2 +- .../components/ConfigEditor.test.tsx | 2 +- .../cloudwatch/components/ConfigEditor.tsx | 4 +- .../components/Dimensions/Dimensions.test.tsx | 6 +- .../components/Dimensions/FilterItem.tsx | 2 +- .../LogGroups/LogGroupsField.test.tsx | 14 ++-- .../components/LogGroups/LogGroupsField.tsx | 15 +++-- .../LogGroups/LogGroupsSelector.test.tsx | 2 +- .../LogGroups/LogGroupsSelector.tsx | 3 +- .../MetricStatEditor.test.tsx | 20 +++--- .../MetricStatEditor/MetricStatEditor.tsx | 8 +-- .../MetricsQueryEditor.test.tsx | 14 ++-- .../components/QueryEditor.test.tsx | 6 +- .../components/QueryHeader.test.tsx | 16 ++--- .../cloudwatch/components/QueryHeader.tsx | 4 +- .../SQLBuilderEditor.test.tsx | 20 +++--- .../SQLBuilderSelectRow.test.tsx | 12 ++-- .../SQLBuilderEditor/SQLBuilderSelectRow.tsx | 2 +- .../components/SQLBuilderEditor/SQLFilter.tsx | 2 +- .../SQLBuilderEditor/SQLGroupBy.test.tsx | 2 +- .../VariableQueryEditor.test.tsx | 21 +++--- .../VariableQueryEditor.tsx | 16 +++-- .../datasource/cloudwatch/datasource.test.ts | 2 +- .../datasource/cloudwatch/datasource.ts | 14 ++-- .../datasource/cloudwatch/hooks.test.ts | 14 ++-- .../plugins/datasource/cloudwatch/hooks.ts | 34 +++++----- .../cloudwatch/language_provider.test.ts | 5 +- .../cloudwatch/language_provider.ts | 2 +- .../completion/CompletionItemProvider.test.ts | 4 +- .../completion/CompletionItemProvider.ts | 6 +- .../monarch/CompletionItemProvider.ts | 8 +-- .../ResourceAPI.test.ts} | 14 ++-- .../{api.ts => resources/ResourcesAPI.ts} | 26 +++---- .../datasource/cloudwatch/resources/types.ts | 63 +++++++++++++++++ .../plugins/datasource/cloudwatch/types.ts | 67 ------------------- .../datasource/cloudwatch/variables.test.ts | 28 ++++---- .../datasource/cloudwatch/variables.ts | 28 ++++---- 41 files changed, 289 insertions(+), 277 deletions(-) rename public/app/plugins/datasource/cloudwatch/__mocks__/{API.ts => ResourcesAPI.ts} (84%) rename public/app/plugins/datasource/cloudwatch/{api.test.ts => resources/ResourceAPI.test.ts} (90%) rename public/app/plugins/datasource/cloudwatch/{api.ts => resources/ResourcesAPI.ts} (95%) create mode 100644 public/app/plugins/datasource/cloudwatch/resources/types.ts diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts index 52f31219faf..72221bc968a 100644 --- a/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts @@ -80,12 +80,12 @@ export function setupMockedDataSource({ const timeSrv = getTimeSrv(); const datasource = new CloudWatchDatasource(customInstanceSettings, templateService, timeSrv); datasource.getVariables = () => ['test']; - datasource.api.getNamespaces = jest.fn().mockResolvedValue([]); - datasource.api.getRegions = jest.fn().mockResolvedValue([]); - datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]); - datasource.api.getMetrics = jest.fn().mockResolvedValue([]); - datasource.api.getAccounts = jest.fn().mockResolvedValue([]); - datasource.api.getLogGroups = jest.fn().mockResolvedValue([]); + datasource.resources.getNamespaces = jest.fn().mockResolvedValue([]); + datasource.resources.getRegions = jest.fn().mockResolvedValue([]); + datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([]); + datasource.resources.getMetrics = jest.fn().mockResolvedValue([]); + datasource.resources.getAccounts = jest.fn().mockResolvedValue([]); + datasource.resources.getLogGroups = jest.fn().mockResolvedValue([]); const fetchMock = jest.fn().mockReturnValue(of({})); setBackendSrv({ ...getBackendSrv(), diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/API.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/ResourcesAPI.ts similarity index 84% rename from public/app/plugins/datasource/cloudwatch/__mocks__/API.ts rename to public/app/plugins/datasource/cloudwatch/__mocks__/ResourcesAPI.ts index 662fc010006..69effa8d23b 100644 --- a/public/app/plugins/datasource/cloudwatch/__mocks__/API.ts +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/ResourcesAPI.ts @@ -3,11 +3,11 @@ import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { CustomVariableModel } from 'app/features/variables/types'; -import { CloudWatchAPI } from '../api'; +import { ResourcesAPI } from '../resources/ResourcesAPI'; import { CloudWatchSettings, setupMockedTemplateService } from './CloudWatchDataSource'; -export function setupMockedAPI({ +export function setupMockedResourcesAPI({ variables, response, getMock, @@ -20,7 +20,7 @@ export function setupMockedAPI({ let templateService = variables ? setupMockedTemplateService(variables) : new TemplateSrv(); const timeSrv = getTimeSrv(); - const api = new CloudWatchAPI(CloudWatchSettings, templateService); + const api = new ResourcesAPI(CloudWatchSettings, templateService); let resourceRequestMock = getMock ? getMock : jest.fn().mockReturnValue(response); setBackendSrv({ ...getBackendSrv(), diff --git a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/CompletionItemProvider.ts b/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/CompletionItemProvider.ts index 75e2a192c03..27e0670047d 100644 --- a/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/CompletionItemProvider.ts +++ b/public/app/plugins/datasource/cloudwatch/cloudwatch-sql/completion/CompletionItemProvider.ts @@ -3,11 +3,11 @@ import { uniq } from 'lodash'; import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import type { Monaco, monacoTypes } from '@grafana/ui'; -import { CloudWatchAPI } from '../../api'; import { CompletionItemProvider } from '../../monarch/CompletionItemProvider'; import { LinkedToken } from '../../monarch/LinkedToken'; import { TRIGGER_SUGGEST } from '../../monarch/commands'; import { SuggestionKind, CompletionItemPriority, StatementPosition } from '../../monarch/types'; +import { ResourcesAPI } from '../../resources/ResourcesAPI'; import { BY, FROM, @@ -34,9 +34,9 @@ type CompletionItem = monacoTypes.languages.CompletionItem; export class SQLCompletionItemProvider extends CompletionItemProvider { region: string; - constructor(api: CloudWatchAPI, templateSrv: TemplateSrv = getTemplateSrv()) { - super(api, templateSrv); - this.region = api.getActualRegion() ?? ''; + constructor(resources: ResourcesAPI, templateSrv: TemplateSrv = getTemplateSrv()) { + super(resources, templateSrv); + this.region = resources.getActualRegion() ?? ''; this.getStatementPosition = getStatementPosition; this.getSuggestionKinds = getSuggestionKinds; this.tokenTypes = SQLTokenTypes; @@ -112,14 +112,14 @@ export class SQLCompletionItemProvider extends CompletionItemProvider { const namespaceToken = getNamespaceToken(currentToken); if (namespaceToken?.value) { // if a namespace is specified, only suggest metrics for the namespace - const metrics = await this.api.getMetrics({ + const metrics = await this.resources.getMetrics({ namespace: namespaceToken?.value.replace(/\"/g, ''), region: this.region, }); metrics.forEach((m) => m.value && addSuggestion(m.value)); } else { // If no namespace is specified in the query, just list all metrics - const metrics = await this.api.getAllMetrics({ region: this.region }); + const metrics = await this.resources.getAllMetrics({ region: this.region }); uniq(metrics.map((m) => m.metricName)).forEach((m) => m && addSuggestion(m, { insertText: m })); } } @@ -147,12 +147,12 @@ export class SQLCompletionItemProvider extends CompletionItemProvider { let namespaces = []; if (metricNameToken?.value) { // if a metric is specified, only suggest namespaces that actually have that metric - const metrics = await this.api.getMetrics({ region: this.region }); + const metrics = await this.resources.getMetrics({ region: this.region }); const metricName = this.templateSrv.replace(metricNameToken.value); namespaces = metrics.filter((m) => m.metricName === metricName).map((m) => m.namespace); } else { // if no metric is specified, just suggest all namespaces - const ns = await this.api.getNamespaces(); + const ns = await this.resources.getNamespaces(); namespaces = ns.map((n) => n.value); } namespaces.map((n) => addSuggestion(`"${n}"`, { insertText: `"${n}"` })); @@ -179,7 +179,7 @@ export class SQLCompletionItemProvider extends CompletionItemProvider { dimensionFilters = (labelKeyTokens || []).reduce((acc, curr) => { return { ...acc, [curr.value]: null }; }, {}); - const keys = await this.api.getDimensionKeys({ + const keys = await this.resources.getDimensionKeys({ namespace: this.templateSrv.replace(namespaceToken.value.replace(/\"/g, '')), region: this.templateSrv.replace(this.region), metricName: metricNameToken?.value, @@ -199,7 +199,7 @@ export class SQLCompletionItemProvider extends CompletionItemProvider { const metricNameToken = getMetricNameToken(currentToken); const labelKey = currentToken?.getPreviousNonWhiteSpaceToken()?.getPreviousNonWhiteSpaceToken(); if (namespaceToken?.value && labelKey?.value && metricNameToken?.value) { - const values = await this.api.getDimensionValues({ + const values = await this.resources.getDimensionValues({ region: this.region, namespace: namespaceToken.value.replace(/\"/g, ''), metricName: metricNameToken.value, diff --git a/public/app/plugins/datasource/cloudwatch/components/AnnotationQueryEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/AnnotationQueryEditor.test.tsx index 082d8ee0583..65de0fc59f7 100644 --- a/public/app/plugins/datasource/cloudwatch/components/AnnotationQueryEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/AnnotationQueryEditor.test.tsx @@ -29,10 +29,10 @@ const q: CloudWatchQuery = { alarmNamePrefix: '', }; -ds.datasource.api.getRegions = jest.fn().mockResolvedValue([]); -ds.datasource.api.getNamespaces = jest.fn().mockResolvedValue([]); -ds.datasource.api.getMetrics = jest.fn().mockResolvedValue([]); -ds.datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]); +ds.datasource.resources.getRegions = jest.fn().mockResolvedValue([]); +ds.datasource.resources.getNamespaces = jest.fn().mockResolvedValue([]); +ds.datasource.resources.getMetrics = jest.fn().mockResolvedValue([]); +ds.datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([]); ds.datasource.getVariables = jest.fn().mockReturnValue([]); const props: QueryEditorProps = { @@ -51,7 +51,9 @@ describe('AnnotationQueryEditor', () => { }); it('should return an error component in case CloudWatchQuery is not CloudWatchAnnotationQuery', async () => { - ds.datasource.api.getDimensionValues = jest.fn().mockResolvedValue([[{ label: 'dimVal1', value: 'dimVal1' }]]); + ds.datasource.resources.getDimensionValues = jest + .fn() + .mockResolvedValue([[{ label: 'dimVal1', value: 'dimVal1' }]]); render( ); @@ -59,7 +61,9 @@ describe('AnnotationQueryEditor', () => { }); it('should not display wildcard option in dimension value dropdown', async () => { - ds.datasource.api.getDimensionValues = jest.fn().mockResolvedValue([[{ label: 'dimVal1', value: 'dimVal1' }]]); + ds.datasource.resources.getDimensionValues = jest + .fn() + .mockResolvedValue([[{ label: 'dimVal1', value: 'dimVal1' }]]); (props.query as CloudWatchAnnotationQuery).dimensions = { instanceId: 'instance-123' }; render(); const valueElement = screen.getByText('instance-123'); @@ -72,7 +76,9 @@ describe('AnnotationQueryEditor', () => { }); it('should not display Accounts component', async () => { - ds.datasource.api.getDimensionValues = jest.fn().mockResolvedValue([[{ label: 'dimVal1', value: 'dimVal1' }]]); + ds.datasource.resources.getDimensionValues = jest + .fn() + .mockResolvedValue([[{ label: 'dimVal1', value: 'dimVal1' }]]); (props.query as CloudWatchAnnotationQuery).dimensions = { instanceId: 'instance-123' }; await waitFor(() => render()); expect(await screen.queryByText('Account')).toBeNull(); diff --git a/public/app/plugins/datasource/cloudwatch/components/CloudWatchLink.tsx b/public/app/plugins/datasource/cloudwatch/components/CloudWatchLink.tsx index e535aa91d38..2ba3364df49 100644 --- a/public/app/plugins/datasource/cloudwatch/components/CloudWatchLink.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/CloudWatchLink.tsx @@ -40,7 +40,7 @@ export function CloudWatchLink({ panelData, query, datasource }: Props) { source: sources ?? [], }; - setHref(encodeUrl(urlProps, datasource.api.getActualRegion(query.region))); + setHref(encodeUrl(urlProps, datasource.resources.getActualRegion(query.region))); } }, [panelData, prevPanelData, datasource, query]); diff --git a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx index d2579d9fc21..d44e1755b7e 100644 --- a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx @@ -99,7 +99,7 @@ describe('Render', () => { putMock.mockImplementation(async () => ({ datasource: setupMockedDataSource().datasource })); getMock.mockImplementation(async () => ({ datasource: setupMockedDataSource().datasource })); loadDataSourceMock.mockResolvedValue(datasource); - datasource.api.getRegions = jest.fn().mockResolvedValue([ + datasource.resources.getRegions = jest.fn().mockResolvedValue([ { label: 'ap-east-1', value: 'ap-east-1', diff --git a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx index e13ce093c1d..61a01609f39 100644 --- a/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx @@ -14,8 +14,8 @@ import { createWarningNotification } from 'app/core/copy/appNotification'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { store } from 'app/store/store'; -import { SelectableResourceValue } from '../api'; import { CloudWatchDatasource } from '../datasource'; +import { SelectableResourceValue } from '../resources/types'; import { CloudWatchJsonData, CloudWatchSecureJsonData } from '../types'; import { LogGroupsField } from './LogGroups/LogGroupsField'; @@ -45,7 +45,7 @@ export const ConfigEditor: FC = (props: Props) => { loadRegions={ datasource && (async () => { - return datasource.api + return datasource.resources .getRegions() .then((regions) => regions.reduce( diff --git a/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.test.tsx b/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.test.tsx index 7b038b53379..6132944f607 100644 --- a/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.test.tsx @@ -10,9 +10,9 @@ const ds = setupMockedDataSource({ variables: [], }); -ds.datasource.api.getNamespaces = jest.fn().mockResolvedValue([]); -ds.datasource.api.getMetrics = jest.fn().mockResolvedValue([]); -ds.datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]); +ds.datasource.resources.getNamespaces = jest.fn().mockResolvedValue([]); +ds.datasource.resources.getMetrics = jest.fn().mockResolvedValue([]); +ds.datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([]); ds.datasource.getVariables = jest.fn().mockReturnValue([]); const q: CloudWatchMetricsQuery = { id: '', diff --git a/public/app/plugins/datasource/cloudwatch/components/Dimensions/FilterItem.tsx b/public/app/plugins/datasource/cloudwatch/components/Dimensions/FilterItem.tsx index 4dea46fead0..ed814a6d99a 100644 --- a/public/app/plugins/datasource/cloudwatch/components/Dimensions/FilterItem.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/Dimensions/FilterItem.tsx @@ -51,7 +51,7 @@ export const FilterItem: FunctionComponent = ({ return []; } - return datasource.api + return datasource.resources .getDimensionValues({ dimensionKey: filter.key, dimensionFilters: dimensionsExcludingCurrentKey, diff --git a/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsField.test.tsx b/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsField.test.tsx index 4e8d6684a76..b670c8486ae 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsField.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsField.test.tsx @@ -32,13 +32,13 @@ describe('LogGroupSelection', () => { it('should call getLogGroups to get associated log group arns and then update props if rendered with legacy log group names', async () => { config.featureToggles.cloudWatchCrossAccountQuerying = true; - defaultProps.datasource.api.getLogGroups = jest + defaultProps.datasource.resources.getLogGroups = jest .fn() .mockResolvedValue([{ value: { arn: 'arn', name: 'loggroupname' } }]); render(); await waitFor(async () => expect(screen.getByText('Select Log Groups')).toBeInTheDocument()); - expect(defaultProps.datasource.api.getLogGroups).toHaveBeenCalledWith({ + expect(defaultProps.datasource.resources.getLogGroups).toHaveBeenCalledWith({ region: defaultProps.region, logGroupNamePrefix: 'loggroupname', }); @@ -48,14 +48,14 @@ describe('LogGroupSelection', () => { it('should not call getLogGroups to get associated log group arns for template variables that were part of the legacy log group names array, only include them in the call to onChange', async () => { config.featureToggles.cloudWatchCrossAccountQuerying = true; defaultProps.datasource = setupMockedDataSource({ variables: [logGroupNamesVariable] }).datasource; - defaultProps.datasource.api.getLogGroups = jest + defaultProps.datasource.resources.getLogGroups = jest .fn() .mockResolvedValue([{ value: { arn: 'arn', name: 'loggroupname' } }]); render(); await waitFor(async () => expect(screen.getByText('Select Log Groups')).toBeInTheDocument()); - expect(defaultProps.datasource.api.getLogGroups).toHaveBeenCalledTimes(1); - expect(defaultProps.datasource.api.getLogGroups).toHaveBeenCalledWith({ + expect(defaultProps.datasource.resources.getLogGroups).toHaveBeenCalledTimes(1); + expect(defaultProps.datasource.resources.getLogGroups).toHaveBeenCalledWith({ region: defaultProps.region, logGroupNamePrefix: 'loggroupname', }); @@ -67,12 +67,12 @@ describe('LogGroupSelection', () => { it('should not call getLogGroups and update props if rendered with log groups', async () => { config.featureToggles.cloudWatchCrossAccountQuerying = true; - defaultProps.datasource.api.getLogGroups = jest + defaultProps.datasource.resources.getLogGroups = jest .fn() .mockResolvedValue([{ value: { arn: 'arn', name: 'loggroupname' } }]); render(); await waitFor(() => expect(screen.getByText('Select Log Groups')).toBeInTheDocument()); - expect(defaultProps.datasource.api.getLogGroups).not.toHaveBeenCalled(); + expect(defaultProps.datasource.resources.getLogGroups).not.toHaveBeenCalled(); expect(defaultProps.onChange).not.toHaveBeenCalled(); }); }); diff --git a/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsField.tsx b/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsField.tsx index 1ec17498541..efb53a7c8fb 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsField.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsField.tsx @@ -3,7 +3,8 @@ import React, { useEffect, useState } from 'react'; import { CloudWatchDatasource } from '../../datasource'; import { useAccountOptions } from '../../hooks'; -import { DescribeLogGroupsRequest, LogGroup } from '../../types'; +import { DescribeLogGroupsRequest } from '../../resources/types'; +import { LogGroup } from '../../types'; import { isTemplateVariable } from '../../utils/templateVariableUtils'; import { LogGroupsSelector } from './LogGroupsSelector'; @@ -32,7 +33,7 @@ export const LogGroupsField = ({ maxNoOfVisibleLogGroups, onBeforeOpen, }: Props) => { - const accountState = useAccountOptions(datasource?.api, region); + const accountState = useAccountOptions(datasource?.resources, region); const [loadingLogGroupsStarted, setLoadingLogGroupsStarted] = useState(false); useEffect(() => { @@ -41,13 +42,15 @@ export const LogGroupsField = ({ setLoadingLogGroupsStarted(true); // there's no need to migrate variables, they will be taken care of in the logs query runner - const variables = legacyLogGroupNames.filter((lgn) => isTemplateVariable(datasource.api.templateSrv, lgn)); + const variables = legacyLogGroupNames.filter((lgn) => isTemplateVariable(datasource.resources.templateSrv, lgn)); const legacyLogGroupNameValues = legacyLogGroupNames.filter( - (lgn) => !isTemplateVariable(datasource.api.templateSrv, lgn) + (lgn) => !isTemplateVariable(datasource.resources.templateSrv, lgn) ); Promise.all( - legacyLogGroupNameValues.map((lg) => datasource.api.getLogGroups({ region: region, logGroupNamePrefix: lg })) + legacyLogGroupNameValues.map((lg) => + datasource.resources.getLogGroups({ region: region, logGroupNamePrefix: lg }) + ) ) .then((results) => { const logGroups = results.flatMap((r) => @@ -70,7 +73,7 @@ export const LogGroupsField = ({
) => - datasource?.api.getLogGroups({ region: region, ...params }) ?? [] + datasource?.resources.getLogGroups({ region: region, ...params }) ?? [] } onChange={onChange} accountOptions={accountState.value} diff --git a/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsSelector.test.tsx b/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsSelector.test.tsx index f65f4551d4a..62b22c6a361 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsSelector.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsSelector.test.tsx @@ -5,7 +5,7 @@ import lodash from 'lodash'; import React from 'react'; import selectEvent from 'react-select-event'; -import { ResourceResponse, LogGroupResponse } from '../../types'; +import { ResourceResponse, LogGroupResponse } from '../../resources/types'; import { LogGroupsSelector } from './LogGroupsSelector'; diff --git a/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsSelector.tsx b/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsSelector.tsx index 03b471beeca..a9949551ea4 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsSelector.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogGroups/LogGroupsSelector.tsx @@ -5,7 +5,8 @@ import { EditorField, Space } from '@grafana/experimental'; import { Button, Checkbox, Icon, Label, LoadingPlaceholder, Modal, Select, useStyles2 } from '@grafana/ui'; import Search from '../../Search'; -import { DescribeLogGroupsRequest, LogGroup, LogGroupResponse, ResourceResponse } from '../../types'; +import { DescribeLogGroupsRequest, ResourceResponse, LogGroupResponse } from '../../resources/types'; +import { LogGroup } from '../../types'; import { Account, ALL_ACCOUNTS_OPTION } from '../Account'; import getStyles from '../styles'; diff --git a/public/app/plugins/datasource/cloudwatch/components/MetricStatEditor/MetricStatEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/MetricStatEditor/MetricStatEditor.test.tsx index 815cbdda001..9393625583b 100644 --- a/public/app/plugins/datasource/cloudwatch/components/MetricStatEditor/MetricStatEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/MetricStatEditor/MetricStatEditor.test.tsx @@ -15,9 +15,9 @@ const ds = setupMockedDataSource({ variables: [], }); -ds.datasource.api.getNamespaces = jest.fn().mockResolvedValue([]); -ds.datasource.api.getMetrics = jest.fn().mockResolvedValue([]); -ds.datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]); +ds.datasource.resources.getNamespaces = jest.fn().mockResolvedValue([]); +ds.datasource.resources.getMetrics = jest.fn().mockResolvedValue([]); +ds.datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([]); ds.datasource.getVariables = jest.fn().mockReturnValue([]); const metricStat: MetricStat = { region: 'us-east-2', @@ -121,8 +121,8 @@ describe('MetricStatEditor', () => { }; beforeEach(() => { - propsNamespaceMetrics.datasource.api.getNamespaces = jest.fn().mockResolvedValue(namespaces); - propsNamespaceMetrics.datasource.api.getMetrics = jest.fn().mockResolvedValue(metrics); + propsNamespaceMetrics.datasource.resources.getNamespaces = jest.fn().mockResolvedValue(namespaces); + propsNamespaceMetrics.datasource.resources.getMetrics = jest.fn().mockResolvedValue(metrics); onChange.mockClear(); }); @@ -146,7 +146,7 @@ describe('MetricStatEditor', () => { }); it('should remove metricName from metricStat if it does not exist in new namespace', async () => { - propsNamespaceMetrics.datasource.api.getMetrics = jest.fn().mockImplementation(({ namespace, region }) => { + propsNamespaceMetrics.datasource.resources.getMetrics = jest.fn().mockImplementation(({ namespace, region }) => { let mockMetrics = namespace === 'n1' && region === props.metricStat.region ? metrics @@ -202,8 +202,8 @@ describe('MetricStatEditor', () => { it('should set value to "all" when its a monitoring account and no account id is defined in the query', async () => { config.featureToggles.cloudWatchCrossAccountQuerying = true; const onChange = jest.fn(); - props.datasource.api.isMonitoringAccount = jest.fn().mockResolvedValue(true); - props.datasource.api.getAccounts = jest.fn().mockResolvedValue([ + props.datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(true); + props.datasource.resources.getAccounts = jest.fn().mockResolvedValue([ { value: '123456789', label: 'test-account1', @@ -231,8 +231,8 @@ describe('MetricStatEditor', () => { it('should unset value when no accounts were found and an account id is defined in the query', async () => { config.featureToggles.cloudWatchCrossAccountQuerying = true; const onChange = jest.fn(); - props.datasource.api.isMonitoringAccount = jest.fn().mockResolvedValue(false); - props.datasource.api.getAccounts = jest.fn().mockResolvedValue([]); + props.datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(false); + props.datasource.resources.getAccounts = jest.fn().mockResolvedValue([]); await act(async () => { render( { - datasource.api.isMonitoringAccount(metricStat.region).then((isMonitoringAccount) => { + datasource.resources.isMonitoringAccount(metricStat.region).then((isMonitoringAccount) => { if (isMonitoringAccount && !accountState.loading && accountState.value?.length && !metricStat.accountId) { onChange({ ...metricStat, accountId: 'all' }); } @@ -43,7 +43,7 @@ export function MetricStatEditor({ onChange({ ...metricStat, accountId: undefined }); } }); - }, [accountState, metricStat, onChange, datasource.api]); + }, [accountState, metricStat, onChange, datasource.resources]); const onNamespaceChange = async (metricStat: MetricStat) => { const validatedQuery = await validateMetricName(metricStat); @@ -55,7 +55,7 @@ export function MetricStatEditor({ if (!metricName) { return metricStat; } - await datasource.api.getMetrics({ namespace, region }).then((result: Array>) => { + await datasource.resources.getMetrics({ namespace, region }).then((result: Array>) => { if (!result.find((metric) => metric.value === metricName)) { metricName = ''; } diff --git a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx index bac03da437f..42be2690b75 100644 --- a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx @@ -46,11 +46,11 @@ const setup = () => { const datasource = new CloudWatchDatasource(instanceSettings, templateSrv as any, {} as any); datasource.metricFindQuery = async () => [{ value: 'test', label: 'test', text: 'test' }]; - datasource.api.getNamespaces = jest.fn().mockResolvedValue([]); - datasource.api.getMetrics = jest.fn().mockResolvedValue([]); - datasource.api.getRegions = jest.fn().mockResolvedValue([]); - datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]); - datasource.api.isMonitoringAccount = jest.fn().mockResolvedValue(false); + datasource.resources.getNamespaces = jest.fn().mockResolvedValue([]); + datasource.resources.getMetrics = jest.fn().mockResolvedValue([]); + datasource.resources.getRegions = jest.fn().mockResolvedValue([]); + datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([]); + datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(false); const props: Props = { query: { @@ -93,7 +93,9 @@ describe('QueryEditor', () => { if (props.query.queryMode !== 'Metrics') { fail(`expected props.query.queryMode to be 'Metrics', got '${props.query.queryMode}' instead`); } - props.datasource.api.getDimensionValues = jest.fn().mockResolvedValue([[{ label: 'dimVal1', value: 'dimVal1' }]]); + props.datasource.resources.getDimensionValues = jest + .fn() + .mockResolvedValue([[{ label: 'dimVal1', value: 'dimVal1' }]]); props.query.metricQueryType = MetricQueryType.Search; props.query.metricEditorMode = MetricEditorMode.Builder; props.query.dimensions = { instanceId: 'instance-123' }; diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor.test.tsx index a7782c0f3a8..dd1ad95e8d6 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor.test.tsx @@ -165,9 +165,9 @@ describe('QueryEditor should render right editor', () => { let datasourceMock: ReturnType; beforeEach(() => { datasourceMock = setupMockedDataSource(); - datasourceMock.datasource.api.isMonitoringAccount = jest.fn().mockResolvedValue(true); - datasourceMock.datasource.api.getMetrics = jest.fn().mockResolvedValue([]); - datasourceMock.datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]); + datasourceMock.datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(true); + datasourceMock.datasource.resources.getMetrics = jest.fn().mockResolvedValue([]); + datasourceMock.datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([]); originalValue = config.featureToggles.cloudWatchCrossAccountQuerying; }); afterEach(() => { diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryHeader.test.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryHeader.test.tsx index 3466b9fcae0..2e782cc07a1 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryHeader.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryHeader.test.tsx @@ -13,7 +13,7 @@ const originalFeatureToggleValue = config.featureToggles.cloudWatchCrossAccountQ const ds = setupMockedDataSource({ variables: [], }); -ds.datasource.api.getRegions = jest.fn().mockResolvedValue([]); +ds.datasource.resources.getRegions = jest.fn().mockResolvedValue([]); describe('QueryHeader', () => { afterEach(() => { @@ -22,14 +22,14 @@ describe('QueryHeader', () => { describe('when changing region', () => { const { datasource } = setupMockedDataSource(); - datasource.api.getRegions = jest.fn().mockResolvedValue([ + datasource.resources.getRegions = jest.fn().mockResolvedValue([ { value: 'us-east-2', label: 'us-east-2' }, { value: 'us-east-1', label: 'us-east-1' }, ]); it('should reset account id if new region is not monitoring account', async () => { config.featureToggles.cloudWatchCrossAccountQuerying = true; const onChange = jest.fn(); - datasource.api.isMonitoringAccount = jest.fn().mockResolvedValue(false); + datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(false); render( { it('should not reset account id if new region is a monitoring account', async () => { config.featureToggles.cloudWatchCrossAccountQuerying = true; const onChange = jest.fn(); - datasource.api.isMonitoringAccount = jest.fn().mockResolvedValue(true); + datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(true); render( { it('should not call isMonitoringAccount if its a logs query', async () => { config.featureToggles.cloudWatchCrossAccountQuerying = true; const onChange = jest.fn(); - datasource.api.isMonitoringAccount = jest.fn().mockResolvedValue(true); + datasource.resources.isMonitoringAccount = jest.fn().mockResolvedValue(true); render( { await act(async () => { await selectEvent.select(screen.getByLabelText(/Region/), 'us-east-2', { container: document.body }); }); - expect(datasource.api.isMonitoringAccount).not.toHaveBeenCalledWith('us-east-2'); + expect(datasource.resources.isMonitoringAccount).not.toHaveBeenCalledWith('us-east-2'); }); it('should not call isMonitoringAccount if feature toggle is not enabled', async () => { config.featureToggles.cloudWatchCrossAccountQuerying = false; const onChange = jest.fn(); - datasource.api.isMonitoringAccount = jest.fn(); + datasource.resources.isMonitoringAccount = jest.fn(); render( { await act(async () => { await selectEvent.select(screen.getByLabelText(/Region/), 'us-east-2', { container: document.body }); }); - expect(datasource.api.isMonitoringAccount).not.toHaveBeenCalledWith(); + expect(datasource.resources.isMonitoringAccount).not.toHaveBeenCalledWith(); }); }); }); diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx index 77235f8a7bc..76f05ee0468 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx @@ -32,7 +32,7 @@ const QueryHeader: React.FC = ({ onRunQuery, }) => { const { queryMode, region } = query; - const isMonitoringAccount = useIsMonitoringAccount(datasource.api, query.region); + const isMonitoringAccount = useIsMonitoringAccount(datasource.resources, query.region); const [regions, regionIsLoading] = useRegions(datasource); const onQueryModeChange = ({ value }: SelectableValue) => { @@ -46,7 +46,7 @@ const QueryHeader: React.FC = ({ }; const onRegionChange = async (region: string) => { if (config.featureToggles.cloudWatchCrossAccountQuerying && isCloudWatchMetricsQuery(query)) { - const isMonitoringAccount = await datasource.api.isMonitoringAccount(region); + const isMonitoringAccount = await datasource.resources.isMonitoringAccount(region); onChange({ ...query, region, accountId: isMonitoringAccount ? query.accountId : undefined }); } else { onChange({ ...query, region }); diff --git a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderEditor.test.tsx index bffc5440a59..c9bc216a967 100644 --- a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderEditor.test.tsx @@ -22,10 +22,10 @@ export const makeSQLQuery = (sql?: SQLExpression): CloudWatchMetricsQuery => ({ describe('Cloudwatch SQLBuilderEditor', () => { beforeEach(() => { - datasource.api.getNamespaces = jest.fn().mockResolvedValue([]); - datasource.api.getMetrics = jest.fn().mockResolvedValue([]); - datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]); - datasource.api.getDimensionValues = jest.fn().mockResolvedValue([]); + datasource.resources.getNamespaces = jest.fn().mockResolvedValue([]); + datasource.resources.getMetrics = jest.fn().mockResolvedValue([]); + datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([]); + datasource.resources.getDimensionValues = jest.fn().mockResolvedValue([]); }); const baseProps = { @@ -46,7 +46,7 @@ describe('Cloudwatch SQLBuilderEditor', () => { }); render(); - await waitFor(() => expect(datasource.api.getNamespaces).toHaveBeenCalled()); + await waitFor(() => expect(datasource.resources.getNamespaces).toHaveBeenCalled()); expect(screen.getByText('AWS/EC2')).toBeInTheDocument(); expect(screen.getByLabelText('With schema')).not.toBeChecked(); @@ -67,7 +67,7 @@ describe('Cloudwatch SQLBuilderEditor', () => { }); render(); - await waitFor(() => expect(datasource.api.getNamespaces).toHaveBeenCalled()); + await waitFor(() => expect(datasource.resources.getNamespaces).toHaveBeenCalled()); expect(screen.getByText('AWS/EC2')).toBeInTheDocument(); expect(screen.getByLabelText('With schema')).toBeChecked(); @@ -94,7 +94,7 @@ describe('Cloudwatch SQLBuilderEditor', () => { render(); await waitFor(() => - expect(datasource.api.getDimensionKeys).toHaveBeenCalledWith({ + expect(datasource.resources.getDimensionKeys).toHaveBeenCalledWith({ namespace: 'AWS/EC2', region: query.region, dimensionFilters: { InstanceId: null }, @@ -121,7 +121,7 @@ describe('Cloudwatch SQLBuilderEditor', () => { }); render(); - await waitFor(() => expect(datasource.api.getNamespaces).toHaveBeenCalled()); + await waitFor(() => expect(datasource.resources.getNamespaces).toHaveBeenCalled()); expect(screen.getByText('AVERAGE')).toBeInTheDocument(); expect(screen.getByText('CPUUtilization')).toBeInTheDocument(); @@ -137,7 +137,7 @@ describe('Cloudwatch SQLBuilderEditor', () => { }); render(); - await waitFor(() => expect(datasource.api.getNamespaces).toHaveBeenCalled()); + await waitFor(() => expect(datasource.resources.getNamespaces).toHaveBeenCalled()); expect(screen.getByText('AVG')).toBeInTheDocument(); const directionElement = screen.getByLabelText('Direction'); @@ -149,7 +149,7 @@ describe('Cloudwatch SQLBuilderEditor', () => { const query = makeSQLQuery({}); render(); - await waitFor(() => expect(datasource.api.getNamespaces).toHaveBeenCalled()); + await waitFor(() => expect(datasource.resources.getNamespaces).toHaveBeenCalled()); expect(screen.queryByText('AVG')).toBeNull(); const directionElement = screen.getByLabelText('Direction'); diff --git a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderSelectRow.test.tsx b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderSelectRow.test.tsx index 8f8c2762340..7e9f61025b7 100644 --- a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderSelectRow.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderSelectRow.test.tsx @@ -60,15 +60,15 @@ const metrics = [ describe('Cloudwatch SQLBuilderSelectRow', () => { beforeEach(() => { - datasource.api.getNamespaces = jest.fn().mockResolvedValue(namespaces); - datasource.api.getMetrics = jest.fn().mockResolvedValue([]); - datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]); - datasource.api.getDimensionValues = jest.fn().mockResolvedValue([]); + datasource.resources.getNamespaces = jest.fn().mockResolvedValue(namespaces); + datasource.resources.getMetrics = jest.fn().mockResolvedValue([]); + datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([]); + datasource.resources.getDimensionValues = jest.fn().mockResolvedValue([]); onQueryChange.mockReset(); }); it('Should not reset metricName when selecting a namespace if metric exist in new namespace', async () => { - datasource.api.getMetrics = jest.fn().mockResolvedValue(metrics); + datasource.resources.getMetrics = jest.fn().mockResolvedValue(metrics); await act(async () => { render(); @@ -103,7 +103,7 @@ describe('Cloudwatch SQLBuilderSelectRow', () => { }); it('Should reset metricName when selecting a namespace if metric does not exist in new namespace', async () => { - datasource.api.getMetrics = jest.fn().mockImplementation((namespace: string, region: string) => { + datasource.resources.getMetrics = jest.fn().mockImplementation((namespace: string, region: string) => { let mockMetrics = namespace === 'n1' && region === baseProps.query.region ? metrics diff --git a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderSelectRow.tsx b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderSelectRow.tsx index ab1a8551dc2..350d36e5cb2 100644 --- a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderSelectRow.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLBuilderSelectRow.tsx @@ -68,7 +68,7 @@ const SQLBuilderSelectRow: React.FC = ({ datasource, q const validateMetricName = async (query: CloudWatchMetricsQuery) => { let { region, sql, namespace } = query; - await datasource.api.getMetrics({ namespace, region }).then((result: Array>) => { + await datasource.resources.getMetrics({ namespace, region }).then((result: Array>) => { if (!result.some((metric) => metric.value === metricName)) { sql = removeMetricName(query).sql; } diff --git a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLFilter.tsx b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLFilter.tsx index c5856f3632b..1bbea200ba5 100644 --- a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLFilter.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLFilter.tsx @@ -109,7 +109,7 @@ const FilterItem: React.FC = (props) => { return []; } - return datasource.api + return datasource.resources .getDimensionValues({ region: query.region, namespace, metricName, dimensionKey: filter.property.name }) .then((result: Array>) => { return appendTemplateVariables(datasource, result); diff --git a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLGroupBy.test.tsx b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLGroupBy.test.tsx index 2492bcb2b11..6ca453eef3f 100644 --- a/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLGroupBy.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/SQLBuilderEditor/SQLGroupBy.test.tsx @@ -22,7 +22,7 @@ const makeSQLQuery = (sql?: SQLExpression): CloudWatchMetricsQuery => ({ sql: sql, }); -datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([]); +datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([]); describe('Cloudwatch SQLGroupBy', () => { const baseProps = { diff --git a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.test.tsx index e8cc5db0a2a..b2a065e8050 100644 --- a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.test.tsx @@ -4,7 +4,8 @@ import React from 'react'; import { select } from 'react-select-event'; import { setupMockedDataSource } from '../../__mocks__/CloudWatchDataSource'; -import { GetDimensionKeysRequest, VariableQueryType } from '../../types'; +import { GetDimensionKeysRequest } from '../../resources/types'; +import { VariableQueryType } from '../../types'; import { VariableQueryEditor, Props } from './VariableQueryEditor'; @@ -22,22 +23,22 @@ const defaultQuery = { const ds = setupMockedDataSource(); -ds.datasource.api.getRegions = jest.fn().mockResolvedValue([ +ds.datasource.resources.getRegions = jest.fn().mockResolvedValue([ { label: 'a1', value: 'a1' }, { label: 'b1', value: 'b1' }, { label: 'c1', value: 'c1' }, ]); -ds.datasource.api.getNamespaces = jest.fn().mockResolvedValue([ +ds.datasource.resources.getNamespaces = jest.fn().mockResolvedValue([ { label: 'x2', value: 'x2' }, { label: 'y2', value: 'y2' }, { label: 'z2', value: 'z2' }, ]); -ds.datasource.api.getMetrics = jest.fn().mockResolvedValue([ +ds.datasource.resources.getMetrics = jest.fn().mockResolvedValue([ { label: 'h3', value: 'h3' }, { label: 'i3', value: 'i3' }, { label: 'j3', value: 'j3' }, ]); -ds.datasource.api.getDimensionKeys = jest +ds.datasource.resources.getDimensionKeys = jest .fn() .mockImplementation(({ namespace: region, dimensionFilters }: GetDimensionKeysRequest) => { if (!!dimensionFilters) { @@ -55,12 +56,12 @@ ds.datasource.api.getDimensionKeys = jest } return Promise.resolve([{ label: 't4', value: 't4' }]); }); -ds.datasource.api.getDimensionValues = jest.fn().mockResolvedValue([ +ds.datasource.resources.getDimensionValues = jest.fn().mockResolvedValue([ { label: 'foo', value: 'foo' }, { label: 'bar', value: 'bar' }, ]); ds.datasource.getVariables = jest.fn().mockReturnValue([]); -ds.datasource.api.getEc2InstanceAttribute = jest.fn().mockReturnValue([]); +ds.datasource.resources.getEc2InstanceAttribute = jest.fn().mockReturnValue([]); const onChange = jest.fn(); const defaultProps: Props = { @@ -142,7 +143,7 @@ describe('VariableEditor', () => { await select(keySelect, 'v4', { container: document.body, }); - expect(ds.datasource.api.getDimensionKeys).toHaveBeenCalledWith({ + expect(ds.datasource.resources.getDimensionKeys).toHaveBeenCalledWith({ namespace: 'z2', region: 'a1', metricName: 'i3', @@ -232,8 +233,8 @@ describe('VariableEditor', () => { }) ); - expect(ds.datasource.api.getMetrics).toHaveBeenCalledWith({ namespace: 'z2', region: 'b1' }); - expect(ds.datasource.api.getDimensionKeys).toHaveBeenCalledWith({ namespace: 'z2', region: 'b1' }); + expect(ds.datasource.resources.getMetrics).toHaveBeenCalledWith({ namespace: 'z2', region: 'b1' }); + expect(ds.datasource.resources.getDimensionKeys).toHaveBeenCalledWith({ namespace: 'z2', region: 'b1' }); expect(props.onChange).toHaveBeenCalledWith({ ...defaultQuery, refId: 'CloudWatchVariableQueryEditor-VariableQuery', diff --git a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.tsx index 5e474a1cf51..02355d2566d 100644 --- a/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/VariableQueryEditor/VariableQueryEditor.tsx @@ -69,19 +69,21 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => { const sanitizeQuery = async (query: VariableQuery) => { let { metricName, dimensionKey, dimensionFilters, namespace, region } = query; if (metricName) { - await datasource.api.getMetrics({ namespace, region }).then((result: Array>) => { + await datasource.resources.getMetrics({ namespace, region }).then((result: Array>) => { if (!result.find((metric) => metric.value === metricName)) { metricName = ''; } }); } if (dimensionKey) { - await datasource.api.getDimensionKeys({ namespace, region }).then((result: Array>) => { - if (!result.find((key) => key.value === dimensionKey)) { - dimensionKey = ''; - dimensionFilters = {}; - } - }); + await datasource.resources + .getDimensionKeys({ namespace, region }) + .then((result: Array>) => { + if (!result.find((key) => key.value === dimensionKey)) { + dimensionKey = ''; + dimensionFilters = {}; + } + }); } return { ...query, metricName, dimensionKey, dimensionFilters }; }; diff --git a/public/app/plugins/datasource/cloudwatch/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/datasource.test.ts index 2be3be9381b..92bf5f4f1e8 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.test.ts @@ -280,7 +280,7 @@ describe('datasource', () => { }, ]), }).datasource; - const allMetrics = await datasource.api.getAllMetrics({ region: 'us-east-2' }); + const allMetrics = await datasource.resources.getAllMetrics({ region: 'us-east-2' }); expect(allMetrics[0].metricName).toEqual('CPUUtilization'); expect(allMetrics[0].namespace).toEqual('AWS/EC2'); expect(allMetrics[1].metricName).toEqual('CPUPercentage'); diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 835c57385db..9651a795da9 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -19,7 +19,6 @@ import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_sr import { RowContextOptions } from '../../../features/logs/components/LogRowContextProvider'; import { CloudWatchAnnotationSupport } from './annotationSupport'; -import { CloudWatchAPI } from './api'; import { SQLCompletionItemProvider } from './cloudwatch-sql/completion/CompletionItemProvider'; import { DEFAULT_METRICS_QUERY, getDefaultLogsQuery } from './defaultQueries'; import { isCloudWatchAnnotationQuery, isCloudWatchLogsQuery, isCloudWatchMetricsQuery } from './guards'; @@ -28,6 +27,7 @@ import { MetricMathCompletionItemProvider } from './metric-math/completion/Compl import { CloudWatchAnnotationQueryRunner } from './query-runner/CloudWatchAnnotationQueryRunner'; import { CloudWatchLogsQueryRunner } from './query-runner/CloudWatchLogsQueryRunner'; import { CloudWatchMetricsQueryRunner } from './query-runner/CloudWatchMetricsQueryRunner'; +import { ResourcesAPI } from './resources/ResourcesAPI'; import { CloudWatchAnnotationQuery, CloudWatchJsonData, @@ -52,7 +52,7 @@ export class CloudWatchDatasource private metricsQueryRunner: CloudWatchMetricsQueryRunner; private annotationQueryRunner: CloudWatchAnnotationQueryRunner; logsQueryRunner: CloudWatchLogsQueryRunner; - api: CloudWatchAPI; + resources: ResourcesAPI; constructor( private instanceSettings: DataSourceInstanceSettings, @@ -61,14 +61,14 @@ export class CloudWatchDatasource ) { super(instanceSettings); this.defaultRegion = instanceSettings.jsonData.defaultRegion; - this.api = new CloudWatchAPI(instanceSettings, templateSrv); + this.resources = new ResourcesAPI(instanceSettings, templateSrv); this.languageProvider = new CloudWatchLanguageProvider(this); - this.sqlCompletionItemProvider = new SQLCompletionItemProvider(this.api, this.templateSrv); - this.metricMathCompletionItemProvider = new MetricMathCompletionItemProvider(this.api, this.templateSrv); + this.sqlCompletionItemProvider = new SQLCompletionItemProvider(this.resources, this.templateSrv); + this.metricMathCompletionItemProvider = new MetricMathCompletionItemProvider(this.resources, this.templateSrv); this.metricsQueryRunner = new CloudWatchMetricsQueryRunner(instanceSettings, templateSrv); this.logsQueryRunner = new CloudWatchLogsQueryRunner(instanceSettings, templateSrv, timeSrv); this.annotationQueryRunner = new CloudWatchAnnotationQueryRunner(instanceSettings, templateSrv); - this.variables = new CloudWatchVariableSupport(this.api); + this.variables = new CloudWatchVariableSupport(this.resources); this.annotations = CloudWatchAnnotationSupport; } @@ -167,7 +167,7 @@ export class CloudWatchDatasource // public getVariables() { - return this.api.getVariables(); + return this.resources.getVariables(); } getActualRegion(region?: string) { diff --git a/public/app/plugins/datasource/cloudwatch/hooks.test.ts b/public/app/plugins/datasource/cloudwatch/hooks.test.ts index 4512ee8723c..97349de75a5 100644 --- a/public/app/plugins/datasource/cloudwatch/hooks.test.ts +++ b/public/app/plugins/datasource/cloudwatch/hooks.test.ts @@ -2,7 +2,6 @@ import { renderHook } from '@testing-library/react-hooks'; import { config } from '@grafana/runtime'; -import { setupMockedAPI } from './__mocks__/API'; import { accountIdVariable, dimensionVariable, @@ -11,6 +10,7 @@ import { regionVariable, setupMockedDataSource, } from './__mocks__/CloudWatchDataSource'; +import { setupMockedResourcesAPI } from './__mocks__/ResourcesAPI'; import { useAccountOptions, useDimensionKeys, useIsMonitoringAccount, useMetrics } from './hooks'; const WAIT_OPTIONS = { @@ -26,7 +26,7 @@ describe('hooks', () => { describe('useIsMonitoringAccount', () => { it('should interpolate variables before calling api', async () => { config.featureToggles.cloudWatchCrossAccountQuerying = true; - const { api } = setupMockedAPI({ + const { api } = setupMockedResourcesAPI({ variables: [regionVariable], }); const isMonitoringAccountMock = jest.fn().mockResolvedValue(true); @@ -44,7 +44,7 @@ describe('hooks', () => { variables: [regionVariable, namespaceVariable, accountIdVariable], }); const getMetricsMock = jest.fn().mockResolvedValue([]); - datasource.api.getMetrics = getMetricsMock; + datasource.resources.getMetrics = getMetricsMock; const { waitForNextUpdate } = renderHook(() => useMetrics(datasource, { @@ -70,7 +70,7 @@ describe('hooks', () => { variables: [regionVariable, namespaceVariable, accountIdVariable, metricVariable, dimensionVariable], }); const getDimensionKeysMock = jest.fn().mockResolvedValue([]); - datasource.api.getDimensionKeys = getDimensionKeysMock; + datasource.resources.getDimensionKeys = getDimensionKeysMock; const { waitForNextUpdate } = renderHook(() => useDimensionKeys(datasource, { @@ -100,7 +100,7 @@ describe('hooks', () => { describe('useAccountOptions', () => { it('does not call the api if the feature toggle is off', async () => { config.featureToggles.cloudWatchCrossAccountQuerying = false; - const { api } = setupMockedAPI({ + const { api } = setupMockedResourcesAPI({ variables: [regionVariable], }); const getAccountsMock = jest.fn().mockResolvedValue([{ id: '123', label: 'accountLabel' }]); @@ -112,7 +112,7 @@ describe('hooks', () => { it('interpolates region variables before calling the api', async () => { config.featureToggles.cloudWatchCrossAccountQuerying = true; - const { api } = setupMockedAPI({ + const { api } = setupMockedResourcesAPI({ variables: [regionVariable], }); const getAccountsMock = jest.fn().mockResolvedValue([{ id: '123', label: 'accountLabel' }]); @@ -125,7 +125,7 @@ describe('hooks', () => { it('returns properly formatted account options, and template variables', async () => { config.featureToggles.cloudWatchCrossAccountQuerying = true; - const { api } = setupMockedAPI({ + const { api } = setupMockedResourcesAPI({ variables: [regionVariable], }); const getAccountsMock = jest.fn().mockResolvedValue([{ id: '123', label: 'accountLabel' }]); diff --git a/public/app/plugins/datasource/cloudwatch/hooks.ts b/public/app/plugins/datasource/cloudwatch/hooks.ts index 04fefb1b5d4..a4a6738580a 100644 --- a/public/app/plugins/datasource/cloudwatch/hooks.ts +++ b/public/app/plugins/datasource/cloudwatch/hooks.ts @@ -4,9 +4,9 @@ import { useAsyncFn, useDeepCompareEffect } from 'react-use'; import { SelectableValue, toOption } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { CloudWatchAPI } from './api'; import { CloudWatchDatasource } from './datasource'; -import { GetDimensionKeysRequest, GetMetricsRequest } from './types'; +import { ResourcesAPI } from './resources/ResourcesAPI'; +import { GetMetricsRequest, GetDimensionKeysRequest } from './resources/types'; import { appendTemplateVariables } from './utils/utils'; export const useRegions = (datasource: CloudWatchDatasource): [Array>, boolean] => { @@ -21,7 +21,7 @@ export const useRegions = (datasource: CloudWatchDatasource): [Array>) => setRegions([...regions, variableOptionGroup])) .finally(() => setRegionsIsLoading(false)); @@ -33,7 +33,7 @@ export const useRegions = (datasource: CloudWatchDatasource): [Array { const [namespaces, setNamespaces] = useState>>([]); useEffect(() => { - datasource.api.getNamespaces().then((namespaces) => { + datasource.resources.getNamespaces().then((namespaces) => { setNamespaces(appendTemplateVariables(datasource, namespaces)); }); }, [datasource]); @@ -56,7 +56,7 @@ export const useMetrics = (datasource: CloudWatchDatasource, { region, namespace accountId = datasource.templateSrv.replace(accountId, {}); } useEffect(() => { - datasource.api.getMetrics({ namespace, region, accountId }).then((result: Array>) => { + datasource.resources.getMetrics({ namespace, region, accountId }).then((result: Array>) => { setMetrics(appendTemplateVariables(datasource, result)); }); }, [datasource, region, namespace, accountId]); @@ -87,12 +87,12 @@ export const useDimensionKeys = ( } if (dimensionFilters) { - dimensionFilters = datasource.api.convertDimensionFormat(dimensionFilters, {}); + dimensionFilters = datasource.resources.convertDimensionFormat(dimensionFilters, {}); } // doing deep comparison to avoid making new api calls to list metrics unless dimension filter object props changes useDeepCompareEffect(() => { - datasource.api + datasource.resources .getDimensionKeys({ namespace, region, metricName, accountId, dimensionFilters }) .then((result: Array>) => { setDimensionKeys(appendTemplateVariables(datasource, result)); @@ -102,37 +102,37 @@ export const useDimensionKeys = ( return dimensionKeys; }; -export const useIsMonitoringAccount = (api: CloudWatchAPI, region: string) => { +export const useIsMonitoringAccount = (resources: ResourcesAPI, region: string) => { const [isMonitoringAccount, setIsMonitoringAccount] = useState(false); // we call this before the use effect to ensure dependency array below // receives the interpolated value so that the effect is triggered when a variable is changed if (region) { - region = api.templateSrv.replace(region, {}); + region = resources.templateSrv.replace(region, {}); } useEffect(() => { if (config.featureToggles.cloudWatchCrossAccountQuerying) { - api.isMonitoringAccount(region).then((result) => setIsMonitoringAccount(result)); + resources.isMonitoringAccount(region).then((result) => setIsMonitoringAccount(result)); } - }, [region, api]); + }, [region, resources]); return isMonitoringAccount; }; export const useAccountOptions = ( - api: Pick | undefined, + resources: Pick | undefined, region: string ) => { // we call this before the use effect to ensure dependency array below // receives the interpolated value so that the effect is triggered when a variable is changed if (region) { - region = api?.templateSrv.replace(region, {}) ?? ''; + region = resources?.templateSrv.replace(region, {}) ?? ''; } const fetchAccountOptions = async () => { if (!config.featureToggles.cloudWatchCrossAccountQuerying) { return Promise.resolve([]); } - const accounts = (await api?.getAccounts({ region })) ?? []; + const accounts = (await resources?.getAccounts({ region })) ?? []; if (accounts.length === 0) { return []; } @@ -143,7 +143,7 @@ export const useAccountOptions = ( description: a.id, })); - const variableOptions = api?.getVariables().map(toOption) || []; + const variableOptions = resources?.getVariables().map(toOption) || []; const variableOptionGroup: SelectableValue = { label: 'Template Variables', @@ -153,11 +153,11 @@ export const useAccountOptions = ( return [...options, variableOptionGroup]; }; - const [state, doFetch] = useAsyncFn(fetchAccountOptions, [api, region]); + const [state, doFetch] = useAsyncFn(fetchAccountOptions, [resources, region]); useEffect(() => { doFetch(); - }, [api, region, doFetch]); + }, [resources, region, doFetch]); return state; }; diff --git a/public/app/plugins/datasource/cloudwatch/language_provider.test.ts b/public/app/plugins/datasource/cloudwatch/language_provider.test.ts index 23ddde3b747..d72ab5ed262 100644 --- a/public/app/plugins/datasource/cloudwatch/language_provider.test.ts +++ b/public/app/plugins/datasource/cloudwatch/language_provider.test.ts @@ -5,6 +5,7 @@ import { TypeaheadOutput } from '@grafana/ui'; import { CloudWatchDatasource } from './datasource'; import { CloudWatchLanguageProvider } from './language_provider'; +import { ResourceResponse } from './resources/types'; import { AGGREGATION_FUNCTIONS_STATS, BOOLEAN_FUNCTIONS, @@ -15,7 +16,7 @@ import { STRING_FUNCTIONS, FIELD_AND_FILTER_FUNCTIONS, } from './syntax'; -import { LogGroupField, ResourceResponse } from './types'; +import { LogGroupField } from './types'; const fields = ['field1', '@message']; @@ -109,7 +110,7 @@ async function runSuggestionTest(query: string, expectedItems: string[][]) { function makeDatasource(): CloudWatchDatasource { return { - api: { + resources: { getLogGroupFields(): Promise>> { return Promise.resolve([{ value: { name: 'field1' } }, { value: { name: '@message' } }]); }, diff --git a/public/app/plugins/datasource/cloudwatch/language_provider.ts b/public/app/plugins/datasource/cloudwatch/language_provider.ts index 6203d27412a..eca53233a6b 100644 --- a/public/app/plugins/datasource/cloudwatch/language_provider.ts +++ b/public/app/plugins/datasource/cloudwatch/language_provider.ts @@ -136,7 +136,7 @@ export class CloudWatchLanguageProvider extends LanguageProvider { ); const results = await Promise.all( interpolatedLogGroups.map((logGroupName) => - this.datasource.api + this.datasource.resources .getLogGroupFields({ logGroupName, region }) .then((fields) => fields.filter((f) => f).map((f) => f.value.name ?? '')) ) diff --git a/public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.test.ts b/public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.test.ts index 0761ed05aa5..2fa961019f4 100644 --- a/public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.test.ts +++ b/public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.test.ts @@ -4,7 +4,7 @@ import { setupMockedTemplateService } from '../../__mocks__/CloudWatchDataSource import * as MetricMathTestData from '../../__mocks__/metric-math-test-data'; import MonacoMock from '../../__mocks__/monarch/Monaco'; import TextModel from '../../__mocks__/monarch/TextModel'; -import { CloudWatchAPI } from '../../api'; +import { ResourcesAPI } from '../../resources/ResourcesAPI'; import cloudWatchMetricMathLanguageDefinition from '../definition'; import { METRIC_MATH_FNS, @@ -20,7 +20,7 @@ const getSuggestions = async (value: string, position: monacoTypes.IPosition) => const setup = new MetricMathCompletionItemProvider( { getActualRegion: () => 'us-east-2', - } as CloudWatchAPI, + } as ResourcesAPI, setupMockedTemplateService([]) ); const monaco = MonacoMock as Monaco; diff --git a/public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.ts b/public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.ts index 9ebfa2729a5..17c061ac534 100644 --- a/public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.ts +++ b/public/app/plugins/datasource/cloudwatch/metric-math/completion/CompletionItemProvider.ts @@ -1,11 +1,11 @@ import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import type { Monaco, monacoTypes } from '@grafana/ui'; -import { CloudWatchAPI } from '../../api'; import { CompletionItemProvider } from '../../monarch/CompletionItemProvider'; import { LinkedToken } from '../../monarch/LinkedToken'; import { TRIGGER_SUGGEST } from '../../monarch/commands'; import { SuggestionKind, CompletionItemPriority, StatementPosition } from '../../monarch/types'; +import { ResourcesAPI } from '../../resources/ResourcesAPI'; import { METRIC_MATH_FNS, METRIC_MATH_KEYWORDS, @@ -21,8 +21,8 @@ import { MetricMathTokenTypes } from './types'; type CompletionItem = monacoTypes.languages.CompletionItem; export class MetricMathCompletionItemProvider extends CompletionItemProvider { - constructor(api: CloudWatchAPI, templateSrv: TemplateSrv = getTemplateSrv()) { - super(api, templateSrv); + constructor(resources: ResourcesAPI, templateSrv: TemplateSrv = getTemplateSrv()) { + super(resources, templateSrv); this.getStatementPosition = getStatementPosition; this.getSuggestionKinds = getSuggestionKinds; this.tokenTypes = MetricMathTokenTypes; diff --git a/public/app/plugins/datasource/cloudwatch/monarch/CompletionItemProvider.ts b/public/app/plugins/datasource/cloudwatch/monarch/CompletionItemProvider.ts index e06b54dac1c..0b74cd46bb2 100644 --- a/public/app/plugins/datasource/cloudwatch/monarch/CompletionItemProvider.ts +++ b/public/app/plugins/datasource/cloudwatch/monarch/CompletionItemProvider.ts @@ -1,7 +1,7 @@ import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import type { Monaco, monacoTypes } from '@grafana/ui'; -import { CloudWatchAPI } from '../api'; +import { ResourcesAPI } from '../resources/ResourcesAPI'; import { LinkedToken } from './LinkedToken'; import { linkedTokenBuilder } from './linkedTokenBuilder'; @@ -18,12 +18,12 @@ CompletionItemProvider is an extendable class which needs to implement : - getSuggestions */ export class CompletionItemProvider implements Completeable { - api: CloudWatchAPI; + resources: ResourcesAPI; templateSrv: TemplateSrv; tokenTypes: TokenTypes; - constructor(api: CloudWatchAPI, templateSrv: TemplateSrv = getTemplateSrv()) { - this.api = api; + constructor(resources: ResourcesAPI, templateSrv: TemplateSrv = getTemplateSrv()) { + this.resources = resources; this.templateSrv = templateSrv; this.templateSrv = templateSrv; diff --git a/public/app/plugins/datasource/cloudwatch/api.test.ts b/public/app/plugins/datasource/cloudwatch/resources/ResourceAPI.test.ts similarity index 90% rename from public/app/plugins/datasource/cloudwatch/api.test.ts rename to public/app/plugins/datasource/cloudwatch/resources/ResourceAPI.test.ts index e786a3f0b4d..371228043ce 100644 --- a/public/app/plugins/datasource/cloudwatch/api.test.ts +++ b/public/app/plugins/datasource/cloudwatch/resources/ResourceAPI.test.ts @@ -1,9 +1,9 @@ -import { setupMockedAPI } from './__mocks__/API'; +import { setupMockedResourcesAPI } from '../__mocks__/ResourcesAPI'; -describe('api', () => { +describe('ResourcesAPI', () => { describe('describeLogGroup', () => { it('replaces region correctly in the query', async () => { - const { api, resourceRequestMock } = setupMockedAPI(); + const { api, resourceRequestMock } = setupMockedResourcesAPI(); await api.getLogGroups({ region: 'default' }); expect(resourceRequestMock.mock.calls[0][1].region).toBe('us-west-1'); @@ -30,7 +30,7 @@ describe('api', () => { }, ]; - const { api } = setupMockedAPI({ response }); + const { api } = setupMockedResourcesAPI({ response }); const expectedLogGroups = [ { text: '/aws/containerinsights/dev303-workshop/application', @@ -58,7 +58,7 @@ describe('api', () => { describe('memoization', () => { it('should not initiate new api request in case a previous request had same args', async () => { const getMock = jest.fn(); - const { api, resourceRequestMock } = setupMockedAPI({ getMock }); + const { api, resourceRequestMock } = setupMockedResourcesAPI({ getMock }); resourceRequestMock.mockResolvedValue([]); await Promise.all([ api.getMetrics({ namespace: 'AWS/EC2', region: 'us-east-1' }), @@ -87,7 +87,7 @@ describe('api', () => { }, }, ]); - const { api } = setupMockedAPI({ getMock }); + const { api } = setupMockedResourcesAPI({ getMock }); const allMetrics = await api.getAllMetrics({ region: 'us-east-2' }); expect(allMetrics).toEqual([ { metricName: 'CPUUtilization', namespace: 'AWS/EC2' }, @@ -110,7 +110,7 @@ describe('api', () => { }, }, ]); - const { api } = setupMockedAPI({ getMock }); + const { api } = setupMockedResourcesAPI({ getMock }); const allMetrics = await api.getMetrics({ region: 'us-east-2', namespace: 'AWS/EC2' }); expect(allMetrics).toEqual([ { label: 'CPUUtilization', value: 'CPUUtilization' }, diff --git a/public/app/plugins/datasource/cloudwatch/api.ts b/public/app/plugins/datasource/cloudwatch/resources/ResourcesAPI.ts similarity index 95% rename from public/app/plugins/datasource/cloudwatch/api.ts rename to public/app/plugins/datasource/cloudwatch/resources/ResourcesAPI.ts index 719d32c30b0..e90a65236c6 100644 --- a/public/app/plugins/datasource/cloudwatch/api.ts +++ b/public/app/plugins/datasource/cloudwatch/resources/ResourcesAPI.ts @@ -4,28 +4,24 @@ import { DataSourceInstanceSettings, SelectableValue } from '@grafana/data'; import { getBackendSrv } from '@grafana/runtime'; import { TemplateSrv } from 'app/features/templating/template_srv'; -import { CloudWatchRequest } from './query-runner/CloudWatchRequest'; +import { CloudWatchRequest } from '../query-runner/CloudWatchRequest'; +import { CloudWatchJsonData, LogGroupField, MultiFilters } from '../types'; + import { - CloudWatchJsonData, + ResourceRequest, + Account, + ResourceResponse, DescribeLogGroupsRequest, + LogGroupResponse, + GetLogGroupFieldsRequest, + GetMetricsRequest, GetDimensionKeysRequest, GetDimensionValuesRequest, - GetMetricsRequest, - LogGroupResponse, MetricResponse, - MultiFilters, - Account, - ResourceRequest, - ResourceResponse, - GetLogGroupFieldsRequest, - LogGroupField, + SelectableResourceValue, } from './types'; -export interface SelectableResourceValue extends SelectableValue { - text: string; -} - -export class CloudWatchAPI extends CloudWatchRequest { +export class ResourcesAPI extends CloudWatchRequest { private memoizedGetRequest; constructor(instanceSettings: DataSourceInstanceSettings, templateSrv: TemplateSrv) { diff --git a/public/app/plugins/datasource/cloudwatch/resources/types.ts b/public/app/plugins/datasource/cloudwatch/resources/types.ts new file mode 100644 index 00000000000..72dd51844d4 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/resources/types.ts @@ -0,0 +1,63 @@ +import { SelectableValue } from '@grafana/data'; + +import { Dimensions } from '../types'; + +export interface ResourceResponse { + accountId?: string; + value: T; +} + +export interface ResourceRequest { + region: string; + accountId?: string; +} + +export interface GetLogGroupFieldsRequest extends ResourceRequest { + arn?: string; + logGroupName: string; +} + +export interface GetDimensionKeysRequest extends ResourceRequest { + metricName?: string; + namespace?: string; + dimensionFilters?: Dimensions; +} + +export interface GetDimensionValuesRequest extends ResourceRequest { + dimensionKey: string; + namespace: string; + metricName?: string; + dimensionFilters?: Dimensions; +} + +export interface GetMetricsRequest extends ResourceRequest { + namespace?: string; +} + +export interface DescribeLogGroupsRequest extends ResourceRequest { + logGroupNamePrefix?: string; + logGroupPattern?: string; + limit?: number; + listAllLogGroups?: boolean; +} + +export interface Account { + arn: string; + id: string; + label: string; + isMonitoringAccount: boolean; +} + +export interface LogGroupResponse { + arn: string; + name: string; +} + +export interface MetricResponse { + name: string; + namespace: string; +} + +export interface SelectableResourceValue extends SelectableValue { + text: string; +} diff --git a/public/app/plugins/datasource/cloudwatch/types.ts b/public/app/plugins/datasource/cloudwatch/types.ts index bd8609cf5fc..ed445ecd9e5 100644 --- a/public/app/plugins/datasource/cloudwatch/types.ts +++ b/public/app/plugins/datasource/cloudwatch/types.ts @@ -336,73 +336,6 @@ export interface LegacyAnnotationQuery extends MetricStat, DataQuery { type: string; } -export interface MetricResponse { - name: string; - namespace: string; -} - -export interface ResourceRequest { - region: string; - accountId?: string; -} - -export interface GetLogGroupFieldsRequest extends ResourceRequest { - /** - * The log group identifier - */ - arn?: string; - /** - * The name of the log group to search. - */ - logGroupName: string; -} - -export interface GetDimensionKeysRequest extends ResourceRequest { - metricName?: string; - namespace?: string; - dimensionFilters?: Dimensions; -} - -export interface GetDimensionValuesRequest extends ResourceRequest { - dimensionKey: string; - namespace: string; - metricName?: string; - dimensionFilters?: Dimensions; -} - -export interface GetMetricsRequest extends ResourceRequest { - namespace?: string; -} - -export interface DescribeLogGroupsRequest extends ResourceRequest { - logGroupNamePrefix?: string; - logGroupPattern?: string; - limit?: number; - listAllLogGroups?: boolean; -} - -export interface Account { - arn: string; - id: string; - label: string; - isMonitoringAccount: boolean; -} - -export interface LogGroupResponse { - arn: string; - name: string; -} - -export interface MetricResponse { - name: string; - namespace: string; -} - -export interface ResourceResponse { - accountId?: string; - value: T; -} - export interface LogGroup { arn: string; name: string; diff --git a/public/app/plugins/datasource/cloudwatch/variables.test.ts b/public/app/plugins/datasource/cloudwatch/variables.test.ts index 2d9b3c456e2..d9b4496a3d6 100644 --- a/public/app/plugins/datasource/cloudwatch/variables.test.ts +++ b/public/app/plugins/datasource/cloudwatch/variables.test.ts @@ -1,5 +1,5 @@ -import { setupMockedAPI } from './__mocks__/API'; import { dimensionVariable, labelsVariable, setupMockedDataSource } from './__mocks__/CloudWatchDataSource'; +import { setupMockedResourcesAPI } from './__mocks__/ResourcesAPI'; import { VariableQuery, VariableQueryType } from './types'; import { CloudWatchVariableSupport } from './variables'; @@ -16,20 +16,20 @@ const defaultQuery: VariableQuery = { }; const mock = setupMockedDataSource({ variables: [labelsVariable, dimensionVariable] }); -mock.datasource.api.getRegions = jest.fn().mockResolvedValue([{ label: 'a', value: 'a' }]); -mock.datasource.api.getNamespaces = jest.fn().mockResolvedValue([{ label: 'b', value: 'b' }]); -mock.datasource.api.getMetrics = jest.fn().mockResolvedValue([{ label: 'c', value: 'c' }]); -mock.datasource.api.getDimensionKeys = jest.fn().mockResolvedValue([{ label: 'd', value: 'd' }]); -mock.datasource.api.getLogGroups = jest +mock.datasource.resources.getRegions = jest.fn().mockResolvedValue([{ label: 'a', value: 'a' }]); +mock.datasource.resources.getNamespaces = jest.fn().mockResolvedValue([{ label: 'b', value: 'b' }]); +mock.datasource.resources.getMetrics = jest.fn().mockResolvedValue([{ label: 'c', value: 'c' }]); +mock.datasource.resources.getDimensionKeys = jest.fn().mockResolvedValue([{ label: 'd', value: 'd' }]); +mock.datasource.resources.getLogGroups = jest .fn() .mockResolvedValue([{ value: { arn: 'a', name: 'a' } }, { value: { arn: 'b', name: 'b' } }]); -mock.datasource.api.getAccounts = jest.fn().mockResolvedValue([]); +mock.datasource.resources.getAccounts = jest.fn().mockResolvedValue([]); const getDimensionValues = jest.fn().mockResolvedValue([{ label: 'e', value: 'e' }]); const getEbsVolumeIds = jest.fn().mockResolvedValue([{ label: 'f', value: 'f' }]); const getEc2InstanceAttribute = jest.fn().mockResolvedValue([{ label: 'g', value: 'g' }]); const getResourceARNs = jest.fn().mockResolvedValue([{ label: 'h', value: 'h' }]); -const variables = new CloudWatchVariableSupport(mock.datasource.api); +const variables = new CloudWatchVariableSupport(mock.datasource.resources); describe('variables', () => { it('should run regions', async () => { @@ -54,7 +54,7 @@ describe('variables', () => { describe('accounts', () => { it('should run accounts', async () => { - const { api } = setupMockedAPI(); + const { api } = setupMockedResourcesAPI(); const getAccountMock = jest.fn().mockResolvedValue([]); api.getAccounts = getAccountMock; const variables = new CloudWatchVariableSupport(api); @@ -63,7 +63,7 @@ describe('variables', () => { }); it('should map accounts to metric find value and insert "all" option', async () => { - const { api } = setupMockedAPI(); + const { api } = setupMockedResourcesAPI(); api.getAccounts = jest.fn().mockResolvedValue([{ id: '123', label: 'Account1' }]); const variables = new CloudWatchVariableSupport(api); const result = await variables.execute({ ...defaultQuery, queryType: VariableQueryType.Accounts }); @@ -83,7 +83,7 @@ describe('variables', () => { dimensionFilters: { a: 'b' }, }; beforeEach(() => { - mock.datasource.api.getDimensionValues = getDimensionValues; + mock.datasource.resources.getDimensionValues = getDimensionValues; getDimensionValues.mockClear(); }); @@ -113,7 +113,7 @@ describe('variables', () => { describe('EBS volume ids', () => { beforeEach(() => { - mock.datasource.api.getEbsVolumeIds = getEbsVolumeIds; + mock.datasource.resources.getEbsVolumeIds = getEbsVolumeIds; getEbsVolumeIds.mockClear(); }); @@ -142,7 +142,7 @@ describe('variables', () => { ec2Filters: { a: ['b'] }, }; beforeEach(() => { - mock.datasource.api.getEc2InstanceAttribute = getEc2InstanceAttribute; + mock.datasource.resources.getEc2InstanceAttribute = getEc2InstanceAttribute; getEc2InstanceAttribute.mockClear(); }); @@ -167,7 +167,7 @@ describe('variables', () => { tags: { a: ['b'] }, }; beforeEach(() => { - mock.datasource.api.getResourceARNs = getResourceARNs; + mock.datasource.resources.getResourceARNs = getResourceARNs; getResourceARNs.mockClear(); }); diff --git a/public/app/plugins/datasource/cloudwatch/variables.ts b/public/app/plugins/datasource/cloudwatch/variables.ts index 2b7fa2e6614..0089878a150 100644 --- a/public/app/plugins/datasource/cloudwatch/variables.ts +++ b/public/app/plugins/datasource/cloudwatch/variables.ts @@ -9,16 +9,16 @@ import { SelectableValue, } from '@grafana/data'; -import { CloudWatchAPI } from './api'; import { ALL_ACCOUNTS_OPTION } from './components/Account'; import { VariableQueryEditor } from './components/VariableQueryEditor/VariableQueryEditor'; import { CloudWatchDatasource } from './datasource'; import { migrateVariableQuery } from './migrations/variableQueryMigrations'; +import { ResourcesAPI } from './resources/ResourcesAPI'; import { standardStatistics } from './standardStatistics'; import { VariableQuery, VariableQueryType } from './types'; export class CloudWatchVariableSupport extends CustomVariableSupport { - constructor(private readonly api: CloudWatchAPI) { + constructor(private readonly resources: ResourcesAPI) { super(); this.query = this.query.bind(this); } @@ -62,7 +62,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport regions.map(selectableValueToMetricFindOption)); + return this.resources.getRegions().then((regions) => regions.map(selectableValueToMetricFindOption)); } async handleNamespacesQuery() { - return this.api.getNamespaces().then((namespaces) => namespaces.map(selectableValueToMetricFindOption)); + return this.resources.getNamespaces().then((namespaces) => namespaces.map(selectableValueToMetricFindOption)); } async handleMetricsQuery({ namespace, region }: VariableQuery) { - return this.api.getMetrics({ namespace, region }).then((metrics) => metrics.map(selectableValueToMetricFindOption)); + return this.resources + .getMetrics({ namespace, region }) + .then((metrics) => metrics.map(selectableValueToMetricFindOption)); } async handleDimensionKeysQuery({ namespace, region }: VariableQuery) { - return this.api.getDimensionKeys({ namespace, region }).then((keys) => keys.map(selectableValueToMetricFindOption)); + return this.resources + .getDimensionKeys({ namespace, region }) + .then((keys) => keys.map(selectableValueToMetricFindOption)); } async handleDimensionValuesQuery({ namespace, region, dimensionKey, metricName, dimensionFilters }: VariableQuery) { if (!dimensionKey || !metricName) { return []; } - return this.api + return this.resources .getDimensionValues({ region, namespace, @@ -114,14 +118,14 @@ export class CloudWatchVariableSupport extends CustomVariableSupport ids.map(selectableValueToMetricFindOption)); + return this.resources.getEbsVolumeIds(region, instanceID).then((ids) => ids.map(selectableValueToMetricFindOption)); } async handleEc2InstanceAttributeQuery({ region, attributeName, ec2Filters }: VariableQuery) { if (!attributeName) { return []; } - return this.api + return this.resources .getEc2InstanceAttribute(region, attributeName, ec2Filters ?? {}) .then((values) => values.map(selectableValueToMetricFindOption)); } @@ -130,7 +134,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport { + return this.resources.getAccounts({ region }).then((accounts) => { const metricFindOptions = accounts.map((account) => ({ text: account.label, value: account.id, From f6e3252c00efab78e34c2ec7d9ae6f6aed6dc66c Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Tue, 17 Jan 2023 14:47:31 -0500 Subject: [PATCH 62/65] chore: move notifications models into notifications service (#61638) --- pkg/api/alerting.go | 3 +- pkg/api/org_invite.go | 7 +-- pkg/api/password.go | 7 +-- .../alerting/notifiers/alertmanager.go | 2 +- pkg/services/alerting/notifiers/dingding.go | 2 +- pkg/services/alerting/notifiers/discord.go | 4 +- pkg/services/alerting/notifiers/email.go | 4 +- pkg/services/alerting/notifiers/googlechat.go | 2 +- pkg/services/alerting/notifiers/hipchat.go | 2 +- pkg/services/alerting/notifiers/kafka.go | 2 +- pkg/services/alerting/notifiers/line.go | 2 +- pkg/services/alerting/notifiers/opsgenie.go | 4 +- pkg/services/alerting/notifiers/pagerduty.go | 2 +- pkg/services/alerting/notifiers/pushover.go | 2 +- pkg/services/alerting/notifiers/sensu.go | 2 +- pkg/services/alerting/notifiers/sensugo.go | 2 +- pkg/services/alerting/notifiers/slack.go | 2 +- pkg/services/alerting/notifiers/teams.go | 2 +- pkg/services/alerting/notifiers/telegram.go | 12 ++--- pkg/services/alerting/notifiers/threema.go | 2 +- pkg/services/alerting/notifiers/victorops.go | 2 +- pkg/services/alerting/notifiers/webhook.go | 2 +- pkg/services/ngalert/notifier/email_test.go | 9 ++-- pkg/services/ngalert/notifier/sender.go | 13 +++-- pkg/services/notifications/mailer.go | 7 ++- pkg/services/notifications/mock.go | 20 ++++--- .../notifications/models.go} | 2 +- pkg/services/notifications/notifications.go | 30 +++++------ .../notifications/notifications_test.go | 54 +++++++++---------- .../send_email_integration_test.go | 3 +- .../alerting/api_notification_channel_test.go | 14 ++--- 31 files changed, 110 insertions(+), 113 deletions(-) rename pkg/{models/notifications.go => services/notifications/models.go} (98%) diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 285d2005a9c..de7a4955591 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/ngalert/notifier/channels_config" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/search" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -645,7 +646,7 @@ func (hs *HTTPServer) NotificationTest(c *models.ReqContext) response.Response { } if err := hs.AlertNotificationService.HandleNotificationTestCommand(c.Req.Context(), cmd); err != nil { - if errors.Is(err, models.ErrSmtpNotEnabled) { + if errors.Is(err, notifications.ErrSmtpNotEnabled) { return response.Error(412, err.Error(), err) } var alertingErr alerting.ValidationError diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go index 5cf7b7f0706..30271ed2f55 100644 --- a/pkg/api/org_invite.go +++ b/pkg/api/org_invite.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/models" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/org" tempuser "github.com/grafana/grafana/pkg/services/temp_user" "github.com/grafana/grafana/pkg/services/user" @@ -113,7 +114,7 @@ func (hs *HTTPServer) AddOrgInvite(c *models.ReqContext) response.Response { // send invite email if inviteDto.SendEmail && util.IsEmail(inviteDto.LoginOrEmail) { - emailCmd := models.SendEmailCommand{ + emailCmd := notifications.SendEmailCommand{ To: []string{inviteDto.LoginOrEmail}, Template: "new_user_invite", Data: map[string]interface{}{ @@ -126,7 +127,7 @@ func (hs *HTTPServer) AddOrgInvite(c *models.ReqContext) response.Response { } if err := hs.AlertNG.NotificationService.SendEmailCommandHandler(c.Req.Context(), &emailCmd); err != nil { - if errors.Is(err, models.ErrSmtpNotEnabled) { + if errors.Is(err, notifications.ErrSmtpNotEnabled) { return response.Error(412, err.Error(), err) } @@ -155,7 +156,7 @@ func (hs *HTTPServer) inviteExistingUserToOrg(c *models.ReqContext, user *user.U } if inviteDto.SendEmail && util.IsEmail(user.Email) { - emailCmd := models.SendEmailCommand{ + emailCmd := notifications.SendEmailCommand{ To: []string{user.Email}, Template: "invited_to_org", Data: map[string]interface{}{ diff --git a/pkg/api/password.go b/pkg/api/password.go index c21c30f0ae2..5d60de08254 100644 --- a/pkg/api/password.go +++ b/pkg/api/password.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -45,7 +46,7 @@ func (hs *HTTPServer) SendResetPasswordEmail(c *models.ReqContext) response.Resp } } - emailCmd := models.SendResetPasswordEmailCommand{User: usr} + emailCmd := notifications.SendResetPasswordEmailCommand{User: usr} if err := hs.NotificationService.SendResetPasswordEmail(c.Req.Context(), &emailCmd); err != nil { return response.Error(500, "Failed to send email", err) } @@ -58,7 +59,7 @@ func (hs *HTTPServer) ResetPassword(c *models.ReqContext) response.Response { if err := web.Bind(c.Req, &form); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - query := models.ValidateResetPasswordCodeQuery{Code: form.Code} + query := notifications.ValidateResetPasswordCodeQuery{Code: form.Code} // For now the only way to know the username to clear login attempts for is // to set it in the function provided to NotificationService @@ -71,7 +72,7 @@ func (hs *HTTPServer) ResetPassword(c *models.ReqContext) response.Response { } if err := hs.NotificationService.ValidateResetPasswordCode(c.Req.Context(), &query, getUserByLogin); err != nil { - if errors.Is(err, models.ErrInvalidEmailCode) { + if errors.Is(err, notifications.ErrInvalidEmailCode) { return response.Error(400, "Invalid or expired reset password code", nil) } return response.Error(500, "Unknown error validating email code", err) diff --git a/pkg/services/alerting/notifiers/alertmanager.go b/pkg/services/alerting/notifiers/alertmanager.go index 92876853470..274a5bb05f3 100644 --- a/pkg/services/alerting/notifiers/alertmanager.go +++ b/pkg/services/alerting/notifiers/alertmanager.go @@ -175,7 +175,7 @@ func (am *AlertmanagerNotifier) Notify(evalContext *alerting.EvalContext) error errCnt := 0 for _, url := range am.URL { - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: strings.TrimSuffix(url, "/") + "/api/v1/alerts", User: am.BasicAuthUser, Password: am.BasicAuthPassword, diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go index 7767f791df2..d88320f0d12 100644 --- a/pkg/services/alerting/notifiers/dingding.go +++ b/pkg/services/alerting/notifiers/dingding.go @@ -86,7 +86,7 @@ func (dd *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error { return err } - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: dd.URL, Body: string(body), } diff --git a/pkg/services/alerting/notifiers/discord.go b/pkg/services/alerting/notifiers/discord.go index f90d1e12e06..7e7ff16f1d3 100644 --- a/pkg/services/alerting/notifiers/discord.go +++ b/pkg/services/alerting/notifiers/discord.go @@ -161,7 +161,7 @@ func (dn *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error { json, _ := bodyJSON.MarshalJSON() - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: dn.WebhookURL, HttpMethod: "POST", ContentType: "application/json", @@ -185,7 +185,7 @@ func (dn *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error { return nil } -func (dn *DiscordNotifier) embedImage(cmd *models.SendWebhookSync, imagePath string, existingJSONBody []byte) error { +func (dn *DiscordNotifier) embedImage(cmd *notifications.SendWebhookSync, imagePath string, existingJSONBody []byte) error { // nolint:gosec // We can ignore the gosec G304 warning on this one because `imagePath` comes // from the alert `evalContext` that generates the images. diff --git a/pkg/services/alerting/notifiers/email.go b/pkg/services/alerting/notifiers/email.go index fb072d59617..69c485cfbf2 100644 --- a/pkg/services/alerting/notifiers/email.go +++ b/pkg/services/alerting/notifiers/email.go @@ -82,8 +82,8 @@ func (en *EmailNotifier) Notify(evalContext *alerting.EvalContext) error { error = evalContext.Error.Error() } - cmd := &models.SendEmailCommandSync{ - SendEmailCommand: models.SendEmailCommand{ + cmd := ¬ifications.SendEmailCommandSync{ + SendEmailCommand: notifications.SendEmailCommand{ Subject: evalContext.GetNotificationTitle(), Data: map[string]interface{}{ "Title": evalContext.GetNotificationTitle(), diff --git a/pkg/services/alerting/notifiers/googlechat.go b/pkg/services/alerting/notifiers/googlechat.go index 90b2f2e1c1f..6eeeda4eb85 100644 --- a/pkg/services/alerting/notifiers/googlechat.go +++ b/pkg/services/alerting/notifiers/googlechat.go @@ -220,7 +220,7 @@ func (gcn *GoogleChatNotifier) Notify(evalContext *alerting.EvalContext) error { } body, _ := json.Marshal(res1D) - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: gcn.URL, HttpMethod: "POST", HttpHeader: headers, diff --git a/pkg/services/alerting/notifiers/hipchat.go b/pkg/services/alerting/notifiers/hipchat.go index c625c96bdd5..5b8757934f5 100644 --- a/pkg/services/alerting/notifiers/hipchat.go +++ b/pkg/services/alerting/notifiers/hipchat.go @@ -173,7 +173,7 @@ func (hc *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error { hipURL := fmt.Sprintf("%s/v2/room/%s/notification?auth_token=%s", hc.URL, hc.RoomID, hc.APIKey) data, _ := json.Marshal(&body) hc.log.Info("Request payload", "json", string(data)) - cmd := &models.SendWebhookSync{Url: hipURL, Body: string(data)} + cmd := ¬ifications.SendWebhookSync{Url: hipURL, Body: string(data)} if err := hc.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { hc.log.Error("Failed to send hipchat notification", "error", err, "webhook", hc.Name) diff --git a/pkg/services/alerting/notifiers/kafka.go b/pkg/services/alerting/notifiers/kafka.go index c9faaea1027..55613d17cbb 100644 --- a/pkg/services/alerting/notifiers/kafka.go +++ b/pkg/services/alerting/notifiers/kafka.go @@ -114,7 +114,7 @@ func (kn *KafkaNotifier) Notify(evalContext *alerting.EvalContext) error { topicURL := kn.Endpoint + "/topics/" + kn.Topic - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: topicURL, Body: string(body), HttpMethod: "POST", diff --git a/pkg/services/alerting/notifiers/line.go b/pkg/services/alerting/notifiers/line.go index 9e360ecfecd..7ddbb7c0465 100644 --- a/pkg/services/alerting/notifiers/line.go +++ b/pkg/services/alerting/notifiers/line.go @@ -82,7 +82,7 @@ func (ln *LineNotifier) createAlert(evalContext *alerting.EvalContext) error { form.Add("imageFullsize", evalContext.ImagePublicURL) } - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: lineNotifyURL, HttpMethod: "POST", HttpHeader: map[string]string{ diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go index 8cf3a9ebfe8..4703415f84b 100644 --- a/pkg/services/alerting/notifiers/opsgenie.go +++ b/pkg/services/alerting/notifiers/opsgenie.go @@ -195,7 +195,7 @@ func (on *OpsGenieNotifier) createAlert(evalContext *alerting.EvalContext) error body, _ := bodyJSON.MarshalJSON() - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: on.APIUrl, Body: string(body), HttpMethod: "POST", @@ -219,7 +219,7 @@ func (on *OpsGenieNotifier) closeAlert(evalContext *alerting.EvalContext) error bodyJSON.Set("source", "Grafana") body, _ := bodyJSON.MarshalJSON() - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: fmt.Sprintf("%s/alertId-%d/close?identifierType=alias", on.APIUrl, evalContext.Rule.ID), Body: string(body), HttpMethod: "POST", diff --git a/pkg/services/alerting/notifiers/pagerduty.go b/pkg/services/alerting/notifiers/pagerduty.go index 358a23d14ec..7c464a668e0 100644 --- a/pkg/services/alerting/notifiers/pagerduty.go +++ b/pkg/services/alerting/notifiers/pagerduty.go @@ -231,7 +231,7 @@ func (pn *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error { return err } - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: pagerdutyEventAPIURL, Body: string(body), HttpMethod: "POST", diff --git a/pkg/services/alerting/notifiers/pushover.go b/pkg/services/alerting/notifiers/pushover.go index 6a6fedce24e..322da15632b 100644 --- a/pkg/services/alerting/notifiers/pushover.go +++ b/pkg/services/alerting/notifiers/pushover.go @@ -280,7 +280,7 @@ func (pn *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error { return err } - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: pushoverEndpoint, HttpMethod: "POST", HttpHeader: headers, diff --git a/pkg/services/alerting/notifiers/sensu.go b/pkg/services/alerting/notifiers/sensu.go index b41cf17ca18..655a3ec9a43 100644 --- a/pkg/services/alerting/notifiers/sensu.go +++ b/pkg/services/alerting/notifiers/sensu.go @@ -138,7 +138,7 @@ func (sn *SensuNotifier) Notify(evalContext *alerting.EvalContext) error { body, _ := bodyJSON.MarshalJSON() - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: sn.URL, User: sn.User, Password: sn.Password, diff --git a/pkg/services/alerting/notifiers/sensugo.go b/pkg/services/alerting/notifiers/sensugo.go index 045484be0bb..802681fd647 100644 --- a/pkg/services/alerting/notifiers/sensugo.go +++ b/pkg/services/alerting/notifiers/sensugo.go @@ -188,7 +188,7 @@ func (sn *SensuGoNotifier) Notify(evalContext *alerting.EvalContext) error { return err } - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: fmt.Sprintf("%s/api/core/v2/namespaces/%s/events", strings.TrimSuffix(sn.URL, "/"), namespace), Body: string(body), HttpMethod: "POST", diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index 86f1b7a4f59..092b366b559 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -415,7 +415,7 @@ func (sn *SlackNotifier) slackFileUpload(evalContext *alerting.EvalContext, log if err != nil { return err } - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: "https://slack.com/api/files.upload", Body: uploadBody.String(), HttpHeader: headers, HttpMethod: "POST", } if err := sn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { diff --git a/pkg/services/alerting/notifiers/teams.go b/pkg/services/alerting/notifiers/teams.go index e85f4a12d82..0add355228d 100644 --- a/pkg/services/alerting/notifiers/teams.go +++ b/pkg/services/alerting/notifiers/teams.go @@ -133,7 +133,7 @@ func (tn *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error { } data, _ := json.Marshal(&body) - cmd := &models.SendWebhookSync{Url: tn.URL, Body: string(data)} + cmd := ¬ifications.SendWebhookSync{Url: tn.URL, Body: string(data)} if err := tn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { tn.log.Error("Failed to send teams notification", "error", err, "webhook", tn.Name) diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go index d93d776b853..92b77d27a4d 100644 --- a/pkg/services/alerting/notifiers/telegram.go +++ b/pkg/services/alerting/notifiers/telegram.go @@ -89,7 +89,7 @@ func NewTelegramNotifier(model *models.AlertNotification, fn alerting.GetDecrypt }, nil } -func (tn *TelegramNotifier) buildMessage(evalContext *alerting.EvalContext, sendImageInline bool) (*models.SendWebhookSync, error) { +func (tn *TelegramNotifier) buildMessage(evalContext *alerting.EvalContext, sendImageInline bool) (*notifications.SendWebhookSync, error) { if sendImageInline { cmd, err := tn.buildMessageInlineImage(evalContext) if err == nil { @@ -102,7 +102,7 @@ func (tn *TelegramNotifier) buildMessage(evalContext *alerting.EvalContext, send return tn.buildMessageLinkedImage(evalContext) } -func (tn *TelegramNotifier) buildMessageLinkedImage(evalContext *alerting.EvalContext) (*models.SendWebhookSync, error) { +func (tn *TelegramNotifier) buildMessageLinkedImage(evalContext *alerting.EvalContext) (*notifications.SendWebhookSync, error) { message := fmt.Sprintf("%s\nState: %s\nMessage: %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message) ruleURL, err := evalContext.GetRuleURL() @@ -132,7 +132,7 @@ func (tn *TelegramNotifier) buildMessageLinkedImage(evalContext *alerting.EvalCo }) } -func (tn *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.EvalContext) (*models.SendWebhookSync, error) { +func (tn *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.EvalContext) (*notifications.SendWebhookSync, error) { var imageFile *os.File var err error @@ -169,7 +169,7 @@ func (tn *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.EvalCo }) } -func (tn *TelegramNotifier) generateTelegramCmd(message string, messageField string, apiAction string, extraConf func(writer *multipart.Writer)) (*models.SendWebhookSync, error) { +func (tn *TelegramNotifier) generateTelegramCmd(message string, messageField string, apiAction string, extraConf func(writer *multipart.Writer)) (*notifications.SendWebhookSync, error) { var body bytes.Buffer w := multipart.NewWriter(&body) defer func() { @@ -203,7 +203,7 @@ func (tn *TelegramNotifier) generateTelegramCmd(message string, messageField str tn.log.Info("Sending telegram notification", "chat_id", tn.ChatID, "bot_token", tn.BotToken, "apiAction", apiAction) url := fmt.Sprintf(telegramAPIURL, tn.BotToken, apiAction) - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: url, Body: body.String(), HttpMethod: "POST", @@ -260,7 +260,7 @@ func appendIfPossible(tlog log.Logger, message string, extra string, sizeLimit i // Notify send an alert notification to Telegram. func (tn *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error { - var cmd *models.SendWebhookSync + var cmd *notifications.SendWebhookSync var err error if evalContext.ImagePublicURL == "" && tn.UploadImage { cmd, err = tn.buildMessage(evalContext, true) diff --git a/pkg/services/alerting/notifiers/threema.go b/pkg/services/alerting/notifiers/threema.go index 19b7ecd6324..07111646d45 100644 --- a/pkg/services/alerting/notifiers/threema.go +++ b/pkg/services/alerting/notifiers/threema.go @@ -152,7 +152,7 @@ func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error headers := map[string]string{ "Content-Type": "application/x-www-form-urlencoded", } - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: url, Body: body, HttpMethod: "POST", diff --git a/pkg/services/alerting/notifiers/victorops.go b/pkg/services/alerting/notifiers/victorops.go index 9cc1416c911..f2904ca39e6 100644 --- a/pkg/services/alerting/notifiers/victorops.go +++ b/pkg/services/alerting/notifiers/victorops.go @@ -154,7 +154,7 @@ func (vn *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error { } data, _ := bodyJSON.MarshalJSON() - cmd := &models.SendWebhookSync{Url: vn.URL, Body: string(data)} + cmd := ¬ifications.SendWebhookSync{Url: vn.URL, Body: string(data)} if err := vn.NotificationService.SendWebhookSync(evalContext.Ctx, cmd); err != nil { vn.log.Error("Failed to send Victorops notification", "error", err, "webhook", vn.Name) diff --git a/pkg/services/alerting/notifiers/webhook.go b/pkg/services/alerting/notifiers/webhook.go index 5ec1659718b..4baade18f5c 100644 --- a/pkg/services/alerting/notifiers/webhook.go +++ b/pkg/services/alerting/notifiers/webhook.go @@ -145,7 +145,7 @@ func (wn *WebhookNotifier) Notify(evalContext *alerting.EvalContext) error { bodyJSON, _ := json.Marshal(body) - cmd := &models.SendWebhookSync{ + cmd := ¬ifications.SendWebhookSync{ Url: wn.URL, User: wn.User, Password: wn.Password, diff --git a/pkg/services/ngalert/notifier/email_test.go b/pkg/services/ngalert/notifier/email_test.go index e1428ed8c17..19c659c3fb5 100644 --- a/pkg/services/ngalert/notifier/email_test.go +++ b/pkg/services/ngalert/notifier/email_test.go @@ -15,7 +15,6 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" ) @@ -240,15 +239,15 @@ func (e emailSender) SendWebhook(ctx context.Context, cmd *channels.SendWebhookS } func (e emailSender) SendEmail(ctx context.Context, cmd *channels.SendEmailSettings) error { - attached := make([]*models.SendEmailAttachFile, 0, len(cmd.AttachedFiles)) + attached := make([]*notifications.SendEmailAttachFile, 0, len(cmd.AttachedFiles)) for _, file := range cmd.AttachedFiles { - attached = append(attached, &models.SendEmailAttachFile{ + attached = append(attached, ¬ifications.SendEmailAttachFile{ Name: file.Name, Content: file.Content, }) } - return e.ns.SendEmailCommandHandlerSync(ctx, &models.SendEmailCommandSync{ - SendEmailCommand: models.SendEmailCommand{ + return e.ns.SendEmailCommandHandlerSync(ctx, ¬ifications.SendEmailCommandSync{ + SendEmailCommand: notifications.SendEmailCommand{ To: cmd.To, SingleEmail: cmd.SingleEmail, Template: cmd.Template, diff --git a/pkg/services/ngalert/notifier/sender.go b/pkg/services/ngalert/notifier/sender.go index fd53694b399..fd016cabec9 100644 --- a/pkg/services/ngalert/notifier/sender.go +++ b/pkg/services/ngalert/notifier/sender.go @@ -5,7 +5,6 @@ import ( "github.com/grafana/alerting/alerting/notifier/channels" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/notifications" ) @@ -14,7 +13,7 @@ type sender struct { } func (s sender) SendWebhook(ctx context.Context, cmd *channels.SendWebhookSettings) error { - return s.ns.SendWebhookSync(ctx, &models.SendWebhookSync{ + return s.ns.SendWebhookSync(ctx, ¬ifications.SendWebhookSync{ Url: cmd.URL, User: cmd.User, Password: cmd.Password, @@ -27,18 +26,18 @@ func (s sender) SendWebhook(ctx context.Context, cmd *channels.SendWebhookSettin } func (s sender) SendEmail(ctx context.Context, cmd *channels.SendEmailSettings) error { - var attached []*models.SendEmailAttachFile + var attached []*notifications.SendEmailAttachFile if cmd.AttachedFiles != nil { - attached = make([]*models.SendEmailAttachFile, 0, len(cmd.AttachedFiles)) + attached = make([]*notifications.SendEmailAttachFile, 0, len(cmd.AttachedFiles)) for _, file := range cmd.AttachedFiles { - attached = append(attached, &models.SendEmailAttachFile{ + attached = append(attached, ¬ifications.SendEmailAttachFile{ Name: file.Name, Content: file.Content, }) } } - return s.ns.SendEmailCommandHandlerSync(ctx, &models.SendEmailCommandSync{ - SendEmailCommand: models.SendEmailCommand{ + return s.ns.SendEmailCommandHandlerSync(ctx, ¬ifications.SendEmailCommandSync{ + SendEmailCommand: notifications.SendEmailCommand{ To: cmd.To, SingleEmail: cmd.SingleEmail, Template: cmd.Template, diff --git a/pkg/services/notifications/mailer.go b/pkg/services/notifications/mailer.go index 1ba3b85e40a..827e2f4973e 100644 --- a/pkg/services/notifications/mailer.go +++ b/pkg/services/notifications/mailer.go @@ -10,7 +10,6 @@ import ( "html/template" "net/mail" - "github.com/grafana/grafana/pkg/models" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) @@ -54,9 +53,9 @@ func (ns *NotificationService) Send(msg *Message) (int, error) { return ns.mailer.Send(messages...) } -func (ns *NotificationService) buildEmailMessage(cmd *models.SendEmailCommand) (*Message, error) { +func (ns *NotificationService) buildEmailMessage(cmd *SendEmailCommand) (*Message, error) { if !ns.Cfg.Smtp.Enabled { - return nil, models.ErrSmtpNotEnabled + return nil, ErrSmtpNotEnabled } data := cmd.Data @@ -120,7 +119,7 @@ func (ns *NotificationService) buildEmailMessage(cmd *models.SendEmailCommand) ( // buildAttachedFiles build attached files func buildAttachedFiles( - attached []*models.SendEmailAttachFile, + attached []*SendEmailAttachFile, ) []*AttachedFile { result := make([]*AttachedFile, 0) diff --git a/pkg/services/notifications/mock.go b/pkg/services/notifications/mock.go index 14229fc797d..2e874ad6570 100644 --- a/pkg/services/notifications/mock.go +++ b/pkg/services/notifications/mock.go @@ -2,22 +2,20 @@ package notifications import ( "context" - - "github.com/grafana/grafana/pkg/models" ) type NotificationServiceMock struct { - Webhook models.SendWebhookSync - EmailSync models.SendEmailCommandSync - Email models.SendEmailCommand + Webhook SendWebhookSync + EmailSync SendEmailCommandSync + Email SendEmailCommand ShouldError error - WebhookHandler func(context.Context, *models.SendWebhookSync) error - EmailHandlerSync func(context.Context, *models.SendEmailCommandSync) error - EmailHandler func(context.Context, *models.SendEmailCommand) error + WebhookHandler func(context.Context, *SendWebhookSync) error + EmailHandlerSync func(context.Context, *SendEmailCommandSync) error + EmailHandler func(context.Context, *SendEmailCommand) error } -func (ns *NotificationServiceMock) SendWebhookSync(ctx context.Context, cmd *models.SendWebhookSync) error { +func (ns *NotificationServiceMock) SendWebhookSync(ctx context.Context, cmd *SendWebhookSync) error { ns.Webhook = *cmd if ns.WebhookHandler != nil { return ns.WebhookHandler(ctx, cmd) @@ -25,7 +23,7 @@ func (ns *NotificationServiceMock) SendWebhookSync(ctx context.Context, cmd *mod return ns.ShouldError } -func (ns *NotificationServiceMock) SendEmailCommandHandlerSync(ctx context.Context, cmd *models.SendEmailCommandSync) error { +func (ns *NotificationServiceMock) SendEmailCommandHandlerSync(ctx context.Context, cmd *SendEmailCommandSync) error { ns.EmailSync = *cmd if ns.EmailHandlerSync != nil { return ns.EmailHandlerSync(ctx, cmd) @@ -33,7 +31,7 @@ func (ns *NotificationServiceMock) SendEmailCommandHandlerSync(ctx context.Conte return ns.ShouldError } -func (ns *NotificationServiceMock) SendEmailCommandHandler(ctx context.Context, cmd *models.SendEmailCommand) error { +func (ns *NotificationServiceMock) SendEmailCommandHandler(ctx context.Context, cmd *SendEmailCommand) error { ns.Email = *cmd if ns.EmailHandler != nil { return ns.EmailHandler(ctx, cmd) diff --git a/pkg/models/notifications.go b/pkg/services/notifications/models.go similarity index 98% rename from pkg/models/notifications.go rename to pkg/services/notifications/models.go index 159d6a04edb..117f354ce82 100644 --- a/pkg/models/notifications.go +++ b/pkg/services/notifications/models.go @@ -1,4 +1,4 @@ -package models +package notifications import ( "errors" diff --git a/pkg/services/notifications/notifications.go b/pkg/services/notifications/notifications.go index bca81599d79..f481f8b355d 100644 --- a/pkg/services/notifications/notifications.go +++ b/pkg/services/notifications/notifications.go @@ -10,10 +10,10 @@ import ( "strings" "github.com/Masterminds/sprig/v3" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" tempuser "github.com/grafana/grafana/pkg/services/temp_user" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -21,11 +21,11 @@ import ( ) type WebhookSender interface { - SendWebhookSync(ctx context.Context, cmd *models.SendWebhookSync) error + SendWebhookSync(ctx context.Context, cmd *SendWebhookSync) error } type EmailSender interface { - SendEmailCommandHandlerSync(ctx context.Context, cmd *models.SendEmailCommandSync) error - SendEmailCommandHandler(ctx context.Context, cmd *models.SendEmailCommand) error + SendEmailCommandHandlerSync(ctx context.Context, cmd *SendEmailCommandSync) error + SendEmailCommandHandler(ctx context.Context, cmd *SendEmailCommand) error } type Service interface { WebhookSender @@ -130,7 +130,7 @@ func (ns *NotificationService) GetMailer() Mailer { return ns.mailer } -func (ns *NotificationService) SendWebhookSync(ctx context.Context, cmd *models.SendWebhookSync) error { +func (ns *NotificationService) SendWebhookSync(ctx context.Context, cmd *SendWebhookSync) error { return ns.sendWebRequestSync(ctx, &Webhook{ Url: cmd.Url, User: cmd.User, @@ -148,8 +148,8 @@ func subjectTemplateFunc(obj map[string]interface{}, value string) string { return "" } -func (ns *NotificationService) SendEmailCommandHandlerSync(ctx context.Context, cmd *models.SendEmailCommandSync) error { - message, err := ns.buildEmailMessage(&models.SendEmailCommand{ +func (ns *NotificationService) SendEmailCommandHandlerSync(ctx context.Context, cmd *SendEmailCommandSync) error { + message, err := ns.buildEmailMessage(&SendEmailCommand{ Data: cmd.Data, Info: cmd.Info, Template: cmd.Template, @@ -169,7 +169,7 @@ func (ns *NotificationService) SendEmailCommandHandlerSync(ctx context.Context, return err } -func (ns *NotificationService) SendEmailCommandHandler(ctx context.Context, cmd *models.SendEmailCommand) error { +func (ns *NotificationService) SendEmailCommandHandler(ctx context.Context, cmd *SendEmailCommand) error { message, err := ns.buildEmailMessage(cmd) if err != nil { @@ -180,12 +180,12 @@ func (ns *NotificationService) SendEmailCommandHandler(ctx context.Context, cmd return nil } -func (ns *NotificationService) SendResetPasswordEmail(ctx context.Context, cmd *models.SendResetPasswordEmailCommand) error { +func (ns *NotificationService) SendResetPasswordEmail(ctx context.Context, cmd *SendResetPasswordEmailCommand) error { code, err := createUserEmailCode(ns.Cfg, cmd.User, "") if err != nil { return err } - return ns.SendEmailCommandHandler(ctx, &models.SendEmailCommand{ + return ns.SendEmailCommandHandler(ctx, &SendEmailCommand{ To: []string{cmd.User.Email}, Template: tmplResetPassword, Data: map[string]interface{}{ @@ -197,10 +197,10 @@ func (ns *NotificationService) SendResetPasswordEmail(ctx context.Context, cmd * type GetUserByLoginFunc = func(c context.Context, login string) (*user.User, error) -func (ns *NotificationService) ValidateResetPasswordCode(ctx context.Context, query *models.ValidateResetPasswordCodeQuery, userByLogin GetUserByLoginFunc) error { +func (ns *NotificationService) ValidateResetPasswordCode(ctx context.Context, query *ValidateResetPasswordCodeQuery, userByLogin GetUserByLoginFunc) error { login := getLoginForEmailCode(query.Code) if login == "" { - return models.ErrInvalidEmailCode + return ErrInvalidEmailCode } user, err := userByLogin(ctx, login) @@ -213,7 +213,7 @@ func (ns *NotificationService) ValidateResetPasswordCode(ctx context.Context, qu return err } if !validEmailCode { - return models.ErrInvalidEmailCode + return ErrInvalidEmailCode } query.Result = user @@ -231,7 +231,7 @@ func (ns *NotificationService) signUpStartedHandler(ctx context.Context, evt *ev return nil } - err := ns.SendEmailCommandHandler(ctx, &models.SendEmailCommand{ + err := ns.SendEmailCommandHandler(ctx, &SendEmailCommand{ To: []string{evt.Email}, Template: tmplSignUpStarted, Data: map[string]interface{}{ @@ -254,7 +254,7 @@ func (ns *NotificationService) signUpCompletedHandler(ctx context.Context, evt * return nil } - return ns.SendEmailCommandHandler(ctx, &models.SendEmailCommand{ + return ns.SendEmailCommandHandler(ctx, &SendEmailCommand{ To: []string{evt.Email}, Template: tmplWelcomeOnSignUp, Data: map[string]interface{}{ diff --git a/pkg/services/notifications/notifications_test.go b/pkg/services/notifications/notifications_test.go index 48ee121127d..4254a8f93a5 100644 --- a/pkg/services/notifications/notifications_test.go +++ b/pkg/services/notifications/notifications_test.go @@ -5,13 +5,13 @@ import ( "regexp" "testing" - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" ) func newBus(t *testing.T) bus.Bus { @@ -53,8 +53,8 @@ func TestSendEmailSync(t *testing.T) { t.Run("When sending emails synchronously", func(t *testing.T) { ns, mailer := createSut(t, bus) - cmd := &models.SendEmailCommandSync{ - SendEmailCommand: models.SendEmailCommand{ + cmd := &SendEmailCommandSync{ + SendEmailCommand: SendEmailCommand{ Subject: "subject", To: []string{"asdf@grafana.com"}, SingleEmail: false, @@ -72,8 +72,8 @@ func TestSendEmailSync(t *testing.T) { t.Run("When using Single Email mode with multiple recipients", func(t *testing.T) { ns, mailer := createSut(t, bus) - cmd := &models.SendEmailCommandSync{ - SendEmailCommand: models.SendEmailCommand{ + cmd := &SendEmailCommandSync{ + SendEmailCommand: SendEmailCommand{ Subject: "subject", To: []string{"1@grafana.com", "2@grafana.com", "3@grafana.com"}, SingleEmail: true, @@ -89,8 +89,8 @@ func TestSendEmailSync(t *testing.T) { t.Run("When using Multi Email mode with multiple recipients", func(t *testing.T) { ns, mailer := createSut(t, bus) - cmd := &models.SendEmailCommandSync{ - SendEmailCommand: models.SendEmailCommand{ + cmd := &SendEmailCommandSync{ + SendEmailCommand: SendEmailCommand{ Subject: "subject", To: []string{"1@grafana.com", "2@grafana.com", "3@grafana.com"}, SingleEmail: false, @@ -106,13 +106,13 @@ func TestSendEmailSync(t *testing.T) { t.Run("When attaching files to emails", func(t *testing.T) { ns, mailer := createSut(t, bus) - cmd := &models.SendEmailCommandSync{ - SendEmailCommand: models.SendEmailCommand{ + cmd := &SendEmailCommandSync{ + SendEmailCommand: SendEmailCommand{ Subject: "subject", To: []string{"asdf@grafana.com"}, SingleEmail: true, Template: "welcome_on_signup", - AttachedFiles: []*models.SendEmailAttachFile{ + AttachedFiles: []*SendEmailAttachFile{ { Name: "attachment.txt", Content: []byte("text file content"), @@ -137,8 +137,8 @@ func TestSendEmailSync(t *testing.T) { cfg.Smtp.Enabled = false ns, mailer, err := createSutWithConfig(t, bus, cfg) require.NoError(t, err) - cmd := &models.SendEmailCommandSync{ - SendEmailCommand: models.SendEmailCommand{ + cmd := &SendEmailCommandSync{ + SendEmailCommand: SendEmailCommand{ Subject: "subject", To: []string{"1@grafana.com", "2@grafana.com", "3@grafana.com"}, SingleEmail: true, @@ -148,7 +148,7 @@ func TestSendEmailSync(t *testing.T) { err = ns.SendEmailCommandHandlerSync(context.Background(), cmd) - require.ErrorIs(t, err, models.ErrSmtpNotEnabled) + require.ErrorIs(t, err, ErrSmtpNotEnabled) require.Empty(t, mailer.Sent) }) @@ -157,8 +157,8 @@ func TestSendEmailSync(t *testing.T) { cfg.Smtp.ContentTypes = append(cfg.Smtp.ContentTypes, "multipart/form-data") ns, mailer, err := createSutWithConfig(t, bus, cfg) require.NoError(t, err) - cmd := &models.SendEmailCommandSync{ - SendEmailCommand: models.SendEmailCommand{ + cmd := &SendEmailCommandSync{ + SendEmailCommand: SendEmailCommand{ Subject: "subject", To: []string{"1@grafana.com", "2@grafana.com", "3@grafana.com"}, SingleEmail: false, @@ -174,8 +174,8 @@ func TestSendEmailSync(t *testing.T) { t.Run("When SMTP dialer is disconnected", func(t *testing.T) { ns := createDisconnectedSut(t, bus) - cmd := &models.SendEmailCommandSync{ - SendEmailCommand: models.SendEmailCommand{ + cmd := &SendEmailCommandSync{ + SendEmailCommand: SendEmailCommand{ Subject: "subject", To: []string{"1@grafana.com", "2@grafana.com", "3@grafana.com"}, SingleEmail: false, @@ -195,7 +195,7 @@ func TestSendEmailAsync(t *testing.T) { t.Run("When sending reset email password", func(t *testing.T) { sut, _ := createSut(t, bus) testuser := user.User{Email: "asd@asd.com", Login: "asd@asd.com"} - err := sut.SendResetPasswordEmail(context.Background(), &models.SendResetPasswordEmailCommand{User: &testuser}) + err := sut.SendResetPasswordEmail(context.Background(), &SendResetPasswordEmailCommand{User: &testuser}) require.NoError(t, err) @@ -212,7 +212,7 @@ func TestSendEmailAsync(t *testing.T) { code := match[len("code="):] // verify code - query := models.ValidateResetPasswordCodeQuery{Code: code} + query := ValidateResetPasswordCodeQuery{Code: code} getUserByLogin := func(ctx context.Context, login string) (*user.User, error) { return &testuser, nil } @@ -225,7 +225,7 @@ func TestSendEmailAsync(t *testing.T) { cfg.Smtp.Enabled = false ns, mailer, err := createSutWithConfig(t, bus, cfg) require.NoError(t, err) - cmd := &models.SendEmailCommand{ + cmd := &SendEmailCommand{ Subject: "subject", To: []string{"1@grafana.com", "2@grafana.com", "3@grafana.com"}, SingleEmail: true, @@ -234,7 +234,7 @@ func TestSendEmailAsync(t *testing.T) { err = ns.SendEmailCommandHandler(context.Background(), cmd) - require.ErrorIs(t, err, models.ErrSmtpNotEnabled) + require.ErrorIs(t, err, ErrSmtpNotEnabled) require.Empty(t, mailer.Sent) }) @@ -243,7 +243,7 @@ func TestSendEmailAsync(t *testing.T) { cfg.Smtp.ContentTypes = append(cfg.Smtp.ContentTypes, "multipart/form-data") ns, mailer, err := createSutWithConfig(t, bus, cfg) require.NoError(t, err) - cmd := &models.SendEmailCommand{ + cmd := &SendEmailCommand{ Subject: "subject", To: []string{"1@grafana.com", "2@grafana.com", "3@grafana.com"}, SingleEmail: false, @@ -258,7 +258,7 @@ func TestSendEmailAsync(t *testing.T) { t.Run("When SMTP dialer is disconnected", func(t *testing.T) { ns := createDisconnectedSut(t, bus) - cmd := &models.SendEmailCommand{ + cmd := &SendEmailCommand{ Subject: "subject", To: []string{"1@grafana.com", "2@grafana.com", "3@grafana.com"}, SingleEmail: false, diff --git a/pkg/services/notifications/send_email_integration_test.go b/pkg/services/notifications/send_email_integration_test.go index a33d2781fa4..99f0cee48ab 100644 --- a/pkg/services/notifications/send_email_integration_test.go +++ b/pkg/services/notifications/send_email_integration_test.go @@ -5,7 +5,6 @@ import ( "os" "testing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/require" @@ -28,7 +27,7 @@ func TestEmailIntegrationTest(t *testing.T) { ns.Cfg.Smtp.ContentTypes = []string{"text/html", "text/plain"} t.Run("When sending reset email password", func(t *testing.T) { - cmd := &models.SendEmailCommand{ + cmd := &SendEmailCommand{ Data: map[string]interface{}{ "Title": "[CRITICAL] Imaginary timeseries alert", diff --git a/pkg/tests/api/alerting/api_notification_channel_test.go b/pkg/tests/api/alerting/api_notification_channel_test.go index 0a90e4ec626..7396e24dbaf 100644 --- a/pkg/tests/api/alerting/api_notification_channel_test.go +++ b/pkg/tests/api/alerting/api_notification_channel_test.go @@ -22,10 +22,10 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/models" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" + "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" @@ -740,7 +740,7 @@ func TestIntegrationNotificationChannels(t *testing.T) { env.NotificationService.EmailHandlerSync = mockEmail.sendEmailCommandHandlerSync // As we are using a NotificationService mock here, but the test expects real NotificationService - // we try to issue a real POST request here - env.NotificationService.WebhookHandler = func(_ context.Context, cmd *models.SendWebhookSync) error { + env.NotificationService.WebhookHandler = func(_ context.Context, cmd *notifications.SendWebhookSync) error { if res, err := http.Post(cmd.Url, "", strings.NewReader(cmd.Body)); err == nil { _ = res.Body.Close() } @@ -1165,10 +1165,10 @@ func (nc *mockNotificationChannel) Close() error { } type mockEmailHandler struct { - emails []*models.SendEmailCommandSync + emails []*notifications.SendEmailCommandSync } -func (e *mockEmailHandler) sendEmailCommandHandlerSync(_ context.Context, cmd *models.SendEmailCommandSync) error { +func (e *mockEmailHandler) sendEmailCommandHandlerSync(_ context.Context, cmd *notifications.SendEmailCommandSync) error { // We 0 out the start time since that is a variable that we cannot predict. alerts := cmd.Data["Alerts"].(channels.ExtendedAlerts) for i := range alerts { @@ -1185,7 +1185,7 @@ type mockEmailHandlerWithTimeout struct { timeout time.Duration } -func (e *mockEmailHandlerWithTimeout) sendEmailCommandHandlerSync(ctx context.Context, cmd *models.SendEmailCommandSync) error { +func (e *mockEmailHandlerWithTimeout) sendEmailCommandHandlerSync(ctx context.Context, cmd *notifications.SendEmailCommandSync) error { select { case <-time.After(e.timeout): return e.mockEmailHandler.sendEmailCommandHandlerSync(ctx, cmd) @@ -2280,9 +2280,9 @@ var expAlertmanagerConfigFromAPI = ` } ` -var expEmailNotifications = []*models.SendEmailCommandSync{ +var expEmailNotifications = []*notifications.SendEmailCommandSync{ { - SendEmailCommand: models.SendEmailCommand{ + SendEmailCommand: notifications.SendEmailCommand{ To: []string{"test@email.com"}, SingleEmail: true, Template: "ng_alert_notification", From 1ac89ea04026473505810a3a7637b1a2f60e74b6 Mon Sep 17 00:00:00 2001 From: Alexander Weaver Date: Tue, 17 Jan 2023 13:58:52 -0600 Subject: [PATCH 63/65] Alerting: Add client configuration for remote Loki historian backend and test connection (#61114) * Create loki client type and ping method * Expose TestConnection on client * Configure and ping Loki URL * Close response body reader if present * Add 30 second timeout * Remove duplicate close --- pkg/services/ngalert/ngalert.go | 10 +++- pkg/services/ngalert/state/historian/loki.go | 22 ++++++-- .../ngalert/state/historian/loki_http.go | 54 +++++++++++++++++++ pkg/setting/setting_unified_alerting.go | 10 ++-- 4 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 pkg/services/ngalert/state/historian/loki_http.go diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 993b35bd2da..fa9622b919a 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -387,7 +387,15 @@ func configureHistorianBackend(cfg setting.UnifiedAlertingStateHistorySettings, return historian.NewAnnotationBackend(ar, ds), nil } if cfg.Backend == "loki" { - return historian.NewRemoteLokiBackend(), nil + baseURL, err := url.Parse(cfg.LokiRemoteURL) + if err != nil { + return nil, fmt.Errorf("failed to parse remote loki URL: %w", err) + } + backend := historian.NewRemoteLokiBackend(baseURL) + if err := backend.TestConnection(); err != nil { + return nil, fmt.Errorf("failed to ping the remote loki historian: %w", err) + } + return backend, nil } if cfg.Backend == "sql" { return historian.NewSqlBackend(), nil diff --git a/pkg/services/ngalert/state/historian/loki.go b/pkg/services/ngalert/state/historian/loki.go index a1ebacddc06..038f4e98065 100644 --- a/pkg/services/ngalert/state/historian/loki.go +++ b/pkg/services/ngalert/state/historian/loki.go @@ -2,21 +2,35 @@ package historian import ( "context" + "net/url" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" ) -type RemoteLokiBackend struct { - log log.Logger +type remoteLokiClient interface { + ping() error } -func NewRemoteLokiBackend() *RemoteLokiBackend { +type RemoteLokiBackend struct { + client remoteLokiClient + log log.Logger +} + +func NewRemoteLokiBackend(url *url.URL) *RemoteLokiBackend { + logger := log.New("ngalert.state.historian", "backend", "loki") return &RemoteLokiBackend{ - log: log.New("ngalert.state.historian"), + client: newLokiClient(url, logger), + log: logger, } } +func (h *RemoteLokiBackend) TestConnection() error { + return h.client.ping() +} + func (h *RemoteLokiBackend) RecordStatesAsync(ctx context.Context, _ *models.AlertRule, _ []state.StateTransition) { + logger := h.log.FromContext(ctx) + logger.Debug("Remote Loki state history backend was called with states") } diff --git a/pkg/services/ngalert/state/historian/loki_http.go b/pkg/services/ngalert/state/historian/loki_http.go new file mode 100644 index 00000000000..78232dde025 --- /dev/null +++ b/pkg/services/ngalert/state/historian/loki_http.go @@ -0,0 +1,54 @@ +package historian + +import ( + "fmt" + "net/http" + "net/url" + "time" + + "github.com/grafana/grafana/pkg/infra/log" +) + +const defaultClientTimeout = 30 * time.Second + +type httpLokiClient struct { + client http.Client + url *url.URL + log log.Logger +} + +func newLokiClient(u *url.URL, logger log.Logger) *httpLokiClient { + return &httpLokiClient{ + client: http.Client{ + Timeout: defaultClientTimeout, + }, + url: u, + log: logger.New("protocol", "http"), + } +} + +func (c *httpLokiClient) ping() error { + uri := c.url.JoinPath("/loki/api/v1/status/buildinfo") + req, err := http.NewRequest(http.MethodGet, uri.String(), nil) + if err != nil { + return fmt.Errorf("error creating request: %w", err) + } + + res, err := c.client.Do(req) + if res != nil { + defer func() { + if err := res.Body.Close(); err != nil { + c.log.Warn("Failed to close response body", "err", err) + } + }() + } + if err != nil { + return fmt.Errorf("error sending request: %w", err) + } + + if res.StatusCode < 200 || res.StatusCode >= 300 { + return fmt.Errorf("request to the loki buildinfo endpoint returned a non-200 status code: %d", res.StatusCode) + } + c.log.Debug("Request to Loki buildinfo endpoint succeeded", "status", res.StatusCode) + return nil +} diff --git a/pkg/setting/setting_unified_alerting.go b/pkg/setting/setting_unified_alerting.go index b9441ca5d5a..9663b44426d 100644 --- a/pkg/setting/setting_unified_alerting.go +++ b/pkg/setting/setting_unified_alerting.go @@ -101,8 +101,9 @@ type UnifiedAlertingReservedLabelSettings struct { } type UnifiedAlertingStateHistorySettings struct { - Enabled bool - Backend string + Enabled bool + Backend string + LokiRemoteURL string } // IsEnabled returns true if UnifiedAlertingSettings.Enabled is either nil or true. @@ -313,8 +314,9 @@ func (cfg *Cfg) ReadUnifiedAlertingSettings(iniFile *ini.File) error { stateHistory := iniFile.Section("unified_alerting.state_history") uaCfgStateHistory := UnifiedAlertingStateHistorySettings{ - Enabled: stateHistory.Key("enabled").MustBool(stateHistoryDefaultEnabled), - Backend: stateHistory.Key("backend").MustString("annotations"), + Enabled: stateHistory.Key("enabled").MustBool(stateHistoryDefaultEnabled), + Backend: stateHistory.Key("backend").MustString("annotations"), + LokiRemoteURL: stateHistory.Key("loki_remote_url").MustString(""), } uaCfg.StateHistory = uaCfgStateHistory From 05a683d4624f767384777dba9959734ba5a92e6f Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Tue, 17 Jan 2023 21:04:12 +0100 Subject: [PATCH 64/65] SupportBundles: Fix list response when we don't have any bundles yet (#61619) * SupportBundles: Return empty slice instead of nil when there are no support bundles --- pkg/services/supportbundles/supportbundlesimpl/store.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/supportbundles/supportbundlesimpl/store.go b/pkg/services/supportbundles/supportbundlesimpl/store.go index d0f8df74094..ca57a072ece 100644 --- a/pkg/services/supportbundles/supportbundlesimpl/store.go +++ b/pkg/services/supportbundles/supportbundlesimpl/store.go @@ -101,7 +101,7 @@ func (s *store) List() ([]supportbundles.Bundle, error) { return nil, err } - var res []supportbundles.Bundle + res := make([]supportbundles.Bundle, 0) for _, items := range data { for _, s := range items { var b supportbundles.Bundle From 7e7daf48f46be378f2496c2bdd8276084e35384a Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Tue, 17 Jan 2023 14:56:23 -0600 Subject: [PATCH 65/65] Transforms: make partitionByValues create field labels by default (#61255) --- .../partitionByValues.test.ts | 88 ++++++++++++++++++- .../partitionByValues/partitionByValues.ts | 66 +++++++++++--- 2 files changed, 138 insertions(+), 16 deletions(-) diff --git a/public/app/features/transformers/partitionByValues/partitionByValues.test.ts b/public/app/features/transformers/partitionByValues/partitionByValues.test.ts index 7b10b7b67aa..3b41ccbab8f 100644 --- a/public/app/features/transformers/partitionByValues/partitionByValues.test.ts +++ b/public/app/features/transformers/partitionByValues/partitionByValues.test.ts @@ -21,6 +21,10 @@ describe('Partition by values transformer', () => { const config: PartitionByValuesTransformerOptions = { fields: ['region'], + keepFields: true, + naming: { + asLabels: false, + }, }; let partitioned = partitionByValuesTransformer.transformer(config, ctx)(source); @@ -57,6 +61,10 @@ describe('Partition by values transformer', () => { const config: PartitionByValuesTransformerOptions = { fields: ['region', 'status'], + keepFields: true, + naming: { + asLabels: false, + }, }; let partitioned = partitionByValuesTransformer.transformer(config, ctx)(source); @@ -100,7 +108,7 @@ describe('Partition by values transformer', () => { expect(partitioned[3].fields[2].values.toArray()).toEqual(['FAIL']); }); - it('should partition by multiple fields with custom frame naming {withFields: true}', () => { + it('should partition by multiple fields with custom frame naming {withNames: true}', () => { const source = [ toDataFrame({ name: 'XYZ', @@ -115,8 +123,10 @@ describe('Partition by values transformer', () => { const config: PartitionByValuesTransformerOptions = { fields: ['region', 'status'], + keepFields: true, naming: { - withFields: true, + asLabels: false, + withNames: true, }, }; @@ -143,7 +153,9 @@ describe('Partition by values transformer', () => { const config: PartitionByValuesTransformerOptions = { fields: ['region', 'status'], + keepFields: true, naming: { + asLabels: false, append: true, }, }; @@ -156,7 +168,7 @@ describe('Partition by values transformer', () => { expect(partitioned[3].name).toEqual('XYZ China FAIL'); }); - it('should partition by multiple fields with custom frame naming {withFields: true, append: true}', () => { + it('should partition by multiple fields with custom frame naming {withNames: true, append: true}', () => { const source = [ toDataFrame({ name: 'XYZ', @@ -171,8 +183,10 @@ describe('Partition by values transformer', () => { const config: PartitionByValuesTransformerOptions = { fields: ['region', 'status'], + keepFields: true, naming: { - withFields: true, + asLabels: false, + withNames: true, append: true, }, }; @@ -184,4 +198,70 @@ describe('Partition by values transformer', () => { expect(partitioned[2].name).toEqual('XYZ region=China status=OK'); expect(partitioned[3].name).toEqual('XYZ region=China status=FAIL'); }); + + it('should partition by multiple fields naming: {asLabels: true}', () => { + const source = [ + toDataFrame({ + name: 'XYZ', + refId: 'A', + fields: [ + { name: 'model', type: FieldType.string, values: ['E1', 'E2', 'C1', 'E3', 'C2', 'C3'] }, + { name: 'region', type: FieldType.string, values: ['Europe', 'Europe', 'China', 'Europe', 'China', 'China'] }, + { name: 'status', type: FieldType.string, values: ['OK', 'FAIL', 'OK', 'FAIL', 'OK', 'FAIL'] }, + ], + }), + ]; + + const config: PartitionByValuesTransformerOptions = { + fields: ['region', 'status'], + keepFields: true, + naming: { + asLabels: true, + }, + }; + + let partitioned = partitionByValuesTransformer.transformer(config, ctx)(source); + + // all frame names are same + expect(partitioned[0].name).toEqual('XYZ'); + expect(partitioned[1].name).toEqual('XYZ'); + expect(partitioned[2].name).toEqual('XYZ'); + expect(partitioned[3].name).toEqual('XYZ'); + + // all frames contain all fields + expect(partitioned[0].fields[0].name).toEqual('model'); + expect(partitioned[0].fields[1].name).toEqual('region'); + expect(partitioned[0].fields[2].name).toEqual('status'); + + // in each frame, every field has same labels + expect(partitioned[0].fields[0].labels).toEqual({ region: 'Europe', status: 'OK' }); + expect(partitioned[1].fields[0].labels).toEqual({ region: 'Europe', status: 'FAIL' }); + expect(partitioned[2].fields[0].labels).toEqual({ region: 'China', status: 'OK' }); + expect(partitioned[3].fields[0].labels).toEqual({ region: 'China', status: 'FAIL' }); + }); + + it('should partition by multiple fields and omit those fields in result', () => { + const source = [ + toDataFrame({ + name: 'XYZ', + refId: 'A', + fields: [ + { name: 'model', type: FieldType.string, values: ['E1', 'E2', 'C1', 'E3', 'C2', 'C3'] }, + { name: 'region', type: FieldType.string, values: ['Europe', 'Europe', 'China', 'Europe', 'China', 'China'] }, + { name: 'status', type: FieldType.string, values: ['OK', 'FAIL', 'OK', 'FAIL', 'OK', 'FAIL'] }, + ], + }), + ]; + + const config: PartitionByValuesTransformerOptions = { + fields: ['region', 'status'], + }; + + let partitioned = partitionByValuesTransformer.transformer(config, ctx)(source); + + // all frames contain only model field + expect(partitioned[0].fields.length).toEqual(1); + expect(partitioned[0].fields[0].name).toEqual('model'); + expect(partitioned[0].fields[0].labels).toEqual({ region: 'Europe', status: 'OK' }); + }); }); diff --git a/public/app/features/transformers/partitionByValues/partitionByValues.ts b/public/app/features/transformers/partitionByValues/partitionByValues.ts index 2b26be6d6b9..1d558728a02 100644 --- a/public/app/features/transformers/partitionByValues/partitionByValues.ts +++ b/public/app/features/transformers/partitionByValues/partitionByValues.ts @@ -14,10 +14,15 @@ import { noopTransformer } from '@grafana/data/src/transformations/transformers/ import { partition } from './partition'; export interface FrameNamingOptions { + /** when true, the frame name is copied unmodified, and discriminator fields' names+values become field labels in new frames */ + asLabels?: boolean; + + /** opts below are used only when asLabels: false */ + /** whether to append to existing frame name, false -> replace */ append?: boolean; // false /** whether to include discriminator field names, e.g. true -> Region=Europe Profession=Chef, false -> 'Europe Chef' */ - withFields?: boolean; // false + withNames?: boolean; // false /** name/value separator, e.g. '=' in 'Region=Europe' */ separator1?: string; /** name/value pair separator, e.g. ' ' in 'Region=Europe Profession=Chef' */ @@ -25,8 +30,10 @@ export interface FrameNamingOptions { } const defaultFrameNameOptions: FrameNamingOptions = { + asLabels: true, + append: false, - withFields: false, + withNames: false, separator1: '=', separator2: ' ', }; @@ -36,14 +43,26 @@ export interface PartitionByValuesTransformerOptions { fields: string[]; /** how the split frames' names should be suffixed (ends up as field prefixes) */ naming?: FrameNamingOptions; + /** should the discriminator fields be kept in the output */ + keepFields?: boolean; } function buildFrameName(opts: FrameNamingOptions, names: string[], values: unknown[]): string { return names - .map((name, i) => (opts.withFields ? `${name}${opts.separator1}${values[i]}` : values[i])) + .map((name, i) => (opts.withNames ? `${name}${opts.separator1}${values[i]}` : values[i])) .join(opts.separator2); } +function buildFieldLabels(names: string[], values: unknown[]) { + const labels: Record = {}; + + names.forEach((name, i) => { + labels[name] = String(values[i]); + }); + + return labels; +} + export const partitionByValuesTransformer: SynchronousDataTransformerInfo = { id: DataTransformerID.partitionByValues, name: 'Partition by values', @@ -78,21 +97,40 @@ export const partitionByValuesTransformer: SynchronousDataTransformerInfo { - let name = buildFrameName( - frameNameOpts, - names, - keyFields.map((f, i) => keyFieldsVals[i][idxs[0]]) - ); + let frameName = frame.name; + let fieldLabels = {}; - if (options.naming?.append && frame.name) { - name = `${frame.name} ${name}`; + if (frameNameOpts.asLabels) { + fieldLabels = buildFieldLabels( + names, + keyFields.map((f, i) => keyFieldsVals[i][idxs[0]]) + ); + } else { + let name = buildFrameName( + frameNameOpts, + names, + keyFields.map((f, i) => keyFieldsVals[i][idxs[0]]) + ); + + if (options.naming?.append && frame.name) { + name = `${frame.name} ${name}`; + } + + frameName = name; + } + + let filteredFields = frame.fields; + + if (!options.keepFields) { + const keyFieldNames = new Set(names); + filteredFields = frame.fields.filter((field) => !keyFieldNames.has(field.name)); } return { ...frame, - name, + name: frameName, length: idxs.length, - fields: frame.fields.map((f) => { + fields: filteredFields.map((f) => { const vals = f.values.toArray(); const vals2 = Array(idxs.length); @@ -102,6 +140,10 @@ export const partitionByValuesTransformer: SynchronousDataTransformerInfo
Created on Requested by Expires +
{dateTimeFormat(b.createdAt * 1000)}{b.creator}{dateTimeFormat(b.expiresAt * 1000)}
{dateTimeFormat(bundle.createdAt * 1000)}{bundle.creator}{dateTimeFormat(bundle.expiresAt * 1000)}{bundle.state === 'pending' && } Download + removeBundle(bundle.uid)} name="trash-alt" variant="destructive" /> +