diff --git a/.betterer.results b/.betterer.results index 54e1a496e23..0ba1e379ac3 100644 --- a/.betterer.results +++ b/.betterer.results @@ -838,9 +838,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "2"], [0, 0, 0, "Unexpected any. Specify a different type.", "3"] ], - "packages/grafana-ui/src/components/PageLayout/PageToolbar.story.tsx:5381": [ - [0, 0, 0, "\'VerticalGroup\' import from \'@grafana/ui\' is restricted from being used by a pattern. Use Stack component instead.", "0"] - ], "packages/grafana-ui/src/components/PanelChrome/PanelContext.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] @@ -958,15 +955,9 @@ exports[`better eslint`] = { "packages/grafana-ui/src/components/Tags/Tag.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "packages/grafana-ui/src/components/TagsInput/TagsInput.story.tsx:5381": [ - [0, 0, 0, "\'VerticalGroup\' import from \'../Layout/Layout\' is restricted from being used by a pattern. Use Stack component instead.", "0"] - ], "packages/grafana-ui/src/components/Text/Text.story.tsx:5381": [ [0, 0, 0, "\'VerticalGroup\' import from \'../Layout/Layout\' is restricted from being used by a pattern. Use Stack component instead.", "0"] ], - "packages/grafana-ui/src/components/ThemeDemos/EmotionPerfTest.tsx:5381": [ - [0, 0, 0, "\'VerticalGroup\' import from \'../Layout/Layout\' is restricted from being used by a pattern. Use Stack component instead.", "0"] - ], "packages/grafana-ui/src/components/ValuePicker/ValuePicker.tsx:5381": [ [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"] ], @@ -6380,9 +6371,6 @@ exports[`no undocumented stories`] = { "packages/grafana-ui/src/components/ButtonCascader/ButtonCascader.story.tsx:5381": [ [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] ], - "packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.story.tsx:5381": [ - [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] - ], "packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.story.tsx:5381": [ [0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"] ], diff --git a/.drone.yml b/.drone.yml index 9f6ba848ad2..b6399fe0c64 100644 --- a/.drone.yml +++ b/.drone.yml @@ -879,7 +879,7 @@ services: - commands: - /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled environment: {} - image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-remove_id_from_grafana_config-d3826b4f8-WIP + image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP name: mimir_backend - environment: {} image: redis:6.2.11-alpine @@ -1327,7 +1327,7 @@ services: - commands: - /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled environment: {} - image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-remove_id_from_grafana_config-d3826b4f8-WIP + image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP name: mimir_backend - environment: {} image: redis:6.2.11-alpine @@ -2329,7 +2329,7 @@ services: - commands: - /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled environment: {} - image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-remove_id_from_grafana_config-d3826b4f8-WIP + image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP name: mimir_backend - environment: {} image: redis:6.2.11-alpine @@ -4127,7 +4127,7 @@ services: - commands: - /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled environment: {} - image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-remove_id_from_grafana_config-d3826b4f8-WIP + image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP name: mimir_backend - environment: {} image: redis:6.2.11-alpine @@ -4646,7 +4646,7 @@ steps: - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM plugins/slack - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM python:3.8 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM postgres:12.3-alpine - - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM us.gcr.io/kubernetes-dev/mimir:santihernandezc-remove_id_from_grafana_config-d3826b4f8-WIP + - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM mysql:5.7.39 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM mysql:8.0.32 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM redis:6.2.11-alpine @@ -4681,7 +4681,7 @@ steps: - trivy --exit-code 1 --severity HIGH,CRITICAL plugins/slack - trivy --exit-code 1 --severity HIGH,CRITICAL python:3.8 - trivy --exit-code 1 --severity HIGH,CRITICAL postgres:12.3-alpine - - trivy --exit-code 1 --severity HIGH,CRITICAL us.gcr.io/kubernetes-dev/mimir:santihernandezc-remove_id_from_grafana_config-d3826b4f8-WIP + - trivy --exit-code 1 --severity HIGH,CRITICAL us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP - trivy --exit-code 1 --severity HIGH,CRITICAL mysql:5.7.39 - trivy --exit-code 1 --severity HIGH,CRITICAL mysql:8.0.32 - trivy --exit-code 1 --severity HIGH,CRITICAL redis:6.2.11-alpine @@ -4925,6 +4925,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 958ed40ca0620498c01fa867b05a1d6c3bcd908067fbb34be691923e95cfc77b +hmac: e67367689de11270bb3139f25a7cabf54e2b980460fd55ad410c590722d122b8 ... diff --git a/devenv/docker/blocks/mimir_backend/docker-compose.yaml b/devenv/docker/blocks/mimir_backend/docker-compose.yaml index 2a9c2dbd98e..76a19755d9f 100644 --- a/devenv/docker/blocks/mimir_backend/docker-compose.yaml +++ b/devenv/docker/blocks/mimir_backend/docker-compose.yaml @@ -1,5 +1,5 @@ mimir_backend: - image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-remove_id_from_grafana_config-d3826b4f8-WIP + image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP container_name: mimir_backend command: - -target=backend 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 16f94bbdef4..9e04b40cf6a 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -131,7 +131,6 @@ Experimental features might be changed or removed without prior notice. | `extraThemes` | Enables extra themes | | `lokiPredefinedOperations` | Adds predefined query operations to Loki query editor | | `pluginsFrontendSandbox` | Enables the plugins frontend sandbox | -| `dashboardEmbed` | Allow embedding dashboard for external use in Code editors | | `frontendSandboxMonitorOnly` | Enables monitor only in the plugin frontend sandbox (if enabled) | | `lokiFormatQuery` | Enables the ability to format Loki queries | | `vizAndWidgetSplit` | Split panels between visualizations and widgets | @@ -150,6 +149,9 @@ Experimental features might be changed or removed without prior notice. | `idForwarding` | Generate signed id token for identity that can be forwarded to plugins and external services | | `enableNativeHTTPHistogram` | Enables native HTTP Histograms | | `kubernetesSnapshots` | Routes snapshot requests from /api to the /apis endpoint | +| `queryService` | Register /apis/query.grafana.app/ -- will eventually replace /api/ds/query | +| `queryServiceRewrite` | Rewrite requests targeting /ds/query to the query service | +| `queryServiceFromUI` | Routes requests to the new query service | | `cachingOptimizeSerializationMemoryUsage` | If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses. | | `prometheusPromQAIL` | Prometheus and AI/ML to assist users in creating a query | | `prometheusCodeModeMetricNamesSearch` | Enables search for metric names in Code Mode, to improve performance when working with an enormous number of metric names | @@ -184,5 +186,4 @@ The following toggles require explicitly setting Grafana's [app mode]({{< relref | `unifiedStorage` | SQL-based k8s storage | | `grafanaAPIServerWithExperimentalAPIs` | Register experimental APIs with the k8s API server | | `grafanaAPIServerEnsureKubectlAccess` | Start an additional https handler and write kubectl options | -| `kubernetesQueryServiceRewrite` | Rewrite requests targeting /ds/query to the query service | | `panelTitleSearchInV1` | Enable searching for dashboards using panel title in search v1 | diff --git a/package.json b/package.json index 4c9bc730654..eeb40a72e9d 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "@emotion/eslint-plugin": "11.11.0", "@grafana/eslint-config": "7.0.0", "@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules", - "@grafana/plugin-e2e": "1.1.0", + "@grafana/plugin-e2e": "1.1.1", "@grafana/tsconfig": "^1.3.0-rc1", "@manypkg/get-packages": "^2.2.0", "@playwright/test": "1.43.1", @@ -100,7 +100,7 @@ "@types/d3-scale-chromatic": "3.0.3", "@types/debounce-promise": "3.1.9", "@types/diff": "^5", - "@types/eslint": "8.56.9", + "@types/eslint": "8.56.10", "@types/eslint-scope": "^3.7.7", "@types/file-saver": "2.0.7", "@types/glob": "^8.0.0", @@ -122,7 +122,7 @@ "@types/papaparse": "5.3.14", "@types/pluralize": "^0.0.33", "@types/prismjs": "1.26.3", - "@types/react": "18.2.78", + "@types/react": "18.2.79", "@types/react-beautiful-dnd": "13.1.8", "@types/react-dom": "18.2.25", "@types/react-grid-layout": "1.3.5", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index b720fb5e97b..fa03bb147f9 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -68,7 +68,7 @@ "@types/lodash": "4.17.0", "@types/node": "20.12.7", "@types/papaparse": "5.3.14", - "@types/react": "18.2.78", + "@types/react": "18.2.79", "@types/react-dom": "18.2.25", "@types/tinycolor2": "1.4.6", "esbuild": "0.18.12", diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index ee22f554a83..63ff19f663e 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -81,7 +81,6 @@ export interface FeatureToggles { extraThemes?: boolean; lokiPredefinedOperations?: boolean; pluginsFrontendSandbox?: boolean; - dashboardEmbed?: boolean; frontendSandboxMonitorOnly?: boolean; sqlDatasourceDatabaseSelection?: boolean; lokiFormatQuery?: boolean; @@ -121,7 +120,9 @@ export interface FeatureToggles { transformationsVariableSupport?: boolean; kubernetesPlaylists?: boolean; kubernetesSnapshots?: boolean; - kubernetesQueryServiceRewrite?: boolean; + queryService?: boolean; + queryServiceRewrite?: boolean; + queryServiceFromUI?: boolean; cloudWatchBatchQueries?: boolean; recoveryThreshold?: boolean; lokiStructuredMetadata?: boolean; diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index 93ba6b7cdd1..aec4c058d74 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -67,7 +67,7 @@ "@types/d3": "^7", "@types/jest": "^29.5.4", "@types/lodash": "4.17.0", - "@types/react": "18.2.78", + "@types/react": "18.2.79", "@types/react-virtualized-auto-sizer": "1.0.4", "@types/tinycolor2": "1.4.6", "babel-jest": "29.7.0", diff --git a/packages/grafana-icons/package.json b/packages/grafana-icons/package.json index b63c9866b9b..f2a8dcee0f3 100644 --- a/packages/grafana-icons/package.json +++ b/packages/grafana-icons/package.json @@ -43,7 +43,7 @@ "@svgr/plugin-prettier": "^8.1.0", "@svgr/plugin-svgo": "^8.1.0", "@types/node": "20.12.7", - "@types/react": "18.2.78", + "@types/react": "18.2.79", "@types/react-dom": "18.2.25", "esbuild": "0.18.12", "prettier": "3.2.5", diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index 82bf0ac3aec..24e31cb00fe 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -34,7 +34,7 @@ "@testing-library/react": "15.0.2", "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", - "@types/react": "18.2.78", + "@types/react": "18.2.79", "@types/systemjs": "6.13.5", "@types/testing-library__jest-dom": "5.14.9", "jest": "^29.6.4", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 004fcd9764d..5c360add184 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -47,8 +47,8 @@ "@leeoniya/ufuzzy": "1.0.14", "@lezer/common": "1.2.1", "@lezer/highlight": "1.2.0", - "@lezer/lr": "1.3.3", - "@prometheus-io/lezer-promql": "^0.37.0-rc.1", + "@lezer/lr": "1.4.0", + "@prometheus-io/lezer-promql": "0.51.2", "@reduxjs/toolkit": "1.9.5", "d3": "7.9.0", "date-fns": "3.6.0", @@ -88,14 +88,14 @@ "@testing-library/user-event": "14.5.2", "@types/d3": "7.4.3", "@types/debounce-promise": "3.1.9", - "@types/eslint": "8.56.9", + "@types/eslint": "8.56.10", "@types/jest": "29.5.12", "@types/jquery": "3.5.29", "@types/lodash": "4.17.0", "@types/node": "20.12.7", "@types/pluralize": "^0.0.33", "@types/prismjs": "1.26.3", - "@types/react": "18.2.78", + "@types/react": "18.2.79", "@types/react-beautiful-dnd": "13.1.8", "@types/react-dom": "18.2.25", "@types/react-highlight-words": "0.16.7", diff --git a/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/situation.test.ts b/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/situation.test.ts index 605d9658174..188eb333415 100644 --- a/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/situation.test.ts +++ b/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/situation.test.ts @@ -183,4 +183,17 @@ describe('situation', () => { ], }); }); + + it('identifies all labels from queries when cursor is in middle', () => { + // Note the extra whitespace, if the cursor is after whitespace, the situation will fail to resolve + assertSituation('{one="val1", ^,two!="val2",three=~"val3",four!~"val4"}', { + type: 'IN_LABEL_SELECTOR_NO_LABEL_NAME', + otherLabels: [ + { name: 'one', value: 'val1', op: '=' }, + { name: 'two', value: 'val2', op: '!=' }, + { name: 'three', value: 'val3', op: '=~' }, + { name: 'four', value: 'val4', op: '!~' }, + ], + }); + }); }); diff --git a/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/situation.ts b/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/situation.ts index f633922d298..decb3c2c9ac 100644 --- a/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/situation.ts +++ b/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/situation.ts @@ -3,6 +3,7 @@ import type { SyntaxNode, Tree } from '@lezer/common'; import { AggregateExpr, AggregateModifier, + BinaryExpr, EqlRegex, EqlSingle, FunctionCallBody, @@ -10,11 +11,9 @@ import { Identifier, LabelMatcher, LabelMatchers, - LabelMatchList, LabelName, MatchOp, MatrixSelector, - MetricIdentifier, Neq, NeqRegex, parser, @@ -36,9 +35,7 @@ type NodeTypeId = | typeof Identifier | typeof LabelMatcher | typeof LabelMatchers - | typeof LabelMatchList | typeof LabelName - | typeof MetricIdentifier | typeof PromQL | typeof StringLiteral | typeof VectorSelector @@ -184,6 +181,10 @@ const RESOLVERS: Resolver[] = [ path: [StringLiteral, LabelMatcher], fun: resolveLabelMatcher, }, + { + path: [ERROR_NODE_NAME, BinaryExpr, PromQL], + fun: resolveTopLevel, + }, { path: [ERROR_NODE_NAME, LabelMatcher], fun: resolveLabelMatcher, @@ -252,30 +253,8 @@ function getLabels(labelMatchersNode: SyntaxNode, text: string): Label[] { return []; } - let listNode: SyntaxNode | null = walk(labelMatchersNode, [['firstChild', LabelMatchList]]); - - const labels: Label[] = []; - - while (listNode !== null) { - const matcherNode = walk(listNode, [['lastChild', LabelMatcher]]); - if (matcherNode === null) { - // unexpected, we stop - return []; - } - - const label = getLabel(matcherNode, text); - if (label !== null) { - labels.push(label); - } - - // there might be more labels - listNode = walk(listNode, [['firstChild', LabelMatchList]]); - } - - // our labels-list is last-first, so we reverse it - labels.reverse(); - - return labels; + const labelNodes = labelMatchersNode.getChildren(LabelMatcher); + return labelNodes.map((ln) => getLabel(ln, text)).filter(notEmpty); } function getNodeChildren(node: SyntaxNode): SyntaxNode[] { @@ -319,17 +298,12 @@ function resolveLabelsForGrouping(node: SyntaxNode, text: string, pos: number): return null; } - const metricIdNode = getNodeInSubtree(bodyNode, MetricIdentifier); + const metricIdNode = getNodeInSubtree(bodyNode, Identifier); if (metricIdNode === null) { return null; } - const idNode = walk(metricIdNode, [['firstChild', Identifier]]); - if (idNode === null) { - return null; - } - - const metricName = getNodeText(idNode, text); + const metricName = getNodeText(metricIdNode, text); return { type: 'IN_GROUPING', metricName, @@ -355,44 +329,11 @@ function resolveLabelMatcher(node: SyntaxNode, text: string, pos: number): Situa const labelName = getNodeText(labelNameNode, text); - // now we need to go up, to the parent of LabelMatcher, - // there can be one or many `LabelMatchList` parents, we have - // to go through all of them - - const firstListNode = walk(parent, [['parent', LabelMatchList]]); - if (firstListNode === null) { + const labelMatchersNode = walk(parent, [['parent', LabelMatchers]]); + if (labelMatchersNode === null) { return null; } - let listNode = firstListNode; - - // we keep going through the parent-nodes - // as long as they are LabelMatchList. - // as soon as we reawch LabelMatchers, we stop - let labelMatchersNode: SyntaxNode | null = null; - while (labelMatchersNode === null) { - const p = listNode.parent; - if (p === null) { - return null; - } - - const { id } = p.type; - - switch (id) { - case LabelMatchList: - //we keep looping - listNode = p; - continue; - case LabelMatchers: - // we reached the end, we can stop the loop - labelMatchersNode = p; - continue; - default: - // we reached some other node, we stop - return null; - } - } - // now we need to find the other names const allLabels = getLabels(labelMatchersNode, text); @@ -401,7 +342,6 @@ function resolveLabelMatcher(node: SyntaxNode, text: string, pos: number): Situa const metricNameNode = walk(labelMatchersNode, [ ['parent', VectorSelector], - ['firstChild', MetricIdentifier], ['firstChild', Identifier], ]); @@ -444,23 +384,10 @@ function resolveDurations(node: SyntaxNode, text: string, pos: number): Situatio }; } -function subTreeHasError(node: SyntaxNode): boolean { - return getNodeInSubtree(node, ERROR_NODE_NAME) !== null; -} - function resolveLabelKeysWithEquals(node: SyntaxNode, text: string, pos: number): Situation | null { - // for example `something{^}` - - // there are some false positives that can end up in this situation, that we want - // to eliminate: - // `something{a~^}` (if this subtree contains any error-node, we stop) - if (subTreeHasError(node)) { - return null; - } - // next false positive: // `something{a="1"^}` - const child = walk(node, [['firstChild', LabelMatchList]]); + const child = walk(node, [['firstChild', LabelMatcher]]); if (child !== null) { // means the label-matching part contains at least one label already. // @@ -477,7 +404,6 @@ function resolveLabelKeysWithEquals(node: SyntaxNode, text: string, pos: number) const metricNameNode = walk(node, [ ['parent', VectorSelector], - ['firstChild', MetricIdentifier], ['firstChild', Identifier], ]); @@ -533,12 +459,12 @@ export function getSituation(text: string, pos: number): Situation | null { }; } - /* - PromQL - Expr - VectorSelector - LabelMatchers - */ + /** + PromQL + Expr + VectorSelector + LabelMatchers + */ const tree = parser.parse(text); // if the tree contains error, it is very probable that @@ -546,7 +472,6 @@ export function getSituation(text: string, pos: number): Situation | null { // also, if there are errors, the node lezer finds us, // might not be the best node. // so first we check if there is an error-node at the cursor-position - // @ts-ignore const maybeErrorNode = getErrorNode(tree, pos); const cur = maybeErrorNode != null ? maybeErrorNode.cursor() : tree.cursorAt(pos); @@ -561,10 +486,13 @@ export function getSituation(text: string, pos: number): Situation | null { // i do not use a foreach because i want to stop as soon // as i find something if (isPathMatch(resolver.path, ids)) { - // @ts-ignore return resolver.fun(currentNode, text, pos); } } return null; } + +function notEmpty(value: TValue | null | undefined): value is TValue { + return value !== null && value !== undefined; +} diff --git a/packages/grafana-prometheus/src/querybuilder/parsing.test.ts b/packages/grafana-prometheus/src/querybuilder/parsing.test.ts index 1522aff295f..3bbb8d32aeb 100644 --- a/packages/grafana-prometheus/src/querybuilder/parsing.test.ts +++ b/packages/grafana-prometheus/src/querybuilder/parsing.test.ts @@ -12,6 +12,7 @@ describe('buildVisualQueryFromString', () => { }) ); }); + it('parses simple binary comparison', () => { expect(buildVisualQueryFromString('{app="aggregator"} == 11')).toEqual({ query: { @@ -56,6 +57,7 @@ describe('buildVisualQueryFromString', () => { errors: [], }); }); + it('parses simple query', () => { expect(buildVisualQueryFromString('counters_logins{app="frontend"}')).toEqual( noErrors({ @@ -87,6 +89,7 @@ describe('buildVisualQueryFromString', () => { ], }); }); + it('throws error when visual query parse with aggregation is ambiguous (scalar)', () => { expect(buildVisualQueryFromString('topk(5, 1 / 2)')).toMatchObject({ errors: [ @@ -98,6 +101,7 @@ describe('buildVisualQueryFromString', () => { ], }); }); + it('throws error when visual query parse with functionCall is ambiguous', () => { expect( buildVisualQueryFromString( @@ -113,6 +117,7 @@ describe('buildVisualQueryFromString', () => { ], }); }); + it('does not throw error when visual query parse is unambiguous', () => { expect( buildVisualQueryFromString('topk(5, node_arp_entries) / node_arp_entries{cluster="dev-eu-west-2"}') @@ -120,12 +125,14 @@ describe('buildVisualQueryFromString', () => { errors: [], }); }); + it('does not throw error when visual query parse is unambiguous (scalar)', () => { // Note this topk query with scalars is not valid in prometheus, but it does not currently throw an error during parse expect(buildVisualQueryFromString('topk(5, 1) / 2')).toMatchObject({ errors: [], }); }); + it('does not throw error when visual query parse is unambiguous, function call', () => { // Note this topk query with scalars is not valid in prometheus, but it does not currently throw an error during parse expect( @@ -291,8 +298,7 @@ describe('buildVisualQueryFromString', () => { }); }); - // enable in #85942 when updated lezer parser is merged - xit('parses a native histogram function correctly', () => { + it('parses a native histogram function correctly', () => { expect( buildVisualQueryFromString('histogram_count(rate(counters_logins{app="backend"}[$__rate_interval]))') ).toEqual({ @@ -306,7 +312,8 @@ describe('buildVisualQueryFromString', () => { params: ['$__rate_interval'], }, { - id: 'histogram_quantile', + id: 'histogram_count', + params: [], }, ], }, @@ -457,6 +464,12 @@ describe('buildVisualQueryFromString', () => { to: 27, parentType: 'VectorSelector', }, + { + text: ')', + from: 38, + to: 39, + parentType: 'PromQL', + }, ], query: { metric: '${func_var}', @@ -710,7 +723,7 @@ describe('buildVisualQueryFromString', () => { errors: [ { from: 6, - parentType: 'Expr', + parentType: 'BinaryExpr', text: '(bar + baz)', to: 17, }, diff --git a/packages/grafana-prometheus/src/querybuilder/parsing.ts b/packages/grafana-prometheus/src/querybuilder/parsing.ts index af31365c564..01fd244012e 100644 --- a/packages/grafana-prometheus/src/querybuilder/parsing.ts +++ b/packages/grafana-prometheus/src/querybuilder/parsing.ts @@ -5,22 +5,18 @@ import { AggregateModifier, AggregateOp, BinaryExpr, - BinModifiers, - Expr, + BoolModifier, FunctionCall, - FunctionCallArgs, FunctionCallBody, FunctionIdentifier, - GroupingLabel, - GroupingLabelList, GroupingLabels, + Identifier, LabelMatcher, LabelName, + MatchingModifierClause, MatchOp, - MetricIdentifier, NumberLiteral, On, - OnOrIgnoring, ParenExpr, parser, StringLiteral, @@ -102,6 +98,7 @@ interface Context { errors: ParsingError[]; } +// TODO find a better approach for grafana global variables function isValidPromQLMinusGrafanaGlobalVariables(expr: string) { const context: Context = { query: { @@ -142,7 +139,7 @@ export function handleExpression(expr: string, node: SyntaxNode, context: Contex const visQuery = context.query; switch (node.type.id) { - case MetricIdentifier: { + case Identifier: { // Expectation is that there is only one of those per query. visQuery.metric = getString(expr, node); break; @@ -183,8 +180,8 @@ export function handleExpression(expr: string, node: SyntaxNode, context: Contex default: { if (node.type.id === ParenExpr) { - // We don't support parenthesis in the query to group expressions. We just report error but go on with the - // parsing. + // We don't support parenthesis in the query to group expressions. + // We just report error but go on with the parsing. context.errors.push(makeError(expr, node)); } // Any other nodes we just ignore and go to its children. This should be fine as there are lots of wrapper @@ -200,8 +197,9 @@ export function handleExpression(expr: string, node: SyntaxNode, context: Contex } } +// TODO check if we still need this function isIntervalVariableError(node: SyntaxNode) { - return node.prevSibling?.type.id === Expr && node.prevSibling?.firstChild?.type.id === VectorSelector; + return node.prevSibling?.firstChild?.type.id === VectorSelector; } function getLabel(expr: string, node: SyntaxNode): QueryBuilderLabelFilter { @@ -229,7 +227,6 @@ function handleFunction(expr: string, node: SyntaxNode, context: Context) { const funcName = getString(expr, nameNode); const body = node.getChild(FunctionCallBody); - const callArgs = body!.getChild(FunctionCallArgs); const params = []; let interval = ''; @@ -249,13 +246,13 @@ function handleFunction(expr: string, node: SyntaxNode, context: Context) { // We unshift operations to keep the more natural order that we want to have in the visual query editor. visQuery.operations.unshift(op); - if (callArgs) { - if (getString(expr, callArgs) === interval + ']') { + if (body) { + if (getString(expr, body) === '([' + interval + '])') { // This is a special case where we have a function with a single argument and it is the interval. // This happens when you start adding operations in query builder and did not set a metric yet. return; } - updateFunctionArgs(expr, callArgs, context, op); + updateFunctionArgs(expr, body, context, op); } } @@ -284,25 +281,14 @@ function handleAggregation(expr: string, node: SyntaxNode, context: Context) { funcName = `__${funcName}_without`; } - labels.push(...getAllByType(expr, modifier, GroupingLabel)); + labels.push(...getAllByType(expr, modifier, LabelName)); } const body = node.getChild(FunctionCallBody); - const callArgs = body!.getChild(FunctionCallArgs); - const callArgsExprChild = callArgs?.getChild(Expr); - const binaryExpressionWithinAggregationArgs = callArgsExprChild?.getChild(BinaryExpr); - - if (binaryExpressionWithinAggregationArgs) { - context.errors.push({ - text: 'Query parsing is ambiguous.', - from: binaryExpressionWithinAggregationArgs.from, - to: binaryExpressionWithinAggregationArgs.to, - }); - } const op: QueryBuilderOperation = { id: funcName, params: [] }; visQuery.operations.unshift(op); - updateFunctionArgs(expr, callArgs, context, op); + updateFunctionArgs(expr, body, context, op); // We add labels after params in the visual query editor. op.params.push(...labels); } @@ -310,8 +296,7 @@ function handleAggregation(expr: string, node: SyntaxNode, context: Context) { /** * Handle (probably) all types of arguments that function or aggregation can have. * - * FunctionCallArgs are nested bit weirdly basically its [firstArg, ...rest] where rest is again FunctionCallArgs so - * we cannot just get all the children and iterate them as arguments we have to again recursively traverse through + * We cannot just get all the children and iterate them as arguments we have to again recursively traverse through * them. * * @param expr @@ -324,15 +309,16 @@ function updateFunctionArgs(expr: string, node: SyntaxNode | null, context: Cont return; } switch (node.type.id) { - // In case we have an expression we don't know what kind so we have to look at the child as it can be anything. - case Expr: - // FunctionCallArgs are nested bit weirdly as mentioned so we have to go one deeper in this case. - case FunctionCallArgs: { + case FunctionCallBody: { let child = node.firstChild; while (child) { - const callArgsExprChild = child.getChild(Expr); - const binaryExpressionWithinFunctionArgs = callArgsExprChild?.getChild(BinaryExpr); + let binaryExpressionWithinFunctionArgs: SyntaxNode | null; + if (child.type.id === BinaryExpr) { + binaryExpressionWithinFunctionArgs = child; + } else { + binaryExpressionWithinFunctionArgs = child.getChild(BinaryExpr); + } if (binaryExpressionWithinFunctionArgs) { context.errors.push({ @@ -345,7 +331,6 @@ function updateFunctionArgs(expr: string, node: SyntaxNode | null, context: Cont updateFunctionArgs(expr, child, context, op); child = child.nextSibling; } - break; } @@ -378,16 +363,16 @@ function handleBinary(expr: string, node: SyntaxNode, context: Context) { const visQuery = context.query; const left = node.firstChild!; const op = getString(expr, left.nextSibling); - const binModifier = getBinaryModifier(expr, node.getChild(BinModifiers)); + const binModifier = getBinaryModifier(expr, node.getChild(BoolModifier) ?? node.getChild(MatchingModifierClause)); const right = node.lastChild!; const opDef = binaryScalarOperatorToOperatorName[op]; - const leftNumber = left.getChild(NumberLiteral); - const rightNumber = right.getChild(NumberLiteral); + const leftNumber = left.type.id === NumberLiteral; + const rightNumber = right.type.id === NumberLiteral; - const rightBinary = right.getChild(BinaryExpr); + const rightBinary = right.type.id === BinaryExpr; if (leftNumber) { // TODO: this should be already handled in case parent is binary expression as it has to be added to parent @@ -433,6 +418,7 @@ function handleBinary(expr: string, node: SyntaxNode, context: Context) { } } +// TODO revisit this function. function getBinaryModifier( expr: string, node: SyntaxNode | null @@ -446,17 +432,17 @@ function getBinaryModifier( if (node.getChild('Bool')) { return { isBool: true, isMatcher: false }; } else { - const matcher = node.getChild(OnOrIgnoring); - if (!matcher) { - // Not sure what this could be, maybe should be an error. - return undefined; + let labels = ''; + const groupingLabels = node.getChild(GroupingLabels); + if (groupingLabels) { + labels = getAllByType(expr, groupingLabels, LabelName).join(', '); } - const labels = getString(expr, matcher.getChild(GroupingLabels)?.getChild(GroupingLabelList)); + return { isMatcher: true, isBool: false, matches: labels, - matchType: matcher.getChild(On) ? 'on' : 'ignoring', + matchType: node.getChild(On) ? 'on' : 'ignoring', }; } } diff --git a/packages/grafana-prometheus/src/querybuilder/parsingUtils.ts b/packages/grafana-prometheus/src/querybuilder/parsingUtils.ts index bc19084122f..2b9cb162d93 100644 --- a/packages/grafana-prometheus/src/querybuilder/parsingUtils.ts +++ b/packages/grafana-prometheus/src/querybuilder/parsingUtils.ts @@ -114,11 +114,10 @@ export function makeBinOp( * not be safe is it would also find arguments of nested functions. * @param expr * @param cur - * @param type - can be string or number, some data-sources (loki) haven't migrated over to using numeric constants defined in the lezer parsing library (e.g. lezer-promql). - * @todo Remove string type definition when all data-sources have migrated to numeric constants + * @param type */ -export function getAllByType(expr: string, cur: SyntaxNode, type: number | string): string[] { - if (cur.type.id === type || cur.name === type) { +export function getAllByType(expr: string, cur: SyntaxNode, type: number): string[] { + if (cur.type.id === type) { return [getString(expr, cur)]; } const values: string[] = []; diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 9b341226b25..0b40a487e4a 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -58,7 +58,7 @@ "@types/history": "4.7.11", "@types/jest": "29.5.12", "@types/lodash": "4.17.0", - "@types/react": "18.2.78", + "@types/react": "18.2.79", "@types/react-dom": "18.2.25", "@types/systemjs": "6.13.5", "esbuild": "0.18.12", diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 624174f13d9..82fb397c7ab 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -214,9 +214,7 @@ export class GrafanaBootConfig implements GrafanaConfig { systemDateFormats.update(this.dateFormats); } - if (this.buildInfo.env === 'development') { - overrideFeatureTogglesFromUrl(this); - } + overrideFeatureTogglesFromUrl(this); overrideFeatureTogglesFromLocalStorage(this); if (this.featureToggles.disableAngular) { @@ -253,15 +251,11 @@ function overrideFeatureTogglesFromUrl(config: GrafanaBootConfig) { return; } - const migrationFeatureFlags = new Set([ - 'autoMigrateOldPanels', - 'autoMigrateGraphPanel', - 'autoMigrateTablePanel', - 'autoMigratePiechartPanel', - 'autoMigrateWorldmapPanel', - 'autoMigrateStatPanel', - 'disableAngular', - ]); + const isDevelopment = config.buildInfo.env === 'development'; + + // Although most flags can not be changed from the URL in production, + // some of them are safe (and useful!) to change dynamically from the browser URL + const safeRuntimeFeatureFlags = new Set(['queryServiceFromUI']); const params = new URLSearchParams(window.location.search); params.forEach((value, key) => { @@ -269,15 +263,14 @@ function overrideFeatureTogglesFromUrl(config: GrafanaBootConfig) { const featureToggles = config.featureToggles as Record; const featureName = key.substring(10); - // skip the migration feature flags - if (migrationFeatureFlags.has(featureName)) { - return; - } - const toggleState = value === 'true' || value === ''; // browser rewrites true as '' if (toggleState !== featureToggles[key]) { - featureToggles[featureName] = toggleState; - console.log(`Setting feature toggle ${featureName} = ${toggleState} via url`); + if (isDevelopment || safeRuntimeFeatureFlags.has(featureName)) { + featureToggles[featureName] = toggleState; + console.log(`Setting feature toggle ${featureName} = ${toggleState} via url`); + } else { + console.log(`Unable to change feature toggle ${featureName} via url in production.`); + } } } }); diff --git a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts index f867d1762cf..77deaa5c797 100644 --- a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts +++ b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts @@ -207,6 +207,18 @@ class DataSourceWithBackend< let url = '/api/ds/query?ds_type=' + this.type; + // Use the new query service + if (config.featureToggles.queryServiceFromUI) { + if (!(config.featureToggles.queryService || config.featureToggles.grafanaAPIServerWithExperimentalAPIs)) { + console.warn('feature toggle queryServiceFromUI also requires the queryService to be running'); + } else { + if (!hasExpr && dsUIDs.size === 1) { + // TODO? can we talk directly to the apiserver? + } + url = `/apis/query.grafana.app/v0alpha1/namespaces/${config.namespace}/query?ds_type=' + this.type`; + } + } + if (hasExpr) { headers[PluginRequestHeaders.FromExpression] = 'true'; url += '&expression=true'; diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index 39b1bfebc0d..749c8f356d5 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -39,7 +39,7 @@ "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", - "@types/react": "18.2.78", + "@types/react": "18.2.79", "@types/systemjs": "6.13.5", "@types/testing-library__jest-dom": "5.14.9", "jest": "^29.6.4", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 9e88abab438..eb2deed0421 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -141,7 +141,7 @@ "@types/mock-raf": "1.0.6", "@types/node": "20.12.7", "@types/prismjs": "1.26.3", - "@types/react": "18.2.78", + "@types/react": "18.2.79", "@types/react-beautiful-dnd": "13.1.8", "@types/react-color": "3.0.12", "@types/react-dom": "18.2.25", diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.mdx b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.mdx index 743d87f9042..2f7421f0876 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.mdx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.mdx @@ -1,5 +1,6 @@ import { Meta, ArgTypes } from '@storybook/blocks'; -import { ColorPicker } from './ColorPicker'; +import { ColorPicker, SeriesColorPicker } from './ColorPicker'; +import { ColorPickerInput } from './ColorPickerInput'; @@ -9,7 +10,7 @@ The `ColorPicker` component group consists of several building blocks that are c The `Popover` is a tabbed view where you can switch between `Palettes`. The `NamedColorsPalette` shows an arrangement of preset colors, while the `SpectrumPalette` is an unlimited HSB color picker. The preset colors are optimized to work well with both light and dark theme. `Popover` is triggered, for example, by the series legend of graphs, or by `Pickers`. -The `Pickers` are single circular color fields that show the currently picked color. On click, they open the `Popover`. +The `Pickers` are by default single circular color fields that show the currently picked color. On click, they open the `Popover`. ## ColorPickerInput @@ -17,4 +18,16 @@ Color picker component, modified to be used inside forms. Supports all usual inp The format in which the color is returned to the `onChange` callback can be customised via `returnColorAs` prop. +## Props + +### ColorPicker + + +### SeriesColorPicker + + + +### ColorPickerInput + + diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx index 81579ec60c4..ec2c5e8aa8f 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx @@ -3,18 +3,13 @@ import { useArgs } from '@storybook/client-api'; import { Meta, StoryFn } from '@storybook/react'; import React from 'react'; -import { SeriesColorPicker, ColorPicker, clearButtonStyles, useStyles2 } from '@grafana/ui'; +import { SeriesColorPicker, ColorPicker, clearButtonStyles, useStyles2, ColorPickerInput } from '@grafana/ui'; import mdx from './ColorPicker.mdx'; -import { ColorPickerInput } from './ColorPickerInput'; const meta: Meta = { title: 'Pickers and Editors/ColorPicker', component: ColorPicker, - // SB7 has broken subcomponent types due to dropping support for the feature - // https://github.com/storybookjs/storybook/issues/20782 - // @ts-ignore - subcomponents: { SeriesColorPicker, ColorPickerInput }, parameters: { docs: { page: mdx, @@ -47,15 +42,31 @@ export const Basic: StoryFn = ({ color, enableNamedColors }) }; export const SeriesPicker: StoryFn = ({ color, enableNamedColors }) => { + const [, updateArgs] = useArgs(); + return ( +
+ {}} + color={color} + onChange={(color) => { + action('Color changed')(color); + updateArgs({ color }); + }} + /> +
+ ); +}; + +export const CustomTrigger: StoryFn = ({ color, enableNamedColors }) => { const [, updateArgs] = useArgs(); const clearButton = useStyles2(clearButtonStyles); return ( - {}} color={color} - onChange={(color) => { + onChange={(color: string) => { action('Color changed')(color); updateArgs({ color }); }} @@ -72,7 +83,7 @@ export const SeriesPicker: StoryFn = ({ color, enableN Open color picker )} - + ); }; diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.story.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.story.tsx index 70aae18a5b1..2a2b6fbba99 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.story.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.story.tsx @@ -3,16 +3,18 @@ import React from 'react'; import { useTheme2 } from '../../themes'; +import mdx from './ColorPicker.mdx'; import { ColorPickerPopover } from './ColorPickerPopover'; import { SeriesColorPickerPopover } from './SeriesColorPickerPopover'; const meta: Meta = { title: 'Pickers and Editors/ColorPicker/Popovers', component: ColorPickerPopover, - // SB7 has broken subcomponent types due to dropping support for the feature - // https://github.com/storybookjs/storybook/issues/20782 - // @ts-ignore - subcomponents: { SeriesColorPickerPopover }, + parameters: { + docs: { + page: mdx, + }, + }, }; export const Basic = () => { @@ -30,11 +32,14 @@ export const Basic = () => { export const SeriesColorPickerPopoverExample = () => { const theme = useTheme2(); + const [yAxis, setYAxis] = React.useState(0); return (
(yAxis ? setYAxis(0) : setYAxis(2))} color="#BC67E6" onChange={(color: string) => { console.log(color); diff --git a/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.story.tsx b/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.story.tsx deleted file mode 100644 index 8f76f18b107..00000000000 --- a/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.story.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Meta, Story } from '@storybook/react'; -import React, { useState } from 'react'; - -import mdx from './ColorPicker.mdx'; -import { NamedColorsPalette, NamedColorsPaletteProps } from './NamedColorsPalette'; - -const meta: Meta = { - title: 'Pickers and Editors/ColorPicker/Palettes/NamedColorsPalette', - component: NamedColorsPalette, - parameters: { - docs: { - page: mdx, - }, - controls: { - exclude: ['theme', 'color'], - }, - }, - argTypes: { - selectedColor: { control: { type: 'select', options: ['green', 'red', 'light-blue', 'yellow'] } }, - }, -}; - -interface StoryProps extends Partial { - selectedColor: string; -} - -export const NamedColors: Story = ({ selectedColor }) => { - const [color, setColor] = useState('green'); - return ; -}; - -NamedColors.args = { - color: 'green', -}; - -export default meta; diff --git a/packages/grafana-ui/src/components/ColorPicker/Palettes.story.tsx b/packages/grafana-ui/src/components/ColorPicker/Palettes.story.tsx new file mode 100644 index 00000000000..0308d31783a --- /dev/null +++ b/packages/grafana-ui/src/components/ColorPicker/Palettes.story.tsx @@ -0,0 +1,43 @@ +import { action } from '@storybook/addon-actions'; +import { useArgs } from '@storybook/client-api'; +import { Meta, StoryFn } from '@storybook/react'; +import React, { useState } from 'react'; + +import mdx from './ColorPicker.mdx'; +import { NamedColorsPalette } from './NamedColorsPalette'; +import SpectrumPalette from './SpectrumPalette'; + +const meta: Meta = { + title: 'Pickers and Editors/ColorPicker/Palettes', + parameters: { + docs: { + page: mdx, + }, + controls: { + exclude: ['theme', 'color'], + }, + }, + args: { + color: 'green', + }, +}; + +export const NamedColors: StoryFn = ({ color }) => { + const [colorVal, setColor] = useState(color); + return ; +}; + +export const Spectrum: StoryFn = ({ color }) => { + const [, updateArgs] = useArgs(); + return ( + { + action('Color changed')(color); + updateArgs({ color }); + }} + /> + ); +}; + +export default meta; diff --git a/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx index d1ca21ad805..90f5e80f605 100644 --- a/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx @@ -1,8 +1,8 @@ -import { css } from '@emotion/css'; import React from 'react'; -import { withTheme2, useStyles2 } from '../../themes'; -import { Switch } from '../Forms/Legacy/Switch/Switch'; +import { withTheme2 } from '../../themes'; +import { InlineField } from '../Forms/InlineField'; +import { InlineSwitch } from '../Switch/Switch'; import { PopoverContentProps } from '../Tooltip'; import { ColorPickerPopover, ColorPickerProps } from './ColorPickerPopover'; @@ -13,7 +13,6 @@ export interface SeriesColorPickerPopoverProps extends ColorPickerProps, Popover } export const SeriesColorPickerPopover = (props: SeriesColorPickerPopoverProps) => { - const styles = useStyles2(getStyles); const { yaxis, onToggleAxis, color, ...colorPickerProps } = props; const customPickers = onToggleAxis @@ -22,18 +21,9 @@ export const SeriesColorPickerPopover = (props: SeriesColorPickerPopoverProps) = name: 'Y-Axis', tabComponent() { return ( - { - if (onToggleAxis) { - onToggleAxis(); - } - }} - /> + + + ); }, }, @@ -44,15 +34,3 @@ export const SeriesColorPickerPopover = (props: SeriesColorPickerPopoverProps) = // This component is to enable SeriesColorPickerPopover usage via series-color-picker-popover directive export const SeriesColorPickerPopoverWithTheme = withTheme2(SeriesColorPickerPopover); - -const getStyles = () => { - return { - colorPickerAxisSwitch: css({ - width: '100%', - }), - colorPickerAxisSwitchLabel: css({ - display: 'flex', - flexGrow: 1, - }), - }; -}; diff --git a/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.story.tsx b/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.story.tsx deleted file mode 100644 index ab03ae9076f..00000000000 --- a/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.story.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { action } from '@storybook/addon-actions'; -import { useArgs } from '@storybook/client-api'; -import { Meta, StoryFn } from '@storybook/react'; - -import { renderComponentWithTheme } from '../../utils/storybook/withTheme'; - -import mdx from './ColorPicker.mdx'; -import SpectrumPalette from './SpectrumPalette'; - -const meta: Meta = { - title: 'Pickers and Editors/ColorPicker/Palettes/SpectrumPalette', - component: SpectrumPalette, - parameters: { - docs: { - page: mdx, - }, - controls: { - exclude: ['onChange'], - }, - }, - args: { - color: 'red', - }, -}; - -export const Simple: StoryFn = ({ color }) => { - const [, updateArgs] = useArgs(); - return renderComponentWithTheme(SpectrumPalette, { - color, - onChange: (color: string) => { - action('Color changed')(color); - updateArgs({ color }); - }, - }); -}; - -export default meta; diff --git a/packages/grafana-ui/src/components/EmptyState/EmptyState.tsx b/packages/grafana-ui/src/components/EmptyState/EmptyState.tsx index 548a1a66477..28d7f67724e 100644 --- a/packages/grafana-ui/src/components/EmptyState/EmptyState.tsx +++ b/packages/grafana-ui/src/components/EmptyState/EmptyState.tsx @@ -1,6 +1,10 @@ +import { css } from '@emotion/css'; import React, { ReactNode } from 'react'; import SVG from 'react-inlinesvg'; +import { GrafanaTheme2 } from '@grafana/data'; + +import { useStyles2 } from '../../themes'; import { Box } from '../Layout/Box/Box'; import { Stack } from '../Layout/Stack/Stack'; import { Text } from '../Text/Text'; @@ -37,22 +41,25 @@ export const EmptyState = ({ hideImage = false, variant, }: React.PropsWithChildren) => { + const styles = useStyles2(getStyles); const imageToShow = image ?? getDefaultImageForVariant(variant); return ( - - {!hideImage && imageToShow} - - - {message} - - {children && ( - - {children} + +
+ {!hideImage && imageToShow} + + + {message} - )} - - {button} + {children && ( + + {children} + + )} + + {button} +
); }; @@ -73,3 +80,13 @@ function getDefaultImageForVariant(variant: Props['variant']) { } } } + +const getStyles = (theme: GrafanaTheme2) => ({ + container: css({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: theme.spacing(4), + maxWidth: '600px', + }), +}); diff --git a/packages/grafana-ui/src/components/PageLayout/PageToolbar.story.tsx b/packages/grafana-ui/src/components/PageLayout/PageToolbar.story.tsx index 0c736075d53..6e9638efdf5 100644 --- a/packages/grafana-ui/src/components/PageLayout/PageToolbar.story.tsx +++ b/packages/grafana-ui/src/components/PageLayout/PageToolbar.story.tsx @@ -2,7 +2,7 @@ import { action } from '@storybook/addon-actions'; import { Meta } from '@storybook/react'; import React from 'react'; -import { ToolbarButton, VerticalGroup } from '@grafana/ui'; +import { ToolbarButton, Stack } from '@grafana/ui'; import { StoryExample } from '../../utils/storybook/StoryExample'; import { IconButton } from '../IconButton/IconButton'; @@ -17,7 +17,7 @@ const meta: Meta = { export const Examples = () => { return ( - + @@ -50,7 +50,7 @@ export const Examples = () => { Apply - +
); }; diff --git a/packages/grafana-ui/src/components/TagsInput/TagsInput.story.tsx b/packages/grafana-ui/src/components/TagsInput/TagsInput.story.tsx index b30cc06ed17..b0d830469bd 100644 --- a/packages/grafana-ui/src/components/TagsInput/TagsInput.story.tsx +++ b/packages/grafana-ui/src/components/TagsInput/TagsInput.story.tsx @@ -2,7 +2,6 @@ import { Meta, StoryFn } from '@storybook/react'; import React, { useState } from 'react'; import { StoryExample } from '../../utils/storybook/StoryExample'; -import { VerticalGroup } from '../Layout/Layout'; import { TagsInput } from './TagsInput'; import mdx from './TagsInput.mdx'; @@ -28,11 +27,9 @@ export const Basic: StoryFn = (props) => { export const WithManyTags = () => { const [tags, setTags] = useState(['dashboard', 'prod', 'server', 'frontend', 'game', 'kubernetes']); return ( - - - - - + + + ); }; diff --git a/packages/grafana-ui/src/components/ThemeDemos/EmotionPerfTest.tsx b/packages/grafana-ui/src/components/ThemeDemos/EmotionPerfTest.tsx index 3399fff3fc0..df6a5b01ad9 100644 --- a/packages/grafana-ui/src/components/ThemeDemos/EmotionPerfTest.tsx +++ b/packages/grafana-ui/src/components/ThemeDemos/EmotionPerfTest.tsx @@ -9,13 +9,13 @@ import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2, useTheme2 } from '../../themes'; import { Button } from '../Button'; -import { VerticalGroup } from '../Layout/Layout'; +import { Stack } from '../Layout/Stack/Stack'; export function EmotionPerfTest() { console.log('process.env.NODE_ENV', process.env.NODE_ENV); return ( - +
Emotion performance tests
@@ -24,7 +24,7 @@ export function EmotionPerfTest() { -
+ ); } diff --git a/pkg/api/api.go b/pkg/api/api.go index 235af6462cb..337f26819d7 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -151,10 +151,6 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/dashboards/*", reqSignedIn, hs.Index) r.Get("/goto/:uid", reqSignedIn, hs.redirectFromShortURL, hs.Index) - if hs.Features.IsEnabledGlobally(featuremgmt.FlagDashboardEmbed) { - r.Get("/d-embed", reqSignedIn, middleware.AddAllowEmbeddingHeader(), hs.Index) - } - if hs.Features.IsEnabledGlobally(featuremgmt.FlagPublicDashboards) && hs.Cfg.PublicDashboardsEnabled { // list public dashboards r.Get("/public-dashboards/list", reqSignedIn, hs.Index) diff --git a/pkg/api/metrics.go b/pkg/api/ds_query.go similarity index 94% rename from pkg/api/metrics.go rename to pkg/api/ds_query.go index 010ca3cb8e2..bac9342383f 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/ds_query.go @@ -7,7 +7,6 @@ import ( "net/http" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/util/proxyutil" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" @@ -40,8 +39,7 @@ func (hs *HTTPServer) handleQueryMetricsError(err error) *response.NormalRespons // metrics.go func (hs *HTTPServer) getDSQueryEndpoint() web.Handler { - if hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesQueryServiceRewrite) { - // DEV ONLY FEATURE FLAG! + if hs.Features.IsEnabledGlobally(featuremgmt.FlagQueryServiceRewrite) { // rewrite requests from /ds/query to the new query service namespaceMapper := request.GetNamespaceMapper(hs.Cfg) return func(w http.ResponseWriter, r *http.Request) { @@ -51,11 +49,9 @@ func (hs *HTTPServer) getDSQueryEndpoint() web.Handler { return } r.URL.Path = "/apis/query.grafana.app/v0alpha1/namespaces/" + namespaceMapper(user.OrgID) + "/query" - r.Header.Add(proxyutil.IDHeaderName, user.GetIDToken()) hs.clientConfigProvider.DirectlyServeHTTP(w, r) } } - return routing.Wrap(hs.QueryMetricsV2) } diff --git a/pkg/api/metrics_test.go b/pkg/api/ds_query_test.go similarity index 100% rename from pkg/api/metrics_test.go rename to pkg/api/ds_query_test.go diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 074139c493e..586b885a5d5 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -66,14 +66,6 @@ func AddDefaultResponseHeaders(cfg *setting.Cfg) web.Handler { } } -func AddAllowEmbeddingHeader() web.Handler { - return func(c *web.Context) { - c.Resp.Before(func(w web.ResponseWriter) { - w.Header().Set("X-Allow-Embedding", "allow") - }) - } -} - // addSecurityHeaders adds HTTP(S) response headers that enable various security protections in the client's browser. func addSecurityHeaders(w web.ResponseWriter, cfg *setting.Cfg) { if cfg.StrictTransportSecurity { diff --git a/pkg/registry/apis/query/README.md b/pkg/registry/apis/query/README.md new file mode 100644 index 00000000000..e8d3ba64a7c --- /dev/null +++ b/pkg/registry/apis/query/README.md @@ -0,0 +1,78 @@ +# Query service + +This query service aims to replace the existing /api/ds/query. + +The key differences are: +1. This service has a stronger type system (not simplejson) +2. Same workflow regardless if expressions exist +3. Datasource settings+access is managed in each datasource, not at the beginning + + + +### Current /api/ds/query workflow + +```mermaid +sequenceDiagram + autonumber + actor User as User or Process + participant api as /api/ds/query + participant db as Storage
(SQL) + participant ds as Datasource
Plugin + participant expr as Expression
Engine + + User->>api: POST Query + loop Each query + api->>api: Parse query + api->>db: Get ds config
and secrets + db->>api: + end + alt No expressions + alt Single datasource + api->>ds: QueryData + else Multiple datasources + loop Each datasource (concurrently) + api->>ds: QueryData + end + api->>api: Wait for results + end + else Expressions exist + api->>expr: Calculate expressions graph + loop Each node (eg, refID) + alt Is query + expr->>ds: QueryData + else Is expression + expr->>expr: Process + end + end + end + api->>User: return results +``` + + + +### /apis/query.grafana.app (in single tenant grafana) + +```mermaid +sequenceDiagram + autonumber + actor User as User or Process + participant api as /apis/query.grafana.app + participant ds as Datasource
Handler/Plugin + participant db as Storage
(SQL) + participant expr as Expression
Engine + + User->>api: POST Query + api->>api: Parse queries + api->>api: Calculate dependencies + loop Each datasource (concurrently) + api->>ds: QueryData + ds->>ds: Verify user access + ds->>db: Get settings
and secrets + end + loop Each expression + api->>expr: Execute + end + api->>api: Verify ResultExpectations + api->>User: return results +``` + diff --git a/pkg/registry/apis/query/client/plugin.go b/pkg/registry/apis/query/client/plugin.go index a5354e35ecf..6e842f7ac45 100644 --- a/pkg/registry/apis/query/client/plugin.go +++ b/pkg/registry/apis/query/client/plugin.go @@ -40,7 +40,7 @@ type pluginRegistry struct { var _ data.QueryDataClient = (*pluginClient)(nil) var _ query.DataSourceApiServerRegistry = (*pluginRegistry)(nil) -// NewDummyTestRunner creates a runner that only works with testdata +// NewQueryClientForPluginClient creates a client that delegates to the internal plugins.Client stack func NewQueryClientForPluginClient(p plugins.Client, ctx *plugincontext.Provider) data.QueryDataClient { return &pluginClient{ pluginClient: p, diff --git a/pkg/registry/apis/query/plugins.go b/pkg/registry/apis/query/plugins.go index 1836602d4de..6c4a4861df9 100644 --- a/pkg/registry/apis/query/plugins.go +++ b/pkg/registry/apis/query/plugins.go @@ -9,7 +9,6 @@ import ( "k8s.io/apiserver/pkg/registry/rest" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" - example "github.com/grafana/grafana/pkg/apis/example/v0alpha1" query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" ) @@ -24,6 +23,9 @@ type pluginsStorage struct { resourceInfo *common.ResourceInfo tableConverter rest.TableConvertor registry query.DataSourceApiServerRegistry + + // Always return an empty list regardless what we think exists + returnEmptyList bool } func newPluginsStorage(reg query.DataSourceApiServerRegistry) *pluginsStorage { @@ -46,7 +48,7 @@ func (s *pluginsStorage) NamespaceScoped() bool { } func (s *pluginsStorage) GetSingularName() string { - return example.DummyResourceInfo.GetSingularName() + return s.resourceInfo.GetSingularName() } func (s *pluginsStorage) NewList() runtime.Object { @@ -58,5 +60,8 @@ func (s *pluginsStorage) ConvertToTable(ctx context.Context, object runtime.Obje } func (s *pluginsStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + if s.returnEmptyList { + return s.NewList(), nil + } return s.registry.GetDatasourceApiServers(ctx) } diff --git a/pkg/registry/apis/query/register.go b/pkg/registry/apis/query/register.go index 1b6460df3e8..a3892beec47 100644 --- a/pkg/registry/apis/query/register.go +++ b/pkg/registry/apis/query/register.go @@ -85,8 +85,9 @@ func RegisterAPIService(features featuremgmt.FeatureToggles, tracer tracing.Tracer, legacy service.LegacyDataSourceLookup, ) (*QueryAPIBuilder, error) { - if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { - return nil, nil // skip registration unless opting into experimental apis + if !(features.IsEnabledGlobally(featuremgmt.FlagQueryService) || + features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs)) { + return nil, nil // skip registration unless explicitly added (or all experimental are added) } builder, err := NewQueryAPIBuilder( @@ -132,10 +133,16 @@ func (b *QueryAPIBuilder) GetAPIGroupInfo( gv := v0alpha1.SchemeGroupVersion apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(gv.Group, scheme, metav1.ParameterCodec, codecs) - plugins := newPluginsStorage(b.registry) - storage := map[string]rest.Storage{} + + plugins := newPluginsStorage(b.registry) storage[plugins.resourceInfo.StoragePath()] = plugins + if !b.features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { + // The plugin registry is still experimental, and not yet accurate + // For standard k8s api discovery to work, at least one resource must be registered + // While this feature is under development, we can return an empty list for non-dev instances + plugins.returnEmptyList = true + } apiGroupInfo.VersionedResourcesStorageMap[gv.Version] = storage return &apiGroupInfo, nil diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 9ecc6f8cf26..31ff12fdaa2 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -471,13 +471,6 @@ var ( FrontendOnly: true, Owner: grafanaPluginsPlatformSquad, }, - { - Name: "dashboardEmbed", - Description: "Allow embedding dashboard for external use in Code editors", - FrontendOnly: true, - Stage: FeatureStageExperimental, - Owner: grafanaAsCodeSquad, - }, { Name: "frontendSandboxMonitorOnly", Description: "Enables monitor only in the plugin frontend sandbox (if enabled)", @@ -769,12 +762,25 @@ var ( RequiresRestart: true, // changes the API routing }, { - Name: "kubernetesQueryServiceRewrite", + Name: "queryService", + Description: "Register /apis/query.grafana.app/ -- will eventually replace /api/ds/query", + Stage: FeatureStageExperimental, + Owner: grafanaAppPlatformSquad, + RequiresRestart: true, // Adds a route at startup + }, + { + Name: "queryServiceRewrite", Description: "Rewrite requests targeting /ds/query to the query service", Stage: FeatureStageExperimental, Owner: grafanaAppPlatformSquad, RequiresRestart: true, // changes the API routing - RequiresDevMode: true, + }, + { + Name: "queryServiceFromUI", + Description: "Routes requests to the new query service", + Stage: FeatureStageExperimental, + Owner: grafanaAppPlatformSquad, + FrontendOnly: true, // and can change at startup }, { Name: "cloudWatchBatchQueries", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 6714b969fc1..6b6a95141b5 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -62,7 +62,6 @@ enableDatagridEditing,preview,@grafana/dataviz-squad,false,false,true extraThemes,experimental,@grafana/grafana-frontend-platform,false,false,true lokiPredefinedOperations,experimental,@grafana/observability-logs,false,false,true pluginsFrontendSandbox,experimental,@grafana/plugins-platform-backend,false,false,true -dashboardEmbed,experimental,@grafana/grafana-as-code,false,false,true frontendSandboxMonitorOnly,experimental,@grafana/plugins-platform-backend,false,false,true sqlDatasourceDatabaseSelection,preview,@grafana/dataviz-squad,false,false,true lokiFormatQuery,experimental,@grafana/observability-logs,false,false,true @@ -102,7 +101,9 @@ formatString,preview,@grafana/dataviz-squad,false,false,true transformationsVariableSupport,preview,@grafana/dataviz-squad,false,false,true kubernetesPlaylists,GA,@grafana/grafana-app-platform-squad,false,true,false kubernetesSnapshots,experimental,@grafana/grafana-app-platform-squad,false,true,false -kubernetesQueryServiceRewrite,experimental,@grafana/grafana-app-platform-squad,true,true,false +queryService,experimental,@grafana/grafana-app-platform-squad,false,true,false +queryServiceRewrite,experimental,@grafana/grafana-app-platform-squad,false,true,false +queryServiceFromUI,experimental,@grafana/grafana-app-platform-squad,false,false,true cloudWatchBatchQueries,preview,@grafana/aws-datasources,false,false,false recoveryThreshold,GA,@grafana/alerting-squad,false,true,false lokiStructuredMetadata,GA,@grafana/observability-logs,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 0b387c29b96..e63bf491bc6 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -259,10 +259,6 @@ const ( // Enables the plugins frontend sandbox FlagPluginsFrontendSandbox = "pluginsFrontendSandbox" - // FlagDashboardEmbed - // Allow embedding dashboard for external use in Code editors - FlagDashboardEmbed = "dashboardEmbed" - // FlagFrontendSandboxMonitorOnly // Enables monitor only in the plugin frontend sandbox (if enabled) FlagFrontendSandboxMonitorOnly = "frontendSandboxMonitorOnly" @@ -419,9 +415,17 @@ const ( // Routes snapshot requests from /api to the /apis endpoint FlagKubernetesSnapshots = "kubernetesSnapshots" - // FlagKubernetesQueryServiceRewrite + // FlagQueryService + // Register /apis/query.grafana.app/ -- will eventually replace /api/ds/query + FlagQueryService = "queryService" + + // FlagQueryServiceRewrite // Rewrite requests targeting /ds/query to the query service - FlagKubernetesQueryServiceRewrite = "kubernetesQueryServiceRewrite" + FlagQueryServiceRewrite = "queryServiceRewrite" + + // FlagQueryServiceFromUI + // Routes requests to the new query service + FlagQueryServiceFromUI = "queryServiceFromUI" // FlagCloudWatchBatchQueries // Runs CloudWatch metrics queries as separate batches diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 0f30ede4d7b..24304131755 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -278,15 +278,17 @@ }, { "metadata": { - "name": "kubernetesQueryServiceRewrite", - "resourceVersion": "1712639261786", - "creationTimestamp": "2024-04-09T05:07:41Z" + "name": "queryServiceRewrite", + "resourceVersion": "1713422970838", + "creationTimestamp": "2024-04-09T05:07:41Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-04-18 06:49:30.838977 +0000 UTC" + } }, "spec": { "description": "Rewrite requests targeting /ds/query to the query service", "stage": "experimental", "codeowner": "@grafana/grafana-app-platform-squad", - "requiresDevMode": true, "requiresRestart": true } }, @@ -1134,7 +1136,8 @@ "metadata": { "name": "dashboardEmbed", "resourceVersion": "1712639261786", - "creationTimestamp": "2024-04-09T05:07:41Z" + "creationTimestamp": "2024-04-09T05:07:41Z", + "deletionTimestamp": "2024-04-19T10:27:36Z" }, "spec": { "description": "Allow embedding dashboard for external use in Code editors", @@ -2134,6 +2137,35 @@ "codeowner": "@grafana/observability-metrics", "requiresRestart": true } + }, + { + "metadata": { + "name": "queryServiceFromUI", + "resourceVersion": "1713422970838", + "creationTimestamp": "2024-04-18T06:49:30Z" + }, + "spec": { + "description": "Routes requests to the new query service", + "stage": "experimental", + "codeowner": "@grafana/grafana-app-platform-squad", + "frontend": true + } + }, + { + "metadata": { + "name": "queryService", + "resourceVersion": "1713504737045", + "creationTimestamp": "2024-04-18T06:49:30Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-04-19 05:32:17.045343 +0000 UTC" + } + }, + "spec": { + "description": "Register /apis/query.grafana.app/ -- will eventually replace /api/ds/query", + "stage": "experimental", + "codeowner": "@grafana/grafana-app-platform-squad", + "requiresRestart": true + } } ] } \ No newline at end of file diff --git a/pkg/services/ngalert/api/api_prometheus.go b/pkg/services/ngalert/api/api_prometheus.go index 883dcf57724..a3161e9eb50 100644 --- a/pkg/services/ngalert/api/api_prometheus.go +++ b/pkg/services/ngalert/api/api_prometheus.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/http" + "net/url" "sort" "strconv" "strings" @@ -33,7 +34,33 @@ type PrometheusSrv struct { const queryIncludeInternalLabels = "includeInternalLabels" +func getBoolWithDefault(vals url.Values, field string, d bool) bool { + f := vals.Get(field) + if f == "" { + return d + } + + v, _ := strconv.ParseBool(f) + return v +} + +func getInt64WithDefault(vals url.Values, field string, d int64) int64 { + f := vals.Get(field) + if f == "" { + return d + } + + v, err := strconv.ParseInt(f, 10, 64) + if err != nil { + return d + } + return v +} + func (srv PrometheusSrv) RouteGetAlertStatuses(c *contextmodel.ReqContext) response.Response { + // As we are using req.Form directly, this triggers a call to ParseForm() if needed. + c.Query("") + alertResponse := apimodels.AlertResponse{ DiscoveryBase: apimodels.DiscoveryBase{ Status: "success", @@ -44,7 +71,7 @@ func (srv PrometheusSrv) RouteGetAlertStatuses(c *contextmodel.ReqContext) respo } var labelOptions []ngmodels.LabelOption - if !c.QueryBoolWithDefault(queryIncludeInternalLabels, false) { + if !getBoolWithDefault(c.Req.Form, queryIncludeInternalLabels, false) { labelOptions = append(labelOptions, ngmodels.WithoutInternalLabels()) } @@ -145,6 +172,9 @@ func getStatesFromRequest(r *http.Request) ([]eval.State, error) { } func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) response.Response { + // As we are using req.Form directly, this triggers a call to ParseForm() if needed. + c.Query("") + dashboardUID := c.Query("dashboard_uid") panelID, err := getPanelIDFromRequest(c.Req) if err != nil { @@ -154,9 +184,9 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon return ErrResp(http.StatusBadRequest, errors.New("panel_id must be set with dashboard_uid"), "") } - limitGroups := c.QueryInt64WithDefault("limit", -1) - limitRulesPerGroup := c.QueryInt64WithDefault("limit_rules", -1) - limitAlertsPerRule := c.QueryInt64WithDefault("limit_alerts", -1) + limitGroups := getInt64WithDefault(c.Req.Form, "limit", -1) + limitRulesPerGroup := getInt64WithDefault(c.Req.Form, "limit_rules", -1) + limitAlertsPerRule := getInt64WithDefault(c.Req.Form, "limit_alerts", -1) matchers, err := getMatchersFromRequest(c.Req) if err != nil { return ErrResp(http.StatusBadRequest, err, "") @@ -180,7 +210,7 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon } var labelOptions []ngmodels.LabelOption - if !c.QueryBoolWithDefault(queryIncludeInternalLabels, false) { + if !getBoolWithDefault(c.Req.Form, queryIncludeInternalLabels, false) { labelOptions = append(labelOptions, ngmodels.WithoutInternalLabels()) } @@ -408,10 +438,14 @@ func (srv PrometheusSrv) toRuleGroup(groupKey ngmodels.AlertRuleGroupKey, folder rulesTotals[newRule.Health] += 1 } - apimodels.AlertsBy(apimodels.AlertsByImportance).Sort(alertingRule.Alerts) + alertsBy := apimodels.AlertsBy(apimodels.AlertsByImportance) if limitAlerts > -1 && int64(len(alertingRule.Alerts)) > limitAlerts { - alertingRule.Alerts = alertingRule.Alerts[0:limitAlerts] + alertingRule.Alerts = alertsBy.TopK(alertingRule.Alerts, int(limitAlerts)) + } else { + // If there is no effective limit, then just sort the alerts. + // For large numbers of alerts, this can be faster. + alertsBy.Sort(alertingRule.Alerts) } alertingRule.Rule = newRule diff --git a/pkg/services/ngalert/api/tooling/definitions/prom.go b/pkg/services/ngalert/api/tooling/definitions/prom.go index 43ae592d329..c97d94d770d 100644 --- a/pkg/services/ngalert/api/tooling/definitions/prom.go +++ b/pkg/services/ngalert/api/tooling/definitions/prom.go @@ -1,6 +1,7 @@ package definitions import ( + "container/heap" "fmt" "sort" "strings" @@ -206,6 +207,71 @@ func (by AlertsBy) Sort(alerts []Alert) { sort.Sort(AlertsSorter{alerts: alerts, by: by}) } +// AlertsHeap extends AlertsSorter for use with container/heap functions. +type AlertsHeap struct { + AlertsSorter +} + +func (h *AlertsHeap) Push(x any) { + h.alerts = append(h.alerts, x.(Alert)) +} + +func (h *AlertsHeap) Pop() any { + old := h.alerts + n := len(old) + x := old[n-1] + h.alerts = old[0 : n-1] + return x +} + +// TopK returns the highest k elements. It does not modify the input. +func (by AlertsBy) TopK(alerts []Alert, k int) []Alert { + // Concept is that instead of sorting the whole list and taking the number + // of items we need, maintain a heap of the top k elements, and update it + // for each element. This vastly reduces the number of comparisons needed, + // which is important for sorting alerts, as the comparison function is + // very expensive. + + // The heap must be in ascending order, so that the root of the heap is + // the current smallest element. + byAscending := func(a1, a2 *Alert) bool { return by(a2, a1) } + + h := AlertsHeap{ + AlertsSorter: AlertsSorter{ + alerts: make([]Alert, 0, k), + by: byAscending, + }, + } + + // Go version of this algorithm taken from Prometheus (promql/engine.go) + + heap.Init(&h) + for i := 0; i < len(alerts); i++ { + a := alerts[i] + + // We build a heap of up to k elements, with the smallest element at heap[0]. + switch { + case len(h.alerts) < k: + heap.Push(&h, a) + + case h.by(&h.alerts[0], &a): + // This new element is bigger than the previous smallest element - overwrite that. + h.alerts[0] = a + // Maintain the heap invariant. + if k > 1 { + heap.Fix(&h, 0) + } + } + } + + // The heap keeps the lowest value on top, so reverse it. + if len(h.alerts) > 1 { + sort.Sort(sort.Reverse(&h)) + } + + return h.alerts +} + // AlertsByImportance orders alerts by importance. An alert is more important // than another alert if its status has higher importance. For example, "alerting" // is more important than "normal". If two alerts have the same importance diff --git a/pkg/services/ngalert/api/tooling/definitions/prom_bench.sh b/pkg/services/ngalert/api/tooling/definitions/prom_bench.sh new file mode 100755 index 00000000000..fadf371a29a --- /dev/null +++ b/pkg/services/ngalert/api/tooling/definitions/prom_bench.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +go test -v -run=^# -bench BenchmarkSortAlertsByImportance -count 5 -topk sort | tee before.txt +go test -v -run=^# -bench BenchmarkSortAlertsByImportance -count 5 -topk heap | tee after.txt +benchstat before.txt after.txt diff --git a/pkg/services/ngalert/api/tooling/definitions/prom_bench_test.go b/pkg/services/ngalert/api/tooling/definitions/prom_bench_test.go new file mode 100644 index 00000000000..64a051751cc --- /dev/null +++ b/pkg/services/ngalert/api/tooling/definitions/prom_bench_test.go @@ -0,0 +1,87 @@ +package definitions + +import ( + "flag" + "fmt" + "math/rand" + "testing" +) + +var topkStrategy = flag.String("topk", "heap", "topk strategy to benchmark. choices: sort, heap") +var showComparisons = flag.Bool("show-comparisons", false, "whether to show the number of comparisons made") + +func makeAlerts(amount int) []Alert { + // A typical distribution of alert states is that most are Normal + // and a few are Alerting, so we assume 99% Normal and 1% Alerting. + percentAlerting := 1 + + // Series will commonly have many labels. + numLabels := 10 + + alerts := make([]Alert, amount) + + for i := 0; i < len(alerts); i++ { + alerts[i].Labels = make(map[string]string) + for label := 0; label < numLabels; label++ { + alerts[i].Labels[fmt.Sprintf("label_%d", label)] = fmt.Sprintf("label_%d_value_%d", label, i%100) + } + + if i%100 < percentAlerting { + alerts[i].State = "alerting" + // Should populate ActiveAt because this prevents needing label comparison + } else { + alerts[i].State = "normal" + } + } + + // Shuffle in a repeatable order to avoid any bias from the initial ordering. + r := rand.New(rand.NewSource(1)) + r.Shuffle(len(alerts), func(i, j int) { alerts[i], alerts[j] = alerts[j], alerts[i] }) + + return alerts +} + +func BenchmarkSortAlertsByImportance(b *testing.B) { + var topkFunc func(AlertsBy, []Alert, int) + + switch *topkStrategy { + case "sort": + topkFunc = func(by AlertsBy, alerts []Alert, limit int) { + by.Sort(alerts) + if len(alerts) > limit { + _ = alerts[0:limit] + } + } + + case "heap": + topkFunc = func(by AlertsBy, alerts []Alert, limit int) { + _ = by.TopK(alerts, limit) + } + } + + for _, n := range []int{1000, 10000, 100000} { + for _, k := range []int{16, 100, 1000, 100000} { + b.Run(fmt.Sprintf("n_%d_k_%d", n, k), func(b *testing.B) { + b.StopTimer() + + for bi := 0; bi < b.N; bi++ { + alerts := makeAlerts(n) + + comparisons := 0 + by := func(a1, a2 *Alert) bool { + comparisons++ + return AlertsByImportance(a1, a2) + } + + b.StartTimer() + topkFunc(by, alerts, k) + b.StopTimer() + + if *showComparisons { + fmt.Printf("Number of comparisons (strategy: %s): %d\n", *topkStrategy, comparisons) + } + } + }) + } + } +} diff --git a/pkg/services/ngalert/api/tooling/definitions/prom_test.go b/pkg/services/ngalert/api/tooling/definitions/prom_test.go index 107eedcb5c2..5546cd374f1 100644 --- a/pkg/services/ngalert/api/tooling/definitions/prom_test.go +++ b/pkg/services/ngalert/api/tooling/definitions/prom_test.go @@ -64,3 +64,51 @@ func TestSortAlertsByImportance(t *testing.T) { }) } } + +func TestTopKAlertsByImportance(t *testing.T) { + // tm1, tm2 := time.Now(), time.Now().Add(time.Second) + tc := []struct { + name string + k int + input []Alert + expected []Alert + }{{ + name: "alerts are ordered in expected importance (k=1)", + k: 1, + input: []Alert{{State: "normal"}, {State: "nodata"}, {State: "error"}, {State: "pending"}, {State: "alerting"}}, + expected: []Alert{{State: "alerting"}}, + }, { + name: "alerts are ordered in expected importance (k=2)", + k: 2, + input: []Alert{{State: "normal"}, {State: "nodata"}, {State: "error"}, {State: "pending"}, {State: "alerting"}}, + expected: []Alert{{State: "alerting"}, {State: "pending"}}, + }, { + name: "alerts are ordered in expected importance (k=3)", + k: 3, + input: []Alert{{State: "normal"}, {State: "nodata"}, {State: "error"}, {State: "pending"}, {State: "alerting"}}, + expected: []Alert{{State: "alerting"}, {State: "pending"}, {State: "error"}}, + }, { + name: "alerts are ordered in expected importance (k=4)", + k: 4, + input: []Alert{{State: "normal"}, {State: "nodata"}, {State: "error"}, {State: "pending"}, {State: "alerting"}}, + expected: []Alert{{State: "alerting"}, {State: "pending"}, {State: "error"}, {State: "nodata"}}, + }, { + name: "alerts are ordered in expected importance (k=5)", + k: 5, + input: []Alert{{State: "normal"}, {State: "nodata"}, {State: "error"}, {State: "pending"}, {State: "alerting"}}, + expected: []Alert{{State: "alerting"}, {State: "pending"}, {State: "error"}, {State: "nodata"}, {State: "normal"}}, + }, { + name: "alerts are ordered in expected importance (k=6)", + k: 6, + input: []Alert{{State: "normal"}, {State: "nodata"}, {State: "error"}, {State: "pending"}, {State: "alerting"}}, + expected: []Alert{{State: "alerting"}, {State: "pending"}, {State: "error"}, {State: "nodata"}, {State: "normal"}}, + }, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + result := AlertsBy(AlertsByImportance).TopK(tt.input, tt.k) + assert.EqualValues(t, tt.expected, result) + }) + } +} diff --git a/pkg/services/ngalert/remote/alertmanager.go b/pkg/services/ngalert/remote/alertmanager.go index 66a78e6747b..322e9d4e4ce 100644 --- a/pkg/services/ngalert/remote/alertmanager.go +++ b/pkg/services/ngalert/remote/alertmanager.go @@ -166,7 +166,6 @@ func (am *Alertmanager) ApplyConfig(ctx context.Context, config *models.AlertCon am.log.Error("Unable to upload the state to the remote Alertmanager", "err", err) } am.log.Debug("Completed state upload to remote Alertmanager", "url", am.url) - return nil } @@ -202,20 +201,16 @@ func (am *Alertmanager) CompareAndSendConfiguration(ctx context.Context, config if err != nil { return err } - rawDecrypted, err := json.Marshal(decrypted) - if err != nil { - return err - } // Send the configuration only if we need to. - if !am.shouldSendConfig(ctx, rawDecrypted) { + if !am.shouldSendConfig(ctx, &decrypted) { return nil } am.metrics.ConfigSyncsTotal.Inc() if err := am.mimirClient.CreateGrafanaAlertmanagerConfig( ctx, - string(rawDecrypted), + &decrypted, config.ConfigurationHash, config.CreatedAt, config.Default, @@ -447,15 +442,25 @@ func (am *Alertmanager) getFullState(ctx context.Context) (string, error) { // shouldSendConfig compares the remote Alertmanager configuration with our local one. // It returns true if the configurations are different. -func (am *Alertmanager) shouldSendConfig(ctx context.Context, rawConfig []byte) bool { +func (am *Alertmanager) shouldSendConfig(ctx context.Context, config *apimodels.PostableUserConfig) bool { rc, err := am.mimirClient.GetGrafanaAlertmanagerConfig(ctx) if err != nil { // Log the error and return true so we try to upload our config anyway. - am.log.Error("Unable to get the remote Alertmanager Configuration for comparison", "err", err) + am.log.Error("Unable to get the remote Alertmanager configuration for comparison", "err", err) return true } - return md5.Sum([]byte(rc.GrafanaAlertmanagerConfig)) != md5.Sum(rawConfig) + rawRemote, err := json.Marshal(rc.GrafanaAlertmanagerConfig) + if err != nil { + am.log.Error("Unable to marshal the remote Alertmanager configuration for comparison", "err", err) + return true + } + rawInternal, err := json.Marshal(config) + if err != nil { + am.log.Error("Unable to marshal the internal Alertmanager configuration for comparison", "err", err) + return true + } + return md5.Sum(rawRemote) != md5.Sum(rawInternal) } // shouldSendState compares the remote Alertmanager state with our local one. diff --git a/pkg/services/ngalert/remote/alertmanager_test.go b/pkg/services/ngalert/remote/alertmanager_test.go index d11d8bf903b..947c270ac2d 100644 --- a/pkg/services/ngalert/remote/alertmanager_test.go +++ b/pkg/services/ngalert/remote/alertmanager_test.go @@ -118,7 +118,9 @@ func TestApplyConfig(t *testing.T) { if r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/config") { var c client.UserGrafanaConfig require.NoError(t, json.NewDecoder(r.Body).Decode(&c)) - configSent = c.GrafanaAlertmanagerConfig + amCfg, err := json.Marshal(c.GrafanaAlertmanagerConfig) + require.NoError(t, err) + configSent = string(amCfg) } w.WriteHeader(http.StatusOK) @@ -179,6 +181,8 @@ func TestApplyConfig(t *testing.T) { } func TestCompareAndSendConfiguration(t *testing.T) { + cfgWithSecret, err := notifier.Load([]byte(testGrafanaConfigWithSecret)) + require.NoError(t, err) testValue := []byte("test") testErr := errors.New("test error") decryptFn := func(_ context.Context, payload []byte) ([]byte, error) { @@ -243,7 +247,7 @@ func TestCompareAndSendConfiguration(t *testing.T) { "no error", strings.Replace(testGrafanaConfigWithSecret, `"password":"test"`, fmt.Sprintf("%q:%q", "password", base64.StdEncoding.EncodeToString(testValue)), 1), &client.UserGrafanaConfig{ - GrafanaAlertmanagerConfig: testGrafanaConfigWithSecret, + GrafanaAlertmanagerConfig: cfgWithSecret, }, "", }, @@ -335,7 +339,10 @@ func TestIntegrationRemoteAlertmanagerApplyConfigOnlyUploadsOnce(t *testing.T) { // Next, we need to verify that Mimir received both the configuration and state. config, err := am.mimirClient.GetGrafanaAlertmanagerConfig(ctx) require.NoError(t, err) - require.Equal(t, testGrafanaConfig, config.GrafanaAlertmanagerConfig) + + rawCfg, err := json.Marshal(config.GrafanaAlertmanagerConfig) + require.NoError(t, err) + require.JSONEq(t, testGrafanaConfig, string(rawCfg)) require.Equal(t, fakeConfigHash, config.Hash) require.Equal(t, fakeConfigCreatedAt, config.CreatedAt) require.Equal(t, true, config.Default) @@ -358,7 +365,10 @@ func TestIntegrationRemoteAlertmanagerApplyConfigOnlyUploadsOnce(t *testing.T) { // Next, we need to verify that the config that was uploaded remains the same. config, err := am.mimirClient.GetGrafanaAlertmanagerConfig(ctx) require.NoError(t, err) - require.Equal(t, testGrafanaConfig, config.GrafanaAlertmanagerConfig) + + rawCfg, err := json.Marshal(config.GrafanaAlertmanagerConfig) + require.NoError(t, err) + require.JSONEq(t, testGrafanaConfig, string(rawCfg)) require.Equal(t, fakeConfigHash, config.Hash) require.Equal(t, fakeConfigCreatedAt, config.CreatedAt) require.Equal(t, true, config.Default) diff --git a/pkg/services/ngalert/remote/client/alertmanager_configuration.go b/pkg/services/ngalert/remote/client/alertmanager_configuration.go index 6ddf6dfdaae..e0f19a8efe3 100644 --- a/pkg/services/ngalert/remote/client/alertmanager_configuration.go +++ b/pkg/services/ngalert/remote/client/alertmanager_configuration.go @@ -6,6 +6,8 @@ import ( "encoding/json" "fmt" "net/http" + + apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) const ( @@ -13,10 +15,10 @@ const ( ) type UserGrafanaConfig struct { - GrafanaAlertmanagerConfig string `json:"configuration"` - Hash string `json:"configuration_hash"` - CreatedAt int64 `json:"created"` - Default bool `json:"default"` + GrafanaAlertmanagerConfig *apimodels.PostableUserConfig `json:"configuration"` + Hash string `json:"configuration_hash"` + CreatedAt int64 `json:"created"` + Default bool `json:"default"` } func (mc *Mimir) GetGrafanaAlertmanagerConfig(ctx context.Context) (*UserGrafanaConfig, error) { @@ -38,7 +40,7 @@ func (mc *Mimir) GetGrafanaAlertmanagerConfig(ctx context.Context) (*UserGrafana return gc, nil } -func (mc *Mimir) CreateGrafanaAlertmanagerConfig(ctx context.Context, cfg, hash string, createdAt int64, isDefault bool) error { +func (mc *Mimir) CreateGrafanaAlertmanagerConfig(ctx context.Context, cfg *apimodels.PostableUserConfig, hash string, createdAt int64, isDefault bool) error { payload, err := json.Marshal(&UserGrafanaConfig{ GrafanaAlertmanagerConfig: cfg, Hash: hash, diff --git a/pkg/services/ngalert/remote/client/mimir.go b/pkg/services/ngalert/remote/client/mimir.go index b74d1df91fe..6db6147ab99 100644 --- a/pkg/services/ngalert/remote/client/mimir.go +++ b/pkg/services/ngalert/remote/client/mimir.go @@ -12,6 +12,7 @@ import ( "strings" "github.com/grafana/grafana/pkg/infra/log" + apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/client" "github.com/grafana/grafana/pkg/services/ngalert/metrics" ) @@ -23,7 +24,7 @@ type MimirClient interface { DeleteGrafanaAlertmanagerState(ctx context.Context) error GetGrafanaAlertmanagerConfig(ctx context.Context) (*UserGrafanaConfig, error) - CreateGrafanaAlertmanagerConfig(ctx context.Context, configuration, hash string, createdAt int64, isDefault bool) error + CreateGrafanaAlertmanagerConfig(ctx context.Context, configuration *apimodels.PostableUserConfig, hash string, createdAt int64, isDefault bool) error DeleteGrafanaAlertmanagerConfig(ctx context.Context) error } diff --git a/public/app/core/components/AppChrome/TopBar/TopSearchBarCommandPaletteTrigger.tsx b/public/app/core/components/AppChrome/TopBar/TopSearchBarCommandPaletteTrigger.tsx index 92ffcfc22d9..40d4e61e33d 100644 --- a/public/app/core/components/AppChrome/TopBar/TopSearchBarCommandPaletteTrigger.tsx +++ b/public/app/core/components/AppChrome/TopBar/TopSearchBarCommandPaletteTrigger.tsx @@ -3,7 +3,7 @@ import { useKBar, VisualState } from 'kbar'; import React, { useMemo, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { getInputStyles, Icon, ToolbarButton, useStyles2, useTheme2 } from '@grafana/ui'; +import { getInputStyles, Icon, Text, ToolbarButton, useStyles2, useTheme2 } from '@grafana/ui'; import { focusCss } from '@grafana/ui/src/themes/mixins'; import { useMediaQueryChange } from 'app/core/hooks/useMediaQueryChange'; import { t } from 'app/core/internationalization'; @@ -70,7 +70,7 @@ function PretendTextInput({ onClick }: PretendTextInputProps) {
- {modKey}+k + {modKey}+k
@@ -91,9 +91,6 @@ const getStyles = (theme: GrafanaTheme2) => { gap: theme.spacing(0.5), }, ]), - shortcut: css({ - fontSize: theme.typography.bodySmall.fontSize, - }), fakeInput: css([ baseStyles.input, { diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index e878af98383..c9132a9415e 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -1,8 +1,6 @@ import { locationUtil } from '@grafana/data'; import { config, getBackendSrv, isFetchError, locationService } from '@grafana/runtime'; -import { updateNavIndex } from 'app/core/actions'; import { StateManagerBase } from 'app/core/services/StateManagerBase'; -import { backendSrv } from 'app/core/services/backend_srv'; import { default as localStorageStore } from 'app/core/store'; import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; @@ -10,8 +8,6 @@ import { DASHBOARD_FROM_LS_KEY, removeDashboardToFetchFromLocalStorage, } from 'app/features/dashboard/state/initDashboard'; -import { buildNavModel } from 'app/features/folders/state/navModel'; -import { store } from 'app/store/store'; import { DashboardDTO, DashboardRoutes } from 'app/types'; import { PanelEditor } from '../panel-edit/PanelEditor'; @@ -19,6 +15,8 @@ import { DashboardScene } from '../scene/DashboardScene'; import { buildNewDashboardSaveModel } from '../serialization/buildNewDashboardSaveModel'; import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; +import { updateNavModel } from './utils'; + export interface DashboardScenePageState { dashboard?: DashboardScene; panelEditor?: PanelEditor; @@ -127,7 +125,9 @@ export class DashboardScenePageStateManager extends StateManagerBase { + it('Should update nav model', async () => { + const reduxStore = configureStore(); + + jest.spyOn(backendSrv, 'getFolderByUid').mockResolvedValue({ + id: 1, + uid: 'new-folder', + title: 'NewFolder', + url: '', + canAdmin: true, + canDelete: true, + canEdit: true, + canSave: true, + created: '', + createdBy: '', + hasAcl: false, + updated: '', + updatedBy: '', + }); + + expect(reduxStore.getState().navIndex[`folder-dashboards-new-folder`]).toBeUndefined(); + + await updateNavModel('new-folder'); + + expect(reduxStore.getState().navIndex[`folder-dashboards-new-folder`]).not.toBeUndefined(); + }); +}); diff --git a/public/app/features/dashboard-scene/pages/utils.ts b/public/app/features/dashboard-scene/pages/utils.ts new file mode 100644 index 00000000000..2e06429589f --- /dev/null +++ b/public/app/features/dashboard-scene/pages/utils.ts @@ -0,0 +1,13 @@ +import { updateNavIndex } from 'app/core/actions'; +import { backendSrv } from 'app/core/services/backend_srv'; +import { buildNavModel } from 'app/features/folders/state/navModel'; +import { store } from 'app/store/store'; + +export async function updateNavModel(folderUid: string) { + try { + const folder = await backendSrv.getFolderByUid(folderUid); + store.dispatch(updateNavIndex(buildNavModel(folder))); + } catch (err) { + console.warn('Error fetching parent folder', folderUid, 'for dashboard', err); + } +} diff --git a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.test.tsx b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.test.tsx index e1e55da7e44..e2f343c383c 100644 --- a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.test.tsx @@ -1,6 +1,7 @@ import { behaviors, SceneGridLayout, SceneTimeRange } from '@grafana/scenes'; import { DashboardCursorSync } from '@grafana/schema'; +import * as utils from '../pages/utils'; import { DashboardControls } from '../scene/DashboardControls'; import { DashboardScene } from '../scene/DashboardScene'; import { activateFullSceneTree } from '../utils/test-utils'; @@ -89,11 +90,14 @@ describe('GeneralSettingsEditView', () => { expect(settings.getRefreshPicker()?.state?.intervals).toEqual(['5s']); }); - it('A change to folder updates the dashboard state', () => { - settings.onFolderChange('folder-2', 'folder 2'); + it('A change to folder updates the dashboard state', async () => { + const updateNavModel = jest.spyOn(utils, 'updateNavModel').mockImplementation(jest.fn()); + + await settings.onFolderChange('folder-2', 'folder 2'); expect(dashboard.state.meta.folderUid).toBe('folder-2'); expect(dashboard.state.meta.folderTitle).toBe('folder 2'); + expect(updateNavModel).toHaveBeenCalledWith('folder-2'); }); it('A change to tooltip settings updates the dashboard state', () => { diff --git a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx index 8c787cda9cb..669dcc109d8 100644 --- a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx +++ b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx @@ -23,6 +23,7 @@ import { DeleteDashboardButton } from 'app/features/dashboard/components/DeleteD import { GenAIDashDescriptionButton } from 'app/features/dashboard/components/GenAI/GenAIDashDescriptionButton'; import { GenAIDashTitleButton } from 'app/features/dashboard/components/GenAI/GenAIDashTitleButton'; +import { updateNavModel } from '../pages/utils'; import { DashboardScene } from '../scene/DashboardScene'; import { NavToolbarActions } from '../scene/NavToolbarActions'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; @@ -96,13 +97,17 @@ export class GeneralSettingsEditView this._dashboard.setState({ tags: value }); }; - public onFolderChange = (newUID: string | undefined, newTitle: string | undefined) => { + public onFolderChange = async (newUID: string | undefined, newTitle: string | undefined) => { const newMeta = { ...this._dashboard.state.meta, folderUid: newUID || this._dashboard.state.meta.folderUid, folderTitle: newTitle || this._dashboard.state.meta.folderTitle, }; + if (newMeta.folderUid) { + await updateNavModel(newMeta.folderUid); + } + this._dashboard.setState({ meta: newMeta }); }; diff --git a/public/app/features/dashboard-scene/settings/variables/editors/IntervalVariableEditor.test.tsx b/public/app/features/dashboard-scene/settings/variables/editors/IntervalVariableEditor.test.tsx index 9e2e16c44ec..5e1ddd110e2 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/IntervalVariableEditor.test.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/IntervalVariableEditor.test.tsx @@ -46,6 +46,7 @@ describe('IntervalVariableEditor', () => { name: 'test', type: 'interval', intervals: ['1m', '10m', '1h', '6h', '1d', '7d'], + value: '10m', }); const onRunQuery = jest.fn(); @@ -61,6 +62,8 @@ describe('IntervalVariableEditor', () => { expect(intervalsInput).toBeInTheDocument(); expect(intervalsInput).toHaveValue('7d,30d, 1y, 5y, 10y'); + // If the value is not in the list, it should be set to the first value + expect(variable.state.value).toBe('7d'); expect(onRunQuery).toHaveBeenCalledTimes(1); }); diff --git a/public/app/features/dashboard-scene/settings/variables/editors/IntervalVariableEditor.tsx b/public/app/features/dashboard-scene/settings/variables/editors/IntervalVariableEditor.tsx index 6ff69c2b0c5..3ffc414d7f4 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/IntervalVariableEditor.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/IntervalVariableEditor.tsx @@ -15,14 +15,21 @@ interface IntervalVariableEditorProps { } export function IntervalVariableEditor({ variable, onRunQuery }: IntervalVariableEditorProps) { - const { intervals, autoStepCount, autoEnabled, autoMinInterval } = variable.useState(); + const { intervals, autoStepCount, autoEnabled, autoMinInterval, value } = variable.useState(); //transform intervals array into string const intervalsCombined = getIntervalsQueryFromNewIntervalModel(intervals); const onIntervalsChange = (event: FormEvent) => { - const intervalsArray = getIntervalsFromQueryString(event.currentTarget.value); - variable.setState({ intervals: intervalsArray }); + const newIntervals = getIntervalsFromQueryString(event.currentTarget.value); + // if the current value is not in the new intervals, set the value to the first interval + const newValue = newIntervals.includes(value) ? value : newIntervals[0]; + + variable.setState({ + intervals: newIntervals, + value: newValue, + }); + onRunQuery(); }; diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index d144ae17556..e663a544930 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -31,7 +31,7 @@ "@types/lodash": "4.17.0", "@types/node": "20.12.7", "@types/prismjs": "1.26.3", - "@types/react": "18.2.78", + "@types/react": "18.2.79", "@types/testing-library__jest-dom": "5.14.9", "react-select-event": "5.5.1", "ts-node": "10.9.2", diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index 0bac9dcda92..722924daac7 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -33,7 +33,7 @@ "@types/lodash": "4.17.0", "@types/node": "20.12.7", "@types/prismjs": "1.26.3", - "@types/react": "18.2.78", + "@types/react": "18.2.79", "@types/react-test-renderer": "18.0.7", "@types/testing-library__jest-dom": "5.14.9", "react-select-event": "5.5.1", diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json index 08737c4f2b3..daf6dae7647 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json @@ -27,7 +27,7 @@ "@types/jest": "29.5.12", "@types/lodash": "4.17.0", "@types/prismjs": "1.26.3", - "@types/react": "18.2.78", + "@types/react": "18.2.79", "@types/react-dom": "18.2.25", "@types/testing-library__jest-dom": "5.14.9", "css-loader": "6.10.0", diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index 2f5f5f4559d..30af532cc33 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -28,7 +28,7 @@ "@types/jest": "29.5.12", "@types/lodash": "4.17.0", "@types/node": "20.12.7", - "@types/react": "18.2.78", + "@types/react": "18.2.79", "@types/testing-library__jest-dom": "5.14.9", "@types/uuid": "9.0.8", "ts-node": "10.9.2", diff --git a/public/app/plugins/datasource/parca/package.json b/public/app/plugins/datasource/parca/package.json index 74f43a29441..6e9e05609f9 100644 --- a/public/app/plugins/datasource/parca/package.json +++ b/public/app/plugins/datasource/parca/package.json @@ -21,7 +21,7 @@ "@testing-library/react": "15.0.2", "@testing-library/user-event": "14.5.2", "@types/lodash": "4.17.0", - "@types/react": "18.2.78", + "@types/react": "18.2.79", "ts-node": "10.9.2", "webpack": "5.91.0" }, diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index 9b918623f88..b4893668436 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -46,7 +46,7 @@ "@types/lodash": "4.17.0", "@types/node": "20.12.7", "@types/prismjs": "1.26.3", - "@types/react": "18.2.78", + "@types/react": "18.2.79", "@types/react-dom": "18.2.25", "@types/semver": "7.5.8", "@types/uuid": "9.0.8", diff --git a/public/app/plugins/datasource/zipkin/package.json b/public/app/plugins/datasource/zipkin/package.json index 1b63941215e..784d41aebeb 100644 --- a/public/app/plugins/datasource/zipkin/package.json +++ b/public/app/plugins/datasource/zipkin/package.json @@ -22,7 +22,7 @@ "@testing-library/react": "15.0.2", "@types/jest": "29.5.12", "@types/lodash": "4.17.0", - "@types/react": "18.2.78", + "@types/react": "18.2.79", "ts-node": "10.9.2", "webpack": "5.91.0" }, diff --git a/scripts/drone/utils/images.star b/scripts/drone/utils/images.star index b8946c746d3..da76d8ce306 100644 --- a/scripts/drone/utils/images.star +++ b/scripts/drone/utils/images.star @@ -21,7 +21,7 @@ images = { "plugins_slack": "plugins/slack", "python": "python:3.8", "postgres_alpine": "postgres:12.3-alpine", - "mimir": "us.gcr.io/kubernetes-dev/mimir:santihernandezc-remove_id_from_grafana_config-d3826b4f8-WIP", + "mimir": "us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP", "mysql5": "mysql:5.7.39", "mysql8": "mysql:8.0.32", "redis_alpine": "redis:6.2.11-alpine", diff --git a/yarn.lock b/yarn.lock index 39e99715e8d..16df3691986 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3317,7 +3317,7 @@ __metadata: "@types/lodash": "npm:4.17.0" "@types/node": "npm:20.12.7" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.78" + "@types/react": "npm:18.2.79" "@types/testing-library__jest-dom": "npm:5.14.9" fast-deep-equal: "npm:^3.1.3" i18next: "npm:^23.0.0" @@ -3354,7 +3354,7 @@ __metadata: "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.17.0" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.78" + "@types/react": "npm:18.2.79" "@types/react-dom": "npm:18.2.25" "@types/testing-library__jest-dom": "npm:5.14.9" css-loader: "npm:6.10.0" @@ -3395,7 +3395,7 @@ __metadata: "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.17.0" "@types/node": "npm:20.12.7" - "@types/react": "npm:18.2.78" + "@types/react": "npm:18.2.79" "@types/testing-library__jest-dom": "npm:5.14.9" "@types/uuid": "npm:9.0.8" d3-random: "npm:^3.0.1" @@ -3459,7 +3459,7 @@ __metadata: "@testing-library/react": "npm:15.0.2" "@testing-library/user-event": "npm:14.5.2" "@types/lodash": "npm:4.17.0" - "@types/react": "npm:18.2.78" + "@types/react": "npm:18.2.79" lodash: "npm:4.17.21" monaco-editor: "npm:0.34.1" react: "npm:18.2.0" @@ -3493,7 +3493,7 @@ __metadata: "@types/lodash": "npm:4.17.0" "@types/node": "npm:20.12.7" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.78" + "@types/react": "npm:18.2.79" "@types/react-test-renderer": "npm:18.0.7" "@types/testing-library__jest-dom": "npm:5.14.9" debounce-promise: "npm:3.1.2" @@ -3545,7 +3545,7 @@ __metadata: "@types/lodash": "npm:4.17.0" "@types/node": "npm:20.12.7" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.78" + "@types/react": "npm:18.2.79" "@types/react-dom": "npm:18.2.25" "@types/semver": "npm:7.5.8" "@types/uuid": "npm:9.0.8" @@ -3590,7 +3590,7 @@ __metadata: "@testing-library/react": "npm:15.0.2" "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.17.0" - "@types/react": "npm:18.2.78" + "@types/react": "npm:18.2.79" lodash: "npm:4.17.21" react: "npm:18.2.0" react-use: "npm:17.5.0" @@ -3636,7 +3636,7 @@ __metadata: "@types/lodash": "npm:4.17.0" "@types/node": "npm:20.12.7" "@types/papaparse": "npm:5.3.14" - "@types/react": "npm:18.2.78" + "@types/react": "npm:18.2.79" "@types/react-dom": "npm:18.2.25" "@types/string-hash": "npm:1.1.3" "@types/tinycolor2": "npm:1.4.6" @@ -3867,7 +3867,7 @@ __metadata: "@types/d3": "npm:^7" "@types/jest": "npm:^29.5.4" "@types/lodash": "npm:4.17.0" - "@types/react": "npm:18.2.78" + "@types/react": "npm:18.2.79" "@types/react-virtualized-auto-sizer": "npm:1.0.4" "@types/tinycolor2": "npm:1.4.6" babel-jest: "npm:29.7.0" @@ -3948,7 +3948,7 @@ __metadata: "@testing-library/react": "npm:15.0.2" "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:^29.5.4" - "@types/react": "npm:18.2.78" + "@types/react": "npm:18.2.79" "@types/systemjs": "npm:6.13.5" "@types/testing-library__jest-dom": "npm:5.14.9" jest: "npm:^29.6.4" @@ -3981,16 +3981,16 @@ __metadata: languageName: unknown linkType: soft -"@grafana/plugin-e2e@npm:1.1.0": - version: 1.1.0 - resolution: "@grafana/plugin-e2e@npm:1.1.0" +"@grafana/plugin-e2e@npm:1.1.1": + version: 1.1.1 + resolution: "@grafana/plugin-e2e@npm:1.1.1" dependencies: semver: "npm:^7.5.4" uuid: "npm:^9.0.1" yaml: "npm:^2.3.4" peerDependencies: "@playwright/test": ^1.41.2 - checksum: 10/3bfbf97501a1e4ec0e80dca23dfbd4f1c8f93b3546dfde6610db7d7c96455db2a59d3a75c3733c2722f6fc11503293208525b51d25086744e6093dd19591810b + checksum: 10/81d7732fc483bf2b2dd096e3a1efa5b12a32a67a79da9e92d8d502521928b2a75c735666f6b6c0de34a0d530243789ce5e12cac7a4655d425ce2e4f0033bbb11 languageName: node linkType: hard @@ -4012,8 +4012,8 @@ __metadata: "@leeoniya/ufuzzy": "npm:1.0.14" "@lezer/common": "npm:1.2.1" "@lezer/highlight": "npm:1.2.0" - "@lezer/lr": "npm:1.3.3" - "@prometheus-io/lezer-promql": "npm:^0.37.0-rc.1" + "@lezer/lr": "npm:1.4.0" + "@prometheus-io/lezer-promql": "npm:0.51.2" "@reduxjs/toolkit": "npm:1.9.5" "@rollup/plugin-image": "npm:3.0.3" "@rollup/plugin-node-resolve": "npm:15.2.3" @@ -4025,14 +4025,14 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/d3": "npm:7.4.3" "@types/debounce-promise": "npm:3.1.9" - "@types/eslint": "npm:8.56.9" + "@types/eslint": "npm:8.56.10" "@types/jest": "npm:29.5.12" "@types/jquery": "npm:3.5.29" "@types/lodash": "npm:4.17.0" "@types/node": "npm:20.12.7" "@types/pluralize": "npm:^0.0.33" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.78" + "@types/react": "npm:18.2.79" "@types/react-beautiful-dnd": "npm:13.1.8" "@types/react-dom": "npm:18.2.25" "@types/react-highlight-words": "npm:0.16.7" @@ -4125,7 +4125,7 @@ __metadata: "@types/history": "npm:4.7.11" "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.17.0" - "@types/react": "npm:18.2.78" + "@types/react": "npm:18.2.79" "@types/react-dom": "npm:18.2.25" "@types/systemjs": "npm:6.13.5" esbuild: "npm:0.18.12" @@ -4161,7 +4161,7 @@ __metadata: "@svgr/plugin-prettier": "npm:^8.1.0" "@svgr/plugin-svgo": "npm:^8.1.0" "@types/node": "npm:20.12.7" - "@types/react": "npm:18.2.78" + "@types/react": "npm:18.2.79" "@types/react-dom": "npm:18.2.25" esbuild: "npm:0.18.12" prettier: "npm:3.2.5" @@ -4235,7 +4235,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:^29.5.4" "@types/lodash": "npm:4.17.0" - "@types/react": "npm:18.2.78" + "@types/react": "npm:18.2.79" "@types/react-virtualized-auto-sizer": "npm:1.0.4" "@types/systemjs": "npm:6.13.5" "@types/testing-library__jest-dom": "npm:5.14.9" @@ -4322,7 +4322,7 @@ __metadata: "@types/mock-raf": "npm:1.0.6" "@types/node": "npm:20.12.7" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.78" + "@types/react": "npm:18.2.79" "@types/react-beautiful-dnd": "npm:13.1.8" "@types/react-color": "npm:3.0.12" "@types/react-dom": "npm:18.2.25" @@ -5041,6 +5041,15 @@ __metadata: languageName: node linkType: hard +"@lezer/lr@npm:1.4.0": + version: 1.4.0 + resolution: "@lezer/lr@npm:1.4.0" + dependencies: + "@lezer/common": "npm:^1.0.0" + checksum: 10/7391d0d08e54cd9e4f4d46e6ee6aa81fbaf079b22ed9c13d01fc9928e0ffd16d0c2d21b2cedd55675ad6c687277db28349ea8db81c9c69222cd7e7c40edd026e + languageName: node + linkType: hard + "@linaria/core@npm:^4.5.4": version: 4.5.4 resolution: "@linaria/core@npm:4.5.4" @@ -6152,13 +6161,23 @@ __metadata: languageName: node linkType: hard +"@prometheus-io/lezer-promql@npm:0.51.2": + version: 0.51.2 + resolution: "@prometheus-io/lezer-promql@npm:0.51.2" + peerDependencies: + "@lezer/highlight": ^1.1.2 + "@lezer/lr": ^1.2.3 + checksum: 10/cee04e8bb24b54caa5da029ab66aade5245c8ed96a99ca2444b45a1a814dc03e01197e4b4d9dd767baa9f81c35441c879939e13517b5fd5854598ceb58087e6b + languageName: node + linkType: hard + "@prometheus-io/lezer-promql@npm:^0.37.0-rc.1": - version: 0.37.0 - resolution: "@prometheus-io/lezer-promql@npm:0.37.0" + version: 0.37.9 + resolution: "@prometheus-io/lezer-promql@npm:0.37.9" peerDependencies: "@lezer/highlight": ^1.0.0 "@lezer/lr": ^1.0.0 - checksum: 10/00a3ef7a292ae17c7059da73e1ebd4568135eb5189be0eb60f039915f1c20a0bf355fe02cec1c11955e9e3885b5ecfdd8a67d57ce25fa09ad74575ba0fbc7386 + checksum: 10/3b1ddd9b47e3ba4f016901d6fc1b3b7b75855fb5da568fb95b30bfc60d35065e89d64162d947312126163a314c8844fa4a72176f9babdf86c63837d3fc0a5e4a languageName: node linkType: hard @@ -9607,13 +9626,13 @@ __metadata: languageName: node linkType: hard -"@types/eslint@npm:*, @types/eslint@npm:8.56.9, @types/eslint@npm:^8.56.5": - version: 8.56.9 - resolution: "@types/eslint@npm:8.56.9" +"@types/eslint@npm:*, @types/eslint@npm:8.56.10, @types/eslint@npm:^8.56.5": + version: 8.56.10 + resolution: "@types/eslint@npm:8.56.10" dependencies: "@types/estree": "npm:*" "@types/json-schema": "npm:*" - checksum: 10/fde20e8f3e5384f0ac78897b04cbaf1c78f4ba6cbdae9aeba876c00b665b498670cfcdf84a39eb4e44a6e27d6de80e24b5833d51a09d5d7e410229feb8b9c401 + checksum: 10/0cdd914b944ebba51c35827d3ef95bc3e16eb82b4c2741f6437fa57cdb00a4407c77f89c220afe9e4c9566982ec8a0fb9b97c956ac3bd4623a3b6af32eed8424 languageName: node linkType: hard @@ -10264,13 +10283,13 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:*, @types/react@npm:18.2.78, @types/react@npm:>=16": - version: 18.2.78 - resolution: "@types/react@npm:18.2.78" +"@types/react@npm:*, @types/react@npm:18.2.79, @types/react@npm:>=16": + version: 18.2.79 + resolution: "@types/react@npm:18.2.79" dependencies: "@types/prop-types": "npm:*" csstype: "npm:^3.0.2" - checksum: 10/a4bf8104c580fab40535cc6058425ac6a47c19b8dad45b566cdb38aaafa3ffa1f24e7b65ec95e391a6c18ccf4a33738a591c1f723c00300f13d61fc456a1eb8d + checksum: 10/2ef833e7d0a5c226beddbbe090811582371f6ae5e2f092a3d9f47cc6087c8bce0b96ee33e351de6d1d470f0a0ec5892d971933f841ef31538c1821681fc6569e languageName: node linkType: hard @@ -18605,7 +18624,7 @@ __metadata: "@grafana/lezer-logql": "npm:0.2.3" "@grafana/monaco-logql": "npm:^0.0.7" "@grafana/o11y-ds-frontend": "workspace:*" - "@grafana/plugin-e2e": "npm:1.1.0" + "@grafana/plugin-e2e": "npm:1.1.1" "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" "@grafana/saga-icons": "workspace:*" @@ -18662,7 +18681,7 @@ __metadata: "@types/d3-scale-chromatic": "npm:3.0.3" "@types/debounce-promise": "npm:3.1.9" "@types/diff": "npm:^5" - "@types/eslint": "npm:8.56.9" + "@types/eslint": "npm:8.56.10" "@types/eslint-scope": "npm:^3.7.7" "@types/file-saver": "npm:2.0.7" "@types/glob": "npm:^8.0.0" @@ -18684,7 +18703,7 @@ __metadata: "@types/papaparse": "npm:5.3.14" "@types/pluralize": "npm:^0.0.33" "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.78" + "@types/react": "npm:18.2.79" "@types/react-beautiful-dnd": "npm:13.1.8" "@types/react-dom": "npm:18.2.25" "@types/react-grid-layout": "npm:1.3.5"