diff --git a/devenv/docker/blocks/prometheus/recording.yml b/devenv/docker/blocks/prometheus/recording.yml
index f45cdbeddf2..2b8ae3e477a 100644
--- a/devenv/docker/blocks/prometheus/recording.yml
+++ b/devenv/docker/blocks/prometheus/recording.yml
@@ -1,4 +1,18 @@
groups:
+ - name: TESTING_RULES
+ rules:
+ - record: test:rate5m
+ expr: rate(ismail_prometheus_tsdb_reloads_total{job="prometheus"}[5m])
+ labels:
+ identifier: special_idx
+ - record: agent:test:rate5m
+ expr: rate(prometheus_tsdb_reloads_total{job="prometheus"}[5m])
+ labels:
+ identifier: special_idx
+ - record: agent:test:rate5m_agg_
+ expr: rate(prometheus_tsdb_reloads_total_agg_{job="aggregated"}[5m])
+ labels:
+ identifier: special_idx
- name: RECORDING_RULES
rules:
- record: instance_path:requests:rate5m
@@ -9,6 +23,14 @@ groups:
expr: rate(prometheus_tsdb_reloads_failures_total{job="prometheus"}[5m])
- record: instance_path:reloads:rate5m
expr: rate(prometheus_tsdb_reloads_total{job="prometheus"}[5m])
+ - record: test:rate5m
+ expr: rate(prometheus_tsdb_reloads_total{job="prometheus"}[5m])
+ - record: test:rate5m_agg_
+ expr: rate(prometheus_tsdb_reloads_total_agg_{job="aggregated"}[5m])
+ - record: agent:test:rate5m
+ expr: rate(prometheus_tsdb_reloads_total{job="prometheus"}[5m])
+ - record: agent:test:rate5m_agg_
+ expr: rate(prometheus_tsdb_reloads_total_agg_{job="aggregated"}[5m])
- record: instance_path:request_failures_per_requests:ratio_rate5m
expr: |2
instance_path:reloads_failures:rate5m{job="prometheus"}
diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
index fecc19478b7..8dae19485ea 100644
--- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
+++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
@@ -229,6 +229,7 @@ Experimental features might be changed or removed without prior notice.
| `k8SFolderCounts` | Enable folder's api server counts |
| `k8SFolderMove` | Enable folder's api server move |
| `teamHttpHeadersMimir` | Enables LBAC for datasources for Mimir to apply LBAC filtering of metrics to the client requests for users in teams |
+| `templateVariablesUsesCombobox` | Use new combobox component for template variables |
| `queryLibraryDashboards` | Enables Query Library feature in Dashboards |
| `grafanaAdvisor` | Enables Advisor app |
| `elasticsearchImprovedParsing` | Enables less memory intensive Elasticsearch result parsing |
diff --git a/package.json b/package.json
index 0cb29eae330..26bc506bbf6 100644
--- a/package.json
+++ b/package.json
@@ -79,7 +79,7 @@
"@emotion/eslint-plugin": "11.12.0",
"@grafana/eslint-config": "8.0.0",
"@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules",
- "@grafana/plugin-e2e": "1.17.0",
+ "@grafana/plugin-e2e": "1.17.1",
"@grafana/tsconfig": "^2.0.0",
"@manypkg/get-packages": "^2.2.0",
"@playwright/test": "1.50.0",
@@ -230,7 +230,7 @@
"sass-loader": "16.0.4",
"smtp-tester": "^2.1.0",
"style-loader": "4.0.0",
- "stylelint": "16.13.2",
+ "stylelint": "16.14.1",
"stylelint-config-sass-guidelines": "12.1.0",
"terser-webpack-plugin": "5.3.11",
"testing-library-selector": "0.3.1",
@@ -398,7 +398,7 @@
"reselect": "5.1.1",
"rxjs": "7.8.1",
"selecto": "1.26.3",
- "semver": "7.6.3",
+ "semver": "7.7.0",
"slate": "0.47.9",
"slate-plain-serializer": "0.7.13",
"slate-react": "0.22.10",
diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts
index 6ab637d841e..f983b75f5fe 100644
--- a/packages/grafana-data/src/types/featureToggles.gen.ts
+++ b/packages/grafana-data/src/types/featureToggles.gen.ts
@@ -248,6 +248,7 @@ export interface FeatureToggles {
improvedExternalSessionHandlingSAML?: boolean;
teamHttpHeadersMimir?: boolean;
ABTestFeatureToggleA?: boolean;
+ templateVariablesUsesCombobox?: boolean;
ABTestFeatureToggleB?: boolean;
queryLibraryDashboards?: boolean;
grafanaAdvisor?: boolean;
diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json
index 5eaf8277ad1..0c797232400 100644
--- a/packages/grafana-e2e-selectors/package.json
+++ b/packages/grafana-e2e-selectors/package.json
@@ -51,7 +51,7 @@
},
"dependencies": {
"@grafana/tsconfig": "^2.0.0",
- "semver": "7.6.3",
+ "semver": "7.7.0",
"tslib": "2.8.1",
"typescript": "5.7.3"
}
diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json
index 48d778736d4..a27c1776ec7 100644
--- a/packages/grafana-prometheus/package.json
+++ b/packages/grafana-prometheus/package.json
@@ -70,7 +70,7 @@
"react-use": "17.6.0",
"react-window": "1.8.11",
"rxjs": "7.8.1",
- "semver": "7.6.3",
+ "semver": "7.7.0",
"tslib": "2.8.1",
"uuid": "11.0.5",
"whatwg-fetch": "3.6.20"
diff --git a/packages/grafana-prometheus/src/datasource.test.ts b/packages/grafana-prometheus/src/datasource.test.ts
index 54cdf88f8c6..1e11c3b54d8 100644
--- a/packages/grafana-prometheus/src/datasource.test.ts
+++ b/packages/grafana-prometheus/src/datasource.test.ts
@@ -1029,6 +1029,23 @@ describe('PrometheusDatasource', () => {
expect(interval).toEqual({ text: '15s', value: '15s' });
expect(intervalMs).toEqual({ text: 15000, value: 15000 });
});
+
+ it('should use the default time range when no range provided in options', () => {
+ const prometheusDatasource = new PrometheusDatasource(
+ { ...instanceSettings, jsonData: { ...instanceSettings.jsonData, cacheLevel: PrometheusCacheLevel.None } },
+ templateSrvStub
+ );
+ const query = 'query_result(topk(5,rate(http_request_duration_microseconds_count[$__interval])))';
+ prometheusDatasource.metricFindQuery(query);
+
+ // Last 6h
+ const range = replaceMock.mock.calls[1][1].__range;
+ const rangeMs = replaceMock.mock.calls[1][1].__range_ms;
+ const rangeS = replaceMock.mock.calls[1][1].__range_s;
+ expect(range).toEqual({ text: '21600s', value: '21600s' });
+ expect(rangeMs).toEqual({ text: 21600000, value: 21600000 });
+ expect(rangeS).toEqual({ text: 21600, value: 21600 });
+ });
});
});
diff --git a/packages/grafana-prometheus/src/datasource.ts b/packages/grafana-prometheus/src/datasource.ts
index a54475cebdd..cb4c80f0afd 100644
--- a/packages/grafana-prometheus/src/datasource.ts
+++ b/packages/grafana-prometheus/src/datasource.ts
@@ -456,13 +456,15 @@ export class PrometheusDatasource
return Promise.resolve([]);
}
+ const timeRange = options?.range ?? this.languageProvider.timeRange ?? getDefaultTimeRange();
+
const scopedVars = {
...this.getIntervalVars(),
- ...this.getRangeScopedVars(options?.range ?? getDefaultTimeRange()),
+ ...this.getRangeScopedVars(timeRange),
};
const interpolated = this.templateSrv.replace(query, scopedVars, this.interpolateQueryExpr);
const metricFindQuery = new PrometheusMetricFindQuery(this, interpolated);
- return metricFindQuery.process(options?.range ?? getDefaultTimeRange());
+ return metricFindQuery.process(timeRange);
}
getIntervalVars() {
diff --git a/packages/grafana-prometheus/src/query_hints.test.ts b/packages/grafana-prometheus/src/query_hints.test.ts
index 5aabdbe7bd4..af2e9eee01a 100644
--- a/packages/grafana-prometheus/src/query_hints.test.ts
+++ b/packages/grafana-prometheus/src/query_hints.test.ts
@@ -8,6 +8,7 @@ import {
getQueryHints,
getQueryLabelsForRuleName,
getRecordingRuleIdentifierIdx,
+ isRuleInQuery,
SUM_HINT_THRESHOLD_COUNT,
} from './query_hints';
import { buildVisualQueryFromString } from './querybuilder/parsing';
@@ -365,6 +366,92 @@ describe('getExpandRulesHints', () => {
},
]);
});
+
+ it('should return expand rule hint, when given query include a non-unique rule name', () => {
+ const extractedMapping: RuleQueryMapping = {
+ 'duration:p95': [
+ {
+ query: 'expanded_duration_p95{}',
+ labels: {},
+ },
+ {
+ query: 'expanded_duration_p95_aggregated{}',
+ labels: {
+ span_name: '__aggregated__',
+ },
+ },
+ ],
+ 'duration:p95:upper_threshold': [
+ {
+ query: 'expanded_duration_p95_upper_threshold{}',
+ labels: {},
+ },
+ ],
+ };
+ const query = 'sum(rate(duration:p95:upper_threshold{label="foo"}[5m])) by(bar)';
+ const hints = getExpandRulesHints(query, extractedMapping);
+ expect(hints).toEqual([
+ {
+ type: 'EXPAND_RULES',
+ label: 'Query contains recording rules.',
+ fix: {
+ label: 'Expand rules',
+ action: {
+ type: 'EXPAND_RULES',
+ query,
+ options: {
+ 'duration:p95:upper_threshold': {
+ expandedQuery: 'expanded_duration_p95_upper_threshold{}',
+ },
+ },
+ },
+ },
+ },
+ ]);
+ });
+
+ it('should return expand rule hint, when given query include a non-unique rule name - second case', () => {
+ const extractedMapping: RuleQueryMapping = {
+ 'duration:p95': [
+ {
+ query: 'expanded_duration_p95{}',
+ labels: {},
+ },
+ {
+ query: 'expanded_duration_p95_aggregated{}',
+ labels: {
+ span_name: '__aggregated__',
+ },
+ },
+ ],
+ 'upper_threshold:duration:p95': [
+ {
+ query: 'expanded_duration_p95_upper_threshold{}',
+ labels: {},
+ },
+ ],
+ };
+ const query = 'sum(rate(upper_threshold:duration:p95{label="foo"}[5m])) by(bar)';
+ const hints = getExpandRulesHints(query, extractedMapping);
+ expect(hints).toEqual([
+ {
+ type: 'EXPAND_RULES',
+ label: 'Query contains recording rules.',
+ fix: {
+ label: 'Expand rules',
+ action: {
+ type: 'EXPAND_RULES',
+ query,
+ options: {
+ 'upper_threshold:duration:p95': {
+ expandedQuery: 'expanded_duration_p95_upper_threshold{}',
+ },
+ },
+ },
+ },
+ },
+ ]);
+ });
});
describe('getRecordingRuleIdentifierIdx', () => {
@@ -505,3 +592,73 @@ describe('getQueryLabelsForRuleName', () => {
expect(result).toEqual(expected);
});
});
+
+describe('ruleInQuery', () => {
+ it('should return true when ruleName is present in the query', () => {
+ expect(isRuleInQuery('rate(http_requests_total{job="api"}[5m])', 'http_requests_total')).toBe(true);
+ });
+
+ it('should return false when ruleName is not present in the query', () => {
+ expect(isRuleInQuery('rate(cpu_usage{instance="localhost"}[5m])', 'http_requests_total')).toBe(false);
+ });
+
+ it('should return true for ruleName at the start of the query', () => {
+ expect(isRuleInQuery('http_requests_total{job="api"}', 'http_requests_total')).toBe(true);
+ });
+
+ it('should return true for ruleName at the end of the query', () => {
+ expect(isRuleInQuery('sum by (instance) (http_requests_total)', 'http_requests_total')).toBe(true);
+ expect(isRuleInQuery('sum(http_requests_total)', 'http_requests_total')).toBe(true);
+ expect(isRuleInQuery('http_requests_total', 'http_requests_total')).toBe(true);
+ });
+
+ it('should return true when ruleName is followed by spaces', () => {
+ expect(isRuleInQuery('http_requests_total { job="api" }', 'http_requests_total')).toBe(true);
+ });
+
+ it('should return false when ruleName is a substring of another metric', () => {
+ expect(isRuleInQuery('rate(http_requests_total_new{job="api"}[5m])', 'http_requests_total')).toBe(false);
+ });
+
+ it('should return false for escaped ruleName usage', () => {
+ expect(isRuleInQuery('rate(\"http_requests_total\"{job="api"}[5m])', 'http_requests_total')).toBe(false);
+ });
+
+ it('should return false when query is empty', () => {
+ expect(isRuleInQuery('', 'http_requests_total')).toBe(false);
+ });
+
+ it('should return false when ruleName is an empty string', () => {
+ expect(isRuleInQuery('rate(http_requests_total{job="api"}[5m])', '')).toBe(false);
+ });
+
+ it('should return false when both query and ruleName are empty', () => {
+ expect(isRuleInQuery('', '')).toBe(false);
+ });
+
+ it('should return true when used with binary operations', () => {
+ expect(isRuleInQuery('rate(http_requests_total{job="api"}[5m]) + my:rule', 'my:rule')).toBe(true);
+ expect(isRuleInQuery('rate(http_requests_total{job="api"}[5m])+my:rule', 'my:rule')).toBe(true);
+ });
+
+ it('should return true when ruleName is inside nested functions', () => {
+ expect(isRuleInQuery('sum(rate(http_requests_total[5m]))', 'http_requests_total')).toBe(true);
+ });
+
+ it('should return false when ruleName is part of a label value', () => {
+ expect(isRuleInQuery('http_requests_total_bytes{rule="http_requests_total"}', 'http_requests_total')).toBe(false);
+ });
+
+ it('should return true when ruleName contains special characters like colon', () => {
+ expect(isRuleInQuery('rate(my_namespace:http_requests_total[5m])', 'my_namespace:http_requests_total')).toBe(true);
+ });
+
+ it('should return false when ruleName appears within string literals', () => {
+ expect(
+ isRuleInQuery(
+ 'label_replace(http_requests_total, "label", "value", "instance", "http_requests_total")',
+ 'http_requests_total'
+ )
+ ).toBe(false);
+ });
+});
diff --git a/packages/grafana-prometheus/src/query_hints.ts b/packages/grafana-prometheus/src/query_hints.ts
index 3fcdd66e0d2..73c3e90960e 100644
--- a/packages/grafana-prometheus/src/query_hints.ts
+++ b/packages/grafana-prometheus/src/query_hints.ts
@@ -175,10 +175,19 @@ export function getInitHints(datasource: PrometheusDatasource): QueryHint[] {
return hints;
}
+export function isRuleInQuery(query: string, ruleName: string) {
+ if (!query || !ruleName) {
+ return false;
+ }
+
+ const getRuleRegex = new RegExp(`(? {
- if (query.search(ruleName) === -1) {
+ if (!isRuleInQuery(query, ruleName)) {
return acc;
}
diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryCodeEditor.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryCodeEditor.tsx
index 9ad1b409456..f6ea9df0e1c 100644
--- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryCodeEditor.tsx
+++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryCodeEditor.tsx
@@ -7,7 +7,6 @@ import { useStyles2 } from '@grafana/ui';
import { PromQueryField } from '../../components/PromQueryField';
import { PromQueryEditorProps } from '../../components/types';
-import { QueryEditorHints } from '../shared/QueryEditorHints';
import { PromQueryBuilderExplained } from './PromQueryBuilderExplained';
@@ -35,7 +34,6 @@ export function PromQueryCodeEditor(props: PromQueryCodeEditorProps) {
app={app}
/>
{showExplain && }
-
);
}
diff --git a/pkg/promlib/library.go b/pkg/promlib/library.go
index 596a6f61c02..cbee4a19ffc 100644
--- a/pkg/promlib/library.go
+++ b/pkg/promlib/library.go
@@ -2,7 +2,6 @@ package promlib
import (
"context"
- "errors"
"fmt"
"strings"
@@ -11,7 +10,6 @@ import (
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
- apiv1 "github.com/prometheus/client_golang/api/prometheus/v1"
"github.com/grafana/grafana/pkg/promlib/client"
"github.com/grafana/grafana/pkg/promlib/instrumentation"
@@ -138,18 +136,3 @@ func (s *Service) getInstance(ctx context.Context, pluginCtx backend.PluginConte
in := i.(instance)
return &in, nil
}
-
-// IsAPIError returns whether err is or wraps a Prometheus error.
-func IsAPIError(err error) bool {
- // Check if the right error type is in err's chain.
- var e *apiv1.Error
- return errors.As(err, &e)
-}
-
-func ConvertAPIError(err error) error {
- var e *apiv1.Error
- if errors.As(err, &e) {
- return fmt.Errorf("%s: %s", e.Msg, e.Detail)
- }
- return err
-}
diff --git a/pkg/promlib/querydata/framing_bench_test.go b/pkg/promlib/querydata/framing_bench_test.go
index b769df2b47d..411c8644e71 100644
--- a/pkg/promlib/querydata/framing_bench_test.go
+++ b/pkg/promlib/querydata/framing_bench_test.go
@@ -43,7 +43,7 @@ func BenchmarkExemplarJson(b *testing.B) {
StatusCode: 200,
Body: io.NopCloser(bytes.NewReader(responseBytes)),
}
- tCtx.httpProvider.setResponse(&res)
+ tCtx.httpProvider.setResponse(&res, &res)
resp, err := tCtx.queryData.Execute(context.Background(), query)
require.NoError(b, err)
for _, r := range resp.Responses {
@@ -74,7 +74,7 @@ func BenchmarkRangeJson(b *testing.B) {
StatusCode: 200,
Body: io.NopCloser(bytes.NewReader(body)),
}
- tCtx.httpProvider.setResponse(&res)
+ tCtx.httpProvider.setResponse(&res, &res)
r, err = tCtx.queryData.Execute(context.Background(), q)
require.NoError(b, err)
}
diff --git a/pkg/promlib/querydata/framing_test.go b/pkg/promlib/querydata/framing_test.go
index 3668438ded4..3fa48bca92b 100644
--- a/pkg/promlib/querydata/framing_test.go
+++ b/pkg/promlib/querydata/framing_test.go
@@ -149,6 +149,6 @@ func runQuery(response []byte, q *backend.QueryDataRequest) (*backend.QueryDataR
StatusCode: 200,
Body: io.NopCloser(bytes.NewReader(response)),
}
- tCtx.httpProvider.setResponse(res)
+ tCtx.httpProvider.setResponse(res, res)
return tCtx.queryData.Execute(context.Background(), q)
}
diff --git a/pkg/promlib/querydata/request_test.go b/pkg/promlib/querydata/request_test.go
index 0977b9036d3..bfbde8dfb96 100644
--- a/pkg/promlib/querydata/request_test.go
+++ b/pkg/promlib/querydata/request_test.go
@@ -27,7 +27,6 @@ import (
func TestPrometheus_parseTimeSeriesResponse(t *testing.T) {
t.Run("exemplars response should be sampled and parsed normally", func(t *testing.T) {
- t.Skip()
exemplars := []apiv1.ExemplarQueryResult{
{
SeriesLabels: p.LabelSet{
@@ -60,6 +59,22 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) {
},
}
+ values := []p.SamplePair{
+ {Value: 1, Timestamp: 1000},
+ {Value: 4, Timestamp: 4000},
+ {Value: 6, Timestamp: 7000},
+ {Value: 8, Timestamp: 1100},
+ }
+ rangeResult := queryResult{
+ Type: p.ValMatrix,
+ Result: p.Matrix{
+ &p.SampleStream{
+ Metric: p.Metric{"app": "Application", "tag2": "tag2"},
+ Values: values,
+ },
+ },
+ }
+
tctx, err := setup()
require.NoError(t, err)
@@ -76,20 +91,20 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) {
RefID: "A",
JSON: b,
}
- res, err := execute(tctx, query, exemplars)
+ res, err := execute(tctx, query, exemplars, rangeResult)
require.NoError(t, err)
// Test fields
- require.Len(t, res, 1)
- // require.Equal(t, res[0].Name, "exemplar")
+ require.Len(t, res, 2)
+ require.Equal(t, res[0].Name, "exemplar")
require.Equal(t, res[0].Fields[0].Name, "Time")
require.Equal(t, res[0].Fields[1].Name, "Value")
require.Len(t, res[0].Fields, 6)
// Test correct values (sampled to 2)
- require.Equal(t, res[0].Fields[1].Len(), 2)
+ require.Equal(t, res[0].Fields[1].Len(), 4)
require.Equal(t, res[0].Fields[1].At(0), 0.009545445)
- require.Equal(t, res[0].Fields[1].At(1), 0.003535405)
+ require.Equal(t, res[0].Fields[1].At(3), 0.003535405)
})
t.Run("matrix response should be parsed normally", func(t *testing.T) {
@@ -128,7 +143,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) {
}
tctx, err := setup()
require.NoError(t, err)
- res, err := execute(tctx, query, result)
+ res, err := execute(tctx, query, result, nil)
require.NoError(t, err)
require.Len(t, res, 1)
@@ -177,7 +192,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) {
}
tctx, err := setup()
require.NoError(t, err)
- res, err := execute(tctx, query, result)
+ res, err := execute(tctx, query, result, nil)
require.NoError(t, err)
require.Len(t, res, 1)
@@ -222,7 +237,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) {
}
tctx, err := setup()
require.NoError(t, err)
- res, err := execute(tctx, query, result)
+ res, err := execute(tctx, query, result, nil)
require.NoError(t, err)
require.Len(t, res, 1)
@@ -266,7 +281,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) {
tctx, err := setup()
require.NoError(t, err)
- res, err := execute(tctx, query, result)
+ res, err := execute(tctx, query, result, nil)
require.NoError(t, err)
require.Equal(t, `{app="Application"}`, res[0].Fields[1].Config.DisplayNameFromDS)
@@ -298,7 +313,7 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) {
}
tctx, err := setup()
require.NoError(t, err)
- res, err := execute(tctx, query, qr)
+ res, err := execute(tctx, query, qr, nil)
require.NoError(t, err)
require.Len(t, res, 1)
@@ -317,7 +332,6 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) {
})
t.Run("scalar response should be parsed normally", func(t *testing.T) {
- t.Skip("TODO: implement scalar responses")
qr := queryResult{
Type: p.ValScalar,
Result: &p.Scalar{
@@ -339,14 +353,15 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) {
}
tctx, err := setup()
require.NoError(t, err)
- res, err := execute(tctx, query, qr)
+ res, err := execute(tctx, query, qr, nil)
require.NoError(t, err)
require.Len(t, res, 1)
require.Len(t, res[0].Fields, 2)
require.Len(t, res[0].Fields[0].Labels, 0)
require.Equal(t, res[0].Fields[0].Name, "Time")
- require.Equal(t, "1", res[0].Fields[1].Name)
+ require.Equal(t, "Value", res[0].Fields[1].Name)
+ require.Equal(t, float64(1), res[0].Fields[1].At(0))
// Ensure the timestamps are UTC zoned
testValue := res[0].Fields[0].At(0)
@@ -360,22 +375,29 @@ type queryResult struct {
Result any `json:"result"`
}
-func executeWithHeaders(tctx *testContext, query backend.DataQuery, qr any, headers map[string]string) (data.Frames, error) {
+func executeWithHeaders(tctx *testContext, query backend.DataQuery, rqr any, eqr any, headers map[string]string) (data.Frames, error) {
req := backend.QueryDataRequest{
Queries: []backend.DataQuery{query},
Headers: headers,
}
- promRes, err := toAPIResponse(qr)
- defer func() {
- if err := promRes.Body.Close(); err != nil {
- fmt.Println(fmt.Errorf("response body close error: %v", err))
- }
- }()
+ rangeRes, err := toAPIResponse(rqr)
if err != nil {
return nil, err
}
- tctx.httpProvider.setResponse(promRes)
+ exemplarRes, err := toAPIResponse(eqr)
+ if err != nil {
+ return nil, err
+ }
+ defer func() {
+ if err := rangeRes.Body.Close(); err != nil {
+ fmt.Println(fmt.Errorf("rangeRes body close error: %v", err))
+ }
+ if err := exemplarRes.Body.Close(); err != nil {
+ fmt.Println(fmt.Errorf("exemplarRes body close error: %v", err))
+ }
+ }()
+ tctx.httpProvider.setResponse(rangeRes, exemplarRes)
res, err := tctx.queryData.Execute(context.Background(), &req)
if err != nil {
@@ -385,8 +407,8 @@ func executeWithHeaders(tctx *testContext, query backend.DataQuery, qr any, head
return res.Responses[req.Queries[0].RefID].Frames, nil
}
-func execute(tctx *testContext, query backend.DataQuery, qr any) (data.Frames, error) {
- return executeWithHeaders(tctx, query, qr, map[string]string{})
+func execute(tctx *testContext, query backend.DataQuery, rqr any, eqr any) (data.Frames, error) {
+ return executeWithHeaders(tctx, query, rqr, eqr, map[string]string{})
}
type apiResponse struct {
@@ -426,7 +448,11 @@ func setup() (*testContext, error) {
opts: httpclient.Options{
Timeouts: &httpclient.DefaultTimeoutOptions,
},
- res: &http.Response{
+ rangeRes: &http.Response{
+ StatusCode: 200,
+ Body: io.NopCloser(bytes.NewReader([]byte(`{}`))),
+ },
+ exemplarRes: &http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewReader([]byte(`{}`))),
},
@@ -456,9 +482,10 @@ func setup() (*testContext, error) {
type fakeHttpClientProvider struct {
httpclient.Provider
- opts httpclient.Options
- req *http.Request
- res *http.Response
+ opts httpclient.Options
+ req *http.Request
+ rangeRes *http.Response
+ exemplarRes *http.Response
}
func (p *fakeHttpClientProvider) New(opts ...httpclient.Options) (*http.Client, error) {
@@ -476,11 +503,18 @@ func (p *fakeHttpClientProvider) GetTransport(opts ...httpclient.Options) (http.
return http.DefaultTransport, nil
}
-func (p *fakeHttpClientProvider) setResponse(res *http.Response) {
- p.res = res
+func (p *fakeHttpClientProvider) setResponse(rangeRes *http.Response, exemplarRes *http.Response) {
+ p.rangeRes = rangeRes
+ p.exemplarRes = exemplarRes
}
func (p *fakeHttpClientProvider) RoundTrip(req *http.Request) (*http.Response, error) {
p.req = req
- return p.res, nil
+ switch req.URL.Path {
+ case "/api/v1/query_range", "/api/v1/query":
+ return p.rangeRes, nil
+ case "/api/v1/query_exemplars":
+ return p.exemplarRes, nil
+ }
+ return nil, fmt.Errorf("no such path: %s", req.URL.Path)
}
diff --git a/pkg/promlib/resource/resource.go b/pkg/promlib/resource/resource.go
index 56988d10805..0673a5e0b41 100644
--- a/pkg/promlib/resource/resource.go
+++ b/pkg/promlib/resource/resource.go
@@ -83,15 +83,6 @@ func (r *Resource) Execute(ctx context.Context, req *backend.CallResourceRequest
return callResponse, err
}
-func (r *Resource) DetectVersion(ctx context.Context, req *backend.CallResourceRequest) (*backend.CallResourceResponse, error) {
- newReq := &backend.CallResourceRequest{
- PluginContext: req.PluginContext,
- Path: "/api/v1/status/buildinfo",
- }
-
- return r.Execute(ctx, newReq)
-}
-
func getSelectors(expr string) ([]string, error) {
parsed, err := parser.ParseExpr(expr)
if err != nil {
diff --git a/pkg/promlib/resource/resource_test.go b/pkg/promlib/resource/resource_test.go
new file mode 100644
index 00000000000..a9b7ed21b60
--- /dev/null
+++ b/pkg/promlib/resource/resource_test.go
@@ -0,0 +1,109 @@
+package resource_test
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "io"
+ "net/http"
+ "testing"
+
+ "github.com/grafana/grafana-plugin-sdk-go/backend"
+ "github.com/grafana/grafana-plugin-sdk-go/backend/log"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/grafana/grafana/pkg/promlib/resource"
+)
+
+type mockRoundTripper struct {
+ Response *http.Response
+ Err error
+}
+
+func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+ return m.Response, m.Err
+}
+
+func setup() (*http.Client, backend.DataSourceInstanceSettings, log.Logger) {
+ // Mock HTTP Response
+ mockResponse := &http.Response{
+ StatusCode: 200,
+ Body: io.NopCloser(bytes.NewReader([]byte(`{"message": "success"}`))),
+ Header: make(http.Header),
+ }
+
+ // Create a mock RoundTripper
+ mockTransport := &mockRoundTripper{
+ Response: mockResponse,
+ }
+
+ // Create a mock HTTP client using the mock RoundTripper
+ mockClient := &http.Client{
+ Transport: mockTransport,
+ }
+
+ settings := backend.DataSourceInstanceSettings{
+ ID: 1,
+ URL: "http://mock-server",
+ JSONData: []byte(`{}`),
+ }
+
+ logger := log.DefaultLogger
+
+ return mockClient, settings, logger
+}
+
+func TestNewResource(t *testing.T) {
+ mockClient, settings, logger := setup()
+ res, err := resource.New(mockClient, settings, logger)
+ require.NoError(t, err)
+ assert.NotNil(t, res)
+}
+
+func TestResource_Execute(t *testing.T) {
+ mockClient, settings, logger := setup()
+ res, err := resource.New(mockClient, settings, logger)
+ require.NoError(t, err)
+
+ req := &backend.CallResourceRequest{
+ URL: "/test",
+ }
+ ctx := context.Background()
+
+ resp, err := res.Execute(ctx, req)
+ require.NoError(t, err)
+ assert.NotNil(t, resp)
+}
+
+func TestResource_GetSuggestions(t *testing.T) {
+ mockClient, _, logger := setup()
+ settings := backend.DataSourceInstanceSettings{
+ ID: 1,
+ URL: "http://localhost:9090",
+ JSONData: []byte(`{"httpMethod": "GET"}`),
+ }
+
+ res, err := resource.New(mockClient, settings, logger)
+ require.NoError(t, err)
+
+ suggestionReq := resource.SuggestionRequest{
+ LabelName: "instance",
+ Queries: []string{"up"},
+ Start: "1609459200",
+ End: "1609462800",
+ Limit: 10,
+ }
+
+ body, err := json.Marshal(suggestionReq)
+ require.NoError(t, err)
+
+ req := &backend.CallResourceRequest{
+ Body: body,
+ }
+ ctx := context.Background()
+
+ resp, err := res.GetSuggestions(ctx, req)
+ require.NoError(t, err)
+ assert.NotNil(t, resp)
+}
diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go
index d37b417b34e..340946621be 100644
--- a/pkg/services/featuremgmt/registry.go
+++ b/pkg/services/featuremgmt/registry.go
@@ -1722,6 +1722,13 @@ var (
Expression: "false",
HideFromDocs: true,
},
+ {
+ Name: "templateVariablesUsesCombobox",
+ Description: "Use new combobox component for template variables",
+ Stage: FeatureStageExperimental,
+ Owner: grafanaFrontendPlatformSquad,
+ FrontendOnly: true,
+ },
{
Name: "ABTestFeatureToggleB",
Description: "Test feature toggle to see how cohorts could be set up AB testing",
diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv
index 6ae52ca70d5..965d4b1fbe5 100644
--- a/pkg/services/featuremgmt/toggles_gen.csv
+++ b/pkg/services/featuremgmt/toggles_gen.csv
@@ -229,6 +229,7 @@ k8SFolderMove,experimental,@grafana/search-and-storage,false,false,false
improvedExternalSessionHandlingSAML,preview,@grafana/identity-access-team,false,false,false
teamHttpHeadersMimir,experimental,@grafana/identity-access-team,false,false,false
ABTestFeatureToggleA,experimental,@grafana/sharing-squad,false,false,false
+templateVariablesUsesCombobox,experimental,@grafana/grafana-frontend-platform,false,false,true
ABTestFeatureToggleB,experimental,@grafana/sharing-squad,false,false,false
queryLibraryDashboards,experimental,@grafana/grafana-frontend-platform,false,false,false
grafanaAdvisor,experimental,@grafana/plugins-platform-backend,false,false,false
diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go
index 0a6e5c4d0b4..64e4b7b2fc3 100644
--- a/pkg/services/featuremgmt/toggles_gen.go
+++ b/pkg/services/featuremgmt/toggles_gen.go
@@ -927,6 +927,10 @@ const (
// Test feature toggle to see how cohorts could be set up AB testing
FlagABTestFeatureToggleA = "ABTestFeatureToggleA"
+ // FlagTemplateVariablesUsesCombobox
+ // Use new combobox component for template variables
+ FlagTemplateVariablesUsesCombobox = "templateVariablesUsesCombobox"
+
// FlagABTestFeatureToggleB
// Test feature toggle to see how cohorts could be set up AB testing
FlagABTestFeatureToggleB = "ABTestFeatureToggleB"
diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json
index 1e79164d138..5c3dcb7040e 100644
--- a/pkg/services/featuremgmt/toggles_gen.json
+++ b/pkg/services/featuremgmt/toggles_gen.json
@@ -3716,6 +3716,19 @@
"codeowner": "@grafana/identity-access-team"
}
},
+ {
+ "metadata": {
+ "name": "templateVariablesUsesCombobox",
+ "resourceVersion": "1738141787383",
+ "creationTimestamp": "2025-01-29T09:09:47Z"
+ },
+ "spec": {
+ "description": "Use new combobox component for template variables",
+ "stage": "experimental",
+ "codeowner": "@grafana/grafana-frontend-platform",
+ "frontend": true
+ }
+ },
{
"metadata": {
"name": "timeRangeProvider",
diff --git a/pkg/services/ngalert/sender/router.go b/pkg/services/ngalert/sender/router.go
index cfb474f63be..637a3c77f21 100644
--- a/pkg/services/ngalert/sender/router.go
+++ b/pkg/services/ngalert/sender/router.go
@@ -90,9 +90,11 @@ func (d *AlertsRouter) SyncAndApplyConfigFromDatabase(ctx context.Context) error
d.logger.Debug("Attempting to sync admin configs", "count", len(cfgs))
disableExternal := d.featureManager.IsEnabled(ctx, featuremgmt.FlagAlertingDisableSendAlertsExternal)
-
orgsFound := make(map[int64]struct{}, len(cfgs))
+
+ // We're holding this lock either until we return an error or right before we stop the senders.
d.adminConfigMtx.Lock()
+
for _, cfg := range cfgs {
_, isDisabledOrg := d.disabledOrgs[cfg.OrgID]
if isDisabledOrg {
@@ -167,6 +169,7 @@ func (d *AlertsRouter) SyncAndApplyConfigFromDatabase(ctx context.Context) error
senderLogger := log.New("ngalert.sender.external-alertmanager")
s, err := NewExternalAlertmanagerSender(senderLogger, prometheus.NewRegistry())
if err != nil {
+ d.adminConfigMtx.Unlock()
return err
}
d.externalAlertmanagers[cfg.OrgID] = s
@@ -190,9 +193,9 @@ func (d *AlertsRouter) SyncAndApplyConfigFromDatabase(ctx context.Context) error
delete(d.externalAlertmanagersCfgHash, orgID)
}
}
- d.adminConfigMtx.Unlock()
- // We can now stop these external Alertmanagers w/o having to hold a lock.
+ // We can now stop these senders w/o having to hold a lock.
+ d.adminConfigMtx.Unlock()
for orgID, s := range sendersToStop {
d.logger.Info("Stopping sender", "org", orgID)
s.Stop()
@@ -318,8 +321,10 @@ func (d *AlertsRouter) Send(ctx context.Context, key models.AlertRuleKey, alerts
}
// Send alerts to local notifier if they need to be handled internally
// or if no external AMs have been discovered yet.
+ d.adminConfigMtx.RLock()
+ defer d.adminConfigMtx.RUnlock()
var localNotifierExist, externalNotifierExist bool
- if d.sendAlertsTo[key.OrgID] == models.ExternalAlertmanagers && len(d.AlertmanagersFor(key.OrgID)) > 0 {
+ if d.sendAlertsTo[key.OrgID] == models.ExternalAlertmanagers && len(d.alertmanagersFor(key.OrgID)) > 0 {
logger.Debug("All alerts for the given org should be routed to external notifiers only. skipping the internal notifier.")
} else {
logger.Info("Sending alerts to local notifier", "count", len(alerts.PostableAlerts))
@@ -340,8 +345,6 @@ func (d *AlertsRouter) Send(ctx context.Context, key models.AlertRuleKey, alerts
// Send alerts to external Alertmanager(s) if we have a sender for this organization
// and alerts are not being handled just internally.
- d.adminConfigMtx.RLock()
- defer d.adminConfigMtx.RUnlock()
s, ok := d.externalAlertmanagers[key.OrgID]
if ok && d.sendAlertsTo[key.OrgID] != models.InternalAlertmanager {
logger.Info("Sending alerts to external notifier", "count", len(alerts.PostableAlerts))
@@ -358,6 +361,10 @@ func (d *AlertsRouter) Send(ctx context.Context, key models.AlertRuleKey, alerts
func (d *AlertsRouter) AlertmanagersFor(orgID int64) []*url.URL {
d.adminConfigMtx.RLock()
defer d.adminConfigMtx.RUnlock()
+ return d.alertmanagersFor(orgID)
+}
+
+func (d *AlertsRouter) alertmanagersFor(orgID int64) []*url.URL {
s, ok := d.externalAlertmanagers[orgID]
if !ok {
return []*url.URL{}
diff --git a/public/app/api/createBaseQuery.ts b/public/app/api/createBaseQuery.ts
new file mode 100644
index 00000000000..29327703bf9
--- /dev/null
+++ b/public/app/api/createBaseQuery.ts
@@ -0,0 +1,43 @@
+import { BaseQueryFn } from '@reduxjs/toolkit/query';
+import { lastValueFrom } from 'rxjs';
+
+import { BackendSrvRequest, getBackendSrv, isFetchError } from '@grafana/runtime';
+
+interface RequestOptions extends BackendSrvRequest {
+ manageError?: (err: unknown) => { error: unknown };
+ body?: BackendSrvRequest['data'];
+}
+
+export function createBaseQuery({ baseURL }: { baseURL: string }): BaseQueryFn {
+ async function backendSrvBaseQuery(requestOptions: RequestOptions) {
+ try {
+ const { data: responseData, ...meta } = await lastValueFrom(
+ getBackendSrv().fetch({
+ ...requestOptions,
+ url: baseURL + requestOptions.url,
+ showErrorAlert: requestOptions.showErrorAlert ?? false,
+ data: requestOptions.body,
+ })
+ );
+ return { data: responseData, meta };
+ } catch (error) {
+ if (requestOptions.manageError) {
+ return requestOptions.manageError(error);
+ } else {
+ return handleRequestError(error);
+ }
+ }
+ }
+
+ return backendSrvBaseQuery;
+}
+
+export function handleRequestError(error: unknown) {
+ if (isFetchError(error)) {
+ return { error: new Error(error.data.message) };
+ } else if (error instanceof Error) {
+ return { error };
+ } else {
+ return { error: new Error('Unknown error') };
+ }
+}
diff --git a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts
index a292b62d479..369bd1bb25e 100644
--- a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts
+++ b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts
@@ -1,10 +1,10 @@
-import { BaseQueryFn, createApi } from '@reduxjs/toolkit/query/react';
-import { lastValueFrom } from 'rxjs';
+import { createApi } from '@reduxjs/toolkit/query/react';
import { AppEvents, isTruthy, locationUtil } from '@grafana/data';
-import { BackendSrvRequest, config, getBackendSrv, locationService } from '@grafana/runtime';
+import { config, getBackendSrv, locationService } from '@grafana/runtime';
import { Dashboard } from '@grafana/schema';
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
+import { createBaseQuery, handleRequestError } from 'app/api/createBaseQuery';
import appEvents from 'app/core/app_events';
import { contextSrv } from 'app/core/core';
import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api';
@@ -30,11 +30,6 @@ import { DashboardTreeSelection } from '../types';
import { PAGE_SIZE } from './services';
-interface RequestOptions extends BackendSrvRequest {
- manageError?: (err: unknown) => { error: unknown };
- showErrorAlert?: boolean;
-}
-
interface DeleteItemsArgs {
selectedItems: Omit;
}
@@ -66,28 +61,6 @@ interface HardDeleteDashboardArgs {
dashboardUID: string;
}
-function createBackendSrvBaseQuery({ baseURL }: { baseURL: string }): BaseQueryFn {
- async function backendSrvBaseQuery(requestOptions: RequestOptions) {
- // Suppress error pop-up for root (aka 'general') folder
- const isGeneralFolder = requestOptions.url === `/folders/general`;
- requestOptions = isGeneralFolder ? { ...requestOptions, showErrorAlert: false } : requestOptions;
-
- try {
- const { data: responseData, ...meta } = await lastValueFrom(
- getBackendSrv().fetch({
- ...requestOptions,
- url: baseURL + requestOptions.url,
- })
- );
- return { data: responseData, meta };
- } catch (error) {
- return requestOptions.manageError ? requestOptions.manageError(error) : { error };
- }
- }
-
- return backendSrvBaseQuery;
-}
-
export interface ListFolderQueryArgs {
page: number;
parentUid: string | undefined;
@@ -98,7 +71,7 @@ export interface ListFolderQueryArgs {
export const browseDashboardsAPI = createApi({
tagTypes: ['getFolder'],
reducerPath: 'browseDashboardsAPI',
- baseQuery: createBackendSrvBaseQuery({ baseURL: '/api' }),
+ baseQuery: createBaseQuery({ baseURL: '/api' }),
endpoints: (builder) => ({
listFolders: builder.query({
providesTags: (result) => result?.map((folder) => ({ type: 'getFolder', id: folder.uid })) ?? [],
@@ -121,7 +94,7 @@ export const browseDashboardsAPI = createApi({
query: ({ title, parentUid }) => ({
method: 'POST',
url: '/folders',
- data: {
+ body: {
title,
parentUid,
},
@@ -148,7 +121,7 @@ export const browseDashboardsAPI = createApi({
query: ({ uid, title, version }) => ({
method: 'PUT',
url: `/folders/${uid}`,
- data: {
+ body: {
title,
version,
},
@@ -171,7 +144,7 @@ export const browseDashboardsAPI = createApi({
query: ({ folder, destinationUID }) => ({
url: `/folders/${folder.uid}/move`,
method: 'POST',
- data: { parentUID: destinationUID },
+ body: { parentUID: destinationUID },
}),
onQueryStarted: ({ folder, destinationUID }, { queryFulfilled, dispatch }) => {
const { parentUid } = folder;
@@ -260,23 +233,34 @@ export const browseDashboardsAPI = createApi({
await baseQuery({
url: `/folders/${folderUID}/move`,
method: 'POST',
- data: { parentUID: destinationUID },
+ body: { parentUID: destinationUID },
});
}
// Move all the dashboards sequentially
// TODO error handling here
for (const dashboardUID of selectedDashboards) {
- const fullDash: DashboardDTO = await getDashboardAPI().getDashboardDTO(dashboardUID);
+ if (config.featureToggles.useV2DashboardsAPI) {
+ const fullDash = await getDashboardAPI('v2').getDashboardDTO(dashboardUID);
- await getDashboardAPI().saveDashboard({
- dashboard: fullDash.dashboard,
- folderUid: destinationUID,
- overwrite: false,
- message: '',
- });
+ await getDashboardAPI('v2').saveDashboard({
+ dashboard: fullDash.spec,
+ folderUid: destinationUID,
+ overwrite: false,
+ message: '',
+ k8s: fullDash.metadata,
+ });
+ } else {
+ const fullDash: DashboardDTO = await getDashboardAPI().getDashboardDTO(dashboardUID);
+
+ await getDashboardAPI().saveDashboard({
+ dashboard: fullDash.dashboard,
+ folderUid: destinationUID,
+ overwrite: false,
+ message: '',
+ });
+ }
}
-
return { data: undefined };
},
onQueryStarted: ({ destinationUID, selectedItems }, { queryFulfilled, dispatch }) => {
@@ -356,7 +340,7 @@ export const browseDashboardsAPI = createApi({
}
throw new Error('Invalid dashboard version');
} catch (error) {
- return { error };
+ return handleRequestError(error);
}
},
@@ -378,7 +362,7 @@ export const browseDashboardsAPI = createApi({
query: ({ dashboard, overwrite, inputs, folderUid }) => ({
method: 'POST',
url: '/dashboards/import',
- data: {
+ body: {
dashboard,
overwrite,
inputs,
@@ -403,7 +387,7 @@ export const browseDashboardsAPI = createApi({
restoreDashboard: builder.mutation({
query: ({ dashboardUID, targetFolderUID }) => ({
url: `/dashboards/uid/${dashboardUID}/trash`,
- data: {
+ body: {
folderUid: targetFolderUID,
},
method: 'PATCH',
diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts
index 64d850e9047..d885e17fdda 100644
--- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts
+++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts
@@ -154,6 +154,22 @@ describe('DashboardScenePageStateManager v1', () => {
status: 500,
});
});
+
+ it('should throw when v2 custom home dashboard is provided', async () => {
+ setBackendSrv({
+ get: () => Promise.resolve({ dashboard: customHomeDashboardV2Spec, meta: {} }),
+ } as unknown as BackendSrv);
+
+ const loader = new DashboardScenePageStateManager({});
+ await loader.loadDashboard({ uid: '', route: DashboardRoutes.Home });
+
+ expect(loader.state.dashboard).toBeUndefined();
+ expect(loader.state.loadError).toEqual({
+ message: 'v2 dashboard spec is not supported. Enable useV2DashboardsAPI feature toggle',
+ messageId: undefined,
+ status: undefined,
+ });
+ });
});
describe('New dashboards', () => {
@@ -423,13 +439,12 @@ describe('DashboardScenePageStateManager v2', () => {
});
describe('Home dashboard', () => {
- // TODO: Unskip when redirect is implemented in v2 API
- it.skip('should handle home dashboard redirect', async () => {
+ it('should handle home dashboard redirect', async () => {
setBackendSrv({
get: () => Promise.resolve({ redirectUri: '/d/asd' }),
} as unknown as BackendSrv);
- const loader = new DashboardScenePageStateManager({});
+ const loader = new DashboardScenePageStateManagerV2({});
await loader.loadDashboard({ uid: '', route: DashboardRoutes.Home });
expect(loader.state.dashboard).toBeUndefined();
@@ -455,6 +470,45 @@ describe('DashboardScenePageStateManager v2', () => {
status: 500,
});
});
+
+ it('should not transform v2 custom home dashboard spec', async () => {
+ setBackendSrv({
+ get: () =>
+ Promise.resolve({
+ dashboard: customHomeDashboardV2Spec,
+ meta: {
+ canSave: false,
+ canEdit: true,
+ canAdmin: false,
+ canStar: false,
+ canDelete: false,
+ slug: '',
+ url: '',
+ expires: '0001-01-01T00:00:00Z',
+ created: '0001-01-01T00:00:00Z',
+ updated: '0001-01-01T00:00:00Z',
+ updatedBy: '',
+ createdBy: '',
+ version: 0,
+ hasAcl: false,
+ isFolder: false,
+ folderId: 0,
+ folderUid: '',
+ folderTitle: 'General',
+ folderUrl: '',
+ provisioned: false,
+ provisionedExternalId: '',
+ annotationsPermissions: null,
+ },
+ }),
+ } as unknown as BackendSrv);
+
+ const loader = new DashboardScenePageStateManagerV2({});
+ await loader.loadDashboard({ uid: '', route: DashboardRoutes.Home });
+
+ expect(loader.state.dashboard?.getInitialSaveModel()).toEqual(customHomeDashboardV2Spec);
+ expect(loader.state.loadError).toBeUndefined();
+ });
});
describe('New dashboards', () => {
@@ -593,3 +647,78 @@ describe('DashboardScenePageStateManager v2', () => {
});
});
});
+
+const customHomeDashboardV2Spec = {
+ title: 'Home Dashboard v2 schema',
+ cursorSync: 'Off',
+ preload: false,
+ editable: true,
+ links: [],
+ tags: [],
+ timeSettings: {
+ timezone: 'browser',
+ from: 'now-6h',
+ to: 'now',
+ autoRefresh: '',
+ autoRefreshIntervals: ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'],
+ quickRanges: ['5m', '15m', '1h', '6h', '12h', '24h', '2d', '7d', '30d'],
+ hideTimepicker: false,
+ weekStart: '',
+ fiscalYearStartMonth: 0,
+ },
+ variables: [],
+ elements: {
+ text_panel: {
+ kind: 'Panel',
+ spec: {
+ id: 0,
+ title: 'Welcome',
+ description: 'Welcome to the home dashboard!',
+ links: [],
+ data: {
+ kind: 'QueryGroup',
+ spec: {
+ queries: [],
+ transformations: [],
+ queryOptions: {},
+ },
+ },
+ vizConfig: {
+ kind: 'text',
+ spec: {
+ pluginVersion: '',
+ options: {
+ mode: 'markdown',
+ content: '# Welcome to the home dashboard!\n\n## Example of v2 schema home dashboard',
+ },
+ fieldConfig: {
+ defaults: {},
+ overrides: [],
+ },
+ },
+ },
+ },
+ },
+ },
+ annotations: [],
+ layout: {
+ kind: 'GridLayout',
+ spec: {
+ items: [
+ {
+ kind: 'GridLayoutItem',
+ spec: {
+ x: 6,
+ y: 0,
+ width: 12,
+ height: 6,
+ element: {
+ kind: 'ElementReference',
+ name: 'text_panel',
+ },
+ },
+ },
+ ],
+ },
+ },
+};
diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts
index 22b527b5bdc..db5a64048c7 100644
--- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts
+++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts
@@ -10,11 +10,18 @@ import { startMeasure, stopMeasure } from 'app/core/utils/metrics';
import { AnnoKeyFolder } from 'app/features/apiserver/types';
import { ResponseTransformers } from 'app/features/dashboard/api/ResponseTransformers';
import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types';
+import { isDashboardV2Spec } from 'app/features/dashboard/api/utils';
import { dashboardLoaderSrv, DashboardLoaderSrvV2 } from 'app/features/dashboard/services/DashboardLoaderSrv';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { emitDashboardViewEvent } from 'app/features/dashboard/state/analyticsProcessor';
import { trackDashboardSceneLoaded } from 'app/features/dashboard/utils/tracking';
-import { DashboardDTO, DashboardRoutes } from 'app/types';
+import {
+ DashboardDataDTO,
+ DashboardDTO,
+ DashboardRoutes,
+ HomeDashboardRedirectDTO,
+ isRedirectResponse,
+} from 'app/types';
import { PanelEditor } from '../panel-edit/PanelEditor';
import { DashboardScene } from '../scene/DashboardScene';
@@ -69,6 +76,10 @@ export interface LoadDashboardOptions {
};
}
+export type HomeDashboardDTO = DashboardDTO & {
+ dashboard: DashboardDataDTO | DashboardV2Spec;
+};
+
interface DashboardScenePageStateManagerLike {
fetchDashboard(options: LoadDashboardOptions): Promise;
getDashboardFromCache(cacheKey: string): T | null;
@@ -168,6 +179,11 @@ abstract class DashboardScenePageStateManagerBase
private async loadScene(options: LoadDashboardOptions): Promise {
this.setState({ dashboard: undefined, isLoading: true });
const rsp = await this.fetchDashboard(options);
+
+ if (!rsp) {
+ return null;
+ }
+
return this.transformResponseToScene(rsp, options);
}
@@ -236,12 +252,6 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
return scene;
}
- if (rsp?.redirectUri) {
- const newUrl = locationUtil.stripBaseFromUrl(rsp.redirectUri);
- locationService.replace(newUrl);
- return null;
- }
-
throw new Error('Dashboard not found');
}
@@ -273,7 +283,7 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
}
}
- let rsp: DashboardDTO;
+ let rsp: DashboardDTO | HomeDashboardRedirectDTO;
try {
switch (route) {
@@ -282,10 +292,16 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
break;
case DashboardRoutes.Home:
- rsp = await getBackendSrv().get('/api/dashboards/home');
+ rsp = await getBackendSrv().get('/api/dashboards/home');
- if (rsp.redirectUri) {
- return rsp;
+ if (isRedirectResponse(rsp)) {
+ const newUrl = locationUtil.stripBaseFromUrl(rsp.redirectUri);
+ locationService.replace(newUrl);
+ return null;
+ }
+
+ if (isDashboardV2Spec(rsp.dashboard)) {
+ throw new Error('v2 dashboard spec is not supported. Enable useV2DashboardsAPI feature toggle');
}
if (rsp?.meta) {
@@ -458,13 +474,6 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan
return scene;
}
- // TOD)[schema v2]: Figure out redirect utl
- // if (rsp?.redirectUri) {
- // const newUrl = locationUtil.stripBaseFromUrl(rsp.redirectUri);
- // locationService.replace(newUrl);
- // return null;
- // }
-
throw new Error('Dashboard not found');
}
@@ -492,17 +501,25 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan
rsp = await buildNewDashboardSaveModelV2(urlFolderUid);
break;
case DashboardRoutes.Home:
- // throw new Error('Method not implemented.');
- const dto = await getBackendSrv().get('/api/dashboards/home');
+ const dto = await getBackendSrv().get('/api/dashboards/home');
+
+ if (isRedirectResponse(dto)) {
+ const newUrl = locationUtil.stripBaseFromUrl(dto.redirectUri);
+ locationService.replace(newUrl);
+ return null;
+ }
+
rsp = ResponseTransformers.ensureV2Response(dto);
+
+ // if custom home dashboard is v2 spec already, ignore the spec transformation
+ if (isDashboardV2Spec(dto.dashboard)) {
+ rsp.spec = dto.dashboard;
+ }
+
rsp.access.canSave = false;
rsp.access.canShare = false;
rsp.access.canStar = false;
- // if (rsp.redirectUri) {
- // return rsp;
- // }
-
break;
case DashboardRoutes.Public: {
return await this.dashboardLoader.loadDashboard('public', '', uid);
diff --git a/public/app/features/dashboard/api/ResponseTransformers.ts b/public/app/features/dashboard/api/ResponseTransformers.ts
index 1831507291b..535265f606d 100644
--- a/public/app/features/dashboard/api/ResponseTransformers.ts
+++ b/public/app/features/dashboard/api/ResponseTransformers.ts
@@ -103,13 +103,8 @@ export function ensureV2Response(
if (isDashboardResource(dto)) {
accessMeta = dto.access;
annotationsMeta = {
- [AnnoKeyCreatedBy]: dto.metadata.annotations?.[AnnoKeyCreatedBy],
- [AnnoKeyUpdatedBy]: dto.metadata.annotations?.[AnnoKeyUpdatedBy],
- [AnnoKeyUpdatedTimestamp]: dto.metadata.annotations?.[AnnoKeyUpdatedTimestamp],
- [AnnoKeyFolder]: dto.metadata.annotations?.[AnnoKeyFolder],
- [AnnoKeySlug]: dto.metadata.annotations?.[AnnoKeySlug],
+ ...dto.metadata.annotations,
[AnnoKeyDashboardGnetId]: dashboard.gnetId ?? undefined,
- [AnnoKeyDashboardIsSnapshot]: dto.metadata.annotations?.[AnnoKeyDashboardIsSnapshot],
};
creationTimestamp = dto.metadata.creationTimestamp;
labelsMeta = {
diff --git a/public/app/features/dashboard/api/publicDashboardApi.ts b/public/app/features/dashboard/api/publicDashboardApi.ts
index 063b55e4d79..40bda98f4c5 100644
--- a/public/app/features/dashboard/api/publicDashboardApi.ts
+++ b/public/app/features/dashboard/api/publicDashboardApi.ts
@@ -1,7 +1,7 @@
-import { BaseQueryFn, createApi } from '@reduxjs/toolkit/query/react';
-import { lastValueFrom } from 'rxjs';
+import { createApi } from '@reduxjs/toolkit/query/react';
-import { BackendSrvRequest, config, FetchError, getBackendSrv, isFetchError } from '@grafana/runtime/src';
+import { config, FetchError, isFetchError } from '@grafana/runtime/src';
+import { createBaseQuery } from 'app/api/createBaseQuery';
import { notifyApp } from 'app/core/actions';
import { createErrorNotification, createSuccessNotification } from 'app/core/copy/appNotification';
import { t } from 'app/core/internationalization';
@@ -19,39 +19,17 @@ import {
PublicDashboardListWithPaginationResponse,
} from 'app/features/manage-dashboards/types';
-type ReqOptions = {
- manageError?: (err: unknown) => { error: unknown };
- showErrorAlert?: boolean;
-};
-
function isFetchBaseQueryError(error: unknown): error is { error: FetchError } {
return typeof error === 'object' && error != null && 'error' in error;
}
-const backendSrvBaseQuery =
- ({ baseUrl }: { baseUrl: string }): BaseQueryFn =>
- async (requestOptions) => {
- try {
- const { data: responseData, ...meta } = await lastValueFrom(
- getBackendSrv().fetch({
- ...requestOptions,
- url: baseUrl + requestOptions.url,
- showErrorAlert: requestOptions.showErrorAlert,
- })
- );
- return { data: responseData, meta };
- } catch (error) {
- return requestOptions.manageError ? requestOptions.manageError(error) : { error };
- }
- };
-
-const getConfigError = (err: unknown) => ({
+export const getConfigError = (err: unknown) => ({
error: isFetchError(err) && err.data.messageId !== 'publicdashboards.notFound' ? err : null,
});
export const publicDashboardApi = createApi({
reducerPath: 'publicDashboardApi',
- baseQuery: backendSrvBaseQuery({ baseUrl: '/api' }),
+ baseQuery: createBaseQuery({ baseURL: '/api' }),
tagTypes: ['PublicDashboard', 'AuditTablePublicDashboard', 'UsersWithActiveSessions', 'ActiveUserDashboards'],
refetchOnMountOrArgChange: true,
endpoints: (builder) => ({
@@ -81,7 +59,7 @@ export const publicDashboardApi = createApi({
return {
url: `/dashboards/uid/${dashUid}/public-dashboards`,
method: 'POST',
- data: params.payload,
+ body: params.payload,
};
},
async onQueryStarted({ dashboard, payload: { share } }, { dispatch, queryFulfilled }) {
@@ -121,7 +99,7 @@ export const publicDashboardApi = createApi({
return {
url: `/dashboards/uid/${dashUid}/public-dashboards/${payload.uid}`,
method: 'PATCH',
- data: payload,
+ body: payload,
};
},
async onQueryStarted({ dashboard }, { dispatch, queryFulfilled }) {
@@ -153,7 +131,7 @@ export const publicDashboardApi = createApi({
return {
url: `/dashboards/uid/${dashUid}/public-dashboards/${payload.uid}`,
method: 'PATCH',
- data: payload,
+ body: payload,
};
},
async onQueryStarted({ dashboard, payload: { isEnabled } }, { dispatch, queryFulfilled }) {
@@ -193,7 +171,7 @@ export const publicDashboardApi = createApi({
return {
url: `/dashboards/uid/${dashUid}/public-dashboards/${payload.uid}`,
method: 'PATCH',
- data: payload,
+ body: payload,
};
},
async onQueryStarted({ dashboard, payload: { share } }, { dispatch, queryFulfilled }) {
diff --git a/public/app/features/dashboard/api/v2.ts b/public/app/features/dashboard/api/v2.ts
index 3a283071091..ec4fa199386 100644
--- a/public/app/features/dashboard/api/v2.ts
+++ b/public/app/features/dashboard/api/v2.ts
@@ -56,6 +56,11 @@ export class K8sDashboardV2API
} catch (e) {
throw new Error('Failed to load folder');
}
+ } else if (result.metadata.annotations && !result.metadata.annotations[AnnoKeyFolder]) {
+ // Set AnnoKeyFolder to empty string for top-level dashboards
+ // This ensures NestedFolderPicker correctly identifies it as being in the "Dashboard" root folder
+ // AnnoKeyFolder undefined -> top-level dashboard -> empty string
+ result.metadata.annotations[AnnoKeyFolder] = '';
}
// Depending on the ui components readiness, we might need to convert the response to v1
@@ -117,6 +122,8 @@ export class K8sDashboardV2API
}
if (obj.metadata.name) {
+ // remove resource version when updating
+ delete obj.metadata.resourceVersion;
return this.client.update(obj).then((v) => this.asSaveDashboardResponseDTO(v));
}
return await this.client.create(obj).then((v) => this.asSaveDashboardResponseDTO(v));
diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts
index 4ee7261cf02..23b36244519 100644
--- a/public/app/features/dashboard/state/initDashboard.ts
+++ b/public/app/features/dashboard/state/initDashboard.ts
@@ -24,6 +24,8 @@ import {
DashboardDTO,
DashboardInitPhase,
DashboardRoutes,
+ HomeDashboardRedirectDTO,
+ isRedirectResponse,
StoreState,
ThunkDispatch,
ThunkResult,
@@ -69,10 +71,10 @@ async function fetchDashboard(
}
// load home dash
- const dashDTO: DashboardDTO = await backendSrv.get('/api/dashboards/home');
+ const dashDTO = await backendSrv.get('/api/dashboards/home');
// if user specified a custom home dashboard redirect to that
- if (dashDTO.redirectUri) {
+ if (isRedirectResponse(dashDTO)) {
const newUrl = locationUtil.stripBaseFromUrl(dashDTO.redirectUri);
locationService.replace(newUrl);
return null;
diff --git a/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx b/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx
index 54dc5951b88..076e1664cb4 100644
--- a/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx
+++ b/public/app/features/explore/QueryLibrary/QueryTemplatesList.tsx
@@ -53,7 +53,7 @@ export function QueryTemplatesList(props: QueryTemplatesListProps) {
return uniqBy(loadQueryMetadataResult.value, 'datasourceName').map((row) => row.datasourceName);
}, [loadQueryMetadataResult.value]);
- if (error) {
+ if (error instanceof Error) {
return (
{error.message}
diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFiltersTags.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFiltersTags.tsx
index 0aabff724a4..10fa8befd62 100644
--- a/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFiltersTags.tsx
+++ b/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFiltersTags.tsx
@@ -3,7 +3,7 @@ import React from 'react';
import { useMount } from 'react-use';
import { GrafanaTheme2, SelectableValue, toOption } from '@grafana/data';
-import { AccessoryButton } from '@grafana/experimental';
+import { AccessoryButton } from '@grafana/plugin-ui';
import { Input, Select, Stack, useStyles2 } from '@grafana/ui';
import { randomId, SearchProps, Tag } from '../../../useSearch';
diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/jsonMarkup.js b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/jsonMarkup.js
index 2dcbb1ee895..7db0889e1e6 100644
--- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/jsonMarkup.js
+++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/jsonMarkup.js
@@ -67,7 +67,7 @@ function escape(str) {
return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
}
-module.exports = function (doc, styleFile) {
+export default function jsonMarkup(doc, styleFile) {
let indent = '';
const style = Stylize(styleFile);
@@ -130,4 +130,4 @@ module.exports = function (doc, styleFile) {
}
return '' + visit(doc) + '
';
-};
+}
diff --git a/public/app/features/explore/TraceView/components/settings/SpanBarSettings.tsx b/public/app/features/explore/TraceView/components/settings/SpanBarSettings.tsx
index a612dee380d..11710cef528 100644
--- a/public/app/features/explore/TraceView/components/settings/SpanBarSettings.tsx
+++ b/public/app/features/explore/TraceView/components/settings/SpanBarSettings.tsx
@@ -7,7 +7,7 @@ import {
toOption,
updateDatasourcePluginJsonDataOption,
} from '@grafana/data';
-import { ConfigDescriptionLink, ConfigSubSection } from '@grafana/experimental';
+import { ConfigDescriptionLink, ConfigSubSection } from '@grafana/plugin-ui';
import { InlineField, InlineFieldRow, Input, Select, useStyles2 } from '@grafana/ui';
export interface SpanBarOptions {
diff --git a/public/app/features/migrate-to-cloud/api/baseAPI.ts b/public/app/features/migrate-to-cloud/api/baseAPI.ts
index 6a140906631..60c18dfd3e3 100644
--- a/public/app/features/migrate-to-cloud/api/baseAPI.ts
+++ b/public/app/features/migrate-to-cloud/api/baseAPI.ts
@@ -1,38 +1,9 @@
-import { BaseQueryFn, createApi } from '@reduxjs/toolkit/query/react';
-import { lastValueFrom } from 'rxjs';
+import { createApi } from '@reduxjs/toolkit/query/react';
-import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime';
-
-interface RequestOptions extends BackendSrvRequest {
- manageError?: (err: unknown) => { error: unknown };
- showErrorAlert?: boolean;
-
- // rtk codegen sets this
- body?: BackendSrvRequest['data'];
-}
-
-function createBackendSrvBaseQuery({ baseURL }: { baseURL: string }): BaseQueryFn {
- async function backendSrvBaseQuery(requestOptions: RequestOptions) {
- try {
- const { data: responseData, ...meta } = await lastValueFrom(
- getBackendSrv().fetch({
- ...requestOptions,
- url: baseURL + requestOptions.url,
- showErrorAlert: false,
- data: requestOptions.body,
- })
- );
- return { data: responseData, meta };
- } catch (error) {
- return requestOptions.manageError ? requestOptions.manageError(error) : { error };
- }
- }
-
- return backendSrvBaseQuery;
-}
+import { createBaseQuery } from 'app/api/createBaseQuery';
export const baseAPI = createApi({
reducerPath: 'migrateToCloudGeneratedAPI',
- baseQuery: createBackendSrvBaseQuery({ baseURL: '/api' }),
+ baseQuery: createBaseQuery({ baseURL: '/api' }),
endpoints: () => ({}),
});
diff --git a/public/app/features/migrate-to-cloud/api/index.ts b/public/app/features/migrate-to-cloud/api/index.ts
index 60a2d7a7b87..0e5c1d8b453 100644
--- a/public/app/features/migrate-to-cloud/api/index.ts
+++ b/public/app/features/migrate-to-cloud/api/index.ts
@@ -1,11 +1,14 @@
-export * from './endpoints.gen';
import { BaseQueryFn, EndpointDefinition } from '@reduxjs/toolkit/query';
import { getLocalPlugins } from 'app/features/plugins/admin/api';
import { LocalPlugin } from 'app/features/plugins/admin/types';
+import { handleRequestError } from '../../../api/createBaseQuery';
+
import { generatedAPI } from './endpoints.gen';
+export * from './endpoints.gen';
+
export const cloudMigrationAPI = generatedAPI
.injectEndpoints({
endpoints: (build) => ({
@@ -16,7 +19,7 @@ export const cloudMigrationAPI = generatedAPI
const list = await getLocalPlugins();
return { data: list };
} catch (error) {
- return { error: error };
+ return handleRequestError(error);
}
},
}),
diff --git a/public/app/features/preferences/api/user/baseAPI.ts b/public/app/features/preferences/api/user/baseAPI.ts
index b1b75e7b036..33316419f6e 100644
--- a/public/app/features/preferences/api/user/baseAPI.ts
+++ b/public/app/features/preferences/api/user/baseAPI.ts
@@ -1,36 +1,9 @@
-import { BaseQueryFn, createApi } from '@reduxjs/toolkit/query/react';
-import { lastValueFrom } from 'rxjs';
+import { createApi } from '@reduxjs/toolkit/query/react';
-import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime';
-
-interface RequestOptions extends BackendSrvRequest {
- manageError?: (err: unknown) => { error: unknown };
- showErrorAlert?: boolean;
- body?: BackendSrvRequest['data'];
-}
-
-function createBackendSrvBaseQuery({ baseURL }: { baseURL: string }): BaseQueryFn {
- async function backendSrvBaseQuery(requestOptions: RequestOptions) {
- try {
- const { data: responseData, ...meta } = await lastValueFrom(
- getBackendSrv().fetch({
- ...requestOptions,
- url: baseURL + requestOptions.url,
- showErrorAlert: requestOptions.showErrorAlert,
- data: requestOptions.body,
- })
- );
- return { data: responseData, meta };
- } catch (error) {
- return requestOptions.manageError ? requestOptions.manageError(error) : { error };
- }
- }
-
- return backendSrvBaseQuery;
-}
+import { createBaseQuery } from 'app/api/createBaseQuery';
export const baseAPI = createApi({
reducerPath: 'userPreferencesAPI',
- baseQuery: createBackendSrvBaseQuery({ baseURL: '/api' }),
+ baseQuery: createBaseQuery({ baseURL: '/api' }),
endpoints: () => ({}),
});
diff --git a/public/app/features/query-library/api/factory.ts b/public/app/features/query-library/api/factory.ts
index 00e6115db7d..d03074092d6 100644
--- a/public/app/features/query-library/api/factory.ts
+++ b/public/app/features/query-library/api/factory.ts
@@ -1,12 +1,14 @@
import { createApi } from '@reduxjs/toolkit/query/react';
-import { baseQuery } from './query';
+import { createBaseQuery } from '../../../api/createBaseQuery';
+
+import { BASE_URL } from './query';
// Currently, we are loading all query templates
// Organizations can have maximum of 1000 query templates
export const QUERY_LIBRARY_GET_LIMIT = 1000;
export const queryLibraryApi = createApi({
- baseQuery,
+ baseQuery: createBaseQuery({ baseURL: BASE_URL }),
endpoints: () => ({}),
});
diff --git a/public/app/features/query-library/api/query.ts b/public/app/features/query-library/api/query.ts
index 610c1556387..d0cc6db0209 100644
--- a/public/app/features/query-library/api/query.ts
+++ b/public/app/features/query-library/api/query.ts
@@ -1,8 +1,3 @@
-import { BaseQueryFn } from '@reduxjs/toolkit/query/react';
-import { lastValueFrom } from 'rxjs';
-
-import { BackendSrvRequest, getBackendSrv, isFetchError } from '@grafana/runtime/src/services/backendSrv';
-
import { getAPINamespace } from '../../../api/utils';
/**
@@ -23,29 +18,3 @@ export enum QueryTemplateKinds {
* @alpha
*/
export const BASE_URL = `/apis/${API_VERSION}/namespaces/${getAPINamespace()}`;
-
-interface QueryLibraryBackendRequest extends BackendSrvRequest {
- body?: BackendSrvRequest['data'];
-}
-
-export const baseQuery: BaseQueryFn = async (requestOptions) => {
- try {
- const responseObservable = getBackendSrv().fetch({
- url: `${BASE_URL}/${requestOptions.url ?? ''}`,
- showErrorAlert: true,
- method: requestOptions.method || 'GET',
- data: requestOptions.body,
- params: requestOptions.params,
- headers: { ...requestOptions.headers },
- });
- return await lastValueFrom(responseObservable);
- } catch (error) {
- if (isFetchError(error)) {
- return { error: new Error(error.data.message) };
- } else if (error instanceof Error) {
- return { error };
- } else {
- return { error: new Error('Unknown error') };
- }
- }
-};
diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/ConfigEditor.tsx b/public/app/plugins/datasource/grafana-pyroscope-datasource/ConfigEditor.tsx
index af6467d7896..3b2e3cbb5d0 100644
--- a/public/app/plugins/datasource/grafana-pyroscope-datasource/ConfigEditor.tsx
+++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/ConfigEditor.tsx
@@ -9,7 +9,7 @@ import {
ConnectionSettings,
DataSourceDescription,
convertLegacyAuthProps,
-} from '@grafana/experimental';
+} from '@grafana/plugin-ui';
import { config } from '@grafana/runtime';
import {
Divider,
diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/GroupByField.tsx b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/GroupByField.tsx
index 6e7ace1ca18..14134fec7ec 100644
--- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/GroupByField.tsx
+++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/GroupByField.tsx
@@ -3,7 +3,7 @@ import { useEffect, useMemo, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { GrafanaTheme2 } from '@grafana/data';
-import { AccessoryButton } from '@grafana/experimental';
+import { AccessoryButton } from '@grafana/plugin-ui';
import { Alert, HorizontalGroup, InputActionMeta, Select, useStyles2 } from '@grafana/ui';
import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen';
diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TagsInput.tsx b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TagsInput.tsx
index d5f69c33064..d9ecd049504 100644
--- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TagsInput.tsx
+++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TagsInput.tsx
@@ -3,7 +3,7 @@ import { useCallback, useEffect } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { GrafanaTheme2 } from '@grafana/data';
-import { AccessoryButton } from '@grafana/experimental';
+import { AccessoryButton } from '@grafana/plugin-ui';
import { FetchError } from '@grafana/runtime';
import { useStyles2 } from '@grafana/ui';
diff --git a/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx b/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx
index 7602cea1487..941bcaadceb 100644
--- a/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx
+++ b/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx
@@ -1,6 +1,13 @@
import { css } from '@emotion/css';
import { DataSourcePluginOptionsEditorProps, GrafanaTheme2 } from '@grafana/data';
+import {
+ NodeGraphSection,
+ SpanBarSection,
+ TraceToLogsSection,
+ TraceToMetricsSection,
+ TraceToProfilesSection,
+} from '@grafana/o11y-ds-frontend';
import {
AdvancedHttpSettings,
Auth,
@@ -10,14 +17,7 @@ import {
ConnectionSettings,
convertLegacyAuthProps,
DataSourceDescription,
-} from '@grafana/experimental';
-import {
- NodeGraphSection,
- SpanBarSection,
- TraceToLogsSection,
- TraceToMetricsSection,
- TraceToProfilesSection,
-} from '@grafana/o11y-ds-frontend';
+} from '@grafana/plugin-ui';
import { config } from '@grafana/runtime';
import { SecureSocksProxySettings, useStyles2, Divider, Stack } from '@grafana/ui';
diff --git a/public/app/plugins/datasource/tempo/configuration/StreamingSection.tsx b/public/app/plugins/datasource/tempo/configuration/StreamingSection.tsx
index bba18945603..e3d63e1ae26 100644
--- a/public/app/plugins/datasource/tempo/configuration/StreamingSection.tsx
+++ b/public/app/plugins/datasource/tempo/configuration/StreamingSection.tsx
@@ -7,7 +7,7 @@ import {
GrafanaTheme2,
updateDatasourcePluginJsonDataOption,
} from '@grafana/data';
-import { ConfigSection } from '@grafana/experimental';
+import { ConfigSection } from '@grafana/plugin-ui';
import { InlineFieldRow, InlineField, InlineSwitch, Alert, Stack, useStyles2 } from '@grafana/ui';
import { FeatureName, featuresToTempoVersion } from '../datasource';
diff --git a/public/app/plugins/datasource/tempo/configuration/TagLimitSettings.tsx b/public/app/plugins/datasource/tempo/configuration/TagLimitSettings.tsx
index edda2b69462..33768a47b9b 100644
--- a/public/app/plugins/datasource/tempo/configuration/TagLimitSettings.tsx
+++ b/public/app/plugins/datasource/tempo/configuration/TagLimitSettings.tsx
@@ -6,7 +6,7 @@ import {
GrafanaTheme2,
updateDatasourcePluginJsonDataOption,
} from '@grafana/data';
-import { ConfigDescriptionLink, ConfigSubSection } from '@grafana/experimental';
+import { ConfigDescriptionLink, ConfigSubSection } from '@grafana/plugin-ui';
import { InlineField, InlineFieldRow, Input, useStyles2 } from '@grafana/ui';
export interface TagLimitOptions extends DataSourceJsonData {
diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json
index 32e5bd74287..970b6048c84 100644
--- a/public/app/plugins/datasource/tempo/package.json
+++ b/public/app/plugins/datasource/tempo/package.json
@@ -7,11 +7,11 @@
"@emotion/css": "11.13.5",
"@grafana/data": "workspace:*",
"@grafana/e2e-selectors": "workspace:*",
- "@grafana/experimental": "2.1.6",
"@grafana/lezer-logql": "0.2.6",
"@grafana/lezer-traceql": "0.0.20",
"@grafana/monaco-logql": "^0.0.8",
"@grafana/o11y-ds-frontend": "workspace:*",
+ "@grafana/plugin-ui": "0.10.1",
"@grafana/runtime": "workspace:*",
"@grafana/schema": "workspace:*",
"@grafana/ui": "workspace:*",
@@ -32,7 +32,7 @@
"react-select": "5.10.0",
"react-use": "17.6.0",
"rxjs": "7.8.1",
- "semver": "7.6.3",
+ "semver": "7.7.0",
"stream-browserify": "3.0.0",
"string_decoder": "1.3.0",
"tslib": "2.8.1",
diff --git a/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx b/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx
index 5834a153594..a6d0ae61fcd 100644
--- a/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx
+++ b/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx
@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import * as React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
-import { EditorField, EditorRow } from '@grafana/experimental';
+import { EditorField, EditorRow } from '@grafana/plugin-ui';
import { AutoSizeInput, RadioButtonGroup, useStyles2 } from '@grafana/ui';
import { QueryOptionGroup } from '../_importedDependencies/datasources/prometheus/QueryOptionGroup';
diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts
index 5c36ad21ad1..a5705803382 100644
--- a/public/app/types/dashboard.ts
+++ b/public/app/types/dashboard.ts
@@ -4,8 +4,11 @@ import { ObjectMeta } from 'app/features/apiserver/types';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { ProvisioningPreview } from 'app/features/provisioning/types';
+export interface HomeDashboardRedirectDTO {
+ redirectUri: string;
+}
+
export interface DashboardDTO {
- redirectUri?: string;
dashboard: DashboardDataDTO;
meta: DashboardMeta;
}
@@ -145,3 +148,7 @@ export interface DashboardState {
}
export const DASHBOARD_FROM_LS_KEY = 'DASHBOARD_FROM_LS_KEY';
+
+export function isRedirectResponse(dto: DashboardDTO | HomeDashboardRedirectDTO): dto is HomeDashboardRedirectDTO {
+ return 'redirectUri' in dto;
+}
diff --git a/yarn.lock b/yarn.lock
index c930abae96b..2c6b0370c1e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3043,12 +3043,12 @@ __metadata:
"@emotion/css": "npm:11.13.5"
"@grafana/data": "workspace:*"
"@grafana/e2e-selectors": "workspace:*"
- "@grafana/experimental": "npm:2.1.6"
"@grafana/lezer-logql": "npm:0.2.6"
"@grafana/lezer-traceql": "npm:0.0.20"
"@grafana/monaco-logql": "npm:^0.0.8"
"@grafana/o11y-ds-frontend": "workspace:*"
"@grafana/plugin-configs": "npm:11.5.0-pre"
+ "@grafana/plugin-ui": "npm:0.10.1"
"@grafana/runtime": "workspace:*"
"@grafana/schema": "workspace:*"
"@grafana/ui": "workspace:*"
@@ -3083,7 +3083,7 @@ __metadata:
react-select-event: "npm:5.5.1"
react-use: "npm:17.6.0"
rxjs: "npm:7.8.1"
- semver: "npm:7.6.3"
+ semver: "npm:7.7.0"
stream-browserify: "npm:3.0.0"
string_decoder: "npm:1.3.0"
ts-node: "npm:10.9.2"
@@ -3272,7 +3272,7 @@ __metadata:
rollup-plugin-dts: "npm:^6.1.1"
rollup-plugin-esbuild: "npm:6.1.1"
rollup-plugin-node-externals: "npm:^8.0.0"
- semver: "npm:7.6.3"
+ semver: "npm:7.7.0"
tslib: "npm:2.8.1"
typescript: "npm:5.7.3"
languageName: unknown
@@ -3547,17 +3547,17 @@ __metadata:
languageName: unknown
linkType: soft
-"@grafana/plugin-e2e@npm:1.17.0":
- version: 1.17.0
- resolution: "@grafana/plugin-e2e@npm:1.17.0"
+"@grafana/plugin-e2e@npm:1.17.1":
+ version: 1.17.1
+ resolution: "@grafana/plugin-e2e@npm:1.17.1"
dependencies:
- "@grafana/e2e-selectors": "npm:^11.5.0-220285"
+ "@grafana/e2e-selectors": "npm:^11.5.0-221187"
semver: "npm:^7.5.4"
uuid: "npm:^11.0.2"
yaml: "npm:^2.3.4"
peerDependencies:
"@playwright/test": ^1.41.2
- checksum: 10/d24465857228cb9588777f92bd7bee8b5b7042a9375cb20a31b04e9af75b307248d3f17bd421eff0640ff9592612ef66e16f1e461af57ca7c2d680052fc02348
+ checksum: 10/d98b06fa04f59e07a8385fd81e8a7b456332d431aa529a0eb748087524f8b7b1bee867e616e01e7d1842b9e3c8dd508e8122fd5fb4d5a0a25162a4657b3dd4c0
languageName: node
linkType: hard
@@ -3713,7 +3713,7 @@ __metadata:
rxjs: "npm:7.8.1"
sass: "npm:1.83.4"
sass-loader: "npm:16.0.4"
- semver: "npm:7.6.3"
+ semver: "npm:7.7.0"
style-loader: "npm:4.0.0"
testing-library-selector: "npm:0.3.1"
ts-node: "npm:10.9.2"
@@ -9374,9 +9374,9 @@ __metadata:
linkType: hard
"@types/diff@npm:^7.0.0":
- version: 7.0.0
- resolution: "@types/diff@npm:7.0.0"
- checksum: 10/11464fe70a81a0d02fdaad4ff4b0a0a5ac71825fa32dde9a98a1a82b1cac50dda302fb1621bea71db6c254e00ce002ea14543cc3502ac44f012598ca2c0fbcae
+ version: 7.0.1
+ resolution: "@types/diff@npm:7.0.1"
+ checksum: 10/ef8c5fe0ea56737e8967c3db5e78518134f704e8e2971cb4ba6f2ed46ed5e13b4bbe41a6b2b78122b8f47c618ac25550f85a681633636a3a020369042523295d
languageName: node
linkType: hard
@@ -17817,7 +17817,7 @@ __metadata:
"@grafana/lezer-logql": "npm:0.2.7"
"@grafana/monaco-logql": "npm:^0.0.8"
"@grafana/o11y-ds-frontend": "workspace:*"
- "@grafana/plugin-e2e": "npm:1.17.0"
+ "@grafana/plugin-e2e": "npm:1.17.1"
"@grafana/plugin-ui": "npm:0.10.1"
"@grafana/prometheus": "workspace:*"
"@grafana/runtime": "workspace:*"
@@ -18095,13 +18095,13 @@ __metadata:
sass: "npm:1.83.4"
sass-loader: "npm:16.0.4"
selecto: "npm:1.26.3"
- semver: "npm:7.6.3"
+ semver: "npm:7.7.0"
slate: "npm:0.47.9"
slate-plain-serializer: "npm:0.7.13"
slate-react: "npm:0.22.10"
smtp-tester: "npm:^2.1.0"
style-loader: "npm:4.0.0"
- stylelint: "npm:16.13.2"
+ stylelint: "npm:16.14.1"
stylelint-config-sass-guidelines: "npm:12.1.0"
swagger-ui-react: "npm:5.18.3"
symbol-observable: "npm:4.0.0"
@@ -18949,7 +18949,7 @@ __metadata:
languageName: node
linkType: hard
-"ignore@npm:^7.0.1":
+"ignore@npm:^7.0.3":
version: 7.0.3
resolution: "ignore@npm:7.0.3"
checksum: 10/ce5e812af3acd6607a3fe0a9f9b5f01d53f009a5ace8cbf5b6491d05a481b55d65186e6a7eaa13126e93f15276bcf3d1e8d6ff3ce5549c312f9bb313fff33365
@@ -24754,7 +24754,7 @@ __metadata:
languageName: node
linkType: hard
-"postcss@npm:8.5.1, postcss@npm:^8.4.33, postcss@npm:^8.4.38, postcss@npm:^8.4.49":
+"postcss@npm:8.5.1, postcss@npm:^8.4.33, postcss@npm:^8.4.38, postcss@npm:^8.5.1":
version: 8.5.1
resolution: "postcss@npm:8.5.1"
dependencies:
@@ -26697,12 +26697,12 @@ __metadata:
linkType: hard
"react-zoom-pan-pinch@npm:^3.3.0":
- version: 3.6.1
- resolution: "react-zoom-pan-pinch@npm:3.6.1"
+ version: 3.7.0
+ resolution: "react-zoom-pan-pinch@npm:3.7.0"
peerDependencies:
react: "*"
react-dom: "*"
- checksum: 10/9146aa5c427dd6d0c8a4ebe3db0c720718eef6262d1b4b36033ee433bc76a9c84e30ca91311211ab95446305d3e2813d9abc576d093efbf5562be984431896cb
+ checksum: 10/5ae7f1ffea86fd19ae57f7b4c6818b282e13e00523200d23759db95a1518333044017583c2af3c179b2634c88c76162aa0ce8de79cd26359f07339415842ba34
languageName: node
linkType: hard
@@ -27855,12 +27855,12 @@ __metadata:
languageName: node
linkType: hard
-"semver@npm:7.6.3, semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.2, semver@npm:^7.6.3":
- version: 7.6.3
- resolution: "semver@npm:7.6.3"
+"semver@npm:7.7.0, semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.2, semver@npm:^7.6.3":
+ version: 7.7.0
+ resolution: "semver@npm:7.7.0"
bin:
semver: bin/semver.js
- checksum: 10/36b1fbe1a2b6f873559cd57b238f1094a053dbfd997ceeb8757d79d1d2089c56d1321b9f1069ce263dc64cfa922fa1d2ad566b39426fe1ac6c723c1487589e10
+ checksum: 10/5d615860a54ff563955c451e467bff3aaf74c8d060489f936f25551d5ca05f5ac683eb46c9ed7ade082e1e53b313f205ed9c5df0b25ebb3517ec25c79e1f0d9c
languageName: node
linkType: hard
@@ -29320,9 +29320,9 @@ __metadata:
languageName: node
linkType: hard
-"stylelint@npm:16.13.2, stylelint@npm:^16.8.2":
- version: 16.13.2
- resolution: "stylelint@npm:16.13.2"
+"stylelint@npm:16.14.1, stylelint@npm:^16.8.2":
+ version: 16.14.1
+ resolution: "stylelint@npm:16.14.1"
dependencies:
"@csstools/css-parser-algorithms": "npm:^3.0.4"
"@csstools/css-tokenizer": "npm:^3.0.3"
@@ -29342,7 +29342,7 @@ __metadata:
globby: "npm:^11.1.0"
globjoin: "npm:^0.1.4"
html-tags: "npm:^3.3.1"
- ignore: "npm:^7.0.1"
+ ignore: "npm:^7.0.3"
imurmurhash: "npm:^0.1.4"
is-plain-object: "npm:^5.0.0"
known-css-properties: "npm:^0.35.0"
@@ -29351,7 +29351,7 @@ __metadata:
micromatch: "npm:^4.0.8"
normalize-path: "npm:^3.0.0"
picocolors: "npm:^1.1.1"
- postcss: "npm:^8.4.49"
+ postcss: "npm:^8.5.1"
postcss-resolve-nested-selector: "npm:^0.1.6"
postcss-safe-parser: "npm:^7.0.1"
postcss-selector-parser: "npm:^7.0.0"
@@ -29364,7 +29364,7 @@ __metadata:
write-file-atomic: "npm:^5.0.1"
bin:
stylelint: bin/stylelint.mjs
- checksum: 10/98385b53d3c822b3b764fe8ff2f7212717127ab40ca9fd34a83bc6e27b5240d4ea02f959e01d4eaf91c87480a0c787b07b837883d4b3ec44133cc7ca03c79b47
+ checksum: 10/2c0c95e5ee77d946efdbe58573da75ff4b12bc89c220e78b69f0c2f94251f5e5a1096f5ab583f28f56ae14f298b1335e212d52fe066719d63b8a41d6cc8083b6
languageName: node
linkType: hard
@@ -30361,9 +30361,9 @@ __metadata:
linkType: hard
"type-fest@npm:^4.18.2, type-fest@npm:^4.26.1":
- version: 4.30.2
- resolution: "type-fest@npm:4.30.2"
- checksum: 10/c5168b159c366e4fd5b74c7f7b786bed9248c03f67e6e07d52dd5d51354447468fa7c92b9f2142c7fe9279814031f783959370242c3520de848931b65ddb48bb
+ version: 4.33.0
+ resolution: "type-fest@npm:4.33.0"
+ checksum: 10/0d179e66fa765bd0a25a785b12dc797f90f2f92bdb8c9c8a789f3fd8e5a4492444e7ef83551b3b8463aeab24fd6195761e26b03174722de636b4b75aa5726fb7
languageName: node
linkType: hard
@@ -31925,11 +31925,11 @@ __metadata:
linkType: hard
"yaml@npm:^2.0.0, yaml@npm:^2.3.4":
- version: 2.6.1
- resolution: "yaml@npm:2.6.1"
+ version: 2.7.0
+ resolution: "yaml@npm:2.7.0"
bin:
yaml: bin.mjs
- checksum: 10/cf412f03a33886db0a3aac70bb4165588f4c5b3c6f8fc91520b71491e5537800b6c2c73ed52015617f6e191eb4644c73c92973960a1999779c62a200ee4c231d
+ checksum: 10/c8c314c62fbd49244a6a51b06482f6d495b37ab10fa685fcafa1bbaae7841b7233ee7d12cab087bcca5a0b28adc92868b6e437322276430c28d00f1c1732eeec
languageName: node
linkType: hard