Merge branch 'main' into eleijonmarck/actionsets/write-path

This commit is contained in:
Eric Leijonmarck
2024-04-19 13:30:35 +01:00
70 changed files with 861 additions and 495 deletions
-12
View File
@@ -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"]
],
+7 -7
View File
@@ -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
...
@@ -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
@@ -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 |
+3 -3
View File
@@ -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",
+1 -1
View File
@@ -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",
@@ -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;
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
@@ -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",
+4 -4
View File
@@ -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",
@@ -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: '!~' },
],
});
});
});
@@ -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<TValue>(value: TValue | null | undefined): value is TValue {
return value !== null && value !== undefined;
}
@@ -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,
},
@@ -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',
};
}
}
@@ -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[] = [];
+1 -1
View File
@@ -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",
+12 -19
View File
@@ -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<string, boolean>;
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.`);
}
}
}
});
@@ -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';
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
@@ -1,5 +1,6 @@
import { Meta, ArgTypes } from '@storybook/blocks';
import { ColorPicker } from './ColorPicker';
import { ColorPicker, SeriesColorPicker } from './ColorPicker';
import { ColorPickerInput } from './ColorPickerInput';
<Meta title="Pickers and Editors/ColorPicker/ColorPicker" />
@@ -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
<ArgTypes of={ColorPicker} />
### SeriesColorPicker
<ArgTypes of={SeriesColorPicker} />
### ColorPickerInput
<ArgTypes of={ColorPickerInput} />
@@ -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<typeof ColorPicker> = {
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<typeof ColorPicker> = ({ color, enableNamedColors })
};
export const SeriesPicker: StoryFn<typeof SeriesColorPicker> = ({ color, enableNamedColors }) => {
const [, updateArgs] = useArgs();
return (
<div style={{ display: 'flex', alignItems: 'flex-start' }}>
<SeriesColorPicker
enableNamedColors={enableNamedColors}
yaxis={1}
onToggleAxis={() => {}}
color={color}
onChange={(color) => {
action('Color changed')(color);
updateArgs({ color });
}}
/>
</div>
);
};
export const CustomTrigger: StoryFn<typeof ColorPicker> = ({ color, enableNamedColors }) => {
const [, updateArgs] = useArgs();
const clearButton = useStyles2(clearButtonStyles);
return (
<SeriesColorPicker
<ColorPicker
enableNamedColors={enableNamedColors}
yaxis={1}
onToggleAxis={() => {}}
color={color}
onChange={(color) => {
onChange={(color: string) => {
action('Color changed')(color);
updateArgs({ color });
}}
@@ -72,7 +83,7 @@ export const SeriesPicker: StoryFn<typeof SeriesColorPicker> = ({ color, enableN
Open color picker
</button>
)}
</SeriesColorPicker>
</ColorPicker>
);
};
@@ -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<typeof ColorPickerPopover> = {
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 (
<div style={{ position: 'absolute' }}>
<SeriesColorPickerPopover
theme={theme}
yaxis={yAxis}
onToggleAxis={() => (yAxis ? setYAxis(0) : setYAxis(2))}
color="#BC67E6"
onChange={(color: string) => {
console.log(color);
@@ -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<NamedColorsPaletteProps> {
selectedColor: string;
}
export const NamedColors: Story<StoryProps> = ({ selectedColor }) => {
const [color, setColor] = useState('green');
return <NamedColorsPalette color={color} onChange={setColor} />;
};
NamedColors.args = {
color: 'green',
};
export default meta;
@@ -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<typeof NamedColorsPalette> = ({ color }) => {
const [colorVal, setColor] = useState(color);
return <NamedColorsPalette color={colorVal} onChange={setColor} />;
};
export const Spectrum: StoryFn<typeof SpectrumPalette> = ({ color }) => {
const [, updateArgs] = useArgs();
return (
<SpectrumPalette
color={color}
onChange={(color: string) => {
action('Color changed')(color);
updateArgs({ color });
}}
/>
);
};
export default meta;
@@ -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 (
<Switch
key="yaxisSwitch"
label="Use right y-axis"
className={styles.colorPickerAxisSwitch}
labelClass={styles.colorPickerAxisSwitchLabel}
checked={yaxis === 2}
onChange={() => {
if (onToggleAxis) {
onToggleAxis();
}
}}
/>
<InlineField labelWidth={20} label="Use right y-axis">
<InlineSwitch value={yaxis === 2} label="Use right y-axis" onChange={onToggleAxis} />
</InlineField>
);
},
},
@@ -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,
}),
};
};
@@ -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<typeof SpectrumPalette> = {
title: 'Pickers and Editors/ColorPicker/Palettes/SpectrumPalette',
component: SpectrumPalette,
parameters: {
docs: {
page: mdx,
},
controls: {
exclude: ['onChange'],
},
},
args: {
color: 'red',
},
};
export const Simple: StoryFn<typeof SpectrumPalette> = ({ color }) => {
const [, updateArgs] = useArgs();
return renderComponentWithTheme(SpectrumPalette, {
color,
onChange: (color: string) => {
action('Color changed')(color);
updateArgs({ color });
},
});
};
export default meta;
@@ -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<Props>) => {
const styles = useStyles2(getStyles);
const imageToShow = image ?? getDefaultImageForVariant(variant);
return (
<Box paddingY={4} gap={4} display="flex" direction="column" alignItems="center">
{!hideImage && imageToShow}
<Stack direction="column" alignItems="center">
<Text variant="h4" textAlignment="center">
{message}
</Text>
{children && (
<Text color="secondary" textAlignment="center">
{children}
<Box paddingY={4} display="flex" direction="column" alignItems="center">
<div className={styles.container}>
{!hideImage && imageToShow}
<Stack direction="column" alignItems="center">
<Text variant="h4" textAlignment="center">
{message}
</Text>
)}
</Stack>
{button}
{children && (
<Text color="secondary" textAlignment="center">
{children}
</Text>
)}
</Stack>
{button}
</div>
</Box>
);
};
@@ -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',
}),
});
@@ -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<typeof PageToolbar> = {
export const Examples = () => {
return (
<VerticalGroup>
<Stack direction="column">
<StoryExample name="With non clickable title">
<PageToolbar pageIcon="bell" title="Dashboard">
<ToolbarButton icon="panel-add" />
@@ -50,7 +50,7 @@ export const Examples = () => {
<ToolbarButton>Apply</ToolbarButton>
</PageToolbar>
</StoryExample>
</VerticalGroup>
</Stack>
);
};
@@ -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<typeof TagsInput> = (props) => {
export const WithManyTags = () => {
const [tags, setTags] = useState<string[]>(['dashboard', 'prod', 'server', 'frontend', 'game', 'kubernetes']);
return (
<VerticalGroup>
<StoryExample name="With many tags">
<TagsInput tags={tags} onChange={setTags} />
</StoryExample>
</VerticalGroup>
<StoryExample name="With many tags">
<TagsInput tags={tags} onChange={setTags} />
</StoryExample>
);
};
@@ -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 (
<VerticalGroup>
<Stack direction="column">
<div>Emotion performance tests</div>
<TestScenario name="No styles" Component={NoStyles} />
<TestScenario name="inline emotion/css" Component={InlineEmotionCSS} />
@@ -24,7 +24,7 @@ export function EmotionPerfTest() {
<TestScenario name="useStyles with css prop" Component={UseStylesWithCSSProp} />
<TestScenario name="useStyles with conditional css prop" Component={UseStylesWithConditionalCSS} />
<TestScenario name="useStyles with conditional classnames" Component={UseStylesWithConditionalClassNames} />
</VerticalGroup>
</Stack>
);
}
-4
View File
@@ -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)
+1 -5
View File
@@ -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)
}
-8
View File
@@ -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 {
+78
View File
@@ -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<br/>(SQL)
participant ds as Datasource<br/>Plugin
participant expr as Expression<br/>Engine
User->>api: POST Query
loop Each query
api->>api: Parse query
api->>db: Get ds config<br>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<br/>Handler/Plugin
participant db as Storage<br/>(SQL)
participant expr as Expression<br/>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 <br> and secrets
end
loop Each expression
api->>expr: Execute
end
api->>api: Verify ResultExpectations
api->>User: return results
```
+1 -1
View File
@@ -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,
+7 -2
View File
@@ -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)
}
+11 -4
View File
@@ -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
+15 -9
View File
@@ -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",
+3 -2
View File
@@ -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
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
62 extraThemes experimental @grafana/grafana-frontend-platform false false true
63 lokiPredefinedOperations experimental @grafana/observability-logs false false true
64 pluginsFrontendSandbox experimental @grafana/plugins-platform-backend false false true
dashboardEmbed experimental @grafana/grafana-as-code false false true
65 frontendSandboxMonitorOnly experimental @grafana/plugins-platform-backend false false true
66 sqlDatasourceDatabaseSelection preview @grafana/dataviz-squad false false true
67 lokiFormatQuery experimental @grafana/observability-logs false false true
101 transformationsVariableSupport preview @grafana/dataviz-squad false false true
102 kubernetesPlaylists GA @grafana/grafana-app-platform-squad false true false
103 kubernetesSnapshots experimental @grafana/grafana-app-platform-squad false true false
104 kubernetesQueryServiceRewrite queryService experimental @grafana/grafana-app-platform-squad true false true false
105 queryServiceRewrite experimental @grafana/grafana-app-platform-squad false true false
106 queryServiceFromUI experimental @grafana/grafana-app-platform-squad false false true
107 cloudWatchBatchQueries preview @grafana/aws-datasources false false false
108 recoveryThreshold GA @grafana/alerting-squad false true false
109 lokiStructuredMetadata GA @grafana/observability-logs false false false
+10 -6
View File
@@ -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
+37 -5
View File
@@ -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
}
}
]
}
+41 -7
View File
@@ -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
@@ -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
@@ -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
@@ -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)
}
}
})
}
}
}
@@ -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)
})
}
}
+15 -10
View File
@@ -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.
@@ -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)
@@ -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,
+2 -1
View File
@@ -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
}
@@ -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) {
<div className={styles.suffix}>
<Icon name="keyboard" />
<span className={styles.shortcut}>{modKey}+k</span>
<Text variant="bodySmall">{modKey}+k</Text>
</div>
</div>
</div>
@@ -91,9 +91,6 @@ const getStyles = (theme: GrafanaTheme2) => {
gap: theme.spacing(0.5),
},
]),
shortcut: css({
fontSize: theme.typography.bodySmall.fontSize,
}),
fakeInput: css([
baseStyles.input,
{
@@ -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<DashboardSc
}
// Populate nav model in global store according to the folder
await this.initNavModel(rsp);
if (rsp.meta.folderUid) {
await updateNavModel(rsp.meta.folderUid);
}
// Do not cache new dashboards
this.dashboardCache = { dashboard: rsp, ts: Date.now(), cacheKey };
@@ -235,20 +235,6 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
return null;
}
private async initNavModel(dashboard: DashboardDTO) {
// only the folder API has information about ancestors
// get parent folder (if it exists) and put it in the store
// this will be used to populate the full breadcrumb trail
if (dashboard.meta.folderUid) {
try {
const folder = await backendSrv.getFolderByUid(dashboard.meta.folderUid);
store.dispatch(updateNavIndex(buildNavModel(folder)));
} catch (err) {
console.warn('Error fetching parent folder', dashboard.meta.folderUid, 'for dashboard', err);
}
}
}
public clearState() {
getDashboardSrv().setCurrent(undefined);
@@ -0,0 +1,32 @@
import { backendSrv } from 'app/core/services/backend_srv';
import { configureStore } from 'app/store/configureStore';
import { updateNavModel } from './utils';
describe('utils', () => {
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();
});
});
@@ -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);
}
}
@@ -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', () => {
@@ -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 });
};
@@ -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);
});
@@ -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<HTMLInputElement>) => {
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();
};
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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"
},
@@ -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",
@@ -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"
},
+1 -1
View File
@@ -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",
+55 -36
View File
@@ -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"