Merge branch 'main' into eleijonmarck/actionsets/write-path
This commit is contained in:
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
+13
@@ -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: '!~' },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+22
-94
@@ -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[] = [];
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
|
||||
+3
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
+10
-3
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user