Merge remote-tracking branch 'origin/main' into grafana-git-ui-sync
This commit is contained in:
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
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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}
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
+2
-2
@@ -67,7 +67,7 @@ function escape(str) {
|
||||
return str.replace(/&/g, '&').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 '<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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user