Merge remote-tracking branch 'origin/main' into grafana-git-ui-sync

This commit is contained in:
Ryan McKinley committed 2025-01-31 16:20:05 +03:00
commit aba47bd0e2
49 files changed
+779 -343

No files matched your search

@@ -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"}
@@ -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 |
+3 -3
View File
@@ -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",
@@ -248,6 +248,7 @@ export interface FeatureToggles {
improvedExternalSessionHandlingSAML?: boolean;
teamHttpHeadersMimir?: boolean;
ABTestFeatureToggleA?: boolean;
templateVariablesUsesCombobox?: boolean;
ABTestFeatureToggleB?: boolean;
queryLibraryDashboards?: boolean;
grafanaAdvisor?: boolean;
+1 -1
View File
@@ -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"
}
+1 -1
View File
@@ -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"
@@ -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 });
});
});
});
@@ -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() {
@@ -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);
});
});
+10 -1
View File
@@ -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(`(?<![\\w:])${ruleName}(?=[\\[{(\\s\\)]|$)`);
return getRuleRegex.test(query);
}
export function getExpandRulesHints(query: string, mapping: RuleQueryMapping): QueryHint[] {
const hints: QueryHint[] = [];
const mappingForQuery = Object.keys(mapping).reduce((acc, ruleName) => {
if (query.search(ruleName) === -1) {
if (!isRuleInQuery(query, ruleName)) {
return acc;
}
@@ -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 && <PromQueryBuilderExplained query={query.expr} />}
<QueryEditorHints query={query} datasource={datasource} data={data} onChange={onChange} onRunQuery={onRunQuery} />
</div>
);
}
-17
View File
@@ -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
}
+2 -2
View File
@@ -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)
}
+1 -1
View File
@@ -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)
}
+65 -31
View File
@@ -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)
}
-9
View File
@@ -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 {
+109
View File
@@ -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)
}
+7
View File
@@ -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",
+1
View File
@@ -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
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
229 improvedExternalSessionHandlingSAML preview @grafana/identity-access-team false false false
230 teamHttpHeadersMimir experimental @grafana/identity-access-team false false false
231 ABTestFeatureToggleA experimental @grafana/sharing-squad false false false
232 templateVariablesUsesCombobox experimental @grafana/grafana-frontend-platform false false true
233 ABTestFeatureToggleB experimental @grafana/sharing-squad false false false
234 queryLibraryDashboards experimental @grafana/grafana-frontend-platform false false false
235 grafanaAdvisor experimental @grafana/plugins-platform-backend false false false
+4
View File
@@ -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"
+13
View File
@@ -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",
+13 -6
View File
@@ -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{}
+43
View File
@@ -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<RequestOptions> {
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') };
}
}
@@ -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<DashboardTreeSelection, 'panel' | '$all'>;
}
@@ -66,28 +61,6 @@ interface HardDeleteDashboardArgs {
dashboardUID: string;
}
function createBackendSrvBaseQuery({ baseURL }: { baseURL: string }): BaseQueryFn<RequestOptions> {
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<FolderListItemDTO[], ListFolderQueryArgs>({
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<void, RestoreDashboardArgs>({
query: ({ dashboardUID, targetFolderUID }) => ({
url: `/dashboards/uid/${dashboardUID}/trash`,
data: {
body: {
folderUid: targetFolderUID,
},
method: 'PATCH',
@@ -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',
},
},
},
],
},
},
};
@@ -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<T> {
fetchDashboard(options: LoadDashboardOptions): Promise<T | null>;
getDashboardFromCache(cacheKey: string): T | null;
@@ -168,6 +179,11 @@ abstract class DashboardScenePageStateManagerBase<T>
private async loadScene(options: LoadDashboardOptions): Promise<DashboardScene | null> {
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<HomeDashboardDTO | HomeDashboardRedirectDTO>('/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<DashboardDTO>('/api/dashboards/home');
const dto = await getBackendSrv().get<HomeDashboardDTO | HomeDashboardRedirectDTO>('/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);
@@ -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 = {
@@ -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<BackendSrvRequest & ReqOptions> =>
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 }) {
+7
View File
@@ -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));
@@ -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<DashboardDTO | HomeDashboardRedirectDTO>('/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;
@@ -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 (
<EmptyState variant="not-found" message={`Something went wrong`}>
{error.message}
@@ -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';
@@ -67,7 +67,7 @@ function escape(str) {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
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 '<div ' + style('json-markup') + '>' + visit(doc) + '</div>';
};
}
@@ -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 {
@@ -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<RequestOptions> {
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: () => ({}),
});
@@ -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);
}
},
}),
@@ -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<RequestOptions> {
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: () => ({}),
});
@@ -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: () => ({}),
});
@@ -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<QueryLibraryBackendRequest, unknown, Error> = 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') };
}
}
};
@@ -9,7 +9,7 @@ import {
ConnectionSettings,
DataSourceDescription,
convertLegacyAuthProps,
} from '@grafana/experimental';
} from '@grafana/plugin-ui';
import { config } from '@grafana/runtime';
import {
Divider,
@@ -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';
@@ -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';
@@ -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';
@@ -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';
@@ -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 {
@@ -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",
@@ -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';
+8 -1
View File
@@ -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;
}
+36 -36
View File
@@ -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