From 421b911c4524b6e9832c6e4d8d8a685f9f84ff7e Mon Sep 17 00:00:00 2001
From: Christopher Moyer <35463610+chri2547@users.noreply.github.com>
Date: Wed, 5 Apr 2023 15:35:16 -0500
Subject: [PATCH 61/80] docs: fixes link (#66051)
* fixes link
* Update docs/sources/setup-grafana/configure-security/configure-authentication/enhanced-ldap/index.md
Co-authored-by: melGL <81323402+melgl@users.noreply.github.com>
---------
Co-authored-by: melGL <81323402+melgl@users.noreply.github.com>
---
.../configure-authentication/enhanced-ldap/index.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/enhanced-ldap/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/enhanced-ldap/index.md
index e03a7457b41..947fab9e93f 100644
--- a/docs/sources/setup-grafana/configure-security/configure-authentication/enhanced-ldap/index.md
+++ b/docs/sources/setup-grafana/configure-security/configure-authentication/enhanced-ldap/index.md
@@ -18,7 +18,8 @@ weight: 900
The enhanced LDAP integration adds additional functionality on top of the [LDAP integration]({{< relref "ldap/" >}}) available in the open source edition of Grafana.
-> **Note:** Available in [Grafana Enterprise]({{< relref "../../../../introduction/grafana-enterprise/" >}}) and [Grafana Cloud Advanced](/docs/grafana-cloud). If you are a Grafana Cloud customer, please [open a support ticket in the Cloud Portal](https://grafana.com/orgs/$org/tickets) to request this feature.
+> **Note:** Available in [Grafana Enterprise]({{< relref "../../../../introduction/grafana-enterprise/" >}}) and [Grafana Cloud Advanced](/docs/grafana-cloud).
+> If you are a Grafana Cloud customer, please [open a support ticket in the Cloud Portal](/profile/org#support) to request this feature.
> To control user access with role-based permissions, refer to [role-based access control]({{< relref "../../../../administration/roles-and-permissions/access-control/" >}}).
From 45df5362639c8b8af2e323ec9be7c01baa6ae751 Mon Sep 17 00:00:00 2001
From: Ivan Ortega Alba
Date: Thu, 6 Apr 2023 09:07:41 +0200
Subject: [PATCH 62/80] Flaky E2E: Wait for the data to be loaded before switch
to table view (#66072)
Fix flaky e2e for panel edit suite
---
e2e/panels-suite/panelEdit_base.spec.ts | 1 +
packages/grafana-e2e-selectors/src/selectors/components.ts | 1 +
2 files changed, 2 insertions(+)
diff --git a/e2e/panels-suite/panelEdit_base.spec.ts b/e2e/panels-suite/panelEdit_base.spec.ts
index 44c3e8d25ee..6d38ba9b1b2 100644
--- a/e2e/panels-suite/panelEdit_base.spec.ts
+++ b/e2e/panels-suite/panelEdit_base.spec.ts
@@ -79,6 +79,7 @@ e2e.scenario({
e2e.components.PluginVisualization.current().should((e) => expect(e).to.contain('Time series'));
// Check that table view works
+ e2e.components.Panels.Panel.loadingBar().should('not.exist');
e2e.components.PanelEditor.toggleTableView().click({ force: true });
e2e.components.Panels.Visualization.Table.header()
.should('be.visible')
diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts
index 245ec1d342a..7ba3ebf6068 100644
--- a/packages/grafana-e2e-selectors/src/selectors/components.ts
+++ b/packages/grafana-e2e-selectors/src/selectors/components.ts
@@ -76,6 +76,7 @@ export const Components = {
menu: (title: string) => `data-testid Panel menu ${title}`,
containerByTitle: (title: string) => `${title} panel`,
headerCornerInfo: (mode: string) => `Panel header ${mode}`,
+ loadingBar: () => `Panel loading bar`,
},
Visualization: {
Graph: {
From c4c406aacdbc6c9c2417ebb9fbee82a01067c4e1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?G=C3=A1bor=20Farkas?=
Date: Thu, 6 Apr 2023 09:14:21 +0200
Subject: [PATCH 63/80] DataFrame: Handle nanosecond-precision timestamp fields
(#64529)
handle nanosec-precision timestamps
---
.../src/dataframe/DataFrameJSON.test.ts | 104 +++++++++++++++++-
.../src/dataframe/DataFrameJSON.ts | 37 ++++++-
packages/grafana-data/src/types/dataFrame.ts | 8 ++
3 files changed, 144 insertions(+), 5 deletions(-)
diff --git a/packages/grafana-data/src/dataframe/DataFrameJSON.test.ts b/packages/grafana-data/src/dataframe/DataFrameJSON.test.ts
index 6cd11677ba0..fe6930eaf2f 100644
--- a/packages/grafana-data/src/dataframe/DataFrameJSON.test.ts
+++ b/packages/grafana-data/src/dataframe/DataFrameJSON.test.ts
@@ -1,6 +1,7 @@
-import { FieldType } from '../types/dataFrame';
+import { ArrayVector } from '..';
+import { DataFrame, FieldType } from '../types/dataFrame';
-import { DataFrameJSON, dataFrameFromJSON } from './DataFrameJSON';
+import { DataFrameJSON, dataFrameFromJSON, dataFrameToJSON } from './DataFrameJSON';
describe('DataFrame JSON', () => {
describe('when called with a DataFrame', () => {
@@ -137,5 +138,104 @@ describe('DataFrame JSON', () => {
}
`);
});
+
+ it('should decode fields with nanos', () => {
+ const json: DataFrameJSON = {
+ schema: {
+ fields: [
+ { name: 'time1', type: FieldType.time },
+ { name: 'time2', type: FieldType.time },
+ ],
+ },
+ data: {
+ values: [
+ [1, 2, 3],
+ [4, 5, 6],
+ ],
+ nanos: [null, [7, 8, 9]],
+ },
+ };
+
+ const frame = dataFrameFromJSON(json);
+ expect(frame).toMatchInlineSnapshot(`
+ {
+ "fields": [
+ {
+ "config": {},
+ "entities": {},
+ "name": "time1",
+ "type": "time",
+ "values": [
+ 1,
+ 2,
+ 3,
+ ],
+ },
+ {
+ "config": {},
+ "entities": {},
+ "name": "time2",
+ "nanos": [
+ 7,
+ 8,
+ 9,
+ ],
+ "type": "time",
+ "values": [
+ 4,
+ 5,
+ 6,
+ ],
+ },
+ ],
+ "length": 3,
+ }
+ `);
+ });
+
+ it('should encode fields with nanos', () => {
+ const inputFrame: DataFrame = {
+ refId: 'A',
+ meta: {},
+ name: 'f1',
+ fields: [
+ {
+ name: 'time1',
+ type: FieldType.time,
+ config: {},
+ values: new ArrayVector([11, 12, 13]),
+ },
+ {
+ name: 'time2',
+ type: FieldType.time,
+ config: {},
+ values: new ArrayVector([14, 15, 16]),
+ nanos: [17, 18, 19],
+ },
+ ],
+ length: 3,
+ };
+
+ const expectedJSON: DataFrameJSON = {
+ schema: {
+ fields: [
+ { name: 'time1', type: FieldType.time, config: {} },
+ { name: 'time2', type: FieldType.time, config: {} },
+ ],
+ meta: {},
+ name: 'f1',
+ refId: 'A',
+ },
+ data: {
+ nanos: [null, [17, 18, 19]],
+ values: [
+ [11, 12, 13],
+ [14, 15, 16],
+ ],
+ },
+ };
+
+ expect(dataFrameToJSON(inputFrame)).toStrictEqual(expectedJSON);
+ });
});
});
diff --git a/packages/grafana-data/src/dataframe/DataFrameJSON.ts b/packages/grafana-data/src/dataframe/DataFrameJSON.ts
index 79dd8e14acd..47b64fe72b6 100644
--- a/packages/grafana-data/src/dataframe/DataFrameJSON.ts
+++ b/packages/grafana-data/src/dataframe/DataFrameJSON.ts
@@ -1,4 +1,4 @@
-import { DataFrame, FieldType, FieldConfig, Labels, QueryResultMeta } from '../types';
+import { DataFrame, FieldType, FieldConfig, Labels, QueryResultMeta, Field } from '../types';
import { ArrayVector } from '../vector';
import { guessFieldTypeFromNameAndValue } from './processDataFrame';
@@ -56,6 +56,13 @@ export interface DataFrameData {
* NOTE: currently only decoding is implemented
*/
enums?: Array;
+
+ /**
+ * Holds integers between 0 and 999999, used by time-fields
+ * to store the nanosecond-precision that cannot be represented
+ * by the millisecond-based base value.
+ */
+ nanos?: Array;
}
/**
@@ -188,9 +195,11 @@ export function dataFrameFromJSON(dto: DataFrameJSON): DataFrame {
type = FieldType.string;
}
+ const nanos = data?.nanos?.[index];
+
// TODO: expand arrays further using bases,factors
- return {
+ const dataFrameField: Field & { entities: FieldValueEntityLookup } = {
...f,
type: type ?? guessFieldType(f.name, buffer),
config: f.config ?? {},
@@ -198,6 +207,12 @@ export function dataFrameFromJSON(dto: DataFrameJSON): DataFrame {
// the presence of this prop is an optimization signal & lookup for consumers
entities: entities ?? {},
};
+
+ if (nanos != null) {
+ dataFrameField.nanos = nanos;
+ }
+
+ return dataFrameField;
});
return {
@@ -216,18 +231,34 @@ export function dataFrameToJSON(frame: DataFrame): DataFrameJSON {
const data: DataFrameData = {
values: [],
};
+
+ const allNanos: Array = [];
+ let hasNanos = false;
+
const schema: DataFrameSchema = {
refId: frame.refId,
meta: frame.meta,
name: frame.name,
fields: frame.fields.map((f) => {
- const { values, state, display, ...sfield } = f;
+ const { values, nanos, state, display, ...sfield } = f;
delete (sfield as any).entities;
data.values.push(values.toArray());
+
+ if (nanos != null) {
+ allNanos.push(nanos);
+ hasNanos = true;
+ } else {
+ allNanos.push(null);
+ }
+
return sfield;
}),
};
+ if (hasNanos) {
+ data.nanos = allNanos;
+ }
+
return {
schema,
data,
diff --git a/packages/grafana-data/src/types/dataFrame.ts b/packages/grafana-data/src/types/dataFrame.ts
index 5ee8a3d30db..d7200416ce4 100644
--- a/packages/grafana-data/src/types/dataFrame.ts
+++ b/packages/grafana-data/src/types/dataFrame.ts
@@ -137,6 +137,14 @@ export interface Field> {
*/
config: FieldConfig;
values: V; // The raw field values
+
+ /**
+ * When type === FieldType.Time, this can optionally store
+ * the nanosecond-precison fractions as integers between
+ * 0 and 999999.
+ */
+ nanos?: number[];
+
labels?: Labels;
/**
From 3843c73162b686a24000b27deca6da4713848cdc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Torkel=20=C3=96degaard?=
Date: Thu, 6 Apr 2023 09:19:26 +0200
Subject: [PATCH 64/80] Table: Fix migrations from old angular table for cell
color modes (#65760)
* Table: Fix migrations from old angular table
* Update defaults
---
.../__snapshots__/migrations.test.ts.snap | 137 -----------------
.../plugins/panel/table/migrations.test.ts | 140 +++++++++++++++++-
public/app/plugins/panel/table/migrations.ts | 14 +-
3 files changed, 150 insertions(+), 141 deletions(-)
diff --git a/public/app/plugins/panel/table/__snapshots__/migrations.test.ts.snap b/public/app/plugins/panel/table/__snapshots__/migrations.test.ts.snap
index 965c68b5c56..e51e742e7d5 100644
--- a/public/app/plugins/panel/table/__snapshots__/migrations.test.ts.snap
+++ b/public/app/plugins/panel/table/__snapshots__/migrations.test.ts.snap
@@ -1,142 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Table Migrations migrates styles to field config overrides and defaults 1`] = `
-{
- "fieldConfig": {
- "defaults": {
- "custom": {
- "align": "right",
- "displayMode": undefined,
- },
- "decimals": 2,
- "displayName": "",
- "unit": "short",
- },
- "overrides": [
- {
- "matcher": {
- "id": "byName",
- "options": "Time",
- },
- "properties": [
- {
- "id": "displayName",
- "value": "Time",
- },
- {
- "id": "unit",
- "value": "time: YYYY-MM-DD HH:mm:ss",
- },
- {
- "id": "custom.align",
- "value": null,
- },
- ],
- },
- {
- "matcher": {
- "id": "byName",
- "options": "ColorCell",
- },
- "properties": [
- {
- "id": "unit",
- "value": "currencyUSD",
- },
- {
- "id": "decimals",
- "value": 2,
- },
- {
- "id": "custom.displayMode",
- "value": "color-background",
- },
- {
- "id": "custom.align",
- "value": "left",
- },
- {
- "id": "thresholds",
- "value": {
- "mode": "absolute",
- "steps": [
- {
- "color": "rgba(245, 54, 54, 0.9)",
- "value": -Infinity,
- },
- {
- "color": "rgba(237, 129, 40, 0.89)",
- "value": 5,
- },
- {
- "color": "rgba(50, 172, 45, 0.97)",
- "value": 10,
- },
- ],
- },
- },
- ],
- },
- {
- "matcher": {
- "id": "byName",
- "options": "ColorValue",
- },
- "properties": [
- {
- "id": "unit",
- "value": "Bps",
- },
- {
- "id": "decimals",
- "value": 2,
- },
- {
- "id": "links",
- "value": [
- {
- "targetBlank": true,
- "title": "",
- "url": "http://www.grafana.com",
- },
- ],
- },
- {
- "id": "custom.displayMode",
- "value": "color-text",
- },
- {
- "id": "custom.align",
- "value": null,
- },
- {
- "id": "thresholds",
- "value": {
- "mode": "absolute",
- "steps": [
- {
- "color": "rgba(245, 54, 54, 0.9)",
- "value": -Infinity,
- },
- {
- "color": "rgba(237, 129, 40, 0.89)",
- "value": 5,
- },
- {
- "color": "rgba(50, 172, 45, 0.97)",
- "value": 10,
- },
- ],
- },
- },
- ],
- },
- ],
- },
- "transformations": [],
-}
-`;
-
exports[`Table Migrations migrates transform out to core transforms 1`] = `
{
"fieldConfig": {
diff --git a/public/app/plugins/panel/table/migrations.test.ts b/public/app/plugins/panel/table/migrations.test.ts
index 587d7907a7b..81b3286a6c3 100644
--- a/public/app/plugins/panel/table/migrations.test.ts
+++ b/public/app/plugins/panel/table/migrations.test.ts
@@ -128,6 +128,144 @@ describe('Table Migrations', () => {
};
const panel = {} as PanelModel;
tablePanelChangedHandler(panel, 'table-old', oldStyles);
- expect(panel).toMatchSnapshot();
+ expect(panel).toMatchInlineSnapshot(`
+ {
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "align": "right",
+ },
+ "decimals": 2,
+ "displayName": "",
+ "unit": "short",
+ },
+ "overrides": [
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "Time",
+ },
+ "properties": [
+ {
+ "id": "displayName",
+ "value": "Time",
+ },
+ {
+ "id": "unit",
+ "value": "time: YYYY-MM-DD HH:mm:ss",
+ },
+ {
+ "id": "custom.align",
+ "value": null,
+ },
+ ],
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "ColorCell",
+ },
+ "properties": [
+ {
+ "id": "unit",
+ "value": "currencyUSD",
+ },
+ {
+ "id": "decimals",
+ "value": 2,
+ },
+ {
+ "id": "custom.cellOptions",
+ "value": {
+ "type": "color-background",
+ },
+ },
+ {
+ "id": "custom.align",
+ "value": "left",
+ },
+ {
+ "id": "thresholds",
+ "value": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "rgba(245, 54, 54, 0.9)",
+ "value": -Infinity,
+ },
+ {
+ "color": "rgba(237, 129, 40, 0.89)",
+ "value": 5,
+ },
+ {
+ "color": "rgba(50, 172, 45, 0.97)",
+ "value": 10,
+ },
+ ],
+ },
+ },
+ ],
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "ColorValue",
+ },
+ "properties": [
+ {
+ "id": "unit",
+ "value": "Bps",
+ },
+ {
+ "id": "decimals",
+ "value": 2,
+ },
+ {
+ "id": "links",
+ "value": [
+ {
+ "targetBlank": true,
+ "title": "",
+ "url": "http://www.grafana.com",
+ },
+ ],
+ },
+ {
+ "id": "custom.cellOptions",
+ "value": {
+ "type": "color-text",
+ },
+ },
+ {
+ "id": "custom.align",
+ "value": null,
+ },
+ {
+ "id": "thresholds",
+ "value": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "rgba(245, 54, 54, 0.9)",
+ "value": -Infinity,
+ },
+ {
+ "color": "rgba(237, 129, 40, 0.89)",
+ "value": 5,
+ },
+ {
+ "color": "rgba(50, 172, 45, 0.97)",
+ "value": 10,
+ },
+ ],
+ },
+ },
+ ],
+ },
+ ],
+ },
+ "transformations": [],
+ }
+ `);
});
});
diff --git a/public/app/plugins/panel/table/migrations.ts b/public/app/plugins/panel/table/migrations.ts
index 2a32ffb8795..926a4907ab4 100644
--- a/public/app/plugins/panel/table/migrations.ts
+++ b/public/app/plugins/panel/table/migrations.ts
@@ -163,8 +163,10 @@ const migrateTableStyleToOverride = (style: Style) => {
if (style.colorMode) {
override.properties.push({
- id: 'custom.displayMode',
- value: colorModeMap[style.colorMode],
+ id: 'custom.cellOptions',
+ value: {
+ type: colorModeMap[style.colorMode],
+ },
});
}
@@ -200,11 +202,11 @@ const migrateDefaults = (prevDefaults: Style) => {
displayName: prevDefaults.alias,
custom: {
align: prevDefaults.align === 'auto' ? null : prevDefaults.align,
- displayMode: colorModeMap[prevDefaults.colorMode],
},
},
isNil
);
+
if (prevDefaults.thresholds.length) {
const thresholds: ThresholdsConfig = {
mode: ThresholdsMode.Absolute,
@@ -212,6 +214,12 @@ const migrateDefaults = (prevDefaults: Style) => {
};
defaults.thresholds = thresholds;
}
+
+ if (prevDefaults.colorMode) {
+ defaults.custom.cellOptions = {
+ type: colorModeMap[prevDefaults.colorMode],
+ };
+ }
}
return defaults;
};
From 2648fcb83369393d175c24547c389816cecc4e6f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ida=20=C5=A0tambuk?=
Date: Thu, 6 Apr 2023 09:52:26 +0200
Subject: [PATCH 65/80] Cloudwatch: Add missing AWS/IVS namespace metrics
(#65985)
---
pkg/tsdb/cloudwatch/constants/metrics.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pkg/tsdb/cloudwatch/constants/metrics.go b/pkg/tsdb/cloudwatch/constants/metrics.go
index 0b6f4fbd9ae..e0d29a1f602 100644
--- a/pkg/tsdb/cloudwatch/constants/metrics.go
+++ b/pkg/tsdb/cloudwatch/constants/metrics.go
@@ -340,7 +340,7 @@ var NamespaceMetricsMap = map[string][]string{
"AWS/Glue": {"glue.driver.BlockManager.disk.diskSpaceUsed_MB", "glue.driver.ExecutorAllocationManager.executors.numberAllExecutors", "glue.driver.ExecutorAllocationManager.executors.numberMaxNeededExecutors", "glue.driver.aggregate.bytesRead", "glue.driver.aggregate.elapsedTime", "glue.driver.aggregate.numCompletedStages", "glue.driver.aggregate.numCompletedTasks", "glue.driver.aggregate.numFailedTasks", "glue.driver.aggregate.numKilledTasks", "glue.driver.aggregate.recordsRead", "glue.driver.aggregate.shuffleBytesWritten", "glue.driver.aggregate.shuffleLocalBytesRead", "glue.driver.jvm.heap.usage glue.executorId.jvm.heap.usage glue.ALL.jvm.heap.usage", "glue.driver.jvm.heap.used glue.executorId.jvm.heap.used glue.ALL.jvm.heap.used", "glue.driver.s3.filesystem.read_bytes glue.executorId.s3.filesystem.read_bytes glue.ALL.s3.filesystem.read_bytes", "glue.driver.s3.filesystem.write_bytes glue.executorId.s3.filesystem.write_bytes glue.ALL.s3.filesystem.write_bytes", "glue.driver.system.cpuSystemLoad glue.executorId.system.cpuSystemLoad glue.ALL.system.cpuSystemLoad"},
"AWS/GroundStation": {"BitErrorRate", "BlockErrorRate", "ReceivedPower", "Es/N0"},
"AWS/Inspector": {"TotalAssessmentRunFindings", "TotalAssessmentRuns", "TotalHealthyAgents", "TotalMatchingAgents"},
- "AWS/IVS": {"ConcurrentViews", "ConcurrentStreams", "LiveDeliveredTime", "LiveInputTime", "RecordedTime"},
+ "AWS/IVS": {"ConcurrentViews", "ConcurrentStreams", "IngestAudioBitrate", "IngestFramerate", "IngestVideoBitrate", "KeyframeInterval", "LiveDeliveredTime", "LiveInputTime", "RecordedTime"},
"AWS/IoT": {"CanceledJobExecutionCount", "CanceledJobExecutionTotalCount", "ClientError", "Connect.AuthError", "Connect.ClientError", "Connect.ServerError", "Connect.Success", "Connect.Throttle", "DeleteThingShadow.Accepted", "FailedJobExecutionCount", "FailedJobExecutionTotalCount", "Failure", "GetThingShadow.Accepted", "InProgressJobExecutionCount", "InProgressJobExecutionTotalCount", "NonCompliantResources", "NumLogBatchesFailedToPublishThrottled", "NumLogEventsFailedToPublishThrottled", "ParseError", "Ping.Success", "PublishIn.AuthError", "PublishIn.ClientError", "PublishIn.ServerError", "PublishIn.Success", "PublishIn.Throttle", "PublishOut.AuthError", "PublishOut.ClientError", "PublishOut.Success", "QueuedJobExecutionCount", "QueuedJobExecutionTotalCount", "RejectedJobExecutionCount", "RejectedJobExecutionTotalCount", "RemovedJobExecutionCount", "RemovedJobExecutionTotalCount", "ResourcesEvaluated", "RuleMessageThrottled", "RuleNotFound", "RulesExecuted", "ServerError", "Subscribe.AuthError", "Subscribe.ClientError", "Subscribe.ServerError", "Subscribe.Success", "Subscribe.Throttle", "SuccededJobExecutionCount", "SuccededJobExecutionTotalCount", "Success", "TopicMatch", "Unsubscribe.ClientError", "Unsubscribe.ServerError", "Unsubscribe.Success", "Unsubscribe.Throttle", "UpdateThingShadow.Accepted", "Violations", "ViolationsCleared", "ViolationsInvalidated"},
"AWS/IoTAnalytics": {"ActionExecution", "ActivityExecutionError", "IncomingMessages"},
"AWS/IoTSiteWise": {"Gateway.Heartbeat", "Gateway.PublishSuccessCount", "Gateway.PublishFailureCount", "Gateway.ProcessFailureCount", "OPCUACollector.Heartbeat", "OPCUACollector.ActiveDataStreamCount", "OPCUACollector.IncomingValuesCount"},
From 988a120d6dbb020a179460ca7b069c8f2ae69d04 Mon Sep 17 00:00:00 2001
From: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com>
Date: Thu, 6 Apr 2023 11:16:15 +0300
Subject: [PATCH 66/80] Search v1: Add support for inherited folder permissions
if nested folders are enabled (#63275)
* Add features dependency to SQLBuilder
* Add features dependency to AccessControlDashboardPermissionFilter
* Add test for folder inheritance
* Dashboard permissions: Return recursive query
* Recursive query for inherited folders
* Modify search builder
* Adjust db.SQLBuilder
* Pass flag to SQLbuilder if CTEs are supported
* Add support for mysql < 8.0
* Add benchmarking for search with nested folders
* Set features to AlertStore
* Update pkg/infra/db/sqlbuilder.go
Co-authored-by: Ieva
* Set features to LibraryElementService
* SQLBuilder tests with nested folder flag set
* Apply suggestion from code review
Co-authored-by: IevaVasiljeva
Co-authored-by: Emil Tullstedt
---
pkg/infra/db/sqlbuilder.go | 42 ++-
pkg/infra/db/sqlbuilder_test.go | 167 +++++++++-
.../conditions/query_interval_test.go | 3 +-
.../alerting/conditions/query_test.go | 3 +-
pkg/services/alerting/extractor_test.go | 4 +-
pkg/services/alerting/store.go | 12 +-
.../annotationsimpl/annotations.go | 4 +-
.../annotations/annotationsimpl/cleanup.go | 10 +-
.../annotationsimpl/cleanup_test.go | 5 +-
.../annotations/annotationsimpl/xorm_store.go | 52 +++-
.../annotationsimpl/xorm_store_test.go | 174 ++++++++++-
pkg/services/dashboards/database/acl.go | 20 +-
pkg/services/dashboards/database/database.go | 7 +-
.../database/database_folder_test.go | 284 +++++++++++++++++-
pkg/services/libraryelements/database.go | 36 ++-
.../libraryelements/libraryelements.go | 5 +-
.../librarypanels/librarypanels_test.go | 2 +-
.../publicdashboards/service/query_test.go | 2 +-
.../sqlstore/permissions/dashboard.go | 191 ++++++++++--
.../sqlstore/permissions/dashboard_test.go | 221 +++++++++++++-
.../permissions/dashboards_bench_test.go | 182 +++++++++--
pkg/services/sqlstore/searchstore/builder.go | 19 ++
pkg/services/sqlstore/searchstore/filters.go | 6 +
.../sqlstore/searchstore/search_test.go | 141 +++++++++
24 files changed, 1487 insertions(+), 105 deletions(-)
diff --git a/pkg/infra/db/sqlbuilder.go b/pkg/infra/db/sqlbuilder.go
index 1b3c8427879..8d55f41fa7c 100644
--- a/pkg/infra/db/sqlbuilder.go
+++ b/pkg/infra/db/sqlbuilder.go
@@ -5,20 +5,26 @@ import (
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards"
+ "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/services/sqlstore/permissions"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
)
-func NewSqlBuilder(cfg *setting.Cfg, dialect migrator.Dialect) SQLBuilder {
- return SQLBuilder{cfg: cfg, dialect: dialect}
+func NewSqlBuilder(cfg *setting.Cfg, features featuremgmt.FeatureToggles, dialect migrator.Dialect, recursiveQueriesAreSupported bool) SQLBuilder {
+ return SQLBuilder{cfg: cfg, features: features, dialect: dialect, recursiveQueriesAreSupported: recursiveQueriesAreSupported}
}
type SQLBuilder struct {
- cfg *setting.Cfg
- sql bytes.Buffer
- params []interface{}
+ cfg *setting.Cfg
+ features featuremgmt.FeatureToggles
+ sql bytes.Buffer
+ params []interface{}
+ recQry string
+ recQryParams []interface{}
+ recursiveQueriesAreSupported bool
+
dialect migrator.Dialect
}
@@ -31,10 +37,22 @@ func (sb *SQLBuilder) Write(sql string, params ...interface{}) {
}
func (sb *SQLBuilder) GetSQLString() string {
- return sb.sql.String()
+ if sb.recQry == "" {
+ return sb.sql.String()
+ }
+
+ var bf bytes.Buffer
+ bf.WriteString(sb.recQry)
+ bf.WriteString(sb.sql.String())
+ return bf.String()
}
func (sb *SQLBuilder) GetParams() []interface{} {
+ if len(sb.recQryParams) == 0 {
+ return sb.params
+ }
+
+ sb.params = append(sb.recQryParams, sb.params...)
return sb.params
}
@@ -44,11 +62,15 @@ func (sb *SQLBuilder) AddParams(params ...interface{}) {
func (sb *SQLBuilder) WriteDashboardPermissionFilter(user *user.SignedInUser, permission dashboards.PermissionType) {
var (
- sql string
- params []interface{}
+ sql string
+ params []interface{}
+ recQry string
+ recQryParams []interface{}
)
if !ac.IsDisabled(sb.cfg) {
- sql, params = permissions.NewAccessControlDashboardPermissionFilter(user, permission, "").Where()
+ filterRBAC := permissions.NewAccessControlDashboardPermissionFilter(user, permission, "", sb.features, sb.recursiveQueriesAreSupported)
+ sql, params = filterRBAC.Where()
+ recQry, recQryParams = filterRBAC.With()
} else {
sql, params = permissions.DashboardPermissionFilter{
OrgRole: user.OrgRole,
@@ -61,4 +83,6 @@ func (sb *SQLBuilder) WriteDashboardPermissionFilter(user *user.SignedInUser, pe
sb.sql.WriteString(" AND " + sql)
sb.params = append(sb.params, params...)
+ sb.recQry = recQry
+ sb.recQryParams = recQryParams
}
diff --git a/pkg/infra/db/sqlbuilder_test.go b/pkg/infra/db/sqlbuilder_test.go
index ce88c870a56..0d1f3e45ba4 100644
--- a/pkg/infra/db/sqlbuilder_test.go
+++ b/pkg/infra/db/sqlbuilder_test.go
@@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/dashboards"
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
+ "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/user"
@@ -24,6 +25,7 @@ func TestIntegrationSQLBuilder(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
+
t.Run("WriteDashboardPermissionFilter", func(t *testing.T) {
t.Run("user ACL", func(t *testing.T) {
test(t,
@@ -31,6 +33,7 @@ func TestIntegrationSQLBuilder(t *testing.T) {
&DashboardPermission{User: true, Permission: dashboards.PERMISSION_VIEW},
Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_VIEW},
shouldFind,
+ featuremgmt.WithFeatures(),
)
test(t,
@@ -38,6 +41,7 @@ func TestIntegrationSQLBuilder(t *testing.T) {
&DashboardPermission{User: true, Permission: dashboards.PERMISSION_VIEW},
Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_EDIT},
shouldNotFind,
+ featuremgmt.WithFeatures(),
)
test(t,
@@ -45,6 +49,7 @@ func TestIntegrationSQLBuilder(t *testing.T) {
&DashboardPermission{User: true, Permission: dashboards.PERMISSION_EDIT},
Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_EDIT},
shouldFind,
+ featuremgmt.WithFeatures(),
)
test(t,
@@ -52,6 +57,41 @@ func TestIntegrationSQLBuilder(t *testing.T) {
&DashboardPermission{User: true, Permission: dashboards.PERMISSION_VIEW},
Search{RequiredPermission: dashboards.PERMISSION_VIEW},
shouldNotFind,
+ featuremgmt.WithFeatures(),
+ )
+ })
+
+ t.Run("user ACL with nested folders", func(t *testing.T) {
+ test(t,
+ DashboardProps{},
+ &DashboardPermission{User: true, Permission: dashboards.PERMISSION_VIEW},
+ Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_VIEW},
+ shouldFind,
+ featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
+ )
+
+ test(t,
+ DashboardProps{},
+ &DashboardPermission{User: true, Permission: dashboards.PERMISSION_VIEW},
+ Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_EDIT},
+ shouldNotFind,
+ featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
+ )
+
+ test(t,
+ DashboardProps{},
+ &DashboardPermission{User: true, Permission: dashboards.PERMISSION_EDIT},
+ Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_EDIT},
+ shouldFind,
+ featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
+ )
+
+ test(t,
+ DashboardProps{},
+ &DashboardPermission{User: true, Permission: dashboards.PERMISSION_VIEW},
+ Search{RequiredPermission: dashboards.PERMISSION_VIEW},
+ shouldNotFind,
+ featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
)
})
@@ -61,6 +101,7 @@ func TestIntegrationSQLBuilder(t *testing.T) {
&DashboardPermission{Role: org.RoleViewer, Permission: dashboards.PERMISSION_VIEW},
Search{UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW},
shouldFind,
+ featuremgmt.WithFeatures(),
)
test(t,
@@ -68,6 +109,7 @@ func TestIntegrationSQLBuilder(t *testing.T) {
&DashboardPermission{Role: org.RoleViewer, Permission: dashboards.PERMISSION_VIEW},
Search{UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_EDIT},
shouldNotFind,
+ featuremgmt.WithFeatures(),
)
test(t,
@@ -75,6 +117,7 @@ func TestIntegrationSQLBuilder(t *testing.T) {
&DashboardPermission{Role: org.RoleEditor, Permission: dashboards.PERMISSION_VIEW},
Search{UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW},
shouldNotFind,
+ featuremgmt.WithFeatures(),
)
test(t,
@@ -82,6 +125,41 @@ func TestIntegrationSQLBuilder(t *testing.T) {
&DashboardPermission{Role: org.RoleEditor, Permission: dashboards.PERMISSION_VIEW},
Search{UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW},
shouldNotFind,
+ featuremgmt.WithFeatures(),
+ )
+ })
+
+ t.Run("role ACL with nested folders", func(t *testing.T) {
+ test(t,
+ DashboardProps{},
+ &DashboardPermission{Role: org.RoleViewer, Permission: dashboards.PERMISSION_VIEW},
+ Search{UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW},
+ shouldFind,
+ featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
+ )
+
+ test(t,
+ DashboardProps{},
+ &DashboardPermission{Role: org.RoleViewer, Permission: dashboards.PERMISSION_VIEW},
+ Search{UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_EDIT},
+ shouldNotFind,
+ featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
+ )
+
+ test(t,
+ DashboardProps{},
+ &DashboardPermission{Role: org.RoleEditor, Permission: dashboards.PERMISSION_VIEW},
+ Search{UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW},
+ shouldNotFind,
+ featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
+ )
+
+ test(t,
+ DashboardProps{},
+ &DashboardPermission{Role: org.RoleEditor, Permission: dashboards.PERMISSION_VIEW},
+ Search{UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW},
+ shouldNotFind,
+ featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
)
})
@@ -91,6 +169,7 @@ func TestIntegrationSQLBuilder(t *testing.T) {
&DashboardPermission{Team: true, Permission: dashboards.PERMISSION_VIEW},
Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_VIEW},
shouldFind,
+ featuremgmt.WithFeatures(),
)
test(t,
@@ -98,6 +177,7 @@ func TestIntegrationSQLBuilder(t *testing.T) {
&DashboardPermission{Team: true, Permission: dashboards.PERMISSION_VIEW},
Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_EDIT},
shouldNotFind,
+ featuremgmt.WithFeatures(),
)
test(t,
@@ -105,6 +185,7 @@ func TestIntegrationSQLBuilder(t *testing.T) {
&DashboardPermission{Team: true, Permission: dashboards.PERMISSION_EDIT},
Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_EDIT},
shouldFind,
+ featuremgmt.WithFeatures(),
)
test(t,
@@ -112,6 +193,41 @@ func TestIntegrationSQLBuilder(t *testing.T) {
&DashboardPermission{Team: true, Permission: dashboards.PERMISSION_EDIT},
Search{UserFromACL: false, RequiredPermission: dashboards.PERMISSION_EDIT},
shouldNotFind,
+ featuremgmt.WithFeatures(),
+ )
+ })
+
+ t.Run("team ACL with nested folders", func(t *testing.T) {
+ test(t,
+ DashboardProps{},
+ &DashboardPermission{Team: true, Permission: dashboards.PERMISSION_VIEW},
+ Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_VIEW},
+ shouldFind,
+ featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
+ )
+
+ test(t,
+ DashboardProps{},
+ &DashboardPermission{Team: true, Permission: dashboards.PERMISSION_VIEW},
+ Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_EDIT},
+ shouldNotFind,
+ featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
+ )
+
+ test(t,
+ DashboardProps{},
+ &DashboardPermission{Team: true, Permission: dashboards.PERMISSION_EDIT},
+ Search{UserFromACL: true, RequiredPermission: dashboards.PERMISSION_EDIT},
+ shouldFind,
+ featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
+ )
+
+ test(t,
+ DashboardProps{},
+ &DashboardPermission{Team: true, Permission: dashboards.PERMISSION_EDIT},
+ Search{UserFromACL: false, RequiredPermission: dashboards.PERMISSION_EDIT},
+ shouldNotFind,
+ featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
)
})
@@ -121,6 +237,7 @@ func TestIntegrationSQLBuilder(t *testing.T) {
nil,
Search{OrgId: -1, UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW},
shouldNotFind,
+ featuremgmt.WithFeatures(),
)
test(t,
@@ -128,6 +245,7 @@ func TestIntegrationSQLBuilder(t *testing.T) {
nil,
Search{OrgId: -1, UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW},
shouldFind,
+ featuremgmt.WithFeatures(),
)
test(t,
@@ -135,6 +253,7 @@ func TestIntegrationSQLBuilder(t *testing.T) {
nil,
Search{OrgId: -1, UsersOrgRole: org.RoleEditor, RequiredPermission: dashboards.PERMISSION_EDIT},
shouldFind,
+ featuremgmt.WithFeatures(),
)
test(t,
@@ -142,6 +261,41 @@ func TestIntegrationSQLBuilder(t *testing.T) {
nil,
Search{OrgId: -1, UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_EDIT},
shouldNotFind,
+ featuremgmt.WithFeatures(),
+ )
+ })
+
+ t.Run("defaults for user ACL with nested folders", func(t *testing.T) {
+ test(t,
+ DashboardProps{},
+ nil,
+ Search{OrgId: -1, UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW},
+ shouldNotFind,
+ featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
+ )
+
+ test(t,
+ DashboardProps{OrgId: -1},
+ nil,
+ Search{OrgId: -1, UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_VIEW},
+ shouldFind,
+ featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
+ )
+
+ test(t,
+ DashboardProps{OrgId: -1},
+ nil,
+ Search{OrgId: -1, UsersOrgRole: org.RoleEditor, RequiredPermission: dashboards.PERMISSION_EDIT},
+ shouldFind,
+ featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
+ )
+
+ test(t,
+ DashboardProps{OrgId: -1},
+ nil,
+ Search{OrgId: -1, UsersOrgRole: org.RoleViewer, RequiredPermission: dashboards.PERMISSION_EDIT},
+ shouldNotFind,
+ featuremgmt.WithFeatures(featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)),
)
})
})
@@ -172,7 +326,7 @@ type dashboardResponse struct {
Id int64
}
-func test(t *testing.T, dashboardProps DashboardProps, dashboardPermission *DashboardPermission, search Search, shouldFind bool) {
+func test(t *testing.T, dashboardProps DashboardProps, dashboardPermission *DashboardPermission, search Search, shouldFind bool, features featuremgmt.FeatureToggles) {
t.Helper()
t.Run("", func(t *testing.T) {
@@ -186,7 +340,7 @@ func test(t *testing.T, dashboardProps DashboardProps, dashboardPermission *Dash
aclUserID = createDummyACL(t, sqlStore, dashboardPermission, search, dashboard.ID)
t.Logf("Created ACL with user ID %d\n", aclUserID)
}
- dashboards := getDashboards(t, sqlStore, search, aclUserID)
+ dashboards := getDashboards(t, sqlStore, search, aclUserID, features)
if shouldFind {
require.Len(t, dashboards, 1, "Should return one dashboard")
@@ -292,7 +446,7 @@ func createDummyACL(t *testing.T, sqlStore *sqlstore.SQLStore, dashboardPermissi
return 0
}
-func getDashboards(t *testing.T, sqlStore *sqlstore.SQLStore, search Search, aclUserID int64) []*dashboardResponse {
+func getDashboards(t *testing.T, sqlStore *sqlstore.SQLStore, search Search, aclUserID int64, features featuremgmt.FeatureToggles) []*dashboardResponse {
t.Helper()
old := sqlStore.Cfg.RBACEnabled
@@ -301,7 +455,10 @@ func getDashboards(t *testing.T, sqlStore *sqlstore.SQLStore, search Search, acl
sqlStore.Cfg.RBACEnabled = old
}()
- builder := NewSqlBuilder(sqlStore.Cfg, sqlStore.GetDialect())
+ recursiveQueriesAreSupported, err := sqlStore.RecursiveQueriesAreSupported()
+ require.NoError(t, err)
+
+ builder := NewSqlBuilder(sqlStore.Cfg, features, sqlStore.GetDialect(), recursiveQueriesAreSupported)
signedInUser := &user.SignedInUser{
UserID: 9999999999,
}
@@ -325,7 +482,7 @@ func getDashboards(t *testing.T, sqlStore *sqlstore.SQLStore, search Search, acl
builder.Write("SELECT * FROM dashboard WHERE true")
builder.WriteDashboardPermissionFilter(signedInUser, search.RequiredPermission)
t.Logf("Searching for dashboards, SQL: %q\n", builder.GetSQLString())
- err := sqlStore.GetEngine().SQL(builder.GetSQLString(), builder.params...).Find(&res)
+ err = sqlStore.GetEngine().SQL(builder.GetSQLString(), builder.params...).Find(&res)
require.NoError(t, err)
return res
}
diff --git a/pkg/services/alerting/conditions/query_interval_test.go b/pkg/services/alerting/conditions/query_interval_test.go
index 289a769ebde..443b0f74d83 100644
--- a/pkg/services/alerting/conditions/query_interval_test.go
+++ b/pkg/services/alerting/conditions/query_interval_test.go
@@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/datasources"
fd "github.com/grafana/grafana/pkg/services/datasources/fakes"
+ "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/validations"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/intervalv2"
@@ -141,7 +142,7 @@ func (rh fakeIntervalTestReqHandler) HandleRequest(ctx context.Context, dsInfo *
func applyScenario(t *testing.T, timeRange string, dataSourceJsonData *simplejson.Json, queryModel string, verifier func(query legacydata.DataSubQuery)) {
t.Run("desc", func(t *testing.T) {
db := dbtest.NewFakeDB()
- store := alerting.ProvideAlertStore(db, localcache.ProvideService(), &setting.Cfg{}, nil)
+ store := alerting.ProvideAlertStore(db, localcache.ProvideService(), &setting.Cfg{}, nil, featuremgmt.WithFeatures())
ctx := &queryIntervalTestContext{}
ctx.result = &alerting.EvalContext{
diff --git a/pkg/services/alerting/conditions/query_test.go b/pkg/services/alerting/conditions/query_test.go
index d3dcea599d6..11e98da1aca 100644
--- a/pkg/services/alerting/conditions/query_test.go
+++ b/pkg/services/alerting/conditions/query_test.go
@@ -18,6 +18,7 @@ import (
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/datasources"
fd "github.com/grafana/grafana/pkg/services/datasources/fakes"
+ "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/validations"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/legacydata"
@@ -38,7 +39,7 @@ func TestQueryCondition(t *testing.T) {
setup := func() *queryConditionTestContext {
ctx := &queryConditionTestContext{}
db := dbtest.NewFakeDB()
- store := alerting.ProvideAlertStore(db, localcache.ProvideService(), &setting.Cfg{}, nil)
+ store := alerting.ProvideAlertStore(db, localcache.ProvideService(), &setting.Cfg{}, nil, featuremgmt.WithFeatures())
ctx.reducer = `{"type":"avg"}`
ctx.evaluator = `{"type":"gt","params":[100]}`
ctx.result = &alerting.EvalContext{
diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go
index 9e2db68b0bc..fa15c9190c3 100644
--- a/pkg/services/alerting/extractor_test.go
+++ b/pkg/services/alerting/extractor_test.go
@@ -17,6 +17,7 @@ import (
"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/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
)
@@ -42,7 +43,8 @@ func TestAlertRuleExtraction(t *testing.T) {
dsService := &fakeDatasourceService{ExpectedDatasource: defaultDs}
db := dbtest.NewFakeDB()
- store := ProvideAlertStore(db, localcache.ProvideService(), &setting.Cfg{}, nil)
+ cfg := &setting.Cfg{}
+ store := ProvideAlertStore(db, localcache.ProvideService(), cfg, nil, featuremgmt.WithFeatures())
extractor := ProvideDashAlertExtractorService(dsPermissions, dsService, store)
t.Run("Parsing alert rules from dashboard json", func(t *testing.T) {
diff --git a/pkg/services/alerting/store.go b/pkg/services/alerting/store.go
index 5f764131dfd..26613090fb4 100644
--- a/pkg/services/alerting/store.go
+++ b/pkg/services/alerting/store.go
@@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
alertmodels "github.com/grafana/grafana/pkg/services/alerting/models"
"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/tag"
"github.com/grafana/grafana/pkg/setting"
@@ -39,17 +40,19 @@ type sqlStore struct {
log *log.ConcreteLogger
cfg *setting.Cfg
tagService tag.Service
+ features featuremgmt.FeatureToggles
}
func ProvideAlertStore(
db db.DB,
- cacheService *localcache.CacheService, cfg *setting.Cfg, tagService tag.Service) AlertStore {
+ cacheService *localcache.CacheService, cfg *setting.Cfg, tagService tag.Service, features featuremgmt.FeatureToggles) AlertStore {
return &sqlStore{
db: db,
cache: cacheService,
log: log.New("alerting.store"),
cfg: cfg,
tagService: tagService,
+ features: features,
}
}
@@ -107,8 +110,13 @@ func deleteAlertByIdInternal(alertId int64, reason string, sess *db.Session, log
}
func (ss *sqlStore) HandleAlertsQuery(ctx context.Context, query *alertmodels.GetAlertsQuery) (res []*alertmodels.AlertListItemDTO, err error) {
+ recursiveQueriesAreSupported, err := ss.db.RecursiveQueriesAreSupported()
+ if err != nil {
+ return res, err
+ }
+
err = ss.db.WithDbSession(ctx, func(sess *db.Session) error {
- builder := db.NewSqlBuilder(ss.cfg, ss.db.GetDialect())
+ builder := db.NewSqlBuilder(ss.cfg, ss.features, ss.db.GetDialect(), recursiveQueriesAreSupported)
builder.Write(`SELECT
alert.id,
diff --git a/pkg/services/annotations/annotationsimpl/annotations.go b/pkg/services/annotations/annotationsimpl/annotations.go
index fd1bfa4e8f6..9929984db1f 100644
--- a/pkg/services/annotations/annotationsimpl/annotations.go
+++ b/pkg/services/annotations/annotationsimpl/annotations.go
@@ -6,6 +6,7 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/annotations"
+ "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/tag"
"github.com/grafana/grafana/pkg/setting"
)
@@ -14,10 +15,11 @@ type RepositoryImpl struct {
store store
}
-func ProvideService(db db.DB, cfg *setting.Cfg, tagService tag.Service) *RepositoryImpl {
+func ProvideService(db db.DB, cfg *setting.Cfg, features featuremgmt.FeatureToggles, tagService tag.Service) *RepositoryImpl {
return &RepositoryImpl{
store: &xormRepositoryImpl{
cfg: cfg,
+ features: features,
db: db,
log: log.New("annotations"),
tagService: tagService,
diff --git a/pkg/services/annotations/annotationsimpl/cleanup.go b/pkg/services/annotations/annotationsimpl/cleanup.go
index af275c97314..a3380d9b4df 100644
--- a/pkg/services/annotations/annotationsimpl/cleanup.go
+++ b/pkg/services/annotations/annotationsimpl/cleanup.go
@@ -5,6 +5,7 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
+ "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
)
@@ -13,12 +14,13 @@ type CleanupServiceImpl struct {
store store
}
-func ProvideCleanupService(db db.DB, cfg *setting.Cfg) *CleanupServiceImpl {
+func ProvideCleanupService(db db.DB, cfg *setting.Cfg, features featuremgmt.FeatureToggles) *CleanupServiceImpl {
return &CleanupServiceImpl{
store: &xormRepositoryImpl{
- cfg: cfg,
- db: db,
- log: log.New("annotations"),
+ cfg: cfg,
+ features: features,
+ db: db,
+ log: log.New("annotations"),
},
}
}
diff --git a/pkg/services/annotations/annotationsimpl/cleanup_test.go b/pkg/services/annotations/annotationsimpl/cleanup_test.go
index feb11585914..b072224d446 100644
--- a/pkg/services/annotations/annotationsimpl/cleanup_test.go
+++ b/pkg/services/annotations/annotationsimpl/cleanup_test.go
@@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/annotations"
+ "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
)
@@ -91,7 +92,7 @@ func TestAnnotationCleanUp(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
cfg := setting.NewCfg()
cfg.AnnotationCleanupJobBatchSize = 1
- cleaner := ProvideCleanupService(fakeSQL, cfg)
+ cleaner := ProvideCleanupService(fakeSQL, cfg, featuremgmt.WithFeatures())
affectedAnnotations, affectedAnnotationTags, err := cleaner.Run(context.Background(), test.cfg)
require.NoError(t, err)
@@ -146,7 +147,7 @@ func TestOldAnnotationsAreDeletedFirst(t *testing.T) {
// run the clean up task to keep one annotation.
cfg := setting.NewCfg()
cfg.AnnotationCleanupJobBatchSize = 1
- cleaner := &xormRepositoryImpl{cfg: cfg, log: log.New("test-logger"), db: fakeSQL}
+ cleaner := &xormRepositoryImpl{cfg: cfg, log: log.New("test-logger"), db: fakeSQL, features: featuremgmt.WithFeatures()}
_, err = cleaner.CleanAnnotations(context.Background(), setting.AnnotationCleanupSettings{MaxCount: 1}, alertAnnotationType)
require.NoError(t, err)
diff --git a/pkg/services/annotations/annotationsimpl/xorm_store.go b/pkg/services/annotations/annotationsimpl/xorm_store.go
index 7d74fd529f8..a78584f07a2 100644
--- a/pkg/services/annotations/annotationsimpl/xorm_store.go
+++ b/pkg/services/annotations/annotationsimpl/xorm_store.go
@@ -13,6 +13,7 @@ import (
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/dashboards"
+ "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/permissions"
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
@@ -42,6 +43,7 @@ func validateTimeRange(item *annotations.Item) error {
type xormRepositoryImpl struct {
cfg *setting.Cfg
+ features featuremgmt.FeatureToggles
db db.DB
log log.Logger
maximumTagsLength int64
@@ -299,13 +301,15 @@ func (r *xormRepositoryImpl) Get(ctx context.Context, query *annotations.ItemQue
}
}
+ var acFilter acFilter
if !ac.IsDisabled(r.cfg) {
- acFilter, acArgs, err := getAccessControlFilter(query.SignedInUser)
+ var err error
+ acFilter, err = r.getAccessControlFilter(query.SignedInUser)
if err != nil {
return err
}
- sql.WriteString(fmt.Sprintf(" AND (%s)", acFilter))
- params = append(params, acArgs...)
+ sql.WriteString(fmt.Sprintf(" AND (%s)", acFilter.where))
+ params = append(params, acFilter.whereParams...)
}
if query.Limit == 0 {
@@ -314,6 +318,14 @@ func (r *xormRepositoryImpl) Get(ctx context.Context, query *annotations.ItemQue
// order of ORDER BY arguments match the order of a sql index for performance
sql.WriteString(" ORDER BY a.org_id, a.epoch_end DESC, a.epoch DESC" + r.db.GetDialect().Limit(query.Limit) + " ) dt on dt.id = annotation.id")
+ if acFilter.recQueries != "" {
+ var sb bytes.Buffer
+ sb.WriteString(acFilter.recQueries)
+ sb.WriteString(sql.String())
+ sql = sb
+ params = append(acFilter.recParams, params...)
+ }
+
if err := sess.SQL(sql.String(), params...).Find(&items); err != nil {
items = nil
return err
@@ -325,13 +337,23 @@ func (r *xormRepositoryImpl) Get(ctx context.Context, query *annotations.ItemQue
return items, err
}
-func getAccessControlFilter(user *user.SignedInUser) (string, []interface{}, error) {
+type acFilter struct {
+ where string
+ whereParams []interface{}
+ recQueries string
+ recParams []interface{}
+}
+
+func (r *xormRepositoryImpl) getAccessControlFilter(user *user.SignedInUser) (acFilter, error) {
+ var recQueries string
+ var recQueriesParams []interface{}
+
if user == nil || user.Permissions[user.OrgID] == nil {
- return "", nil, errors.New("missing permissions")
+ return acFilter{}, errors.New("missing permissions")
}
scopes, has := user.Permissions[user.OrgID][ac.ActionAnnotationsRead]
if !has {
- return "", nil, errors.New("missing permissions")
+ return acFilter{}, errors.New("missing permissions")
}
types, hasWildcardScope := ac.ParseScopes(ac.ScopeAnnotationsProvider.GetResourceScopeType(""), scopes)
if hasWildcardScope {
@@ -347,13 +369,27 @@ func getAccessControlFilter(user *user.SignedInUser) (string, []interface{}, err
}
// annotation read permission with scope annotations:type:dashboard allows listing annotations from dashboards which the user can view
if t == annotations.Dashboard.String() {
- dashboardFilter, dashboardParams := permissions.NewAccessControlDashboardPermissionFilter(user, dashboards.PERMISSION_VIEW, searchstore.TypeDashboard).Where()
+ recursiveQueriesAreSupported, err := r.db.RecursiveQueriesAreSupported()
+ if err != nil {
+ return acFilter{}, err
+ }
+
+ filterRBAC := permissions.NewAccessControlDashboardPermissionFilter(user, dashboards.PERMISSION_VIEW, searchstore.TypeDashboard, r.features, recursiveQueriesAreSupported)
+ dashboardFilter, dashboardParams := filterRBAC.Where()
+ recQueries, recQueriesParams = filterRBAC.With()
filter := fmt.Sprintf("a.dashboard_id IN(SELECT id FROM dashboard WHERE %s)", dashboardFilter)
filters = append(filters, filter)
params = dashboardParams
}
}
- return strings.Join(filters, " OR "), params, nil
+
+ f := acFilter{
+ where: strings.Join(filters, " OR "),
+ whereParams: params,
+ recQueries: recQueries,
+ recParams: recQueriesParams,
+ }
+ return f, nil
}
func (r *xormRepositoryImpl) Delete(ctx context.Context, params *annotations.DeleteParams) error {
diff --git a/pkg/services/annotations/annotationsimpl/xorm_store_test.go b/pkg/services/annotations/annotationsimpl/xorm_store_test.go
index 3f949efb763..630e266911e 100644
--- a/pkg/services/annotations/annotationsimpl/xorm_store_test.go
+++ b/pkg/services/annotations/annotationsimpl/xorm_store_test.go
@@ -2,6 +2,7 @@ package annotationsimpl
import (
"context"
+ "errors"
"fmt"
"strings"
"testing"
@@ -10,14 +11,20 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "github.com/grafana/grafana/pkg/bus"
"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/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
+ "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/dashboards"
dashboardstore "github.com/grafana/grafana/pkg/services/dashboards/database"
"github.com/grafana/grafana/pkg/services/featuremgmt"
+ "github.com/grafana/grafana/pkg/services/folder"
+ "github.com/grafana/grafana/pkg/services/folder/folderimpl"
+ "github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
@@ -495,7 +502,7 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) {
sql := db.InitTestDB(t)
var maximumTagsLength int64 = 60
- repo := xormRepositoryImpl{db: sql, cfg: setting.NewCfg(), log: log.New("annotation.test"), tagService: tagimpl.ProvideService(sql, sql.Cfg), maximumTagsLength: maximumTagsLength}
+ repo := xormRepositoryImpl{db: sql, cfg: setting.NewCfg(), log: log.New("annotation.test"), tagService: tagimpl.ProvideService(sql, sql.Cfg), maximumTagsLength: maximumTagsLength, features: featuremgmt.WithFeatures()}
quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardstore.ProvideDashboardStore(sql, sql.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sql, sql.Cfg), quotaService)
require.NoError(t, err)
@@ -550,7 +557,7 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) {
UserID: 1,
OrgID: 1,
}
- role := setupRBACRole(t, repo, user)
+ role := setupRBACRole(t, sql, user)
type testStruct struct {
description string
@@ -611,7 +618,7 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
user.Permissions = map[int64]map[string][]string{1: tc.permissions}
- setupRBACPermission(t, repo, role, user)
+ setupRBACPermission(t, sql, role, user)
results, err := repo.Get(context.Background(), &annotations.ItemQuery{
OrgID: 1,
@@ -630,10 +637,163 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) {
}
}
-func setupRBACRole(t *testing.T, repo xormRepositoryImpl, user *user.SignedInUser) *accesscontrol.Role {
+func TestIntegrationAnnotationListingWithInheritedRBAC(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
+
+ orgID := int64(1)
+ permissions := []accesscontrol.Permission{
+ {
+ Action: dashboards.ActionFoldersCreate,
+ }, {
+ Action: dashboards.ActionFoldersWrite,
+ Scope: dashboards.ScopeFoldersAll,
+ },
+ }
+ usr := &user.SignedInUser{
+ UserID: 1,
+ OrgID: orgID,
+ Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction(permissions)},
+ }
+
+ var role *accesscontrol.Role
+
+ dashboardIDs := make([]int64, 0, folder.MaxNestedFolderDepth+1)
+ annotationsTexts := make([]string, 0, folder.MaxNestedFolderDepth+1)
+
+ setupFolderStructure := func() *sqlstore.SQLStore {
+ db := db.InitTestDB(t)
+
+ // enable nested folders so that the folder table is populated for all the tests
+ features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
+
+ origNewGuardian := guardian.New
+ guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true, CanSaveValue: true})
+ t.Cleanup(func() {
+ guardian.New = origNewGuardian
+ })
+
+ // dashboard store commands that should be called.
+ dashStore, err := dashboardstore.ProvideDashboardStore(db, db.Cfg, features, tagimpl.ProvideService(db, db.Cfg), quotatest.New(false, nil))
+ require.NoError(t, err)
+
+ folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), db.Cfg, dashStore, folderimpl.ProvideDashboardFolderStore(db), db, features)
+
+ var maximumTagsLength int64 = 60
+ repo := xormRepositoryImpl{db: db, cfg: setting.NewCfg(), log: log.New("annotation.test"), tagService: tagimpl.ProvideService(db, db.Cfg), maximumTagsLength: maximumTagsLength, features: features}
+
+ parentUID := ""
+ for i := 0; ; i++ {
+ uid := fmt.Sprintf("f%d", i)
+ f, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
+ UID: uid,
+ OrgID: orgID,
+ Title: uid,
+ SignedInUser: usr,
+ ParentUID: parentUID,
+ })
+ if err != nil {
+ if errors.Is(err, folder.ErrMaximumDepthReached) {
+ break
+ }
+
+ t.Log("unexpected error", "error", err)
+ t.Fail()
+ }
+
+ dashInFolder := dashboards.SaveDashboardCommand{
+ UserID: usr.UserID,
+ OrgID: orgID,
+ IsFolder: false,
+ Dashboard: simplejson.NewFromAny(map[string]interface{}{
+ "title": fmt.Sprintf("Dashboard under %s", f.UID),
+ }),
+ FolderID: f.ID,
+ FolderUID: f.UID,
+ }
+ dashboard, err := dashStore.SaveDashboard(context.Background(), dashInFolder)
+ require.NoError(t, err)
+
+ dashboardIDs = append(dashboardIDs, dashboard.ID)
+
+ parentUID = f.UID
+
+ annotationTxt := fmt.Sprintf("annotation %d", i)
+ dash1Annotation := &annotations.Item{
+ OrgID: orgID,
+ DashboardID: dashboard.ID,
+ Epoch: 10,
+ Text: annotationTxt,
+ }
+ err = repo.Add(context.Background(), dash1Annotation)
+ require.NoError(t, err)
+
+ annotationsTexts = append(annotationsTexts, annotationTxt)
+ }
+
+ role = setupRBACRole(t, db, usr)
+ return db
+ }
+
+ db := setupFolderStructure()
+
+ testCases := []struct {
+ desc string
+ features featuremgmt.FeatureToggles
+ permissions map[string][]string
+ expectedAnnotationText []string
+ expectedError bool
+ }{
+ {
+ desc: "Should find only annotations from dashboards under folders that user can read",
+ features: featuremgmt.WithFeatures(),
+ permissions: map[string][]string{
+ accesscontrol.ActionAnnotationsRead: {accesscontrol.ScopeAnnotationsTypeDashboard},
+ dashboards.ActionDashboardsRead: {"folders:uid:f0"},
+ },
+ expectedAnnotationText: annotationsTexts[:1],
+ },
+ {
+ desc: "Should find only annotations from dashboards under inherited folders if nested folder are enabled",
+ features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
+ permissions: map[string][]string{
+ accesscontrol.ActionAnnotationsRead: {accesscontrol.ScopeAnnotationsTypeDashboard},
+ dashboards.ActionDashboardsRead: {"folders:uid:f0"},
+ },
+ expectedAnnotationText: annotationsTexts[:],
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ var maximumTagsLength int64 = 60
+ repo := xormRepositoryImpl{db: db, cfg: setting.NewCfg(), log: log.New("annotation.test"), tagService: tagimpl.ProvideService(db, db.Cfg), maximumTagsLength: maximumTagsLength, features: tc.features}
+
+ usr.Permissions = map[int64]map[string][]string{1: tc.permissions}
+ setupRBACPermission(t, db, role, usr)
+
+ results, err := repo.Get(context.Background(), &annotations.ItemQuery{
+ OrgID: 1,
+ SignedInUser: usr,
+ })
+ if tc.expectedError {
+ require.Error(t, err)
+ return
+ }
+ require.NoError(t, err)
+ require.Len(t, results, len(tc.expectedAnnotationText))
+ for _, r := range results {
+ assert.Contains(t, tc.expectedAnnotationText, r.Text)
+ }
+ })
+ }
+}
+
+func setupRBACRole(t *testing.T, db *sqlstore.SQLStore, user *user.SignedInUser) *accesscontrol.Role {
t.Helper()
var role *accesscontrol.Role
- err := repo.db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
+ err := db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
role = &accesscontrol.Role{
OrgID: user.OrgID,
UID: "test_role",
@@ -662,9 +822,9 @@ func setupRBACRole(t *testing.T, repo xormRepositoryImpl, user *user.SignedInUse
return role
}
-func setupRBACPermission(t *testing.T, repo xormRepositoryImpl, role *accesscontrol.Role, user *user.SignedInUser) {
+func setupRBACPermission(t *testing.T, db *sqlstore.SQLStore, role *accesscontrol.Role, user *user.SignedInUser) {
t.Helper()
- err := repo.db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
+ err := db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
if _, err := sess.Exec("DELETE FROM permission WHERE role_id = ?", role.ID); err != nil {
return err
}
diff --git a/pkg/services/dashboards/database/acl.go b/pkg/services/dashboards/database/acl.go
index 66408182727..f5b29c069a6 100644
--- a/pkg/services/dashboards/database/acl.go
+++ b/pkg/services/dashboards/database/acl.go
@@ -5,6 +5,7 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/dashboards"
+ "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/org"
)
@@ -103,8 +104,14 @@ func (d *dashboardStore) HasEditPermissionInFolders(ctx context.Context, query *
queryResult = true
return queryResult, nil
}
- err := d.store.WithDbSession(ctx, func(dbSession *db.Session) error {
- builder := db.NewSqlBuilder(d.cfg, d.store.GetDialect())
+
+ recursiveQueriesAreSupported, err := d.store.RecursiveQueriesAreSupported()
+ if err != nil {
+ return queryResult, err
+ }
+
+ err = d.store.WithDbSession(ctx, func(dbSession *db.Session) error {
+ builder := db.NewSqlBuilder(d.cfg, featuremgmt.WithFeatures(), d.store.GetDialect(), recursiveQueriesAreSupported)
builder.Write("SELECT COUNT(dashboard.id) AS count FROM dashboard WHERE dashboard.org_id = ? AND dashboard.is_folder = ?",
query.SignedInUser.OrgID, d.store.GetDialect().BooleanStr(true))
builder.WriteDashboardPermissionFilter(query.SignedInUser, dashboards.PERMISSION_EDIT)
@@ -131,13 +138,18 @@ func (d *dashboardStore) HasEditPermissionInFolders(ctx context.Context, query *
func (d *dashboardStore) HasAdminPermissionInDashboardsOrFolders(ctx context.Context, query *folder.HasAdminPermissionInDashboardsOrFoldersQuery) (bool, error) {
var queryResult bool
- err := d.store.WithDbSession(ctx, func(dbSession *db.Session) error {
+ recursiveQueriesAreSupported, err := d.store.RecursiveQueriesAreSupported()
+ if err != nil {
+ return queryResult, err
+ }
+
+ err = d.store.WithDbSession(ctx, func(dbSession *db.Session) error {
if query.SignedInUser.HasRole(org.RoleAdmin) {
queryResult = true
return nil
}
- builder := db.NewSqlBuilder(d.cfg, d.store.GetDialect())
+ builder := db.NewSqlBuilder(d.cfg, featuremgmt.WithFeatures(), d.store.GetDialect(), recursiveQueriesAreSupported)
builder.Write("SELECT COUNT(dashboard.id) AS count FROM dashboard WHERE dashboard.org_id = ?", query.SignedInUser.OrgID)
builder.WriteDashboardPermissionFilter(query.SignedInUser, dashboards.PERMISSION_ADMIN)
diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go
index a6b5725f68b..979de85fe25 100644
--- a/pkg/services/dashboards/database/database.go
+++ b/pkg/services/dashboards/database/database.go
@@ -934,9 +934,14 @@ func (d *dashboardStore) FindDashboards(ctx context.Context, query *dashboards.F
}
if !ac.IsDisabled(d.cfg) {
+ recursiveQueriesAreSupported, err := d.store.RecursiveQueriesAreSupported()
+ if err != nil {
+ return nil, err
+ }
+
// if access control is enabled, overwrite the filters so far
filters = []interface{}{
- permissions.NewAccessControlDashboardPermissionFilter(query.SignedInUser, query.Permission, query.Type),
+ permissions.NewAccessControlDashboardPermissionFilter(query.SignedInUser, query.Permission, query.Type, d.features, recursiveQueriesAreSupported),
}
}
diff --git a/pkg/services/dashboards/database/database_folder_test.go b/pkg/services/dashboards/database/database_folder_test.go
index efb4785899e..615f87e6694 100644
--- a/pkg/services/dashboards/database/database_folder_test.go
+++ b/pkg/services/dashboards/database/database_folder_test.go
@@ -2,21 +2,33 @@ package database
import (
"context"
+ "errors"
+ "fmt"
"testing"
+ "time"
+ "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db"
+ "github.com/grafana/grafana/pkg/infra/tracing"
+ "github.com/grafana/grafana/pkg/services/accesscontrol"
+ "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
+ "github.com/grafana/grafana/pkg/services/folder/folderimpl"
+ "github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/org"
+ "github.com/grafana/grafana/pkg/services/org/orgimpl"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/sqlstore"
+ "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/user"
- "github.com/grafana/grafana/pkg/setting"
+ "github.com/grafana/grafana/pkg/services/user/userimpl"
)
var testFeatureToggles = featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch)
@@ -36,7 +48,7 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) {
sqlStore.Cfg.RBACEnabled = false
quotaService := quotatest.New(false, nil)
var err error
- dashboardStore, err = ProvideDashboardStore(sqlStore, &setting.Cfg{}, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
+ dashboardStore, err = ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err)
flder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod", "webapp")
dashInRoot = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, false, "prod", "webapp")
@@ -477,6 +489,216 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) {
})
}
+func TestIntegrationDashboardInheritedFolderRBAC(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test")
+ }
+
+ // the maximux nested folder hierarchy starting from parent down to subfolders
+ nestedFolders := make([]*folder.Folder, 0, folder.MaxNestedFolderDepth+1)
+
+ var sqlStore *sqlstore.SQLStore
+ const (
+ dashInRootTitle = "dashboard in root"
+ dashInParentTitle = "dashboard in parent"
+ dashInSubfolderTitle = "dashboard in subfolder"
+ )
+ var viewer user.SignedInUser
+ var role *accesscontrol.Role
+
+ setup := func() {
+ sqlStore = db.InitTestDB(t)
+ sqlStore.Cfg.RBACEnabled = true
+ quotaService := quotatest.New(false, nil)
+
+ // enable nested folders so that the folder table is populated for all the tests
+ features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
+
+ var err error
+ dashboardWriteStore, err := ProvideDashboardStore(sqlStore, sqlStore.Cfg, features, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
+ require.NoError(t, err)
+
+ usr := createUser(t, sqlStore, "viewer", "Viewer", false)
+ viewer = user.SignedInUser{
+ UserID: usr.ID,
+ OrgID: usr.OrgID,
+ OrgRole: org.RoleViewer,
+ }
+
+ orgService, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaService)
+ require.NoError(t, err)
+ usrSvc, err := userimpl.ProvideService(sqlStore, orgService, sqlStore.Cfg, nil, nil, quotaService, supportbundlestest.NewFakeBundleService())
+ require.NoError(t, err)
+
+ // create admin user in the same org
+ currentUserCmd := user.CreateUserCommand{Login: "admin", Email: "admin@test.com", Name: "an admin", IsAdmin: false, OrgID: viewer.OrgID}
+ u, err := usrSvc.Create(context.Background(), ¤tUserCmd)
+ require.NoError(t, err)
+ admin := user.SignedInUser{
+ UserID: u.ID,
+ OrgID: u.OrgID,
+ OrgRole: org.RoleAdmin,
+ Permissions: map[int64]map[string][]string{u.OrgID: accesscontrol.GroupScopesByAction([]accesscontrol.Permission{
+ {
+ Action: dashboards.ActionFoldersCreate,
+ }, {
+ Action: dashboards.ActionFoldersWrite,
+ Scope: dashboards.ScopeFoldersAll,
+ }}),
+ },
+ }
+ require.NotEqual(t, viewer.UserID, admin.UserID)
+
+ origNewGuardian := guardian.New
+ guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true, CanSaveValue: true})
+ t.Cleanup(func() {
+ guardian.New = origNewGuardian
+ })
+
+ folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), sqlStore.Cfg, dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(sqlStore), sqlStore, features)
+
+ parentUID := ""
+ for i := 0; ; i++ {
+ uid := fmt.Sprintf("f%d", i)
+ f, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
+ UID: uid,
+ OrgID: admin.OrgID,
+ Title: uid,
+ SignedInUser: &admin,
+ ParentUID: parentUID,
+ })
+ if err != nil {
+ if errors.Is(err, folder.ErrMaximumDepthReached) {
+ break
+ }
+
+ t.Log("unexpected error", "error", err)
+ t.Fail()
+ }
+
+ nestedFolders = append(nestedFolders, f)
+
+ parentUID = f.UID
+ }
+ require.LessOrEqual(t, 2, len(nestedFolders))
+
+ saveDashboardCmd := dashboards.SaveDashboardCommand{
+ UserID: admin.UserID,
+ OrgID: admin.OrgID,
+ IsFolder: false,
+ Dashboard: simplejson.NewFromAny(map[string]interface{}{
+ "title": dashInRootTitle,
+ }),
+ }
+ _, err = dashboardWriteStore.SaveDashboard(context.Background(), saveDashboardCmd)
+ require.NoError(t, err)
+
+ saveDashboardCmd = dashboards.SaveDashboardCommand{
+ UserID: admin.UserID,
+ OrgID: admin.OrgID,
+ IsFolder: false,
+ Dashboard: simplejson.NewFromAny(map[string]interface{}{
+ "title": dashInParentTitle,
+ }),
+ FolderID: nestedFolders[0].ID,
+ FolderUID: nestedFolders[0].UID,
+ }
+ _, err = dashboardWriteStore.SaveDashboard(context.Background(), saveDashboardCmd)
+ require.NoError(t, err)
+
+ saveDashboardCmd = dashboards.SaveDashboardCommand{
+ UserID: admin.UserID,
+ OrgID: admin.OrgID,
+ IsFolder: false,
+ Dashboard: simplejson.NewFromAny(map[string]interface{}{
+ "title": dashInSubfolderTitle,
+ }),
+ FolderID: nestedFolders[1].ID,
+ FolderUID: nestedFolders[1].UID,
+ }
+ _, err = dashboardWriteStore.SaveDashboard(context.Background(), saveDashboardCmd)
+ require.NoError(t, err)
+
+ role = setupRBACRole(t, *sqlStore, &viewer)
+ }
+
+ setup()
+
+ nestedFolderTitles := make([]string, 0, len(nestedFolders))
+ for _, f := range nestedFolders {
+ nestedFolderTitles = append(nestedFolderTitles, f.Title)
+ }
+
+ testCases := []struct {
+ desc string
+ features featuremgmt.FeatureToggles
+ permissions map[string][]string
+ expectedTitles []string
+ }{
+ {
+ desc: "it should not return folder if ACL is not set for parent folder",
+ features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch),
+ permissions: nil,
+ expectedTitles: nil,
+ },
+ {
+ desc: "it should not return dashboard in subfolder if nested folders are disabled and the user has permission to read dashboards under parent folder",
+ features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch),
+ permissions: map[string][]string{
+ dashboards.ActionDashboardsRead: {fmt.Sprintf("folders:uid:%s", nestedFolders[0].UID)},
+ },
+ expectedTitles: []string{dashInParentTitle},
+ },
+ {
+ desc: "it should return dashboard in subfolder if nested folders are enabled and the user has permission to read dashboards under parent folder",
+ features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch, featuremgmt.FlagNestedFolders),
+ permissions: map[string][]string{
+ dashboards.ActionDashboardsRead: {fmt.Sprintf("folders:uid:%s", nestedFolders[0].UID)},
+ },
+ expectedTitles: []string{dashInParentTitle, dashInSubfolderTitle},
+ },
+ {
+ desc: "it should not return subfolder if nested folders are disabled and the user has permission to read folders under parent folder",
+ features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch),
+ permissions: map[string][]string{
+ dashboards.ActionFoldersRead: {fmt.Sprintf("folders:uid:%s", nestedFolders[0].UID)},
+ },
+ expectedTitles: []string{nestedFolders[0].Title},
+ },
+ {
+ desc: "it should return subfolder if nested folders are enabled and the user has permission to read folders under parent folder",
+ features: featuremgmt.WithFeatures(featuremgmt.FlagPanelTitleSearch, featuremgmt.FlagNestedFolders),
+ permissions: map[string][]string{
+ dashboards.ActionFoldersRead: {fmt.Sprintf("folders:uid:%s", nestedFolders[0].UID)},
+ },
+ expectedTitles: nestedFolderTitles,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ dashboardReadStore, err := ProvideDashboardStore(sqlStore, sqlStore.Cfg, tc.features, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotatest.New(false, nil))
+ require.NoError(t, err)
+
+ viewer.Permissions = map[int64]map[string][]string{viewer.OrgID: tc.permissions}
+ setupRBACPermission(t, *sqlStore, role, &viewer)
+
+ query := &dashboards.FindPersistedDashboardsQuery{
+ SignedInUser: &viewer,
+ OrgId: viewer.OrgID,
+ }
+
+ res, err := testSearchDashboards(dashboardReadStore, query)
+ require.NoError(t, err)
+
+ require.Equal(t, len(tc.expectedTitles), len(res))
+ for i, tlt := range tc.expectedTitles {
+ assert.Equal(t, tlt, res[i].Title)
+ }
+ })
+ }
+}
+
func moveDashboard(t *testing.T, dashboardStore dashboards.Store, orgId int64, dashboard *simplejson.Json,
newFolderId int64) *dashboards.Dashboard {
t.Helper()
@@ -492,3 +714,61 @@ func moveDashboard(t *testing.T, dashboardStore dashboards.Store, orgId int64, d
return dash
}
+
+func setupRBACRole(t *testing.T, db sqlstore.SQLStore, user *user.SignedInUser) *accesscontrol.Role {
+ t.Helper()
+ var role *accesscontrol.Role
+ err := db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
+ role = &accesscontrol.Role{
+ OrgID: user.OrgID,
+ UID: "test_role",
+ Name: "test:role",
+ Updated: time.Now(),
+ Created: time.Now(),
+ }
+ _, err := sess.Insert(role)
+ if err != nil {
+ return err
+ }
+
+ _, err = sess.Insert(accesscontrol.UserRole{
+ OrgID: role.OrgID,
+ RoleID: role.ID,
+ UserID: user.UserID,
+ Created: time.Now(),
+ })
+ if err != nil {
+ return err
+ }
+ return nil
+ })
+
+ require.NoError(t, err)
+ return role
+}
+
+func setupRBACPermission(t *testing.T, db sqlstore.SQLStore, role *accesscontrol.Role, user *user.SignedInUser) {
+ t.Helper()
+ err := db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
+ if _, err := sess.Exec("DELETE FROM permission WHERE role_id = ?", role.ID); err != nil {
+ return err
+ }
+
+ var acPermission []accesscontrol.Permission
+ for action, scopes := range user.Permissions[user.OrgID] {
+ for _, scope := range scopes {
+ acPermission = append(acPermission, accesscontrol.Permission{
+ RoleID: role.ID, Action: action, Scope: scope, Created: time.Now(), Updated: time.Now(),
+ })
+ }
+ }
+
+ if _, err := sess.InsertMulti(&acPermission); err != nil {
+ return err
+ }
+
+ return nil
+ })
+
+ require.NoError(t, err)
+}
diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go
index 15fe4406a33..79262251670 100644
--- a/pkg/services/libraryelements/database.go
+++ b/pkg/services/libraryelements/database.go
@@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/kinds/librarypanel"
"github.com/grafana/grafana/pkg/services/dashboards"
+ "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/libraryelements/model"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/search"
@@ -227,10 +228,16 @@ func (l *LibraryElementService) deleteLibraryElement(c context.Context, signedIn
}
// getLibraryElements gets a Library Element where param == value
-func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signedInUser *user.SignedInUser, params []Pair) ([]model.LibraryElementDTO, error) {
+func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signedInUser *user.SignedInUser, params []Pair, features featuremgmt.FeatureToggles) ([]model.LibraryElementDTO, error) {
libraryElements := make([]model.LibraryElementWithMeta, 0)
- err := store.WithDbSession(c, func(session *db.Session) error {
- builder := db.NewSqlBuilder(cfg, store.GetDialect())
+
+ recursiveQueriesAreSupported, err := store.RecursiveQueriesAreSupported()
+ if err != nil {
+ return nil, err
+ }
+
+ err = store.WithDbSession(c, func(session *db.Session) error {
+ builder := db.NewSqlBuilder(cfg, features, store.GetDialect(), recursiveQueriesAreSupported)
builder.Write(selectLibraryElementDTOWithMeta)
builder.Write(", 'General' as folder_name ")
builder.Write(", '' as folder_uid ")
@@ -299,7 +306,7 @@ func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signed
// getLibraryElementByUid gets a Library Element by uid.
func (l *LibraryElementService) getLibraryElementByUid(c context.Context, signedInUser *user.SignedInUser, UID string) (model.LibraryElementDTO, error) {
- libraryElements, err := getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{key: "org_id", value: signedInUser.OrgID}, {key: "uid", value: UID}})
+ libraryElements, err := getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{key: "org_id", value: signedInUser.OrgID}, {key: "uid", value: UID}}, l.features)
if err != nil {
return model.LibraryElementDTO{}, err
}
@@ -312,13 +319,19 @@ func (l *LibraryElementService) getLibraryElementByUid(c context.Context, signed
// getLibraryElementByName gets a Library Element by name.
func (l *LibraryElementService) getLibraryElementsByName(c context.Context, signedInUser *user.SignedInUser, name string) ([]model.LibraryElementDTO, error) {
- return getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{"org_id", signedInUser.OrgID}, {"name", name}})
+ return getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{"org_id", signedInUser.OrgID}, {"name", name}}, l.features)
}
// getAllLibraryElements gets all Library Elements.
func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedInUser *user.SignedInUser, query model.SearchLibraryElementsQuery) (model.LibraryElementSearchResult, error) {
elements := make([]model.LibraryElementWithMeta, 0)
result := model.LibraryElementSearchResult{}
+
+ recursiveQueriesAreSupported, err := l.SQLStore.RecursiveQueriesAreSupported()
+ if err != nil {
+ return result, err
+ }
+
if query.PerPage <= 0 {
query.PerPage = 100
}
@@ -333,8 +346,8 @@ func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedI
if folderFilter.parseError != nil {
return model.LibraryElementSearchResult{}, folderFilter.parseError
}
- err := l.SQLStore.WithDbSession(c, func(session *db.Session) error {
- builder := db.NewSqlBuilder(l.Cfg, l.SQLStore.GetDialect())
+ err = l.SQLStore.WithDbSession(c, func(session *db.Session) error {
+ builder := db.NewSqlBuilder(l.Cfg, l.features, l.SQLStore.GetDialect(), recursiveQueriesAreSupported)
if folderFilter.includeGeneralFolder {
builder.Write(selectLibraryElementDTOWithMeta)
builder.Write(", 'General' as folder_name ")
@@ -563,13 +576,18 @@ func (l *LibraryElementService) patchLibraryElement(c context.Context, signedInU
// getConnections gets all connections for a Library Element.
func (l *LibraryElementService) getConnections(c context.Context, signedInUser *user.SignedInUser, uid string) ([]model.LibraryElementConnectionDTO, error) {
connections := make([]model.LibraryElementConnectionDTO, 0)
- err := l.SQLStore.WithDbSession(c, func(session *db.Session) error {
+ recursiveQueriesAreSupported, err := l.SQLStore.RecursiveQueriesAreSupported()
+ if err != nil {
+ return nil, err
+ }
+
+ err = l.SQLStore.WithDbSession(c, func(session *db.Session) error {
element, err := getLibraryElement(l.SQLStore.GetDialect(), session, uid, signedInUser.OrgID)
if err != nil {
return err
}
var libraryElementConnections []model.LibraryElementConnectionWithMeta
- builder := db.NewSqlBuilder(l.Cfg, l.SQLStore.GetDialect())
+ builder := db.NewSqlBuilder(l.Cfg, l.features, l.SQLStore.GetDialect(), recursiveQueriesAreSupported)
builder.Write("SELECT lec.*, u1.login AS created_by_name, u1.email AS created_by_email, dashboard.uid AS connection_uid")
builder.Write(" FROM " + model.LibraryElementConnectionTableName + " AS lec")
builder.Write(" LEFT JOIN " + l.SQLStore.GetDialect().Quote("user") + " AS u1 ON lec.created_by = u1.id")
diff --git a/pkg/services/libraryelements/libraryelements.go b/pkg/services/libraryelements/libraryelements.go
index 53a3998a41a..562bc4b2160 100644
--- a/pkg/services/libraryelements/libraryelements.go
+++ b/pkg/services/libraryelements/libraryelements.go
@@ -6,19 +6,21 @@ import (
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
+ "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/libraryelements/model"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
)
-func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.RouteRegister, folderService folder.Service) *LibraryElementService {
+func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.RouteRegister, folderService folder.Service, features featuremgmt.FeatureToggles) *LibraryElementService {
l := &LibraryElementService{
Cfg: cfg,
SQLStore: sqlStore,
RouteRegister: routeRegister,
folderService: folderService,
log: log.New("library-elements"),
+ features: features,
}
l.registerAPIEndpoints()
return l
@@ -41,6 +43,7 @@ type LibraryElementService struct {
RouteRegister routing.RouteRegister
folderService folder.Service
log log.Logger
+ features featuremgmt.FeatureToggles
}
// CreateElement creates a Library Element.
diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go
index 4c5c07e9f37..8c3f363e3f7 100644
--- a/pkg/services/librarypanels/librarypanels_test.go
+++ b/pkg/services/librarypanels/librarypanels_test.go
@@ -843,7 +843,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
folderService := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, dashboardStore, folderStore, nil, features)
- elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService)
+ elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService, featuremgmt.WithFeatures())
service := LibraryPanelService{
Cfg: cfg,
SQLStore: sqlStore,
diff --git a/pkg/services/publicdashboards/service/query_test.go b/pkg/services/publicdashboards/service/query_test.go
index 2dbd20ec3a8..ecc0aff01db 100644
--- a/pkg/services/publicdashboards/service/query_test.go
+++ b/pkg/services/publicdashboards/service/query_test.go
@@ -712,7 +712,7 @@ func TestFindAnnotations(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t)
config := setting.NewCfg()
tagService := tagimpl.ProvideService(sqlStore, sqlStore.Cfg)
- annotationsRepo := annotationsimpl.ProvideService(sqlStore, config, tagService)
+ annotationsRepo := annotationsimpl.ProvideService(sqlStore, config, featuremgmt.WithFeatures(), tagService)
fakeStore := FakePublicDashboardStore{}
service := &PublicDashboardServiceImpl{
log: log.New("test.logger"),
diff --git a/pkg/services/sqlstore/permissions/dashboard.go b/pkg/services/sqlstore/permissions/dashboard.go
index bb50a034061..0a1501795c1 100644
--- a/pkg/services/sqlstore/permissions/dashboard.go
+++ b/pkg/services/sqlstore/permissions/dashboard.go
@@ -1,16 +1,23 @@
package permissions
import (
+ "bytes"
+ "fmt"
"strings"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards"
+ "github.com/grafana/grafana/pkg/services/featuremgmt"
+ "github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
"github.com/grafana/grafana/pkg/services/user"
)
+// maximum possible capacity for recursive queries array: one query for folder and one for dashboard actions
+const maximumRecursiveQueries = 2
+
type DashboardPermissionFilter struct {
OrgRole org.RoleType
Dialect migrator.Dialect
@@ -78,14 +85,25 @@ func (d DashboardPermissionFilter) Where() (string, []interface{}) {
return sql, params
}
-type AccessControlDashboardPermissionFilter struct {
+type clause struct {
+ string
+ params []interface{}
+}
+
+type accessControlDashboardPermissionFilter struct {
user *user.SignedInUser
dashboardActions []string
folderActions []string
+ features featuremgmt.FeatureToggles
+
+ where clause
+ // any recursive CTE queries (if supported)
+ recQueries []clause
+ recursiveQueriesAreSupported bool
}
// NewAccessControlDashboardPermissionFilter creates a new AccessControlDashboardPermissionFilter that is configured with specific actions calculated based on the dashboards.PermissionType and query type
-func NewAccessControlDashboardPermissionFilter(user *user.SignedInUser, permissionLevel dashboards.PermissionType, queryType string) AccessControlDashboardPermissionFilter {
+func NewAccessControlDashboardPermissionFilter(user *user.SignedInUser, permissionLevel dashboards.PermissionType, queryType string, features featuremgmt.FeatureToggles, recursiveQueriesAreSupported bool) *accessControlDashboardPermissionFilter {
needEdit := permissionLevel > dashboards.PERMISSION_VIEW
var folderActions []string
@@ -121,12 +139,26 @@ func NewAccessControlDashboardPermissionFilter(user *user.SignedInUser, permissi
}
}
- return AccessControlDashboardPermissionFilter{user: user, folderActions: folderActions, dashboardActions: dashboardActions}
+ f := accessControlDashboardPermissionFilter{user: user, folderActions: folderActions, dashboardActions: dashboardActions, features: features,
+ recursiveQueriesAreSupported: recursiveQueriesAreSupported,
+ }
+
+ f.buildClauses()
+
+ return &f
}
-func (f AccessControlDashboardPermissionFilter) Where() (string, []interface{}) {
+// Where returns:
+// - a where clause for filtering dashboards with expected permissions
+// - an array with the query parameters
+func (f *accessControlDashboardPermissionFilter) Where() (string, []interface{}) {
+ return f.where.string, f.where.params
+}
+
+func (f *accessControlDashboardPermissionFilter) buildClauses() {
if f.user == nil || f.user.Permissions == nil || f.user.Permissions[f.user.OrgID] == nil {
- return "(1 = 0)", nil
+ f.where = clause{string: "(1 = 0)"}
+ return
}
dashWildcards := accesscontrol.WildcardsFromPrefix(dashboards.ScopeDashboardsPrefix)
folderWildcards := accesscontrol.WildcardsFromPrefix(dashboards.ScopeFoldersPrefix)
@@ -136,6 +168,10 @@ func (f AccessControlDashboardPermissionFilter) Where() (string, []interface{})
var args []interface{}
builder := strings.Builder{}
builder.WriteRune('(')
+
+ permSelector := strings.Builder{}
+ var permSelectorArgs []interface{}
+
if len(f.dashboardActions) > 0 {
toCheck := actionsToCheck(f.dashboardActions, f.user.Permissions[f.user.OrgID], dashWildcards, folderWildcards)
@@ -155,24 +191,50 @@ func (f AccessControlDashboardPermissionFilter) Where() (string, []interface{})
builder.WriteString(") AND NOT dashboard.is_folder)")
builder.WriteString(" OR ")
- builder.WriteString("(dashboard.folder_id IN (SELECT id FROM dashboard as d WHERE d.uid IN (SELECT substr(scope, 13) FROM permission WHERE scope LIKE 'folders:uid:%' ")
- builder.WriteString(rolesFilter)
- args = append(args, params...)
+ permSelector.WriteString("(SELECT substr(scope, 13) FROM permission WHERE scope LIKE 'folders:uid:%' ")
+ permSelector.WriteString(rolesFilter)
+ permSelectorArgs = append(permSelectorArgs, params...)
if len(toCheck) == 1 {
- builder.WriteString(" AND action = ?")
- args = append(args, toCheck[0])
+ permSelector.WriteString(" AND action = ?")
+ permSelectorArgs = append(permSelectorArgs, toCheck[0])
} else {
- builder.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?")
- args = append(args, toCheck...)
- args = append(args, len(toCheck))
+ permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?")
+ permSelectorArgs = append(permSelectorArgs, toCheck...)
+ permSelectorArgs = append(permSelectorArgs, len(toCheck))
}
- builder.WriteString(")) AND NOT dashboard.is_folder)")
+ permSelector.WriteRune(')')
+
+ switch f.features.IsEnabled(featuremgmt.FlagNestedFolders) {
+ case true:
+ switch f.recursiveQueriesAreSupported {
+ case true:
+ recQueryName := fmt.Sprintf("RecQry%d", len(f.recQueries))
+ f.addRecQry(recQueryName, permSelector.String(), permSelectorArgs)
+ builder.WriteString("(dashboard.folder_id IN (SELECT d.id FROM dashboard as d ")
+ builder.WriteString(fmt.Sprintf("WHERE d.uid IN (SELECT uid FROM %s)", recQueryName))
+ default:
+ nestedFoldersSelectors, nestedFoldersArgs := nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "folder_id", "id")
+ builder.WriteRune('(')
+ builder.WriteString(nestedFoldersSelectors)
+ args = append(args, nestedFoldersArgs...)
+ }
+ default:
+ builder.WriteString("(dashboard.folder_id IN (SELECT d.id FROM dashboard as d ")
+ builder.WriteString("WHERE d.uid IN ")
+ builder.WriteString(permSelector.String())
+ args = append(args, permSelectorArgs...)
+ }
+ builder.WriteString(") AND NOT dashboard.is_folder)")
} else {
builder.WriteString("NOT dashboard.is_folder")
}
}
+ // recycle and reuse
+ permSelector.Reset()
+ permSelectorArgs = permSelectorArgs[:0]
+
if len(f.folderActions) > 0 {
if len(f.dashboardActions) > 0 {
builder.WriteString(" OR ")
@@ -180,24 +242,80 @@ func (f AccessControlDashboardPermissionFilter) Where() (string, []interface{})
toCheck := actionsToCheck(f.folderActions, f.user.Permissions[f.user.OrgID], folderWildcards)
if len(toCheck) > 0 {
- builder.WriteString("(dashboard.uid IN (SELECT substr(scope, 13) FROM permission WHERE scope LIKE 'folders:uid:%'")
- builder.WriteString(rolesFilter)
- args = append(args, params...)
+ permSelector.WriteString("(SELECT substr(scope, 13) FROM permission WHERE scope LIKE 'folders:uid:%'")
+ permSelector.WriteString(rolesFilter)
+ permSelectorArgs = append(permSelectorArgs, params...)
if len(toCheck) == 1 {
- builder.WriteString(" AND action = ?")
- args = append(args, toCheck[0])
+ permSelector.WriteString(" AND action = ?")
+ permSelectorArgs = append(permSelectorArgs, toCheck[0])
} else {
- builder.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?")
- args = append(args, toCheck...)
- args = append(args, len(toCheck))
+ permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ") GROUP BY role_id, scope HAVING COUNT(action) = ?")
+ permSelectorArgs = append(permSelectorArgs, toCheck...)
+ permSelectorArgs = append(permSelectorArgs, len(toCheck))
}
- builder.WriteString(") AND dashboard.is_folder)")
+ permSelector.WriteRune(')')
+
+ switch f.features.IsEnabled(featuremgmt.FlagNestedFolders) {
+ case true:
+ switch f.recursiveQueriesAreSupported {
+ case true:
+ recQueryName := fmt.Sprintf("RecQry%d", len(f.recQueries))
+ f.addRecQry(recQueryName, permSelector.String(), permSelectorArgs)
+ builder.WriteString("(dashboard.uid IN ")
+ builder.WriteString(fmt.Sprintf("(SELECT uid FROM %s)", recQueryName))
+ default:
+ nestedFoldersSelectors, nestedFoldersArgs := nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "uid", "uid")
+ builder.WriteRune('(')
+ builder.WriteString(nestedFoldersSelectors)
+ builder.WriteRune(')')
+ args = append(args, nestedFoldersArgs...)
+ }
+ default:
+ builder.WriteString("(dashboard.uid IN ")
+ builder.WriteString(permSelector.String())
+ args = append(args, permSelectorArgs...)
+ }
+ builder.WriteString(" AND dashboard.is_folder)")
} else {
builder.WriteString("dashboard.is_folder")
}
}
builder.WriteRune(')')
- return builder.String(), args
+
+ f.where = clause{string: builder.String(), params: args}
+}
+
+// With returns:
+// - a with clause for fetching folders with inherited permissions if nested folders are enabled or an empty string
+func (f *accessControlDashboardPermissionFilter) With() (string, []interface{}) {
+ var sb bytes.Buffer
+ var params []interface{}
+ if len(f.recQueries) > 0 {
+ sb.WriteString("WITH RECURSIVE ")
+ sb.WriteString(f.recQueries[0].string)
+ params = append(params, f.recQueries[0].params...)
+ for _, r := range f.recQueries[1:] {
+ sb.WriteRune(',')
+ sb.WriteString(r.string)
+ params = append(params, r.params...)
+ }
+ }
+ return sb.String(), params
+}
+
+func (f *accessControlDashboardPermissionFilter) addRecQry(queryName string, whereUIDSelect string, whereParams []interface{}) {
+ if f.recQueries == nil {
+ f.recQueries = make([]clause, 0, maximumRecursiveQueries)
+ }
+ c := make([]interface{}, len(whereParams))
+ copy(c, whereParams)
+ f.recQueries = append(f.recQueries, clause{
+ string: fmt.Sprintf(`%s AS (
+ SELECT uid, parent_uid, org_id FROM folder WHERE uid IN %s
+ UNION ALL SELECT f.uid, f.parent_uid, f.org_id FROM folder f INNER JOIN %s r ON f.parent_uid = r.uid and f.org_id = r.org_id
+ )`, queryName, whereUIDSelect, queryName),
+ params: c,
+ })
}
func actionsToCheck(actions []string, permissions map[string][]string, wildcards ...accesscontrol.Wildcards) []interface{} {
@@ -222,3 +340,28 @@ func actionsToCheck(actions []string, permissions map[string][]string, wildcards
}
return toCheck
}
+
+func nestedFoldersSelectors(permSelector string, permSelectorArgs []interface{}, leftTableCol string, rightTableCol string) (string, []interface{}) {
+ wheres := make([]string, 0, folder.MaxNestedFolderDepth+1)
+ args := make([]interface{}, 0, len(permSelectorArgs)*(folder.MaxNestedFolderDepth+1))
+
+ joins := make([]string, 0, folder.MaxNestedFolderDepth+2)
+
+ tmpl := "INNER JOIN folder %s ON %s.%s = %s.uid AND %s.org_id = %s.org_id "
+
+ prev := "d"
+ onCol := "uid"
+ for i := 1; i <= folder.MaxNestedFolderDepth+2; i++ {
+ t := fmt.Sprintf("f%d", i)
+ s := fmt.Sprintf(tmpl, t, prev, onCol, t, prev, t)
+ joins = append(joins, s)
+
+ wheres = append(wheres, fmt.Sprintf("(dashboard.%s IN (SELECT d.%s FROM dashboard d %s WHERE %s.uid IN %s)", leftTableCol, rightTableCol, strings.Join(joins, " "), t, permSelector))
+ args = append(args, permSelectorArgs...)
+
+ prev = t
+ onCol = "parent_uid"
+ }
+
+ return strings.Join(wheres, ") OR "), args
+}
diff --git a/pkg/services/sqlstore/permissions/dashboard_test.go b/pkg/services/sqlstore/permissions/dashboard_test.go
index d9552da351c..071e6ace713 100644
--- a/pkg/services/sqlstore/permissions/dashboard_test.go
+++ b/pkg/services/sqlstore/permissions/dashboard_test.go
@@ -9,14 +9,24 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db"
+ "github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
+ "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/dashboards"
+ "github.com/grafana/grafana/pkg/services/dashboards/database"
+ "github.com/grafana/grafana/pkg/services/featuremgmt"
+ "github.com/grafana/grafana/pkg/services/folder"
+ "github.com/grafana/grafana/pkg/services/folder/folderimpl"
+ "github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/org"
+ "github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/permissions"
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
+ "github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/user"
)
@@ -129,13 +139,18 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) {
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
store := setupTest(t, 10, 100, tt.permissions)
+ recursiveQueriesAreSupported, err := store.RecursiveQueriesAreSupported()
+ require.NoError(t, err)
+
usr := &user.SignedInUser{OrgID: 1, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}}
- filter := permissions.NewAccessControlDashboardPermissionFilter(usr, tt.permission, tt.queryType)
+ filter := permissions.NewAccessControlDashboardPermissionFilter(usr, tt.permission, tt.queryType, featuremgmt.WithFeatures(), recursiveQueriesAreSupported)
var result int
- err := store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
+ err = store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
q, params := filter.Where()
- _, err := sess.SQL("SELECT COUNT(*) FROM dashboard WHERE "+q, params...).Get(&result)
+ recQry, recQryParams := filter.With()
+ params = append(recQryParams, params...)
+ _, err := sess.SQL(recQry+"\nSELECT COUNT(*) FROM dashboard WHERE "+q, params...).Get(&result)
return err
})
require.NoError(t, err)
@@ -145,7 +160,115 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) {
}
}
+func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) {
+ testCases := []struct {
+ desc string
+ queryType string
+ permission dashboards.PermissionType
+ permissions []accesscontrol.Permission
+ expectedResult []string
+ features featuremgmt.FeatureToggles
+ }{
+ {
+ desc: "Should be able to view dashboards under inherited folders if nested folders are enabled",
+ queryType: searchstore.TypeDashboard,
+ permission: dashboards.PERMISSION_VIEW,
+ permissions: []accesscontrol.Permission{
+ {Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:parent"},
+ },
+ features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
+ expectedResult: []string{"dashboard under parent folder", "dashboard under subfolder"},
+ },
+ {
+ desc: "Should not be able to view dashboards under inherited folders if nested folders are not enabled",
+ queryType: searchstore.TypeDashboard,
+ permission: dashboards.PERMISSION_VIEW,
+ permissions: []accesscontrol.Permission{
+ {Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:parent"},
+ },
+ features: featuremgmt.WithFeatures(),
+ expectedResult: []string{"dashboard under parent folder"},
+ },
+ {
+ desc: "Should be able to view inherited folders if nested folders are enabled",
+ queryType: searchstore.TypeFolder,
+ permission: dashboards.PERMISSION_VIEW,
+ permissions: []accesscontrol.Permission{
+ {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent"},
+ },
+ features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
+ expectedResult: []string{"parent", "subfolder"},
+ },
+ {
+ desc: "Should not be able to view inherited folders if nested folders are not enabled",
+ queryType: searchstore.TypeFolder,
+ permission: dashboards.PERMISSION_VIEW,
+ permissions: []accesscontrol.Permission{
+ {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent"},
+ },
+ features: featuremgmt.WithFeatures(),
+ expectedResult: []string{"parent"},
+ },
+ {
+ desc: "Should be able to view inherited dashboards and folders if nested folders are enabled",
+ permission: dashboards.PERMISSION_VIEW,
+ permissions: []accesscontrol.Permission{
+ {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent"},
+ {Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:parent"},
+ },
+ features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
+ expectedResult: []string{"parent", "subfolder", "dashboard under parent folder", "dashboard under subfolder"},
+ },
+ {
+ desc: "Should not be able to view inherited dashboards and folders if nested folders are not enabled",
+ permission: dashboards.PERMISSION_VIEW,
+ permissions: []accesscontrol.Permission{
+ {Action: dashboards.ActionFoldersRead, Scope: "folders:uid:parent"},
+ {Action: dashboards.ActionDashboardsRead, Scope: "folders:uid:parent"},
+ },
+ features: featuremgmt.WithFeatures(),
+ expectedResult: []string{"parent", "dashboard under parent folder"},
+ },
+ }
+
+ origNewGuardian := guardian.New
+ guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true, CanSaveValue: true})
+ t.Cleanup(func() {
+ guardian.New = origNewGuardian
+ })
+
+ var orgID int64 = 1
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ tc.permissions = append(tc.permissions, accesscontrol.Permission{
+ Action: dashboards.ActionFoldersCreate,
+ }, accesscontrol.Permission{
+ Action: dashboards.ActionFoldersWrite,
+ Scope: dashboards.ScopeFoldersAll,
+ })
+ usr := &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction(tc.permissions)}}
+ db := setupNestedTest(t, usr, tc.permissions, orgID, tc.features)
+ recursiveQueriesAreSupported, err := db.RecursiveQueriesAreSupported()
+ require.NoError(t, err)
+ filter := permissions.NewAccessControlDashboardPermissionFilter(usr, tc.permission, tc.queryType, tc.features, recursiveQueriesAreSupported)
+ var result []string
+ err = db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
+ q, params := filter.Where()
+ recQry, recQryParams := filter.With()
+ params = append(recQryParams, params...)
+ err := sess.SQL(recQry+"\nSELECT title FROM dashboard WHERE "+q, params...).Find(&result)
+ return err
+ })
+ require.NoError(t, err)
+ assert.Equal(t, tc.expectedResult, result)
+ })
+ }
+}
+
func setupTest(t *testing.T, numFolders, numDashboards int, permissions []accesscontrol.Permission) db.DB {
+ t.Helper()
+
store := db.InitTestDB(t)
err := store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
dashes := make([]dashboards.Dashboard, 0, numFolders+numDashboards)
@@ -227,3 +350,95 @@ func setupTest(t *testing.T, numFolders, numDashboards int, permissions []access
require.NoError(t, err)
return store
}
+
+func setupNestedTest(t *testing.T, usr *user.SignedInUser, perms []accesscontrol.Permission, orgID int64, features featuremgmt.FeatureToggles) db.DB {
+ t.Helper()
+
+ db := sqlstore.InitTestDB(t)
+
+ // dashboard store commands that should be called.
+ dashStore, err := database.ProvideDashboardStore(db, db.Cfg, features, tagimpl.ProvideService(db, db.Cfg), quotatest.New(false, nil))
+ require.NoError(t, err)
+
+ folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), db.Cfg, dashStore, folderimpl.ProvideDashboardFolderStore(db), db, features)
+
+ // create parent folder
+ parent, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
+ UID: "parent",
+ OrgID: orgID,
+ Title: "parent",
+ SignedInUser: usr,
+ })
+ require.NoError(t, err)
+
+ // create subfolder
+ subfolder, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
+ UID: "subfolder",
+ ParentUID: "parent",
+ OrgID: orgID,
+ Title: "subfolder",
+ SignedInUser: usr,
+ })
+ require.NoError(t, err)
+
+ // create dashboard under parent folder
+ _, err = dashStore.SaveDashboard(context.Background(), dashboards.SaveDashboardCommand{
+ OrgID: orgID,
+ FolderID: parent.ID,
+ Dashboard: simplejson.NewFromAny(map[string]interface{}{
+ "title": "dashboard under parent folder",
+ }),
+ })
+ require.NoError(t, err)
+
+ // create dashboard under subfolder
+ _, err = dashStore.SaveDashboard(context.Background(), dashboards.SaveDashboardCommand{
+ OrgID: orgID,
+ FolderID: subfolder.ID,
+ Dashboard: simplejson.NewFromAny(map[string]interface{}{
+ "title": "dashboard under subfolder",
+ }),
+ })
+ require.NoError(t, err)
+
+ err = db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
+ role := &accesscontrol.Role{
+ OrgID: 0,
+ UID: "basic_viewer",
+ Name: "basic:viewer",
+ Updated: time.Now(),
+ Created: time.Now(),
+ }
+ _, err = sess.Insert(role)
+ if err != nil {
+ return err
+ }
+ _, err = sess.Insert(accesscontrol.BuiltinRole{
+ OrgID: 0,
+ RoleID: role.ID,
+ Role: "Viewer",
+ Created: time.Now(),
+ Updated: time.Now(),
+ })
+ if err != nil {
+ return err
+ }
+
+ for i := range perms {
+ perms[i].RoleID = role.ID
+ perms[i].Created = time.Now()
+ perms[i].Updated = time.Now()
+ }
+ if len(perms) > 0 {
+ _, err = sess.InsertMulti(&perms)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+ })
+ require.NoError(t, err)
+
+ return db
+}
diff --git a/pkg/services/sqlstore/permissions/dashboards_bench_test.go b/pkg/services/sqlstore/permissions/dashboards_bench_test.go
index d213fee5ac8..84e8cb94718 100644
--- a/pkg/services/sqlstore/permissions/dashboards_bench_test.go
+++ b/pkg/services/sqlstore/permissions/dashboards_bench_test.go
@@ -10,26 +10,59 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db"
+ "github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
+ "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/dashboards"
+ "github.com/grafana/grafana/pkg/services/dashboards/database"
+ "github.com/grafana/grafana/pkg/services/featuremgmt"
+ "github.com/grafana/grafana/pkg/services/folder"
+ "github.com/grafana/grafana/pkg/services/folder/folderimpl"
+ "github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/org"
+ "github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/permissions"
+ "github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/user"
)
-func benchmarkDashboardPermissionFilter(b *testing.B, numUsers, numDashboards int) {
- store := setupBenchMark(b, numUsers, numDashboards)
+func benchmarkDashboardPermissionFilter(b *testing.B, numUsers, numDashboards, numFolders, nestingLevel int) {
+ usr := user.SignedInUser{UserID: 1, OrgID: 1, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{
+ 1: accesscontrol.GroupScopesByAction([]accesscontrol.Permission{
+ {
+ Action: dashboards.ActionFoldersCreate,
+ },
+ {
+ Action: dashboards.ActionFoldersWrite,
+ Scope: dashboards.ScopeFoldersAll,
+ },
+ }),
+ }}
+
+ features := featuremgmt.WithFeatures()
+ // if nestingLevel > 0 enable nested folders
+ if nestingLevel > 0 {
+ features = featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
+ }
+
+ store := setupBenchMark(b, usr, features, numUsers, numDashboards, numFolders, nestingLevel)
+
+ recursiveQueriesAreSupported, err := store.RecursiveQueriesAreSupported()
+ require.NoError(b, err)
+
b.ResetTimer()
for i := 0; i < b.N; i++ {
- usr := &user.SignedInUser{UserID: 1, OrgID: 1, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{1: {}}}
- filter := permissions.NewAccessControlDashboardPermissionFilter(usr, dashboards.PERMISSION_VIEW, "")
+ filter := permissions.NewAccessControlDashboardPermissionFilter(&usr, dashboards.PERMISSION_VIEW, "", features, recursiveQueriesAreSupported)
var result int
err := store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
q, params := filter.Where()
- _, err := sess.SQL("SELECT COUNT(*) FROM dashboard WHERE "+q, params...).Get(&result)
+ recQry, recQryParams := filter.With()
+ params = append(recQryParams, params...)
+ _, err := sess.SQL(recQry+"SELECT COUNT(*) FROM dashboard WHERE "+q, params...).Get(&result)
return err
})
require.NoError(b, err)
@@ -37,15 +70,78 @@ func benchmarkDashboardPermissionFilter(b *testing.B, numUsers, numDashboards in
}
}
-func setupBenchMark(b *testing.B, numUsers, numDashboards int) db.DB {
+func setupBenchMark(b *testing.B, usr user.SignedInUser, features featuremgmt.FeatureToggles, numUsers, numDashboards, numFolders, nestingLevel int) db.DB {
+ if nestingLevel > folder.MaxNestedFolderDepth {
+ nestingLevel = folder.MaxNestedFolderDepth
+ }
+
store := db.InitTestDB(b)
- now := time.Now()
- err := store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
- dashes := make([]dashboards.Dashboard, 0, numDashboards)
- for i := 1; i <= numDashboards; i++ {
+
+ quotaService := quotatest.New(false, nil)
+
+ dashboardWriteStore, err := database.ProvideDashboardStore(store, store.Cfg, features, tagimpl.ProvideService(store, store.Cfg), quotaService)
+ require.NoError(b, err)
+
+ folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), store.Cfg, dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(store), store, features)
+
+ origNewGuardian := guardian.New
+ guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true, CanSaveValue: true})
+ b.Cleanup(func() {
+ guardian.New = origNewGuardian
+ })
+
+ rootFolders := make([]*folder.Folder, 0, numFolders)
+ dashes := make([]dashboards.Dashboard, 0, numDashboards)
+ parentUID := ""
+ for i := 0; i < numFolders; i++ {
+ uid := fmt.Sprintf("f%d", i)
+ f, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
+ UID: uid,
+ OrgID: usr.OrgID,
+ Title: uid,
+ SignedInUser: &usr,
+ ParentUID: parentUID,
+ })
+ require.NoError(b, err)
+ rootFolders = append(rootFolders, f)
+
+ parentUID := f.UID
+ var leaf *folder.Folder
+ for j := 1; j <= nestingLevel; j++ {
+ uid := fmt.Sprintf("f%d_%d", i, j)
+ sf, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
+ UID: uid,
+ OrgID: usr.OrgID,
+ Title: uid,
+ SignedInUser: &usr,
+ ParentUID: parentUID,
+ })
+ require.NoError(b, err)
+ parentUID = sf.UID
+ leaf = sf
+ }
+
+ str := fmt.Sprintf("dashboard under folder %s", leaf.Title)
+ now := time.Now()
+ dashes = append(dashes, dashboards.Dashboard{
+ OrgID: usr.OrgID,
+ IsFolder: false,
+ UID: str,
+ Slug: str,
+ Title: str,
+ Data: simplejson.New(),
+ Created: now,
+ Updated: now,
+ FolderID: leaf.ID,
+ })
+ }
+
+ err = store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
+ now := time.Now()
+ for i := len(dashes); i < numDashboards; i++ {
str := strconv.Itoa(i)
dashes = append(dashes, dashboards.Dashboard{
- OrgID: 1,
+ OrgID: usr.OrgID,
IsFolder: false,
UID: str,
Slug: str,
@@ -79,10 +175,24 @@ func setupBenchMark(b *testing.B, numUsers, numDashboards int) db.DB {
Created: now,
})
for _, dash := range dashes {
+ // add permission to read dashboards under the general
+ if dash.FolderID == 0 {
+ permissions = append(permissions, accesscontrol.Permission{
+ RoleID: int64(i),
+ Action: dashboards.ActionDashboardsRead,
+ Scope: dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dash.UID),
+ Updated: now,
+ Created: now,
+ })
+ }
+ }
+
+ for _, f := range rootFolders {
+ // add permission to read folders under specific folders
permissions = append(permissions, accesscontrol.Permission{
RoleID: int64(i),
Action: dashboards.ActionDashboardsRead,
- Scope: dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dash.UID),
+ Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(f.UID),
Updated: now,
Created: now,
})
@@ -113,16 +223,52 @@ func setupBenchMark(b *testing.B, numUsers, numDashboards int) db.DB {
return store
}
-func BenchmarkDashboardPermissionFilter_100_100(b *testing.B) {
- benchmarkDashboardPermissionFilter(b, 100, 100)
+func BenchmarkDashboardPermissionFilter_100_100_0_0(b *testing.B) {
+ benchmarkDashboardPermissionFilter(b, 100, 100, 0, 0)
}
-func BenchmarkDashboardPermissionFilter_100_1000(b *testing.B) {
- benchmarkDashboardPermissionFilter(b, 100, 1000)
+func BenchmarkDashboardPermissionFilter_100_100_10_2(b *testing.B) {
+ benchmarkDashboardPermissionFilter(b, 100, 100, 10, 2)
}
-func BenchmarkDashboardPermissionFilter_300_10000(b *testing.B) {
- benchmarkDashboardPermissionFilter(b, 300, 10000)
+func BenchmarkDashboardPermissionFilter_100_100_10_4(b *testing.B) {
+ benchmarkDashboardPermissionFilter(b, 100, 100, 10, 4)
+}
+
+func BenchmarkDashboardPermissionFilter_100_100_10_8(b *testing.B) {
+ benchmarkDashboardPermissionFilter(b, 100, 100, 10, 8)
+}
+
+func BenchmarkDashboardPermissionFilter_100_1000_0_0(b *testing.B) {
+ benchmarkDashboardPermissionFilter(b, 100, 1000, 0, 0)
+}
+
+func BenchmarkDashboardPermissionFilter_100_1000_10_2(b *testing.B) {
+ benchmarkDashboardPermissionFilter(b, 100, 1000, 10, 2)
+}
+
+func BenchmarkDashboardPermissionFilter_100_1000_10_4(b *testing.B) {
+ benchmarkDashboardPermissionFilter(b, 100, 1000, 10, 4)
+}
+
+func BenchmarkDashboardPermissionFilter_100_1000_10_8(b *testing.B) {
+ benchmarkDashboardPermissionFilter(b, 100, 1000, 10, 8)
+}
+
+func BenchmarkDashboardPermissionFilter_300_10000_0_0(b *testing.B) {
+ benchmarkDashboardPermissionFilter(b, 300, 10000, 0, 0)
+}
+
+func BenchmarkDashboardPermissionFilter_300_10000_10_2(b *testing.B) {
+ benchmarkDashboardPermissionFilter(b, 300, 10000, 10, 2)
+}
+
+func BenchmarkDashboardPermissionFilter_300_10000_10_4(b *testing.B) {
+ benchmarkDashboardPermissionFilter(b, 300, 10000, 10, 4)
+}
+
+func BenchmarkDashboardPermissionFilter_300_10000_10_8(b *testing.B) {
+ benchmarkDashboardPermissionFilter(b, 300, 10000, 10, 8)
}
func batch(count, batchSize int, eachFn func(start, end int) error) error {
diff --git a/pkg/services/sqlstore/searchstore/builder.go b/pkg/services/sqlstore/searchstore/builder.go
index 1ae89d05de9..3fb805262e4 100644
--- a/pkg/services/sqlstore/searchstore/builder.go
+++ b/pkg/services/sqlstore/searchstore/builder.go
@@ -44,6 +44,9 @@ func (b *Builder) ToSQL(limit, page int64) (string, []interface{}) {
}
func (b *Builder) buildSelect() {
+ var recQuery string
+ var recQueryParams []interface{}
+
b.sql.WriteString(
`SELECT
dashboard.id,
@@ -61,9 +64,25 @@ func (b *Builder) buildSelect() {
if f, ok := f.(FilterSelect); ok {
b.sql.WriteString(fmt.Sprintf(", %s", f.Select()))
}
+
+ if f, ok := f.(FilterWith); ok {
+ recQuery, recQueryParams = f.With()
+ }
}
b.sql.WriteString(` FROM `)
+
+ if recQuery == "" {
+ return
+ }
+
+ // prepend recursive queries
+ var bf bytes.Buffer
+ bf.WriteString(recQuery)
+ bf.WriteString(b.sql.String())
+
+ b.sql = bf
+ b.params = append(recQueryParams, b.params...)
}
func (b *Builder) applyFilters() (ordering string) {
diff --git a/pkg/services/sqlstore/searchstore/filters.go b/pkg/services/sqlstore/searchstore/filters.go
index f5cc63e421a..170d6a2cf4d 100644
--- a/pkg/services/sqlstore/searchstore/filters.go
+++ b/pkg/services/sqlstore/searchstore/filters.go
@@ -14,6 +14,12 @@ type FilterWhere interface {
Where() (string, []interface{})
}
+// FilterWith returns any recursive CTE queries (if supported)
+// and their parameters
+type FilterWith interface {
+ With() (string, []interface{})
+}
+
// FilterGroupBy should be used after performing an outer join on the
// search result to ensure there is only one of each ID in the results.
// The id column must be present in the result.
diff --git a/pkg/services/sqlstore/searchstore/search_test.go b/pkg/services/sqlstore/searchstore/search_test.go
index 9d0945aac3d..b092579ab17 100644
--- a/pkg/services/sqlstore/searchstore/search_test.go
+++ b/pkg/services/sqlstore/searchstore/search_test.go
@@ -10,7 +10,9 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db"
+ "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/sqlstore/permissions"
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
@@ -148,6 +150,145 @@ func TestBuilder_Permissions(t *testing.T) {
assert.Len(t, res, 0)
}
+func TestBuilder_RBAC(t *testing.T) {
+ testsCases := []struct {
+ desc string
+ userPermissions []accesscontrol.Permission
+ features featuremgmt.FeatureToggles
+ expectedParams []interface{}
+ }{
+ {
+ desc: "no user permissions",
+ features: featuremgmt.WithFeatures(),
+ expectedParams: []interface{}{
+ int64(1),
+ },
+ },
+ {
+ desc: "user with view permission",
+ userPermissions: []accesscontrol.Permission{
+ {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:1"},
+ },
+ features: featuremgmt.WithFeatures(),
+ expectedParams: []interface{}{
+ int64(1),
+ int64(1),
+ int64(1),
+ 0,
+ "Viewer",
+ int64(1),
+ 0,
+ "dashboards:read",
+ "dashboards:write",
+ 2,
+ int64(1),
+ int64(1),
+ 0,
+ "Viewer",
+ int64(1),
+ 0,
+ "dashboards:read",
+ "dashboards:write",
+ 2,
+ int64(1),
+ int64(1),
+ 0,
+ "Viewer",
+ int64(1),
+ 0,
+ "folders:read",
+ "dashboards:create",
+ 2,
+ },
+ },
+ {
+ desc: "user with view permission with nesting",
+ userPermissions: []accesscontrol.Permission{
+ {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:1"},
+ },
+ features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
+ expectedParams: []interface{}{
+ int64(1),
+ int64(1),
+ 0,
+ "Viewer",
+ int64(1),
+ 0,
+ "dashboards:read",
+ "dashboards:write",
+ 2,
+ int64(1),
+ int64(1),
+ 0,
+ "Viewer",
+ int64(1),
+ 0,
+ "folders:read",
+ "dashboards:create",
+ 2,
+ int64(1),
+ int64(1),
+ int64(1),
+ 0,
+ "Viewer",
+ int64(1),
+ 0,
+ "dashboards:read",
+ "dashboards:write",
+ 2,
+ },
+ },
+ }
+
+ user := &user.SignedInUser{
+ UserID: 1,
+ OrgID: 1,
+ OrgRole: org.RoleViewer,
+ }
+
+ store := setupTestEnvironment(t)
+ createDashboards(t, store, 0, 1, user.OrgID)
+
+ recursiveQueriesAreSupported, err := store.RecursiveQueriesAreSupported()
+ require.NoError(t, err)
+
+ for _, tc := range testsCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ if len(tc.userPermissions) > 0 {
+ user.Permissions = map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tc.userPermissions)}
+ }
+
+ level := dashboards.PERMISSION_EDIT
+
+ builder := &searchstore.Builder{
+ Filters: []interface{}{
+ searchstore.OrgFilter{OrgId: user.OrgID},
+ searchstore.TitleSorter{},
+ permissions.NewAccessControlDashboardPermissionFilter(
+ user,
+ level,
+ "",
+ tc.features,
+ recursiveQueriesAreSupported,
+ ),
+ },
+ Dialect: store.GetDialect(),
+ }
+
+ res := []dashboards.DashboardSearchProjection{}
+ err := store.WithDbSession(context.Background(), func(sess *db.Session) error {
+ sql, params := builder.ToSQL(limit, page)
+ // TODO: replace with a proper test
+ assert.Equal(t, tc.expectedParams, params)
+ return sess.SQL(sql, params...).Find(&res)
+ })
+ require.NoError(t, err)
+
+ assert.Len(t, res, 0)
+ })
+ }
+}
+
func setupTestEnvironment(t *testing.T) db.DB {
t.Helper()
store := db.InitTestDB(t)
From 7808e74260037a63dfd1188363125f242818b4d8 Mon Sep 17 00:00:00 2001
From: Will Browne
Date: Thu, 6 Apr 2023 11:50:50 +0100
Subject: [PATCH 67/80] Plugins: Skip instrumenting plugin build info for core
and bundled plugins (#66105)
* only instrument plugin build for non core/bundled plugins
* fix import
---
pkg/plugins/manager/loader/loader.go | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/pkg/plugins/manager/loader/loader.go b/pkg/plugins/manager/loader/loader.go
index 6a75a5fb48f..2349b4d255e 100644
--- a/pkg/plugins/manager/loader/loader.go
+++ b/pkg/plugins/manager/loader/loader.go
@@ -166,8 +166,6 @@ func (l *Loader) loadPlugins(ctx context.Context, src plugins.PluginSource, foun
if err != nil {
return nil, err
}
- metrics.SetPluginBuildInformation(p.ID, string(p.Type), p.Info.Version, string(p.Signature))
-
if errDeclareRoles := l.roleRegistry.DeclarePluginRoles(ctx, p.ID, p.Name, p.Roles); errDeclareRoles != nil {
l.log.Warn("Declare plugin roles failed.", "pluginID", p.ID, "err", errDeclareRoles)
}
@@ -177,6 +175,10 @@ func (l *Loader) loadPlugins(ctx context.Context, src plugins.PluginSource, foun
if err := l.load(ctx, p); err != nil {
l.log.Error("Could not start plugin", "pluginId", p.ID, "err", err)
}
+
+ if !p.IsCorePlugin() && !p.IsBundledPlugin() {
+ metrics.SetPluginBuildInformation(p.ID, string(p.Type), p.Info.Version, string(p.Signature))
+ }
}
return verifiedPlugins, nil
From 75f5cb061ec0df76fbb92533fcdf3d7958b2e83b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Zolt=C3=A1n=20Bedi?=
Date: Thu, 6 Apr 2023 14:40:26 +0200
Subject: [PATCH 68/80] Docs: Add content to what's new 9.4 (#65895)
* Docs: Add content to what's new 9.4
* Update docs/sources/whatsnew/whats-new-in-v9-4.md
Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com>
* Update docs/sources/whatsnew/whats-new-in-v9-4.md
Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com>
---------
Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com>
---
docs/sources/whatsnew/whats-new-in-v9-4.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/docs/sources/whatsnew/whats-new-in-v9-4.md b/docs/sources/whatsnew/whats-new-in-v9-4.md
index d47d7a20c23..feea36c9224 100644
--- a/docs/sources/whatsnew/whats-new-in-v9-4.md
+++ b/docs/sources/whatsnew/whats-new-in-v9-4.md
@@ -296,6 +296,10 @@ We've added support for JWT authentication.
We've added support for custom session parameters.
+## Postgres, MySQL, and MSSQL data sources
+
+The `database` property is now under the `jsonData` key in the data source configuration. This change is backward compatible, and existing configurations will continue to work.
+
## Before you upgrade
There are no known breaking changes associated with this version of Grafana.
From a6a43268204246d9eba97a155f306c7ca2cfea8e Mon Sep 17 00:00:00 2001
From: Josh Hunt
Date: Thu, 6 Apr 2023 14:18:24 +0100
Subject: [PATCH 69/80] Chore: remove console.log from search (#66124)
---
public/app/features/search/service/utils.ts | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/public/app/features/search/service/utils.ts b/public/app/features/search/service/utils.ts
index 083ddc613e1..357bbfe912e 100644
--- a/public/app/features/search/service/utils.ts
+++ b/public/app/features/search/service/utils.ts
@@ -52,8 +52,7 @@ export function getIconForKind(kind: string): IconName {
export function queryResultToViewItem(
item: DashboardQueryResult,
- view?: DataFrameView,
- index = -1
+ view?: DataFrameView
): DashboardViewItem {
const meta = view?.dataFrame.meta?.custom as SearchResultMeta | undefined;
@@ -68,7 +67,6 @@ export function queryResultToViewItem(
// Set enterprise sort value property
const sortFieldName = meta?.sortBy;
if (sortFieldName) {
- console.log('have sortFieldName', sortFieldName);
const sortFieldValue = item[sortFieldName];
if (typeof sortFieldValue === 'string' || typeof sortFieldValue === 'number') {
viewItem.sortMetaName = sortFieldName;
From 55553322c8b7688b54eb47fc4ceeb80503b88390 Mon Sep 17 00:00:00 2001
From: Mofeng <52260316+DiamondMofeng@users.noreply.github.com>
Date: Thu, 6 Apr 2023 21:32:39 +0800
Subject: [PATCH 70/80] Design System: Remove unused type parameter in
`SegmentProps` and `SegmentInput` (#64919)
* fix: Remove unused type parameter in `SegmentProps` and `SegmentInput`
* lint: prettier auto fix
* fix( SegmentInput.story.tsx ): adapt to changes
---
packages/grafana-ui/src/components/Segment/Segment.tsx | 2 +-
.../grafana-ui/src/components/Segment/SegmentAsync.tsx | 2 +-
.../src/components/Segment/SegmentInput.story.tsx | 8 +++-----
.../grafana-ui/src/components/Segment/SegmentInput.tsx | 8 ++++----
packages/grafana-ui/src/components/Segment/types.ts | 2 +-
5 files changed, 10 insertions(+), 12 deletions(-)
diff --git a/packages/grafana-ui/src/components/Segment/Segment.tsx b/packages/grafana-ui/src/components/Segment/Segment.tsx
index 5c7d2e1e881..fd8401c6c82 100644
--- a/packages/grafana-ui/src/components/Segment/Segment.tsx
+++ b/packages/grafana-ui/src/components/Segment/Segment.tsx
@@ -11,7 +11,7 @@ import { getSegmentStyles } from './styles';
import { SegmentSelect, useExpandableLabel, SegmentProps } from './';
-export interface SegmentSyncProps extends SegmentProps, Omit, 'value' | 'onChange'> {
+export interface SegmentSyncProps extends SegmentProps, Omit, 'value' | 'onChange'> {
value?: T | SelectableValue;
onChange: (item: SelectableValue) => void;
options: Array>;
diff --git a/packages/grafana-ui/src/components/Segment/SegmentAsync.tsx b/packages/grafana-ui/src/components/Segment/SegmentAsync.tsx
index 99e485027a3..5d9e90c8fbc 100644
--- a/packages/grafana-ui/src/components/Segment/SegmentAsync.tsx
+++ b/packages/grafana-ui/src/components/Segment/SegmentAsync.tsx
@@ -15,7 +15,7 @@ import { getSegmentStyles } from './styles';
import { useExpandableLabel, SegmentProps } from '.';
-export interface SegmentAsyncProps extends SegmentProps, Omit, 'value' | 'onChange'> {
+export interface SegmentAsyncProps extends SegmentProps, Omit, 'value' | 'onChange'> {
value?: T | SelectableValue;
loadOptions: (query?: string) => Promise>>;
/**
diff --git a/packages/grafana-ui/src/components/Segment/SegmentInput.story.tsx b/packages/grafana-ui/src/components/Segment/SegmentInput.story.tsx
index 68958c0e6cd..5f5516c86ad 100644
--- a/packages/grafana-ui/src/components/Segment/SegmentInput.story.tsx
+++ b/packages/grafana-ui/src/components/Segment/SegmentInput.story.tsx
@@ -100,12 +100,10 @@ export const InputWithAutoFocus = () => {
);
};
-export const Basic: ComponentStory>> = (
- args: SegmentInputProps
-) => {
+export const Basic: ComponentStory> = (args: SegmentInputProps) => {
const [value, setValue] = useState(args.value);
- const props: SegmentInputProps = {
+ const props: SegmentInputProps = {
...args,
value,
onChange: (value) => {
@@ -117,7 +115,7 @@ export const Basic: ComponentStory
- {...props} />
+
);
};
diff --git a/packages/grafana-ui/src/components/Segment/SegmentInput.tsx b/packages/grafana-ui/src/components/Segment/SegmentInput.tsx
index acfaef89d9d..d4d3d581b00 100644
--- a/packages/grafana-ui/src/components/Segment/SegmentInput.tsx
+++ b/packages/grafana-ui/src/components/Segment/SegmentInput.tsx
@@ -10,8 +10,8 @@ import { getSegmentStyles } from './styles';
import { useExpandableLabel, SegmentProps } from '.';
-export interface SegmentInputProps
- extends Omit, 'allowCustomValue' | 'allowEmptyValue'>,
+export interface SegmentInputProps
+ extends Omit,
Omit, 'value' | 'onChange'> {
value: string | number;
onChange: (text: string | number) => void;
@@ -19,7 +19,7 @@ export interface SegmentInputProps
const FONT_SIZE = 14;
-export function SegmentInput({
+export function SegmentInput({
value: initialValue,
onChange,
Component,
@@ -30,7 +30,7 @@ export function SegmentInput({
autofocus = false,
onExpandedChange,
...rest
-}: React.PropsWithChildren>) {
+}: React.PropsWithChildren) {
const ref = useRef(null);
const [value, setValue] = useState(initialValue);
const [inputWidth, setInputWidth] = useState(measureText((initialValue || '').toString(), FONT_SIZE).width);
diff --git a/packages/grafana-ui/src/components/Segment/types.ts b/packages/grafana-ui/src/components/Segment/types.ts
index 06cb23bef32..ec2d8756e82 100644
--- a/packages/grafana-ui/src/components/Segment/types.ts
+++ b/packages/grafana-ui/src/components/Segment/types.ts
@@ -1,6 +1,6 @@
import { ReactElement } from 'react';
-export interface SegmentProps {
+export interface SegmentProps {
Component?: ReactElement;
className?: string;
allowCustomValue?: boolean;
From 3e85c9075993dcf47994eb75a6fbf219ff57f2e5 Mon Sep 17 00:00:00 2001
From: linoman <2051016+linoman@users.noreply.github.com>
Date: Thu, 6 Apr 2023 16:48:31 +0200
Subject: [PATCH 71/80] Restructure the org upsert method (#64763)
---
pkg/services/sqlstore/user.go | 12 ++++--------
1 file changed, 4 insertions(+), 8 deletions(-)
diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go
index d197040a1d9..19bcc640cd5 100644
--- a/pkg/services/sqlstore/user.go
+++ b/pkg/services/sqlstore/user.go
@@ -146,6 +146,9 @@ func verifyExistingOrg(sess *DBSession, orgId int64) error {
func (ss *SQLStore) getOrCreateOrg(sess *DBSession, orgName string) (int64, error) {
var org org.Org
+ org.Created = time.Now()
+ org.Updated = org.Created
+
if ss.Cfg.AutoAssignOrg {
has, err := sess.Where("id=?", ss.Cfg.AutoAssignOrgId).Get(&org)
if err != nil {
@@ -164,18 +167,11 @@ func (ss *SQLStore) getOrCreateOrg(sess *DBSession, orgName string) (int64, erro
org.Name = mainOrgName
org.ID = int64(ss.Cfg.AutoAssignOrgId)
- } else {
- org.Name = orgName
- }
-
- org.Created = time.Now()
- org.Updated = time.Now()
-
- if org.ID != 0 {
if err := sess.InsertId(&org, ss.Dialect); err != nil {
return 0, err
}
} else {
+ org.Name = orgName
if _, err := sess.InsertOne(&org); err != nil {
return 0, err
}
From 1c3ce0735f3b6e5609c3a7d8988d7f0700ee75b1 Mon Sep 17 00:00:00 2001
From: gotjosh
Date: Thu, 6 Apr 2023 16:02:28 +0100
Subject: [PATCH 72/80] Alerting: Tiny refactor on the eval and schedule
packages (#66130)
* Alerting: Tiny refactor on the eval and schedule packages
two very small things:
- We had a constructor on something called a `Context` which is not a `context.Context` so let's just name that constructor `NewContext`
- The user that we use to run query evaluations is the same (with some variation) abstract it to a function so that it can be re-used when necessary.
* Update pkg/services/ngalert/schedule/schedule.go
Co-authored-by: Alexander Weaver
* Update pkg/services/ngalert/schedule/schedule.go
Co-authored-by: Alexander Weaver
---------
Co-authored-by: Alexander Weaver
---
pkg/services/ngalert/api/api_ruler.go | 2 +-
pkg/services/ngalert/api/api_testing.go | 4 +--
pkg/services/ngalert/eval/context.go | 2 +-
pkg/services/ngalert/eval/eval.go | 9 +++---
pkg/services/ngalert/eval/eval_test.go | 2 +-
pkg/services/ngalert/models/alert_query.go | 4 +++
pkg/services/ngalert/schedule/schedule.go | 33 ++++++++++++----------
7 files changed, 32 insertions(+), 24 deletions(-)
diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go
index ca9aa225a34..fd82c004a57 100644
--- a/pkg/services/ngalert/api/api_ruler.go
+++ b/pkg/services/ngalert/api/api_ruler.go
@@ -303,7 +303,7 @@ func (srv RulerSrv) RoutePostNameRulesConfig(c *contextmodel.ReqContext, ruleGro
}
rules, err := validateRuleGroup(&ruleGroupConfig, c.SignedInUser.OrgID, namespace, func(condition ngmodels.Condition) error {
- return srv.conditionValidator.Validate(eval.Context(c.Req.Context(), c.SignedInUser), condition)
+ return srv.conditionValidator.Validate(eval.NewContext(c.Req.Context(), c.SignedInUser), condition)
}, srv.cfg)
if err != nil {
return ErrResp(http.StatusBadRequest, err, "")
diff --git a/pkg/services/ngalert/api/api_testing.go b/pkg/services/ngalert/api/api_testing.go
index 83728417e80..f5dd34c03b2 100644
--- a/pkg/services/ngalert/api/api_testing.go
+++ b/pkg/services/ngalert/api/api_testing.go
@@ -52,7 +52,7 @@ func (srv TestingApiSrv) RouteTestGrafanaRuleConfig(c *contextmodel.ReqContext,
Condition: body.GrafanaManagedCondition.Condition,
Data: queries,
}
- ctx := eval.Context(c.Req.Context(), c.SignedInUser)
+ ctx := eval.NewContext(c.Req.Context(), c.SignedInUser)
conditionEval, err := srv.evaluator.Create(ctx, evalCond)
if err != nil {
@@ -128,7 +128,7 @@ func (srv TestingApiSrv) RouteEvalQueries(c *contextmodel.ReqContext, cmd apimod
if len(cmd.Data) > 0 {
cond.Condition = cmd.Data[0].RefID
}
- evaluator, err := srv.evaluator.Create(eval.Context(c.Req.Context(), c.SignedInUser), cond)
+ evaluator, err := srv.evaluator.Create(eval.NewContext(c.Req.Context(), c.SignedInUser), cond)
if err != nil {
return ErrResp(http.StatusBadRequest, err, "Failed to build evaluator for queries and expressions")
diff --git a/pkg/services/ngalert/eval/context.go b/pkg/services/ngalert/eval/context.go
index f84a0e607f6..3cdaefceb22 100644
--- a/pkg/services/ngalert/eval/context.go
+++ b/pkg/services/ngalert/eval/context.go
@@ -12,7 +12,7 @@ type EvaluationContext struct {
User *user.SignedInUser
}
-func Context(ctx context.Context, user *user.SignedInUser) EvaluationContext {
+func NewContext(ctx context.Context, user *user.SignedInUser) EvaluationContext {
return EvaluationContext{
Ctx: ctx,
User: user,
diff --git a/pkg/services/ngalert/eval/eval.go b/pkg/services/ngalert/eval/eval.go
index c08bbec75e0..fd9f4b6084b 100644
--- a/pkg/services/ngalert/eval/eval.go
+++ b/pkg/services/ngalert/eval/eval.go
@@ -220,7 +220,7 @@ func (s State) String() string {
return [...]string{"Normal", "Alerting", "Pending", "NoData", "Error"}[s]
}
-func buildDatasourceHeaders(ctx EvaluationContext) map[string]string {
+func buildDatasourceHeaders(ctx context.Context) map[string]string {
headers := map[string]string{
// Many data sources check this in query method as sometimes alerting needs special considerations.
// Several existing systems also compare against the value of this header. Altering this constitutes a breaking change.
@@ -233,7 +233,7 @@ func buildDatasourceHeaders(ctx EvaluationContext) map[string]string {
models.CacheSkipHeaderName: "true",
}
- key, ok := models.RuleKeyFromContext(ctx.Ctx)
+ key, ok := models.RuleKeyFromContext(ctx)
if ok {
headers["X-Rule-Uid"] = key.UID
headers["X-Grafana-Org-Id"] = strconv.FormatInt(key.OrgID, 10)
@@ -246,7 +246,7 @@ func buildDatasourceHeaders(ctx EvaluationContext) map[string]string {
func getExprRequest(ctx EvaluationContext, data []models.AlertQuery, dsCacheService datasources.CacheService) (*expr.Request, error) {
req := &expr.Request{
OrgId: ctx.User.OrgID,
- Headers: buildDatasourceHeaders(ctx),
+ Headers: buildDatasourceHeaders(ctx.Ctx),
}
datasources := make(map[string]*datasources.DataSource, len(data))
@@ -295,7 +295,8 @@ func getExprRequest(ctx EvaluationContext, data []models.AlertQuery, dsCacheServ
type NumberValueCapture struct {
Var string // RefID
Labels data.Labels
- Value *float64
+
+ Value *float64
}
func queryDataResponseToExecutionResults(c models.Condition, execResp *backend.QueryDataResponse) ExecutionResults {
diff --git a/pkg/services/ngalert/eval/eval_test.go b/pkg/services/ngalert/eval/eval_test.go
index 14fdd65cb54..a69c54e31e7 100644
--- a/pkg/services/ngalert/eval/eval_test.go
+++ b/pkg/services/ngalert/eval/eval_test.go
@@ -533,7 +533,7 @@ func TestValidate(t *testing.T) {
})
evaluator := NewEvaluatorFactory(setting.UnifiedAlertingSettings{}, cacheService, expr.ProvideService(&setting.Cfg{ExpressionsEnabled: true}, nil, nil), store)
- evalCtx := Context(context.Background(), u)
+ evalCtx := NewContext(context.Background(), u)
err := evaluator.Validate(evalCtx, condition)
if testCase.error {
diff --git a/pkg/services/ngalert/models/alert_query.go b/pkg/services/ngalert/models/alert_query.go
index 0d4d1410a9f..6f6c437e72a 100644
--- a/pkg/services/ngalert/models/alert_query.go
+++ b/pkg/services/ngalert/models/alert_query.go
@@ -97,6 +97,10 @@ type AlertQuery struct {
modelProps map[string]interface{}
}
+func (aq *AlertQuery) String() string {
+ return fmt.Sprintf("refID: %s, queryType: %s, datasourceUID: %s", aq.RefID, aq.QueryType, aq.DatasourceUID)
+}
+
func (aq *AlertQuery) setModelProps() error {
aq.modelProps = make(map[string]interface{})
err := json.Unmarshal(aq.Model, &aq.modelProps)
diff --git a/pkg/services/ngalert/schedule/schedule.go b/pkg/services/ngalert/schedule/schedule.go
index a57bf6c133d..eefed6e9a4c 100644
--- a/pkg/services/ngalert/schedule/schedule.go
+++ b/pkg/services/ngalert/schedule/schedule.go
@@ -373,21 +373,7 @@ func (sch *schedule) ruleRoutine(grafanaCtx context.Context, key ngmodels.AlertR
logger := logger.New("version", e.rule.Version, "attempt", attempt, "now", e.scheduledAt)
start := sch.clock.Now()
- schedulerUser := &user.SignedInUser{
- UserID: -1,
- IsServiceAccount: true,
- Login: "grafana_scheduler",
- OrgID: e.rule.OrgID,
- OrgRole: org.RoleAdmin,
- Permissions: map[int64]map[string][]string{
- e.rule.OrgID: {
- datasources.ActionQuery: []string{
- datasources.ScopeAll,
- },
- },
- },
- }
- evalCtx := eval.Context(ctx, schedulerUser)
+ evalCtx := eval.NewContext(ctx, SchedulerUserFor(e.rule.OrgID))
ruleEval, err := sch.evaluatorFactory.Create(evalCtx, e.rule.GetEvalCondition())
var results eval.Results
var dur time.Duration
@@ -580,3 +566,20 @@ func (sch *schedule) getRuleExtraLabels(evalCtx *evaluation) map[string]string {
}
return extraLabels
}
+
+func SchedulerUserFor(orgID int64) *user.SignedInUser {
+ return &user.SignedInUser{
+ UserID: -1,
+ IsServiceAccount: true,
+ Login: "grafana_scheduler",
+ OrgID: orgID,
+ OrgRole: org.RoleAdmin,
+ Permissions: map[int64]map[string][]string{
+ orgID: {
+ datasources.ActionQuery: []string{
+ datasources.ScopeAll,
+ },
+ },
+ },
+ }
+}
From 63187fae0ccb925ac603acf66da84d4695be3480 Mon Sep 17 00:00:00 2001
From: Matthew Jacobson
Date: Thu, 6 Apr 2023 12:06:25 -0400
Subject: [PATCH 73/80] Alerting: Remove and revert flag
alertingBigTransactions (#65976)
* Alerting: Remove and revert flag alertingBigTransactions
This is a partial revert of #56575 and a removal of the `alertingBigTransactions` flag.
Real-word use has seen no clear performance incentive to maintain this flag. Lowered db connection count
came at the cost of significant increase in CPU usage and query latency.
* Fix lint backend
* Removed last bits of alertingBigTransactions
---------
Co-authored-by: Armand Grillet <2117580+armandgrillet@users.noreply.github.com>
---
.../feature-toggles/index.md | 1 -
.../src/types/featureToggles.gen.ts | 1 -
pkg/services/featuremgmt/registry.go | 6 --
pkg/services/featuremgmt/toggles_gen.csv | 1 -
pkg/services/featuremgmt/toggles_gen.go | 4 -
pkg/services/ngalert/state/manager.go | 22 +----
pkg/services/ngalert/state/manager_test.go | 36 ++++---
pkg/services/ngalert/state/persist.go | 2 +-
pkg/services/ngalert/state/testing.go | 6 +-
.../ngalert/store/instance_database.go | 93 -------------------
.../ngalert/store/instance_database_test.go | 88 +++---------------
11 files changed, 42 insertions(+), 218 deletions(-)
diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
index 9d7b628917a..7b9aff01b69 100644
--- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
+++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
@@ -60,7 +60,6 @@ Alpha features might be changed or removed without prior notice.
| Feature toggle name | Description |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `alertingBigTransactions` | Use big transactions for alerting database writes |
| `dashboardPreviews` | Create and show thumbnails for dashboard search results |
| `live-service-web-worker` | This will use a webworker thread to processes events rather than the main thread |
| `queryOverLive` | Use Grafana Live WebSocket to execute backend queries |
diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts
index e62f5f0f32d..d2414f1c493 100644
--- a/packages/grafana-data/src/types/featureToggles.gen.ts
+++ b/packages/grafana-data/src/types/featureToggles.gen.ts
@@ -18,7 +18,6 @@
* @public
*/
export interface FeatureToggles {
- alertingBigTransactions?: boolean;
trimDefaults?: boolean;
disableEnvelopeEncryption?: boolean;
database_metrics?: boolean;
diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go
index 2143c9d4ad3..dd8d6e5241f 100644
--- a/pkg/services/featuremgmt/registry.go
+++ b/pkg/services/featuremgmt/registry.go
@@ -9,12 +9,6 @@ package featuremgmt
var (
// Register each toggle here
standardFeatureFlags = []FeatureFlag{
- {
- Name: "alertingBigTransactions",
- Description: "Use big transactions for alerting database writes",
- State: FeatureStateAlpha,
- Owner: grafanaAlertingSquad,
- },
{
Name: "trimDefaults",
Description: "Use cue schema to remove values that will be applied automatically",
diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv
index 0286f61d16e..43e42e059e5 100644
--- a/pkg/services/featuremgmt/toggles_gen.csv
+++ b/pkg/services/featuremgmt/toggles_gen.csv
@@ -1,5 +1,4 @@
Name,State,Owner,requiresDevMode,RequiresLicense,RequiresRestart,FrontendOnly
-alertingBigTransactions,alpha,@grafana/alerting-squad,false,false,false,false
trimDefaults,beta,@grafana/grafana-as-code,false,false,false,false
disableEnvelopeEncryption,stable,@grafana/grafana-as-code,false,false,false,false
database_metrics,stable,@grafana/hosted-grafana-team,false,false,false,false
diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go
index a648556ad6f..90fad46a56f 100644
--- a/pkg/services/featuremgmt/toggles_gen.go
+++ b/pkg/services/featuremgmt/toggles_gen.go
@@ -7,10 +7,6 @@
package featuremgmt
const (
- // FlagAlertingBigTransactions
- // Use big transactions for alerting database writes
- FlagAlertingBigTransactions = "alertingBigTransactions"
-
// FlagTrimDefaults
// Use cue schema to remove values that will be applied automatically
FlagTrimDefaults = "trimDefaults"
diff --git a/pkg/services/ngalert/state/manager.go b/pkg/services/ngalert/state/manager.go
index 8b523828cb6..5161ba957cf 100644
--- a/pkg/services/ngalert/state/manager.go
+++ b/pkg/services/ngalert/state/manager.go
@@ -351,8 +351,6 @@ func (st *Manager) saveAlertStates(ctx context.Context, logger log.Logger, state
}
logger.Debug("Saving alert states", "count", len(states))
- instances := make([]ngModels.AlertInstance, 0, len(states))
-
for _, s := range states {
// Do not save normal state to database and remove transition to Normal state but keep mapped states
if st.doNotSaveNormalState && IsNormalStateWithNoReason(s.State) && !s.Changed() {
@@ -364,7 +362,7 @@ func (st *Manager) saveAlertStates(ctx context.Context, logger log.Logger, state
logger.Error("Failed to create a key for alert state to save it to database. The state will be ignored ", "cacheID", s.CacheID, "error", err, "labels", s.Labels.String())
continue
}
- fields := ngModels.AlertInstance{
+ instance := ngModels.AlertInstance{
AlertInstanceKey: key,
Labels: ngModels.InstanceLabels(s.Labels),
CurrentState: ngModels.InstanceStateType(s.State.State.String()),
@@ -373,23 +371,11 @@ func (st *Manager) saveAlertStates(ctx context.Context, logger log.Logger, state
CurrentStateSince: s.StartsAt,
CurrentStateEnd: s.EndsAt,
}
- instances = append(instances, fields)
- }
- if len(instances) == 0 {
- return
- }
-
- if err := st.instanceStore.SaveAlertInstances(ctx, instances...); err != nil {
- type debugInfo struct {
- State string
- Labels string
+ err = st.instanceStore.SaveAlertInstance(ctx, instance)
+ if err != nil {
+ logger.Error("Failed to save alert state", "labels", s.Labels.String(), "state", s.State, "error", err)
}
- debug := make([]debugInfo, 0)
- for _, inst := range instances {
- debug = append(debug, debugInfo{string(inst.CurrentState), data.Labels(inst.Labels).String()})
- }
- logger.Error("Failed to save alert states", "states", debug, "error", err)
}
}
diff --git a/pkg/services/ngalert/state/manager_test.go b/pkg/services/ngalert/state/manager_test.go
index 67b82976d1a..b4abfee70f1 100644
--- a/pkg/services/ngalert/state/manager_test.go
+++ b/pkg/services/ngalert/state/manager_test.go
@@ -113,9 +113,11 @@ func TestWarmStateCache(t *testing.T) {
},
}
+ instances := make([]models.AlertInstance, 0)
+
labels := models.InstanceLabels{"test1": "testValue1"}
_, hash, _ := labels.StringAndHash()
- instance1 := models.AlertInstance{
+ instances = append(instances, models.AlertInstance{
AlertInstanceKey: models.AlertInstanceKey{
RuleOrgID: rule.OrgID,
RuleUID: rule.UID,
@@ -126,11 +128,11 @@ func TestWarmStateCache(t *testing.T) {
CurrentStateSince: evaluationTime.Add(-1 * time.Minute),
CurrentStateEnd: evaluationTime.Add(1 * time.Minute),
Labels: labels,
- }
+ })
labels = models.InstanceLabels{"test2": "testValue2"}
_, hash, _ = labels.StringAndHash()
- instance2 := models.AlertInstance{
+ instances = append(instances, models.AlertInstance{
AlertInstanceKey: models.AlertInstanceKey{
RuleOrgID: rule.OrgID,
RuleUID: rule.UID,
@@ -141,11 +143,11 @@ func TestWarmStateCache(t *testing.T) {
CurrentStateSince: evaluationTime.Add(-1 * time.Minute),
CurrentStateEnd: evaluationTime.Add(1 * time.Minute),
Labels: labels,
- }
+ })
labels = models.InstanceLabels{"test3": "testValue3"}
_, hash, _ = labels.StringAndHash()
- instance3 := models.AlertInstance{
+ instances = append(instances, models.AlertInstance{
AlertInstanceKey: models.AlertInstanceKey{
RuleOrgID: rule.OrgID,
RuleUID: rule.UID,
@@ -156,11 +158,11 @@ func TestWarmStateCache(t *testing.T) {
CurrentStateSince: evaluationTime.Add(-1 * time.Minute),
CurrentStateEnd: evaluationTime.Add(1 * time.Minute),
Labels: labels,
- }
+ })
labels = models.InstanceLabels{"test4": "testValue4"}
_, hash, _ = labels.StringAndHash()
- instance4 := models.AlertInstance{
+ instances = append(instances, models.AlertInstance{
AlertInstanceKey: models.AlertInstanceKey{
RuleOrgID: rule.OrgID,
RuleUID: rule.UID,
@@ -171,11 +173,11 @@ func TestWarmStateCache(t *testing.T) {
CurrentStateSince: evaluationTime.Add(-1 * time.Minute),
CurrentStateEnd: evaluationTime.Add(1 * time.Minute),
Labels: labels,
- }
+ })
labels = models.InstanceLabels{"test5": "testValue5"}
_, hash, _ = labels.StringAndHash()
- instance5 := models.AlertInstance{
+ instances = append(instances, models.AlertInstance{
AlertInstanceKey: models.AlertInstanceKey{
RuleOrgID: rule.OrgID,
RuleUID: rule.UID,
@@ -186,8 +188,10 @@ func TestWarmStateCache(t *testing.T) {
CurrentStateSince: evaluationTime.Add(-1 * time.Minute),
CurrentStateEnd: evaluationTime.Add(1 * time.Minute),
Labels: labels,
+ })
+ for _, instance := range instances {
+ _ = dbstore.SaveAlertInstance(ctx, instance)
}
- _ = dbstore.SaveAlertInstances(ctx, instance1, instance2, instance3, instance4, instance5)
cfg := state.ManagerCfg{
Metrics: testMetrics.GetStateMetrics(),
@@ -2370,7 +2374,9 @@ func TestStaleResultsHandler(t *testing.T) {
},
}
- _ = dbstore.SaveAlertInstances(ctx, instances...)
+ for _, instance := range instances {
+ _ = dbstore.SaveAlertInstance(ctx, instance)
+ }
testCases := []struct {
desc string
@@ -2621,7 +2627,9 @@ func TestDeleteStateByRuleUID(t *testing.T) {
},
}
- _ = dbstore.SaveAlertInstances(ctx, instances...)
+ for _, instance := range instances {
+ _ = dbstore.SaveAlertInstance(ctx, instance)
+ }
testCases := []struct {
desc string
@@ -2755,7 +2763,9 @@ func TestResetStateByRuleUID(t *testing.T) {
},
}
- _ = dbstore.SaveAlertInstances(ctx, instances...)
+ for _, instance := range instances {
+ _ = dbstore.SaveAlertInstance(ctx, instance)
+ }
testCases := []struct {
desc string
diff --git a/pkg/services/ngalert/state/persist.go b/pkg/services/ngalert/state/persist.go
index c6a23980fbc..b12deba5af6 100644
--- a/pkg/services/ngalert/state/persist.go
+++ b/pkg/services/ngalert/state/persist.go
@@ -11,7 +11,7 @@ import (
type InstanceStore interface {
FetchOrgIds(ctx context.Context) ([]int64, error)
ListAlertInstances(ctx context.Context, cmd *models.ListAlertInstancesQuery) ([]*models.AlertInstance, error)
- SaveAlertInstances(ctx context.Context, cmd ...models.AlertInstance) error
+ SaveAlertInstance(ctx context.Context, instance models.AlertInstance) error
DeleteAlertInstances(ctx context.Context, keys ...models.AlertInstanceKey) error
DeleteAlertInstancesByRule(ctx context.Context, key models.AlertRuleKey) error
}
diff --git a/pkg/services/ngalert/state/testing.go b/pkg/services/ngalert/state/testing.go
index e14b8e88d78..1b9a6fa694c 100644
--- a/pkg/services/ngalert/state/testing.go
+++ b/pkg/services/ngalert/state/testing.go
@@ -28,12 +28,10 @@ func (f *FakeInstanceStore) ListAlertInstances(_ context.Context, q *models.List
return nil, nil
}
-func (f *FakeInstanceStore) SaveAlertInstances(_ context.Context, q ...models.AlertInstance) error {
+func (f *FakeInstanceStore) SaveAlertInstance(_ context.Context, q models.AlertInstance) error {
f.mtx.Lock()
defer f.mtx.Unlock()
- for _, inst := range q {
- f.RecordedOps = append(f.RecordedOps, inst)
- }
+ f.RecordedOps = append(f.RecordedOps, q)
return nil
}
diff --git a/pkg/services/ngalert/store/instance_database.go b/pkg/services/ngalert/store/instance_database.go
index 8583a40e474..0c31bc620fe 100644
--- a/pkg/services/ngalert/store/instance_database.go
+++ b/pkg/services/ngalert/store/instance_database.go
@@ -43,99 +43,6 @@ func (st DBstore) ListAlertInstances(ctx context.Context, cmd *models.ListAlertI
return result, err
}
-// SaveAlertInstances saves all the provided alert instances to the store.
-func (st DBstore) SaveAlertInstances(ctx context.Context, cmd ...models.AlertInstance) error {
- if !st.FeatureToggles.IsEnabled(featuremgmt.FlagAlertingBigTransactions) {
- // This mimics the replace code-path by calling SaveAlertInstance in a loop, with a transaction per call.
- for _, c := range cmd {
- err := st.SaveAlertInstance(ctx, c)
- if err != nil {
- return err
- }
- }
- return nil
- } else {
- // Batches write into statements with `maxRows` instances per statements.
- // This makes sure we don't create statements that are too long for some
- // databases to process. For example, SQLite has a limit of 999 variables
- // per write.
- keyNames := []string{"rule_org_id", "rule_uid", "labels_hash"}
- fieldNames := []string{
- "rule_org_id", "rule_uid", "labels", "labels_hash", "current_state",
- "current_reason", "current_state_since", "current_state_end", "last_eval_time",
- }
- fieldsPerRow := len(fieldNames)
- maxRows := 20
- maxArgs := maxRows * fieldsPerRow
-
- bigUpsertSQL, err := st.SQLStore.GetDialect().UpsertMultipleSQL(
- "alert_instance", keyNames, fieldNames, maxRows)
- if err != nil {
- return err
- }
-
- // Args contains the SQL statement, and the values to fill into the SQL statement.
- args := make([]interface{}, 0, maxArgs)
- args = append(args, bigUpsertSQL)
- values := func(a []interface{}) int {
- return len(a) - 1
- }
-
- // Generate batches of `maxRows` and write the statements when full.
- for _, alertInstance := range cmd {
- labelTupleJSON, err := alertInstance.Labels.StringKey()
- if err != nil {
- return err
- }
-
- if err := models.ValidateAlertInstance(alertInstance); err != nil {
- return err
- }
-
- args = append(args,
- alertInstance.RuleOrgID, alertInstance.RuleUID, labelTupleJSON, alertInstance.LabelsHash,
- alertInstance.CurrentState, alertInstance.CurrentReason, alertInstance.CurrentStateSince.Unix(),
- alertInstance.CurrentStateEnd.Unix(), alertInstance.LastEvalTime.Unix())
-
- // If we've reached the maximum batch size, write to the database.
- if values(args) >= maxArgs {
- err = st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error {
- _, err := sess.Exec(args...)
- return err
- })
- if err != nil {
- return fmt.Errorf("failed to save alert instances: %w", err)
- }
-
- // Reset args so we can re-use the allocated interface pointers.
- args = args[:1]
- }
- }
-
- // Write the final batch of up to maxRows in size.
- if values(args) != 0 && values(args)%fieldsPerRow == 0 {
- upsertSQL, err := st.SQLStore.GetDialect().UpsertMultipleSQL(
- "alert_instance", keyNames, fieldNames, values(args)/fieldsPerRow)
- if err != nil {
- return err
- }
-
- args[0] = upsertSQL
- err = st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error {
- _, err := sess.Exec(args...)
- return err
- })
- if err != nil {
- return fmt.Errorf("failed to save alert instances: %w", err)
- }
- } else if values(args) != 0 {
- return fmt.Errorf("failed to upsert alert instances. Last statements had %v fields, which is not a multiple of the number of fields, %v", len(args), fieldsPerRow)
- }
-
- return nil
- }
-}
-
// SaveAlertInstance is a handler for saving a new alert instance.
func (st DBstore) SaveAlertInstance(ctx context.Context, alertInstance models.AlertInstance) error {
return st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error {
diff --git a/pkg/services/ngalert/store/instance_database_test.go b/pkg/services/ngalert/store/instance_database_test.go
index 5cb8088bcf9..3a055dab3cc 100644
--- a/pkg/services/ngalert/store/instance_database_test.go
+++ b/pkg/services/ngalert/store/instance_database_test.go
@@ -19,7 +19,6 @@ func BenchmarkAlertInstanceOperations(b *testing.B) {
b.StopTimer()
ctx := context.Background()
_, dbstore := tests.SetupTestEnv(b, baseIntervalSeconds)
- dbstore.FeatureToggles = featuremgmt.WithFeatures(featuremgmt.FlagAlertingBigTransactions)
const mainOrgID int64 = 1
@@ -48,78 +47,13 @@ func BenchmarkAlertInstanceOperations(b *testing.B) {
b.StartTimer()
for i := 0; i < b.N; i++ {
- _ = dbstore.SaveAlertInstances(ctx, instances...)
+ for _, instance := range instances {
+ _ = dbstore.SaveAlertInstance(ctx, instance)
+ }
_ = dbstore.DeleteAlertInstances(ctx, keys...)
}
}
-func TestIntegrationAlertInstanceBulkWrite(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test")
- }
- ctx := context.Background()
- _, dbstore := tests.SetupTestEnv(t, baseIntervalSeconds)
-
- orgIDs := []int64{1, 2, 3, 4, 5}
- counts := []int{10_000, 200, 503, 0, 1256}
- instances := make([]models.AlertInstance, 0, 10_000+200+503+0+1256)
- keys := make([]models.AlertInstanceKey, 0, 10_000+200+503+0+1256)
-
- for i, id := range orgIDs {
- alertRule := tests.CreateTestAlertRule(t, ctx, dbstore, 60, id)
-
- // Create some instances to write down and then delete.
- for j := 0; j < counts[i]; j++ {
- labels := models.InstanceLabels{"test": fmt.Sprint(j)}
- _, labelsHash, _ := labels.StringAndHash()
- instance := models.AlertInstance{
- AlertInstanceKey: models.AlertInstanceKey{
- RuleOrgID: alertRule.OrgID,
- RuleUID: alertRule.UID,
- LabelsHash: labelsHash,
- },
- CurrentState: models.InstanceStateFiring,
- CurrentReason: string(models.InstanceStateError),
- Labels: labels,
- }
- instances = append(instances, instance)
- keys = append(keys, instance.AlertInstanceKey)
- }
- }
-
- for _, bigStmts := range []bool{false, true} {
- dbstore.FeatureToggles = featuremgmt.WithFeatures([]interface{}{featuremgmt.FlagAlertingBigTransactions, bigStmts})
- t.Log("Saving")
- err := dbstore.SaveAlertInstances(ctx, instances...)
- require.NoError(t, err)
- t.Log("Finished database write")
-
- // List our instances. Make sure we have the right count.
- for i, id := range orgIDs {
- q := &models.ListAlertInstancesQuery{
- RuleOrgID: id,
- }
- alerts, err := dbstore.ListAlertInstances(ctx, q)
- require.NoError(t, err)
- require.Equal(t, counts[i], len(alerts), "Org %v: Expected %v instances but got %v", id, counts[i], len(alerts))
- }
- t.Log("Finished database read")
-
- err = dbstore.DeleteAlertInstances(ctx, keys...)
- require.NoError(t, err)
- t.Log("Finished database delete")
-
- for _, id := range orgIDs {
- q := &models.ListAlertInstancesQuery{
- RuleOrgID: id,
- }
- alerts, err := dbstore.ListAlertInstances(ctx, q)
- require.NoError(t, err)
- require.Zero(t, len(alerts), "Org %v: Deleted instances but still had %v", id, len(alerts))
- }
- }
-}
-
func TestIntegrationAlertInstanceOperations(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
@@ -164,7 +98,7 @@ func TestIntegrationAlertInstanceOperations(t *testing.T) {
CurrentReason: string(models.InstanceStateError),
Labels: labels,
}
- err := dbstore.SaveAlertInstances(ctx, instance)
+ err := dbstore.SaveAlertInstance(ctx, instance)
require.NoError(t, err)
listCmd := &models.ListAlertInstancesQuery{
@@ -193,7 +127,7 @@ func TestIntegrationAlertInstanceOperations(t *testing.T) {
CurrentState: models.InstanceStateNormal,
Labels: labels,
}
- err := dbstore.SaveAlertInstances(ctx, instance)
+ err := dbstore.SaveAlertInstance(ctx, instance)
require.NoError(t, err)
listCmd := &models.ListAlertInstancesQuery{
@@ -223,7 +157,7 @@ func TestIntegrationAlertInstanceOperations(t *testing.T) {
Labels: labels,
}
- err := dbstore.SaveAlertInstances(ctx, instance1)
+ err := dbstore.SaveAlertInstance(ctx, instance1)
require.NoError(t, err)
labels = models.InstanceLabels{"test": "testValue2"}
@@ -237,7 +171,7 @@ func TestIntegrationAlertInstanceOperations(t *testing.T) {
CurrentState: models.InstanceStateFiring,
Labels: labels,
}
- err = dbstore.SaveAlertInstances(ctx, instance2)
+ err = dbstore.SaveAlertInstance(ctx, instance2)
require.NoError(t, err)
listQuery := &models.ListAlertInstancesQuery{
@@ -284,7 +218,9 @@ func TestIntegrationAlertInstanceOperations(t *testing.T) {
CurrentReason: models.StateReasonError,
Labels: labels,
}
- err := dbstore.SaveAlertInstances(ctx, instance1, instance2)
+ err := dbstore.SaveAlertInstance(ctx, instance1)
+ require.NoError(t, err)
+ err = dbstore.SaveAlertInstance(ctx, instance2)
require.NoError(t, err)
listQuery := &models.ListAlertInstancesQuery{
@@ -327,7 +263,7 @@ func TestIntegrationAlertInstanceOperations(t *testing.T) {
Labels: labels,
}
- err := dbstore.SaveAlertInstances(ctx, instance1)
+ err := dbstore.SaveAlertInstance(ctx, instance1)
require.NoError(t, err)
instance2 := models.AlertInstance{
@@ -339,7 +275,7 @@ func TestIntegrationAlertInstanceOperations(t *testing.T) {
CurrentState: models.InstanceStateNormal,
Labels: instance1.Labels,
}
- err = dbstore.SaveAlertInstances(ctx, instance2)
+ err = dbstore.SaveAlertInstance(ctx, instance2)
require.NoError(t, err)
listQuery := &models.ListAlertInstancesQuery{
From 3685dd56e1a33aae2b3b18a807fbc3574601bdc5 Mon Sep 17 00:00:00 2001
From: sarah-spang <86264026+sarah-spang@users.noreply.github.com>
Date: Thu, 6 Apr 2023 10:22:57 -0600
Subject: [PATCH 74/80] Docs: Small fixes for Template Variables Doc (#65947)
* Docs: Small fixes for Template Variables Doc
* Docs: Minor fix to Variables Docs
- Added missing word
* Update docs/sources/dashboards/variables/add-template-variables/index.md
Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com>
* empty commit to trigger linting
* small change to trigger linting
---------
Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com>
Co-authored-by: Isabel Matwawana
---
.../variables/add-template-variables/index.md | 30 ++++++++++---------
1 file changed, 16 insertions(+), 14 deletions(-)
diff --git a/docs/sources/dashboards/variables/add-template-variables/index.md b/docs/sources/dashboards/variables/add-template-variables/index.md
index d26e4b0e434..deda7868f77 100644
--- a/docs/sources/dashboards/variables/add-template-variables/index.md
+++ b/docs/sources/dashboards/variables/add-template-variables/index.md
@@ -44,17 +44,17 @@ weight: 100
The following table lists the types of variables shipped with Grafana.
-| Variable type | Description |
-| :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| Query | Query-generated list of values such as metric names, server names, sensor IDs, data centers, and so on. [Add a query variable]({{< relref "#add-a-query-variable" >}}). |
-| Custom | Define the variable options manually using a comma-separated list. [Add a custom variable]({{< relref "#add-a-custom-variable" >}}). |
-| Text box | Display a free text input field with an optional default value. [Add a text box variable]({{< relref "#add-a-text-box-variable" >}}). |
-| Constant | Define a hidden constant. [Add a constant variable]({{< relref "#add-a-constant-variable" >}}). |
-| Data source | Quickly change the data source for an entire dashboard. [Add a data source variable]({{< relref "#add-a-data-source-variable" >}}). |
-| Interval | Interval variables represent time spans. [Add an interval variable]({{< relref "#add-an-interval-variable" >}}). |
-| Ad hoc filters | Key/value filters that are automatically added to all metric queries for a data source (InfluxDB, Prometheus, and Elasticsearch only). [Add ad hoc filters]({{< relref "#add-ad-hoc-filters" >}}). |
-| Global variables | Built-in variables that can be used in expressions in the query editor. Refer to [Global variables]({{< relref "#global-variables" >}}). |
-| Chained variables | Variable queries can contain other variables. Refer to [Chained variables]({{< relref "#chained-variables" >}}). |
+| Variable type | Description |
+| :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Query | Query-generated list of values such as metric names, server names, sensor IDs, data centers, and so on. [Add a query variable]({{< relref "#add-a-query-variable" >}}). |
+| Custom | Define the variable options manually using a comma-separated list. [Add a custom variable]({{< relref "#add-a-custom-variable" >}}). |
+| Text box | Display a free text input field with an optional default value. [Add a text box variable]({{< relref "#add-a-text-box-variable" >}}). |
+| Constant | Define a hidden constant. [Add a constant variable]({{< relref "#add-a-constant-variable" >}}). |
+| Data source | Quickly change the data source for an entire dashboard. [Add a data source variable]({{< relref "#add-a-data-source-variable" >}}). |
+| Interval | Interval variables represent time spans. [Add an interval variable]({{< relref "#add-an-interval-variable" >}}). |
+| Ad hoc filters | Key/value filters that are automatically added to all metric queries for a data source (Prometheus, Loki, InfluxDB, and Elasticsearch only). [Add ad hoc filters]({{< relref "#add-ad-hoc-filters" >}}). |
+| Global variables | Built-in variables that can be used in expressions in the query editor. Refer to [Global variables]({{< relref "#global-variables" >}}). |
+| Chained variables | Variable queries can contain other variables. Refer to [Chained variables]({{< relref "#chained-variables" >}}). |
## Enter General options
@@ -194,7 +194,7 @@ Ad hoc filters are one of the most complex and flexible variable options availab
Interpolating a variable with multiple values selected is tricky as it is not straight forward how to format the multiple values into a string that is valid in the given context where the variable is used. Grafana tries to solve this by allowing each data source plugin to inform the templating interpolation engine what format to use for multiple values.
-> **Note:** The **Custom all value** option on the variable must be blank for Grafana to format all values into a single string. If leave it blank, then the Grafana concatenates (adds together) all the values in the query. Something like `value1,value2,value3`. If a custom `all` value is used, then instead the value will be something like `*` or `all`.
+> **Note:** The **Custom all value** option on the variable must be blank for Grafana to format all values into a single string. If it is left blank, then Grafana concatenates (adds together) all the values in the query. Something like `value1,value2,value3`. If a custom `all` value is used, then instead the value will be something like `*` or `all`.
#### Multi-value variables with a Graphite data source
@@ -281,6 +281,8 @@ This variable is the `$__interval` variable in milliseconds, not a time interval
This variable is only available in the Singlestat panel and can be used in the prefix or suffix fields on the Options tab. The variable will be replaced with the series name or alias.
+> **Note:** The Singlestat panel is no longer available from Grafana 8.0.
+
### $\_\_org
This variable is the ID of the current organization.
@@ -398,7 +400,7 @@ apps.fakesite.web_server_01.cpu.*
#### InfluxDB example
-In this example, you have several data centers. Each data center has a different subset of hosts. It is based on the [InfluxDB Templated](https://play.grafana.org/d/000000002/influxdb-templated?orgId=1).
+In this example, you have several data centers. Each data center has a different subset of hosts. It is based on the [InfluxDB Templated](https://play.grafana.org/d/000000002/influxdb-templated?orgId=1) dashboard.
In this example, when the user changes the value of the `datacenter` variable, it changes the dropdown options returned by the `host` variable. The `host` variable uses the **Multi-value** option and **Include all option**, allowing users to select some or all options presented at any time. The `datacenter` does not use either option, so you can only select one data center at a time.
@@ -407,7 +409,7 @@ In this example, when the user changes the value of the `datacenter` variable, i
The query for this variable basically says, "Give me all the data centers that exist."
```
-SHOW TAG VALUES WITH KEY = "datacenter"
+SHOW TAG VALUES WITH KEY = "datacenter"
```
The values returned are `America`, `Africa`, `Asia`, and `Europe`.
From 3d589cbed909c0b8a6318831025564688b6ebef8 Mon Sep 17 00:00:00 2001
From: Josh Hunt
Date: Thu, 6 Apr 2023 17:42:40 +0100
Subject: [PATCH 75/80] Docs: Correct contributing.md i18n message (#66103)
---
CONTRIBUTING.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d35fb8b890c..6dcef56e776 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -15,7 +15,7 @@ You can contribute to Grafana in several ways. Here are some examples:
- Organize meetups and user groups in your local area.
- Help others by answering questions about Grafana.
-**Please note:** We do not currently accept contributions for translations. Please do not submit pull requests for messages.po files - they will be rejected.
+**Please note:** We do not currently accept contributions for translations. Please do not submit pull requests translating grafana.json files - they will be rejected. We do accept contributions to mark up phrases for translation. See [Internationalization](contribute/internationalization.md).
For more ways to contribute, check out the [Open Source Guides](https://opensource.guide/how-to-contribute/).
From 6a91f1a9b49237626b38195b4987e0dc4b501f15 Mon Sep 17 00:00:00 2001
From: Isabel <76437239+imatwawana@users.noreply.github.com>
Date: Thu, 6 Apr 2023 12:45:45 -0400
Subject: [PATCH 76/80] docs: nav related updates for data sources (#66080)
* nav related text updates for data sources
* fixed table rows
---
.../data-source-management/index.md | 22 +++++++----
.../datasources/alertmanager/_index.md | 31 ++++++++--------
.../datasources/aws-cloudwatch/_index.md | 9 +++--
docs/sources/datasources/graphite/_index.md | 37 ++++++++++---------
docs/sources/datasources/parca.md | 27 +++++++++-----
docs/sources/datasources/phlare.md | 29 ++++++++++-----
6 files changed, 92 insertions(+), 63 deletions(-)
diff --git a/docs/sources/administration/data-source-management/index.md b/docs/sources/administration/data-source-management/index.md
index c51e53d282b..aff513500ea 100644
--- a/docs/sources/administration/data-source-management/index.md
+++ b/docs/sources/administration/data-source-management/index.md
@@ -46,7 +46,8 @@ By default, data sources in an organization can be queried by any user in that o
You can assign data source permissions to users, teams, and roles which will allow access to query or edit the data source.
-1. Navigate to **Administration > Data sources**.
+1. Click **Connections** in the left-side menu.
+1. Under Your connections, click **Data sources**.
1. Select the data source to which you want to assign permissions.
1. On the Permissions tab, click **Add a permission**.
1. Select **User**, **Team**, or **Role**.
@@ -58,7 +59,8 @@ You can assign data source permissions to users, teams, and roles which will all
### Edit data source permissions for users, teams, or roles
-1. Navigate to **Administration > Data sources**.
+1. Click **Connections** in the left-side menu.
+1. Under Your connections, click **Data sources**.
1. Select the data source for which you want to edit permissions.
1. On the Permissions tab, find the user, team, or role permission you want to update.
1. Select a different option in the **Permission** dropdown.
@@ -67,7 +69,8 @@ You can assign data source permissions to users, teams, and roles which will all
### Remove data source permissions for users, teams, or roles
-1. Navigate to **Administration > Data sources**.
+1. Click **Connections** in the left-side menu.
+1. Under Your connections, click **Data sources**.
1. Select the data source from which you want to remove permissions.
1. On the Permissions tab, find the user, team, or role permission you want to remove.
1. Click the **X** next to the permission.
@@ -122,9 +125,10 @@ You must be an Org admin or Grafana admin to enable query caching for a data sou
By default, data source queries are not cached. To enable query caching for a single data source:
-1. On the left-side menu, click **Administration > Data sources**.
+1. Click **Connections** in the left-side menu.
+1. Under Your Connections, click **Data sources**.
1. In the data source list, click the data source that you want to turn on caching for.
-1. Open the Cache tab.
+1. Go to the Cache tab.
1. Click **Enable**.
1. (Optional) Choose custom TTLs for the data source's queries and resources caching. If you skip this step, then Grafana uses the default TTL.
@@ -140,9 +144,10 @@ To configure global settings for query caching, refer to the [Query caching sect
To disable query caching for a single data source:
-1. On the left-side menu, click **Administration > Data sources**.
+1. Click **Connections** in the left-side menu.
+1. Under Your Connections, click **Data sources**.
1. In the data source list, click the data source that you want to turn off caching for.
-1. In the Cache tab, click **Disable**.
+1. On the Cache tab, click **Disable**.
To disable query caching for an entire Grafana instance, set the `enabled` flag to `false` in the [Query caching section of Enterprise Configuration]({{< relref "../../setup-grafana/configure-grafana/enterprise-configuration/#caching" >}}). You will no longer see the Cache tab on any data sources, and no data source queries will be cached.
@@ -152,7 +157,8 @@ If you experience performance issues or repeated queries become slower to execut
> **Note:** This action impacts all cache-enabled data sources. If you are using Memcached, the system clears all data from the Memcached instance.
-1. On the left-side menu, click **Administration > Data sources**.
+1. Click **Connections** in the left-side menu.
+1. Under Your Connections, click **Data sources**.
1. In the data source list, click the data source that you want to clear the cache for.
1. In the Cache tab, click **Clear cache**.
diff --git a/docs/sources/datasources/alertmanager/_index.md b/docs/sources/datasources/alertmanager/_index.md
index 0e69a550f71..3c4ac6b793d 100644
--- a/docs/sources/datasources/alertmanager/_index.md
+++ b/docs/sources/datasources/alertmanager/_index.md
@@ -28,24 +28,25 @@ When using Prometheus, contact points and notification policies are read-only in
## Configure the data source
-**To access the data source configuration page:**
+To configure basic settings for the data source, complete the following steps:
-1. Select the **Data sources** section from the **Administration** menu
-2. Select the **Alertmanager** data source
+1. Click **Connections** in the left-side menu.
+1. Under Your connections, click **Data sources**.
+1. Enter `Alertmanager` in the search bar.
+1. Click **Alertmanager**.
-Set the data source's basic configuration options carefully:
+ The **Settings** tab of the data source is displayed.
-| Name | Description |
-| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **Name** | Sets the name you use to refer to the data source |
-| **Default** | Sets whether the data source is pre-selected for new panels and queries |
-| **Alertmanager** | |
-| **Implementation** | Alertmanager implementation. **Mimir**, **Cortex,** and **Prometheus** are supported |
-| **Receive Grafana Alerts** | When enabled the Alertmanager receives alert instances from Grafana-managed alert rules. **Important:** It works only if Grafana alerting is configured to send its alert instances to external Alertmanagers |
-| **HTTP** | |
-| **URL** | Sets the HTTP protocol, IP, and port of your Alertmanager instance, such as `https://alertmanager.example.org:9093` |
-| **Access** | Only **Server** access mode is functional |
-| | |
+1. Set the data source's basic configuration options:
+
+ | Name | Description |
+ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+ | **Name** | Sets the name you use to refer to the data source |
+ | **Default** | Sets whether the data source is pre-selected for new panels and queries |
+ | **Alertmanager Implementation** | Alertmanager implementation. **Mimir**, **Cortex,** and **Prometheus** are supported |
+ | **Receive Grafana Alerts** | When enabled the Alertmanager receives alert instances from Grafana-managed alert rules. **Important:** It works only if Grafana alerting is configured to send its alert instances to external Alertmanagers |
+ | **HTTP URL** | Sets the HTTP protocol, IP, and port of your Alertmanager instance, such as `https://alertmanager.example.org:9093` |
+ | **Access** | Only **Server** access mode is functional |
## Provision the Alertmanager data source
diff --git a/docs/sources/datasources/aws-cloudwatch/_index.md b/docs/sources/datasources/aws-cloudwatch/_index.md
index 39a0053a405..3cefaa1c55d 100644
--- a/docs/sources/datasources/aws-cloudwatch/_index.md
+++ b/docs/sources/datasources/aws-cloudwatch/_index.md
@@ -31,11 +31,12 @@ Once you've added the data source, you can [configure it]({{< relref "#configure
## Configure the data source
-**To access the data source configuration page:**
+1. Click **Connections** in the left-side menu.
+1. Under Your connections, click **Data sources**.
+1. Enter `CloudWatch` in the search bar.
+1. Click **CloudWatch**.
-1. Hover the cursor over the **Configuration** (gear) icon.
-1. Select **Data Sources**.
-1. Select the CloudWatch data source.
+ The **Settings** tab of the data source is displayed.
### Configure AWS authentication
diff --git a/docs/sources/datasources/graphite/_index.md b/docs/sources/datasources/graphite/_index.md
index f8c05a0925a..11275b7d75e 100644
--- a/docs/sources/datasources/graphite/_index.md
+++ b/docs/sources/datasources/graphite/_index.md
@@ -24,26 +24,29 @@ Once you've added the Graphite data source, you can [configure it]({{< relref "#
## Configure the data source
-**To access the data source configuration page:**
+To configure basic settings for the data source, complete the following steps:
-1. Hover the cursor over the **Configuration** (gear) icon.
-1. Select **Data Sources**.
-1. Select the Graphite data source.
+1. Click **Connections** in the left-side menu.
+1. Under Your connections, click **Data sources**.
+1. Enter `Graphite` in the search bar.
+1. Click **Graphite**.
-Set the data source's basic configuration options carefully:
+ The **Settings** tab of the data source is displayed.
-| Name | Description |
-| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
-| **Name** | Sets the name you use to refer to the data source in panels and queries. |
-| **Default** | Sets whether the data source is pre-selected for new panels. You can set only one default data source per organization. |
-| **URL** | Sets the HTTP protocol, IP, and port of your graphite-web or graphite-api installation. |
-| **Auth** | For details, refer to [Configure Authentication]({{< relref "../../setup-grafana/configure-security/configure-authentication/" >}}). |
-| **Basic Auth** | Enables basic authentication to the data source. |
-| **User** | Sets the user name for basic authentication. |
-| **Password** | Sets the password for basic authentication. |
-| **Custom HTTP Headers** | Click **Add header** to add a custom HTTP header. |
-| **Header** | Defines the custom header name. |
-| **Value** | Defines the custom header value. |
+1. Set the data source's basic configuration options:
+
+ | Name | Description |
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
+ | **Name** | Sets the name you use to refer to the data source in panels and queries. |
+ | **Default** | Sets whether the data source is pre-selected for new panels. You can set only one default data source per organization. |
+ | **URL** | Sets the HTTP protocol, IP, and port of your graphite-web or graphite-api installation. |
+ | **Auth** | For details, refer to [Configure Authentication]({{< relref "../../setup-grafana/configure-security/configure-authentication/" >}}). |
+ | **Basic Auth** | Enables basic authentication to the data source. |
+ | **User** | Sets the user name for basic authentication. |
+ | **Password** | Sets the password for basic authentication. |
+ | **Custom HTTP Headers** | Click **Add header** to add a custom HTTP header. |
+ | **Header** | Defines the custom header name. |
+ | **Value** | Defines the custom header value. |
You can also configure settings specific to the Graphite data source:
diff --git a/docs/sources/datasources/parca.md b/docs/sources/datasources/parca.md
index b37f88022d2..8ef350f6bbd 100644
--- a/docs/sources/datasources/parca.md
+++ b/docs/sources/datasources/parca.md
@@ -18,16 +18,25 @@ Grafana ships with built-in support for Parca, a continuous profiling OSS databa
## Configure the Parca data source
-To access Parca settings, click the **Configuration** (gear) icon, then click **Data Sources** > **Parca**.
+To configure basic settings for the data source, complete the following steps:
-| Name | Description |
-| ------------ | ------------------------------------------------------------------ |
-| `Name` | A name to specify the data source in panels, queries, and Explore. |
-| `Default` | The default data source will be pre-selected for new panels. |
-| `URL` | The URL of the Parca instance, e.g., `http://localhost:4100` |
-| `Basic Auth` | Enable basic authentication to the Parca data source. |
-| `User` | User name for basic authentication. |
-| `Password` | Password for basic authentication. |
+1. Click **Connections** in the left-side menu.
+1. Under Your connections, click **Data sources**.
+1. Enter `Parca` in the search bar.
+1. Click **Parca**.
+
+ The **Settings** tab of the data source is displayed.
+
+1. Set the data source's basic configuration options:
+
+ | Name | Description |
+ | ------------ | ------------------------------------------------------------------ |
+ | `Name` | A name to specify the data source in panels, queries, and Explore. |
+ | `Default` | The default data source will be pre-selected for new panels. |
+ | `URL` | The URL of the Parca instance, e.g., `http://localhost:4100` |
+ | `Basic Auth` | Enable basic authentication to the Parca data source. |
+ | `User` | User name for basic authentication. |
+ | `Password` | Password for basic authentication. |
## Querying
diff --git a/docs/sources/datasources/phlare.md b/docs/sources/datasources/phlare.md
index ee0a9dab141..09d19dbedb2 100644
--- a/docs/sources/datasources/phlare.md
+++ b/docs/sources/datasources/phlare.md
@@ -18,17 +18,26 @@ Grafana ships with built-in support for Phlare, a horizontally scalable, highly-
## Configure the Phlare data source
-To access Phlare settings, click the **Configuration** (gear) icon, then click **Data Sources** > **Phlare**.
+To configure basic settings for the data source, complete the following steps:
-| Name | Description |
-| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| `Name` | A name to specify the data source in panels, queries, and Explore. |
-| `Default` | The default data source will be pre-selected for new panels. |
-| `URL` | The URL of the Phlare instance, e.g., `http://localhost:4100` |
-| `Basic Auth` | Enable basic authentication to the Phlare data source. |
-| `User` | User name for basic authentication. |
-| `Password` | Password for basic authentication. |
-| `Minimal step` | Similar to Prometheus, Phlare scrapes profiles at certain intervals. To prevent querying at smaller interval use Minimal step same or higher than your Phlare scrape interval. |
+1. Click **Connections** in the left-side menu.
+1. Under Your connections, click **Data sources**.
+1. Enter `Phlare` in the search bar.
+1. Click **Phlare**.
+
+ The **Settings** tab of the data source is displayed.
+
+1. Set the data source's basic configuration options:
+
+ | Name | Description |
+ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+ | `Name` | A name to specify the data source in panels, queries, and Explore. |
+ | `Default` | The default data source will be pre-selected for new panels. |
+ | `URL` | The URL of the Phlare instance, e.g., `http://localhost:4100` |
+ | `Basic Auth` | Enable basic authentication to the Phlare data source. |
+ | `User` | User name for basic authentication. |
+ | `Password` | Password for basic authentication. |
+ | `Minimal step` | Similar to Prometheus, Phlare scrapes profiles at certain intervals. To prevent querying at smaller interval use Minimal step same or higher than your Phlare scrape interval. |
## Querying
From 3725463c4381ee13069949b33fe1e6c93bc456ba Mon Sep 17 00:00:00 2001
From: Isabel <76437239+imatwawana@users.noreply.github.com>
Date: Thu, 6 Apr 2023 12:45:55 -0400
Subject: [PATCH 77/80] docs: nav updates for dashboards (#66078)
* navigation related text and image updates in dashboards topics
* fix typo
* fixed style and formatting issues
---
.../manage-library-panels/index.md | 13 ++--
.../manage-version-history/index.md | 14 ++---
.../create-manage-playlists/index.md | 60 ++++++++++++-------
.../dashboards/manage-dashboards/index.md | 26 ++++----
.../share-dashboards-panels/index.md | 22 ++++---
5 files changed, 79 insertions(+), 56 deletions(-)
diff --git a/docs/sources/dashboards/build-dashboards/manage-library-panels/index.md b/docs/sources/dashboards/build-dashboards/manage-library-panels/index.md
index ab1c8e1f06f..56d67efafca 100644
--- a/docs/sources/dashboards/build-dashboards/manage-library-panels/index.md
+++ b/docs/sources/dashboards/build-dashboards/manage-library-panels/index.md
@@ -25,8 +25,8 @@ When you create a library panel, the panel on the source dashboard is converted
1. Open a panel in edit mode.
1. In the panel display options, click the down arrow option to bring changes to the visualization.
- {{< figure src="/static/img/docs/library-panels/create-lib-panel-from-edit-8-0.png" class="docs-image--no-shadow" max-width= "800px" caption="Screenshot of the edit panel" >}}
-1. Click the **Library panels** option, and then click **Create library panel** to open the create dialog.
+ {{< figure src="/media/docs/grafana/panels-visualizations/screenshot-create-lib-panel-from-edit-9-5.png" class="docs-image--no-shadow" max-width= "800px" >}}
+1. Click **Library panels**, and then click **+ Create library panel** to open the create dialog.
1. In **Library panel name**, enter the name.
1. In **Save in folder**, select the folder to save the library panel.
1. Click **Create library panel** to save your changes.
@@ -34,14 +34,16 @@ When you create a library panel, the panel on the source dashboard is converted
Once created, you can modify the library panel using any dashboard on which it appears. After you save the changes, all instances of the library panel reflect these modifications.
-{{< figure src="/static/img/docs/library-panels/create-from-more-8-0.png" class="docs-image--no-shadow" max-width= "900px" caption="Screenshot of the edit panel" >}}
+You can also create a library panel directly from the edit menu of any panel.
+
+{{< figure src="/media/docs/grafana/panels-visualizations/screenshot-create-from-more-9-5.png" class="docs-image--no-shadow" max-width= "900px" >}}
## Add a library panel to a dashboard
Add a Grafana library panel to a dashboard when you want to provide visualizations to other dashboard users.
1. Click **Dashboards** in the left-side menu.
-1. Click **New** and select **New Dashboard**.
+1. Click **New** and select **New Dashboard** in the dropdown.
1. On the empty dashboard, click **+ Import library panel**.
You will see a list of your library panels.
@@ -60,6 +62,7 @@ Unlink a library panel when you want to make a change to the panel and not affec
1. Hover over any part of the panel to display the actions menu on the top right corner.
1. Click the menu and select **Edit**.
1. Click **Unlink** on the top right corner of the page.
+1. Click **Yes, unlink**.
## View a list of library panels
@@ -69,7 +72,7 @@ You can view a list of available library panels and search for a library panel.
1. Click **Library panels**.
You can see a list of previously defined library panels.
- {{< figure src="/static/img/docs/library-panels/library-panel-list-8-0.png" class="docs-image--no-shadow" max-width= "900px" caption="Screenshot of the edit panel" >}}
+ {{< figure src="/media/docs/grafana/panels-visualizations/screenshot-library-panel-list-9-5.png" class="docs-image--no-shadow" max-width= "900px" >}}
1. Search for a specific library panel if you know its name.
diff --git a/docs/sources/dashboards/build-dashboards/manage-version-history/index.md b/docs/sources/dashboards/build-dashboards/manage-version-history/index.md
index 8ab7439f047..c2b8e3fa286 100644
--- a/docs/sources/dashboards/build-dashboards/manage-version-history/index.md
+++ b/docs/sources/dashboards/build-dashboards/manage-version-history/index.md
@@ -17,7 +17,7 @@ weight: 400
Whenever you save a version of your dashboard, a copy of that version is saved so that previous versions of your dashboard are never lost. A list of these versions is available by entering the dashboard settings and then selecting "Versions" in the left side menu.
-
+
The dashboard version history feature lets you compare and restore to previously saved dashboard versions.
@@ -25,20 +25,18 @@ The dashboard version history feature lets you compare and restore to previously
To compare two dashboard versions, select the two versions from the list that you wish to compare. Once selected, the "Compare versions" button will become clickable. Click the button to view the diff between the two versions.
-
+
Upon clicking the button, you'll be brought to the diff view. By default, you'll see a textual summary of the changes, like in the image below.
-
+
-If you want to view the diff of the raw JSON that represents your dashboard, you can do that as well by clicking the "View JSON Diff" button at the bottom.
-
-If you want to restore to the version you are diffing against, you can do so by clicking the "Restore to version \" button in the top right.
+If you want to view the diff of the raw JSON that represents your dashboard, you can do that as well by clicking the expand icon for the View JSON Diff section at the bottom.
## Restoring to a previously saved dashboard version
-If you need to restore to a previously saved dashboard version, you can do so by either clicking the "Restore" button on the right of a row in the dashboard version list, or by clicking the "Restore to version \" button appearing in the diff view. Clicking the button will bring up the following popup prompting you to confirm the restoration.
+If you need to restore to a previously saved dashboard version, you can do so by either clicking the "Restore" button on the right of a row in the dashboard version list, or by clicking the **Restore to version \** button appearing in the diff view. Clicking the button will bring up the following popup prompting you to confirm the restoration.
-
+
After restoring to a previous version, a new version will be created containing the same exact data as the previous version, only with a different version number. This is indicated in the "Notes column" for the row in the new dashboard version. This is done simply to ensure your previous dashboard versions are not affected by the change.
diff --git a/docs/sources/dashboards/create-manage-playlists/index.md b/docs/sources/dashboards/create-manage-playlists/index.md
index e3c12996a26..9be403a304d 100644
--- a/docs/sources/dashboards/create-manage-playlists/index.md
+++ b/docs/sources/dashboards/create-manage-playlists/index.md
@@ -26,21 +26,22 @@ Use the information in this section to access existing playlists. Start and cont
### Access a playlist
-1. Hover your cursor over Grafana’s side menu.
-1. Click **Playlists**. You will see a list of existing playlists.
+1. Click **Dashboards** in the left-side menu.
+1. Click **Playlists** to see a list of existing playlists.
### Start a playlist
-You can start a playlist in five different view modes. View mode determine how the menus and navigation bar appear on the dashboards.
+You can start a playlist in five different view modes. View modes determine how the menus and navigation bar appear on the dashboards.
By default, each dashboard is displayed for the amount of time entered in the Interval field, which you set when you create or edit a playlist. After you start a playlist, you can control it with the navbar at the top of the page.
-1. [Access](#access-playlist) the playlist page to see a list of existing playlists.
-1. Find the playlist you want to start, then click **Start playlist**. The start playlist dialog opens.
-1. Select one of the five playlist modes available based on the information in the following table.
-1. Click **Start **.
+1. Click **Dashboards** in the left-side menu.
+1. Click **Playlists** to see a list of existing playlists.
+1. Find the playlist you want to start, then click **Start playlist**.
+1. In the modal that opens, select one of the five playlist modes available, based on the information in the table below.
+1. Click **Start \**.
-The playlist displays each dashboard for the time specified in the `Interval` field, set when creating or editing a playlist. Once a playlist starts, you can [control](#control-a-playlist) it using the navbar at the top of your screen.
+The playlist displays each dashboard for the time specified in the **Interval** field, set when creating or editing a playlist. Once a playlist starts, you can [control](#control-a-playlist) it using the navbar at the top of your screen.
| Mode | Description |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -67,7 +68,9 @@ You can control a playlist in **Normal** or **TV** mode after it's started, usin
You can create a playlist to present dashboards in a sequence, with a set order and time interval between dashboards.
-1. In the playlist page, click **New playlist**. The New playlist page opens.
+1. Click **Dashboards** in the left-side menu.
+1. Click **Playlists** to see a list of existing playlists.
+1. Click **New playlist**. The New playlist page opens.
1. In the **Name** text box, enter a descriptive name.
1. In the **Interval** text box, enter a time interval. Grafana displays a particular dashboard for the interval of time specified here before moving on to the next dashboard.
1. In Dashboards, add existing dashboards to the playlist using **Add by title** and **Add by tag** drop-down options. The dashboards you add are listed in a sequential order.
@@ -82,8 +85,8 @@ You can create a playlist to present dashboards in a sequence, with a set order
You can save a playlist and add it to your **Playlists** page, where you can start it. Be sure that all the dashboards you want to appear in your playlist are added when creating or editing the playlist before saving it.
-1. To access the Playlist feature, hover your cursor over Grafana's side menu.
-1. Click **Playlists**.
+1. Click **Dashboards** in the left-side menu.
+1. Click **Playlists** to see a list of existing playlists.
1. Click on the playlist.
1. Edit the playlist.
1. Ensure that your playlist has a **Name**, **Interval**, and at least one **Dashboard** added to it.
@@ -95,36 +98,47 @@ You can edit a playlist by updating its name, interval time, and by adding, remo
### Edit a playlist
-1. In the playlist page, click **Edit playlist**. The Edit playlist page opens.
+1. Click **Dashboards** in the left-side menu.
+1. Click **Playlists** to see a list of existing playlists.
+1. Find the playlist you want to update and click **Edit playlist**.
1. Update the name and time interval, then add or remove dashboards from the playlist using instructions in [Create a playlist](#create-a-playlist).
1. Click **Save** to save your changes.
### Delete a playlist
-1. Click **Playlists**.
-1. Next to the Playlist you want to delete, click **Remove[x]**.
+1. Click **Dashboards** in the left-side menu.
+1. Click **Playlists** to see a list of existing playlists.
+1. Find the playlist you want to remove.
+1. Click **Delete playlist**.
### Rearrange dashboard order
-1. Next to the dashboard you want to move, click the up or down arrow.
+1. Click **Dashboards** in the left-side menu.
+1. Click **Playlists** to see a list of existing playlists.
+1. Find the playlist you want to update and click **Edit playlist**.
+1. Click and drag the dashboards into your desired order.
1. Click **Save** to save your changes.
### Remove a dashboard
-1. Click **Remove[x]** to remove a dashboard from the playlist.
+1. Click **Dashboards** in the left-side menu.
+1. Click **Playlists** to see a list of existing playlists.
+1. Find the playlist you want to update and click **Edit playlist**.
+1. Click **\[x\]** on the name of the dashboard you want to remove from the playlist.
1. Click **Save** to save your changes.
## Share a playlist in a view mode
You can share a playlist by copying the link address on the view mode you prefer, and pasting the URL to your destination.
-1. From the Dashboards submenu, click **Playlists**.
-1. Next to the playlist you want to share, click **Start playlist**.
-1. In the dropdown, right click the view mode you prefer.
-1. Click **Copy Link Address** to copy the URL to your clipboard.
+1. Click **Dashboards** in the left-side menu.
+1. Click **Playlists** to see a list of existing playlists.
+1. Click the share icon of the playlist you want to share.
+1. Select the view mode you prefer.
+1. Click **Copy** next to the Link URL to copy it to your clipboard.
- Example: The URL for the first playlist on the Grafana Play site in Kiosk mode will look like this:
+ For example, the URL for the first playlist on the Grafana Play site in Kiosk mode will look like this:
- [https://play.grafana.org/playlists/play/1?kiosk](https://play.grafana.org/playlists/play/1?kiosk).
+ [https://play.grafana.org/playlists/play/1?kiosk](https://play.grafana.org/playlists/play/1?kiosk).
-1. Paste the URL to your destination.
+1. Paste the URL to your destination.
diff --git a/docs/sources/dashboards/manage-dashboards/index.md b/docs/sources/dashboards/manage-dashboards/index.md
index 2a18a59b3cd..fb16052b0ef 100644
--- a/docs/sources/dashboards/manage-dashboards/index.md
+++ b/docs/sources/dashboards/manage-dashboards/index.md
@@ -48,14 +48,16 @@ Folders help you organize and group dashboards, which is useful when you have ma
**To create a dashboard folder:**
-1. Sign in to Grafana and on the side menu, click **Dashboards > New folder**.
+1. Sign in to Grafana.
+1. Click **Dashboards** in the left-side menu.
+1. On the Dashboards page, click **New** and select **New folder** in the dropdown.
1. Enter a unique name and click **Create**.
When you save a dashboard, you can either select a folder for the dashboard to be saved in or create a new folder.
## Manage dashboards
-On the **Manage dashboards and folders** page, you can:
+On the Dashboards page, you can:
- create a folder
- create a dashboard
@@ -65,17 +67,17 @@ On the **Manage dashboards and folders** page, you can:
### Dashboard folder page
-You can complete the following tasks on the **Dashboard Folder** page:
+You can complete the following tasks on a dashboard folder page:
- Move or delete dashboards in a folder
-- Rename a folder (available under the **Settings** tab)
+- Rename a folder (available on the Settings tab)
- Assign permissions to folders (which are inherited by the dashboards in the folder)
-To navigate to the dashboard folder page, click the cog appears when you hover over a folder in the dashboard search result list or the **Manage dashboards and folders** page.
+To navigate to the dashboard folder page, hover over the name of the folder and click **Go to folder** in the dashboard search result list or on the Dashboards page.
### Dashboard permissions
-You can assign permissions to a folder. Any permissions you assign are inherited by the dashboards in the folder. An Access Control List (ACL) is used where **Organization Role**, **Team** and a **User** can be assigned permissions.
+You can assign permissions to a folder. Any permissions you assign are inherited by the dashboards in the folder. An Access Control List (ACL) is used where **Organization Role**, **Team**, and a **User** can be assigned permissions.
For more information about dashboard permissions, refer to [Dashboard permissions]({{< relref "../../administration/roles-and-permissions/#dashboard-permissions" >}}).
@@ -87,10 +89,11 @@ You can use the Grafana UI or the [HTTP API]({{< relref "../../developers/http_a
The dashboard export action creates a Grafana JSON file that contains everything you need, including layout, variables, styles, data sources, queries, and so on, so that you can later import the dashboard.
+1. Click **Dashboards** in the left-side menu.
1. Open the dashboard you want to export.
-2. Click the **Share** icon.
-3. Click **Export**.
-4. Click **Save to file**.
+1. Click the **Share** icon.
+1. Click **Export**.
+1. Click **Save to file**.
Grafana downloads a JSON file to your local machine.
@@ -102,7 +105,8 @@ A template variable of the type `Constant` will automatically be hidden in the d
### Import a dashboard
-1. Click **Dashboards > Import** in the side menu.
+1. Click **Dashboards** in the left-side menu.
+1. Click **New** and select **Import** in the dropdown menu.
1. Perform one of the following steps:
- Upload a dashboard JSON file
@@ -118,7 +122,7 @@ The import process enables you to change the name of the dashboard, pick the dat
Find dashboards for common server applications at [Grafana.com/dashboards](https://grafana.com/dashboards).
-{{< figure src="/static/img/docs/v50/gcom_dashboard_list.png" max-width="700px" >}}
+{{< figure src="/media/docs/grafana/dashboards/screenshot-gcom-dashboards.png" >}}
## Troubleshoot dashboards
diff --git a/docs/sources/dashboards/share-dashboards-panels/index.md b/docs/sources/dashboards/share-dashboards-panels/index.md
index 868a1fe6d9a..3c677ad6226 100644
--- a/docs/sources/dashboards/share-dashboards-panels/index.md
+++ b/docs/sources/dashboards/share-dashboards-panels/index.md
@@ -52,10 +52,11 @@ You can share a dashboard as a direct link or as a snapshot. You can also export
> **Note:** If you change a dashboard, ensure that you save the changes before sharing.
-1. Navigate to the home page of your Grafana instance.
-1. Click on the share icon in the top navigation.
+1. Click **Dashboards** in the left-side menu.
+1. Click the dashboard you want to share.
+1. Click the share icon at the top of the screen.
- The share dialog opens and shows the **Link** tab.
+ The share dialog opens and shows the Link tab.
### Share a direct link
@@ -73,7 +74,8 @@ A dashboard snapshot shares an interactive dashboard publicly. Grafana strips se
You can publish snapshots to your local instance or to [snapshots.raintank.io](http://snapshots.raintank.io). The latter is a free service provided by Grafana Labs that enables you to publish dashboard snapshots to an external Grafana instance. Anyone with the link can view it. You can set an expiration time if you want the snapshot removed after a certain time period.
-1. Click **Local Snapshot** or **Publish to snapshots.raintank.io**.
+1. Click the **Snapshot** tab.
+1. Click **Publish to snapshots.raintank.io** or **Local Snapshot**.
Grafana generates a link of the snapshot.
@@ -91,7 +93,9 @@ You can generate and save PDF files of any dashboard.
> **Note:** Available in [Grafana Enterprise]({{< relref "../../introduction/grafana-enterprise/" >}}).
-1. In the upper-right corner of the dashboard that you want to export as PDF, click the **Share dashboard** icon.
+1. Click **Dashboards** in the left-side menu.
+1. Click the dashboard you want to share.
+1. Click the share icon at the top of the screen.
1. On the PDF tab, select a layout option for the exported dashboard: **Portrait** or **Landscape**.
1. Click **Save as PDF** to render the dashboard as a PDF file.
@@ -101,8 +105,8 @@ You can generate and save PDF files of any dashboard.
You can share a panel as a direct link, as a snapshot, or as an embedded link. You can also create library panels using the **Share** option on any panel.
-1. Click a panel title to open the panel menu.
-1. Click **Share**.
+1. Hover over any part of the panel to display the actions menu on the top right corner.
+1. Click the menu and select **Share**.
The share dialog opens and shows the **Link** tab.
@@ -139,8 +143,8 @@ A panel snapshot shares an interactive panel publicly. Grafana strips sensitive
You can publish snapshots to your local instance or to [snapshots.raintank.io](http://snapshots.raintank.io). The latter is a free service provided by [Grafana Labs](https://grafana.com), that enables you to publish dashboard snapshots to an external Grafana instance. You can optionally set an expiration time if you want the snapshot to be removed after a certain time period.
-1. In the **Share Panel** dialog, click **Snapshot** to open the tab.
-1. Click **Local Snapshot** or **Publish to snapshots.raintank.io**.
+1. In the **Share Panel** dialog, click **Snapshot** to go to the tab.
+1. Click **Publish to snapshots.raintank.io** or **Local Snapshot**.
Grafana generates the link of the snapshot.
From 38482c90bfd5e837e66a15563c8ead6a56a2a163 Mon Sep 17 00:00:00 2001
From: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com>
Date: Thu, 6 Apr 2023 14:43:10 -0500
Subject: [PATCH 78/80] Build URL params and include requestId (#65742)
* add requestId and ds_type to datasource request url
---
.../dashboard-public-create.spec.ts | 20 ++++++++--
.../load-options-from-url.spec.ts | 6 +--
.../set-options-from-ui.spec.ts | 8 +++-
e2e/panels-suite/panelEdit_base.spec.ts | 6 ++-
e2e/sql-suite/mysql.spec.ts | 40 +++++++++++--------
e2e/various-suite/exemplars.spec.ts | 23 ++++++-----
.../src/utils/DataSourceWithBackend.test.ts | 6 +--
.../src/utils/DataSourceWithBackend.ts | 10 ++++-
8 files changed, 79 insertions(+), 40 deletions(-)
diff --git a/e2e/dashboards-suite/dashboard-public-create.spec.ts b/e2e/dashboards-suite/dashboard-public-create.spec.ts
index 304f502dfa3..5b0bb7b159d 100644
--- a/e2e/dashboards-suite/dashboard-public-create.spec.ts
+++ b/e2e/dashboards-suite/dashboard-public-create.spec.ts
@@ -8,7 +8,11 @@ e2e.scenario({
skipScenario: false,
scenario: () => {
// Opening a dashboard without template variables
- e2e().intercept('POST', '/api/ds/query').as('query');
+ e2e()
+ .intercept({
+ pathname: '/api/ds/query',
+ })
+ .as('query');
e2e.flows.openDashboard({ uid: 'ZqZnVvFZz' });
e2e().wait('@query');
@@ -65,7 +69,12 @@ e2e.scenario({
skipScenario: false,
scenario: () => {
// Opening a dashboard without template variables
- e2e().intercept('POST', '/api/ds/query').as('query');
+ e2e()
+ .intercept({
+ method: 'POST',
+ pathname: '/api/ds/query',
+ })
+ .as('query');
e2e.flows.openDashboard({ uid: 'ZqZnVvFZz' });
e2e().wait('@query');
@@ -109,7 +118,12 @@ e2e.scenario({
skipScenario: false,
scenario: () => {
// Opening a dashboard without template variables
- e2e().intercept('/api/ds/query').as('query');
+ e2e()
+ .intercept({
+ method: 'POST',
+ pathname: '/api/ds/query',
+ })
+ .as('query');
e2e.flows.openDashboard({ uid: 'ZqZnVvFZz' });
e2e().wait('@query');
diff --git a/e2e/dashboards-suite/load-options-from-url.spec.ts b/e2e/dashboards-suite/load-options-from-url.spec.ts
index d4e2f486eed..e42a55d7056 100644
--- a/e2e/dashboards-suite/load-options-from-url.spec.ts
+++ b/e2e/dashboards-suite/load-options-from-url.spec.ts
@@ -9,7 +9,7 @@ describe('Variables - Load options from Url', () => {
e2e()
.intercept({
method: 'POST',
- url: '/api/ds/query',
+ pathname: '/api/ds/query*',
})
.as('query');
@@ -61,7 +61,7 @@ describe('Variables - Load options from Url', () => {
e2e()
.intercept({
method: 'POST',
- url: '/api/ds/query',
+ pathname: '/api/ds/query',
})
.as('query');
@@ -124,7 +124,7 @@ describe('Variables - Load options from Url', () => {
e2e()
.intercept({
method: 'POST',
- url: '/api/ds/query',
+ pathname: '/api/ds/query',
})
.as('query');
diff --git a/e2e/dashboards-suite/set-options-from-ui.spec.ts b/e2e/dashboards-suite/set-options-from-ui.spec.ts
index 94d1fbe60fd..c6d61d78c1d 100644
--- a/e2e/dashboards-suite/set-options-from-ui.spec.ts
+++ b/e2e/dashboards-suite/set-options-from-ui.spec.ts
@@ -61,7 +61,11 @@ describe('Variables - Set options from ui', () => {
it('adding a value that is not part of dependents options should add the new values dependant options', () => {
e2e.flows.login('admin', 'admin');
e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&var-datacenter=A&var-server=AA&var-pod=AAA` });
- e2e().intercept('/api/ds/query').as('query');
+ e2e()
+ .intercept({
+ pathname: '/api/ds/query',
+ })
+ .as('query');
e2e().wait('@query');
@@ -119,7 +123,7 @@ describe('Variables - Set options from ui', () => {
e2e.flows.openDashboard({
uid: `${PAGE_UNDER_TEST}?orgId=1&var-datacenter=A&var-datacenter=B&var-server=AA&var-server=BB&var-pod=AAA&var-pod=BBB`,
});
- e2e().intercept('/api/ds/query').as('query');
+ e2e().intercept({ pathname: '/api/ds/query' }).as('query');
e2e().wait('@query');
diff --git a/e2e/panels-suite/panelEdit_base.spec.ts b/e2e/panels-suite/panelEdit_base.spec.ts
index 6d38ba9b1b2..2ae5daa1cd2 100644
--- a/e2e/panels-suite/panelEdit_base.spec.ts
+++ b/e2e/panels-suite/panelEdit_base.spec.ts
@@ -9,7 +9,11 @@ e2e.scenario({
addScenarioDashBoard: false,
skipScenario: false,
scenario: () => {
- e2e().intercept('/api/ds/query').as('query');
+ e2e()
+ .intercept({
+ pathname: '/api/ds/query',
+ })
+ .as('query');
e2e.flows.openDashboard({ uid: 'TkZXxlNG3' });
e2e().wait('@query');
diff --git a/e2e/sql-suite/mysql.spec.ts b/e2e/sql-suite/mysql.spec.ts
index ab535220c98..91ab9d7379a 100644
--- a/e2e/sql-suite/mysql.spec.ts
+++ b/e2e/sql-suite/mysql.spec.ts
@@ -11,24 +11,30 @@ describe('MySQL datasource', () => {
it('code editor autocomplete should handle table name escaping/quoting', () => {
e2e.flows.login('admin', 'admin');
- e2e().intercept('POST', '**/api/ds/query', (req) => {
- if (req.body.queries[0].refId === 'datasets') {
- req.alias = 'datasets';
- req.reply({
- body: datasetResponse,
- });
- } else if (req.body.queries[0].refId === 'tables') {
- req.alias = 'tables';
- req.reply({
- body: tablesResponse,
- });
- } else if (req.body.queries[0].refId === 'fields') {
- req.alias = 'fields';
- req.reply({
- body: fieldsResponse,
- });
+ e2e().intercept(
+ 'POST',
+ {
+ pathname: '/api/ds/query',
+ },
+ (req) => {
+ if (req.body.queries[0].refId === 'datasets') {
+ req.alias = 'datasets';
+ req.reply({
+ body: datasetResponse,
+ });
+ } else if (req.body.queries[0].refId === 'tables') {
+ req.alias = 'tables';
+ req.reply({
+ body: tablesResponse,
+ });
+ } else if (req.body.queries[0].refId === 'fields') {
+ req.alias = 'fields';
+ req.reply({
+ body: fieldsResponse,
+ });
+ }
}
- });
+ );
e2e.pages.Explore.visit();
diff --git a/e2e/various-suite/exemplars.spec.ts b/e2e/various-suite/exemplars.spec.ts
index 164296d70e0..70c286902a0 100644
--- a/e2e/various-suite/exemplars.spec.ts
+++ b/e2e/various-suite/exemplars.spec.ts
@@ -32,16 +32,21 @@ describe('Exemplars', () => {
});
it('should be able to navigate to configured data source', () => {
- e2e().intercept('/api/ds/query', (req) => {
- const datasourceType = req.body.queries[0].datasource.type;
- if (datasourceType === 'prometheus') {
- req.reply({ fixture: 'exemplars-query-response.json' });
- } else if (datasourceType === 'tempo') {
- req.reply({ fixture: 'tempo-response.json' });
- } else {
- req.reply({});
+ e2e().intercept(
+ {
+ pathname: '/api/ds/query',
+ },
+ (req) => {
+ const datasourceType = req.body.queries[0].datasource.type;
+ if (datasourceType === 'prometheus') {
+ req.reply({ fixture: 'exemplars-query-response.json' });
+ } else if (datasourceType === 'tempo') {
+ req.reply({ fixture: 'tempo-response.json' });
+ } else {
+ req.reply({});
+ }
}
- });
+ );
e2e.pages.Explore.visit();
diff --git a/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts b/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts
index f703528d10a..40b17f3fdb0 100644
--- a/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts
+++ b/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts
@@ -98,7 +98,7 @@ describe('DataSourceWithBackend', () => {
"hideFromInspector": false,
"method": "POST",
"requestId": undefined,
- "url": "/api/ds/query",
+ "url": "/api/ds/query?ds_type=dummy",
}
`);
});
@@ -153,7 +153,7 @@ describe('DataSourceWithBackend', () => {
"hideFromInspector": false,
"method": "POST",
"requestId": undefined,
- "url": "/api/ds/query?expression=true",
+ "url": "/api/ds/query?ds_type=dummy&expression=true",
}
`);
});
@@ -222,7 +222,7 @@ describe('DataSourceWithBackend', () => {
"hideFromInspector": true,
"method": "POST",
"requestId": undefined,
- "url": "/api/ds/query",
+ "url": "/api/ds/query?ds_type=dummy",
}
`);
});
diff --git a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts
index 8b8b142948c..5b3793cfcbf 100644
--- a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts
+++ b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts
@@ -202,10 +202,16 @@ class DataSourceWithBackend<
headers[PluginRequestHeaders.PluginID] = Array.from(pluginIDs).join(', ');
headers[PluginRequestHeaders.DatasourceUID] = Array.from(dsUIDs).join(', ');
- let url = '/api/ds/query';
+ let url = '/api/ds/query?ds_type=' + this.type;
+
if (hasExpr) {
headers[PluginRequestHeaders.FromExpression] = 'true';
- url += '?expression=true';
+ url += '&expression=true';
+ }
+
+ // Appending request ID to url to facilitate client-side performance metrics. See #65244 for more context.
+ if (requestId) {
+ url += `&requestId=${requestId}`;
}
if (request.dashboardUID) {
From 13e097f3e3d9ff4bdf65d424c70a527c217d21a0 Mon Sep 17 00:00:00 2001
From: lwandz13 <126723338+lwandz13@users.noreply.github.com>
Date: Thu, 6 Apr 2023 15:35:37 -0500
Subject: [PATCH 79/80] Docs/fix cross account section in cloudwatch doc
(#65572)
* fixed headings
* added Cross-account observability section back
* Update docs/sources/datasources/aws-cloudwatch/query-editor/index.md
Co-authored-by: Sarah Zinger
* Update docs/sources/datasources/aws-cloudwatch/query-editor/index.md
Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com>
* broke out to 2 steps
* changed headings under Configure AWS aut
* changed headings, added link to IAM
* changed headings
* updated link
* formatting fixes
* more formatting fixes
* made requested change
* cleaned up wording
* Update docs/sources/datasources/aws-cloudwatch/query-editor/index.md
Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com>
* Update docs/sources/datasources/aws-cloudwatch/query-editor/index.md
Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com>
* Update docs/sources/datasources/aws-cloudwatch/query-editor/index.md
Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com>
* removed new
---------
Co-authored-by: Sarah Zinger
Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com>
---
.../sources/datasources/aws-cloudwatch/_index.md | 16 ++++++++--------
.../aws-cloudwatch/query-editor/index.md | 14 +++++++++++---
2 files changed, 19 insertions(+), 11 deletions(-)
diff --git a/docs/sources/datasources/aws-cloudwatch/_index.md b/docs/sources/datasources/aws-cloudwatch/_index.md
index 3cefaa1c55d..ab5b0372780 100644
--- a/docs/sources/datasources/aws-cloudwatch/_index.md
+++ b/docs/sources/datasources/aws-cloudwatch/_index.md
@@ -50,7 +50,7 @@ For authentication options and configuration details, refer to [AWS authenticati
To read CloudWatch metrics and EC2 tags, instances, regions, and alarms, you must grant Grafana permissions via IAM.
You can attach these permissions to the IAM role or IAM user you configured in [AWS authentication]({{< relref "./aws-authentication/" >}}).
-**Metrics-only:**
+##### Metrics-only permissions
```json
{
@@ -85,7 +85,7 @@ You can attach these permissions to the IAM role or IAM user you configured in [
}
```
-**Logs-only:**
+##### Logs-only permissions
```json
{
@@ -120,7 +120,7 @@ You can attach these permissions to the IAM role or IAM user you configured in [
}
```
-**Metrics and Logs:**
+##### Metrics and logs permissions
```json
{
@@ -168,7 +168,7 @@ You can attach these permissions to the IAM role or IAM user you configured in [
}
```
-**Cross-account observability: (see below) **
+##### Cross-account observability permissions
```json
{
@@ -235,7 +235,7 @@ For more information about provisioning, and for available configuration options
#### Provisioning examples
-**Using AWS SDK (default):**
+##### Using AWS SDK (default)
```yaml
apiVersion: 1
@@ -247,7 +247,7 @@ datasources:
defaultRegion: eu-west-2
```
-**Using credentials' profile name (non-default):**
+##### Using credentials' profile name (non-default)
```yaml
apiVersion: 1
@@ -262,7 +262,7 @@ datasources:
profile: secondary
```
-**Using accessKey and secretKey:**
+##### Using accessKey and secretKey
```yaml
apiVersion: 1
@@ -278,7 +278,7 @@ datasources:
secretKey: ''
```
-**Using AWS SDK Default and ARN of IAM Role to Assume:**
+##### Using AWS SDK Default and ARN of IAM Role to Assume
```yaml
apiVersion: 1
diff --git a/docs/sources/datasources/aws-cloudwatch/query-editor/index.md b/docs/sources/datasources/aws-cloudwatch/query-editor/index.md
index 08fb9717366..2911fa63d72 100644
--- a/docs/sources/datasources/aws-cloudwatch/query-editor/index.md
+++ b/docs/sources/datasources/aws-cloudwatch/query-editor/index.md
@@ -92,7 +92,7 @@ For example, to apply arithmetic operations to a metric, apply a unique string i
> **Note:** If you use the expression field to reference another query, like `queryA * 2`, you can't create an alert rule based on that query.
-##### Period macro
+#### Period macro
If you're using a CloudWatch [`SEARCH`](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/search-expression-syntax.html) expression, you may want to use the `$__period_auto` macro rather than specifying a period explicitly. The `$__period_auto` macro will resolve to a [CloudWatch period](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_GetMetricData.html) that is suitable for the chosen time range.
@@ -218,9 +218,17 @@ When making `stats` queries in [Explore]({{< relref "../../../explore/" >}}), ma
{{< figure src="/static/img/docs/v70/explore-mode-switcher.png" max-width="500px" class="docs-image--right" caption="Explore mode switcher" >}}
-### Getting started
+## Cross-account observability
-To enable cross-account observability, first enable it in CloudWatch using the official [CloudWatch docs](http://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Unified-Cross-Account.html), then add [two new API actions]({{< relref "../#cross-account-observability" >}}) to the IAM policy attached to the role/user running the plugin.
+The CloudWatch plugin allows monitoring and troubleshooting applications that span multiple accounts within a region. Using cross-account observability, you can seamlessly search, visualize, and analyze metrics and logs without worrying about account boundaries.
+
+### Get started
+
+To enable cross-account observability, complete the following steps:
+
+1. Go to the [Amazon CloudWatch docs](http://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Unified-Cross-Account.html) and follow the instructions on enabling cross-account observability.
+
+1. Add [two API actions](/docs/grafana/latest/datasources/aws-cloudwatch/#cross-account-observability-permissions) to the IAM policy attached to the role/user running the plugin.
Cross-account querying is available in the plugin through the `Logs` mode and the `Metric search` mode. Once you have it configured correctly, you'll see a "Monitoring account" badge displayed in the query editor header.
From 8600a8ce2e44f10a67ad98252184901b63dbe135 Mon Sep 17 00:00:00 2001
From: Isabel <76437239+imatwawana@users.noreply.github.com>
Date: Thu, 6 Apr 2023 16:43:38 -0400
Subject: [PATCH 80/80] docs: nav updates for use dashboard topic (#66151)
* updated main image and supporting text
* fixed icon name
* updated nav path
---
.../administration/plugin-management/index.md | 2 +-
.../dashboards/use-dashboards/index.md | 31 ++++++++++---------
2 files changed, 17 insertions(+), 16 deletions(-)
diff --git a/docs/sources/administration/plugin-management/index.md b/docs/sources/administration/plugin-management/index.md
index 6ca96b2baa3..6e3eff53e43 100644
--- a/docs/sources/administration/plugin-management/index.md
+++ b/docs/sources/administration/plugin-management/index.md
@@ -77,7 +77,7 @@ Before following the steps below, make sure you are logged in as a Grafana admin
-- Administrators can find the Plugin catalog at **Configuration > Plugins**.
+- Administrators can find the Plugin catalog at **Administration > Plugins**.
### Browse plugins
diff --git a/docs/sources/dashboards/use-dashboards/index.md b/docs/sources/dashboards/use-dashboards/index.md
index d10eb022fa9..9753b8884df 100644
--- a/docs/sources/dashboards/use-dashboards/index.md
+++ b/docs/sources/dashboards/use-dashboards/index.md
@@ -27,27 +27,28 @@ The dashboard user interface provides a number of features that you can use to c
The following image and descriptions highlights all dashboards features.
-{{< figure src="/static/img/docs/v91/dashboard-features/dashboard-features.png" width="700px" >}}
+{{< figure src="/media/docs/grafana/dashboards/screenshot-dashboard-annotated-9-5-0.png" width="700px" >}}
-- **Grafana home** (1): Click the Grafana home icon to be redirected to the home page configured in the Grafana instance.
+- **Grafana home** (1): Click **Home** in the breadcrumb to be redirected to the home page configured in the Grafana instance.
- **Dashboard title** (2): When you click the dashboard title you can search for dashboard contained in the current folder.
-- **Share dashboard** (3): Use this option to share the current dashboard by link or snapshot. You can also export the dashboard definition from the share modal.
-- **Add a new panel** (4): Use this option to add a panel, dashboard row, or library panel to the current dashboard.
-- **Dashboard settings** (5): Use this option to change dashboard name, folder, and tags and manage variables and annotation queries. For more information about dashboard settings, refer to [Modify dashboard settings]({{< relref "../build-dashboards/modify-dashboard-settings/" >}}).
-- **Time picker dropdown** (6): Click to select relative time range options and set custom absolute time ranges.
+- **Share dashboard or panel** (3): Use this option to share the current dashboard or panel by link or snapshot. You can also export the dashboard definition from the share modal.
+- **Add** (4): Use this option to add a panel, dashboard row, or library panel to the current dashboard.
+- **Save dashboard** (5): Click to save changes to your dashboard.
+- **Dashboard insights** (6): Click to view analytics about your dashboard including information about users, activity, query counts.
+- **Dashboard settings** (7): Use this option to change dashboard name, folder, and tags and manage variables and annotation queries. For more information about dashboard settings, refer to [Modify dashboard settings]({{< relref "../build-dashboards/modify-dashboard-settings/" >}}).
+- **Time picker dropdown** (8): Click to select relative time range options and set custom absolute time ranges.
- You can change the **Timezone** and **fiscal year** settings from the time range controls by clicking the **Change time settings** button.
- Time settings are saved on a per-dashboard basis.
-- **Zoom out time range** (7): Click to zoom out the time range. For more information about how to use time range controls, refer to [Common time range controls]({{< relref "#common-time-range-controls" >}}).
-- **Refresh dashboard** (8): Click to immediately trigger queries and refresh dashboard data.
-- **Refresh dashboard time interval** (9): Click to select a dashboard auto refresh time interval.
-- **View mode** (10): Click to display the dashboard on a large screen such as a TV or a kiosk. View mode hides irrelevant information such as navigation menus. For more information about view mode, refer to [How to Create Kiosks to Display Dashboards on a TV](https://grafana.com/blog/2019/05/02/grafana-tutorial-how-to-create-kiosks-to-display-dashboards-on-a-tv/).
-- **Dashboard panel** (11): The primary building block of a dashboard is the panel. To add a new panel, dashboard row, or library panel, click **Add panel**.
+- **Zoom out time range** (9): Click to zoom out the time range. For more information about how to use time range controls, refer to [Common time range controls]({{< relref "#common-time-range-controls" >}}).
+- **Refresh dashboard** (10): Click to immediately trigger queries and refresh dashboard data.
+- **Refresh dashboard time interval** (11): Click to select a dashboard auto refresh time interval.
+- **View mode** (12): Click to display the dashboard on a large screen such as a TV or a kiosk. View mode hides irrelevant information such as navigation menus. For more information about view mode, refer to [How to Create Kiosks to Display Dashboards on a TV](https://grafana.com/blog/2019/05/02/grafana-tutorial-how-to-create-kiosks-to-display-dashboards-on-a-tv/).
+- **Dashboard panel** (13): The primary building block of a dashboard is the panel. To add a new panel, dashboard row, or library panel, click **Add panel**.
- Library panels can be shared among many dashboards.
- To move a panel, drag the panel header to another location.
- To resize a panel, click and drag the lower right corner of the panel.
-- **Graph legend** (12): Change series colors, y-axis and series visibility directly from the legend.
-- **Search** (13): Click **Search** to search for dashboards by name or panel title.
-- **Dashboard row** (14): A dashboard row is a logical divider within a dashboard that groups panels together.
+- **Graph legend** (14): Change series colors, y-axis and series visibility directly from the legend.
+- **Dashboard row** (15): A dashboard row is a logical divider within a dashboard that groups panels together.
- Rows can be collapsed or expanded allowing you to hide parts of the dashboard.
- Panels inside a collapsed row do not issue queries.
- Use [repeating rows]({{< relref "../build-dashboards/create-dashboard/#configure-repeating-rows" >}}) to dynamically create rows based on a template variable.
@@ -129,7 +130,7 @@ Hover your cursor over the field to see the exact time stamps in the range and t
Click the current time range to change it. You can change the current time using a _relative time range_, such as the last 15 minutes, or an _absolute time range_, such as `2020-05-14 00:00:00 to 2020-05-15 23:59:59`.
-
+
#### Relative time range
|