Compare commits

..

4 Commits

Author SHA1 Message Date
renovate-sh-app[bot] 08d5c77ecc chore(deps): update dependency storybook to v10.1.10 [security]
| datasource | package   | from   | to      |
| ---------- | --------- | ------ | ------- |
| npm        | storybook | 8.6.15 | 10.1.10 |
| npm        | storybook | 10.0.8 | 10.1.10 |


Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com>
2026-01-15 00:35:03 +00:00
ismail simsek 7ae2eed876 Prometheus: Make sure "Min Step" has precedence (#115941)
* set minStep value as final step value when set explicitly.

* enhance it with tests

* improve function readability

* a bit more improvement for readability
2026-01-15 00:50:10 +01:00
Isabel Matwawana cddc4776ef Docs: Fix annotations step (#116302) 2026-01-14 22:09:03 +00:00
Renato Costa ec941b42ef unified-storage: lift name requirement in storage backend ListHistory (#116291) 2026-01-14 16:36:33 -05:00
8 changed files with 869 additions and 519 deletions
@@ -304,7 +304,8 @@ When things go bad, it often helps if you understand the context in which the fa
In the next part of the tutorial, we simulate some common use cases that someone would add annotations for.
1. To manually add an annotation, click anywhere in your graph, then click **Add annotation**.
1. To manually add an annotation, click anywhere on a graph line to open the data tooltip, then click **Add annotation**.
You can also press `Ctrl` or `Command` and click anywhere in the graph to open the **Add annotation** dialog box.
Note: you might need to save the dashboard first.
1. In **Description**, enter **Migrated user database**.
1. Click **Save**.
+1 -1
View File
@@ -200,7 +200,7 @@
"rollup-plugin-node-externals": "^8.0.0",
"rollup-plugin-svg-import": "3.0.0",
"sass-loader": "16.0.5",
"storybook": "^8.6.15",
"storybook": "^10.0.0",
"style-loader": "4.0.0",
"typescript": "5.9.2",
"webpack": "5.101.0"
+602
View File
@@ -0,0 +1,602 @@
package models
import (
"context"
"testing"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"github.com/grafana/grafana/pkg/promlib/intervalv2"
)
var (
testNow = time.Now()
testIntervalCalculator = intervalv2.NewCalculator()
testTracer = otel.Tracer("test/interval")
)
func TestCalculatePrometheusInterval(t *testing.T) {
_, span := testTracer.Start(context.Background(), "test")
defer span.End()
tests := []struct {
name string
queryInterval string
dsScrapeInterval string
intervalMs int64
intervalFactor int64
query backend.DataQuery
want time.Duration
wantErr bool
}{
{
name: "min step 2m with 300000 intervalMs",
queryInterval: "2m",
dsScrapeInterval: "",
intervalMs: 300000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(48 * time.Hour),
},
Interval: 5 * time.Minute,
MaxDataPoints: 761,
},
want: 2 * time.Minute,
wantErr: false,
},
{
name: "min step 2m with 900000 intervalMs",
queryInterval: "2m",
dsScrapeInterval: "",
intervalMs: 900000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(48 * time.Hour),
},
Interval: 15 * time.Minute,
MaxDataPoints: 175,
},
want: 2 * time.Minute,
wantErr: false,
},
{
name: "with step parameter",
queryInterval: "",
dsScrapeInterval: "15s",
intervalMs: 0,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(12 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 30 * time.Second,
wantErr: false,
},
{
name: "without step parameter",
queryInterval: "",
dsScrapeInterval: "15s",
intervalMs: 0,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(1 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 15 * time.Second,
wantErr: false,
},
{
name: "with high intervalFactor",
queryInterval: "",
dsScrapeInterval: "15s",
intervalMs: 0,
intervalFactor: 10,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(48 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 20 * time.Minute,
wantErr: false,
},
{
name: "with low intervalFactor",
queryInterval: "",
dsScrapeInterval: "15s",
intervalMs: 0,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(48 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 2 * time.Minute,
wantErr: false,
},
{
name: "with specified scrape-interval in data source",
queryInterval: "",
dsScrapeInterval: "240s",
intervalMs: 0,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(48 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 4 * time.Minute,
wantErr: false,
},
{
name: "with zero intervalFactor defaults to 1",
queryInterval: "",
dsScrapeInterval: "15s",
intervalMs: 0,
intervalFactor: 0,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(1 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 15 * time.Second,
wantErr: false,
},
{
name: "with $__interval variable",
queryInterval: "$__interval",
dsScrapeInterval: "15s",
intervalMs: 60000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(48 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 120 * time.Second,
wantErr: false,
},
{
name: "with ${__interval} variable",
queryInterval: "${__interval}",
dsScrapeInterval: "15s",
intervalMs: 60000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(48 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 120 * time.Second,
wantErr: false,
},
{
name: "with ${__interval} variable and explicit interval",
queryInterval: "1m",
dsScrapeInterval: "15s",
intervalMs: 60000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(48 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 1 * time.Minute,
wantErr: false,
},
{
name: "with $__rate_interval variable",
queryInterval: "$__rate_interval",
dsScrapeInterval: "30s",
intervalMs: 100000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(2 * 24 * time.Hour),
},
Interval: 100 * time.Second,
MaxDataPoints: 12384,
},
want: 130 * time.Second,
wantErr: false,
},
{
name: "with ${__rate_interval} variable",
queryInterval: "${__rate_interval}",
dsScrapeInterval: "30s",
intervalMs: 100000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(2 * 24 * time.Hour),
},
Interval: 100 * time.Second,
MaxDataPoints: 12384,
},
want: 130 * time.Second,
wantErr: false,
},
{
name: "intervalMs 100s, minStep override 150s and scrape interval 30s",
queryInterval: "150s",
dsScrapeInterval: "30s",
intervalMs: 100000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(2 * 24 * time.Hour),
},
Interval: 100 * time.Second,
MaxDataPoints: 12384,
},
want: 150 * time.Second,
wantErr: false,
},
{
name: "intervalMs 120s, minStep override 150s and ds scrape interval 30s",
queryInterval: "150s",
dsScrapeInterval: "30s",
intervalMs: 120000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(2 * 24 * time.Hour),
},
Interval: 120 * time.Second,
MaxDataPoints: 12384,
},
want: 150 * time.Second,
wantErr: false,
},
{
name: "intervalMs 120s, minStep auto (interval not overridden) and ds scrape interval 30s",
queryInterval: "120s",
dsScrapeInterval: "30s",
intervalMs: 120000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(2 * 24 * time.Hour),
},
Interval: 120 * time.Second,
MaxDataPoints: 12384,
},
want: 120 * time.Second,
wantErr: false,
},
{
name: "interval and minStep are automatically calculated and ds scrape interval 30s and time range 1 hour",
queryInterval: "30s",
dsScrapeInterval: "30s",
intervalMs: 30000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(1 * time.Hour),
},
Interval: 30 * time.Second,
MaxDataPoints: 12384,
},
want: 30 * time.Second,
wantErr: false,
},
{
name: "minStep is $__rate_interval and ds scrape interval 30s and time range 1 hour",
queryInterval: "$__rate_interval",
dsScrapeInterval: "30s",
intervalMs: 30000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(1 * time.Hour),
},
Interval: 30 * time.Second,
MaxDataPoints: 12384,
},
want: 2 * time.Minute,
wantErr: false,
},
{
name: "minStep is $__rate_interval and ds scrape interval 30s and time range 2 days",
queryInterval: "$__rate_interval",
dsScrapeInterval: "30s",
intervalMs: 120000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(2 * 24 * time.Hour),
},
Interval: 120 * time.Second,
MaxDataPoints: 12384,
},
want: 150 * time.Second,
wantErr: false,
},
{
name: "minStep is $__interval and ds scrape interval 15s and time range 2 days",
queryInterval: "$__interval",
dsScrapeInterval: "15s",
intervalMs: 120000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(2 * 24 * time.Hour),
},
Interval: 120 * time.Second,
MaxDataPoints: 12384,
},
want: 120 * time.Second,
wantErr: false,
},
{
name: "with empty dsScrapeInterval defaults to 15s",
queryInterval: "",
dsScrapeInterval: "",
intervalMs: 0,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(1 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 15 * time.Second,
wantErr: false,
},
{
name: "with very short time range",
queryInterval: "",
dsScrapeInterval: "15s",
intervalMs: 0,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(1 * time.Minute),
},
Interval: 1 * time.Minute,
},
want: 15 * time.Second,
wantErr: false,
},
{
name: "with very long time range",
queryInterval: "",
dsScrapeInterval: "15s",
intervalMs: 0,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(30 * 24 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 30 * time.Minute,
wantErr: false,
},
{
name: "with manual interval override",
queryInterval: "5m",
dsScrapeInterval: "15s",
intervalMs: 0,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(48 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 5 * time.Minute,
wantErr: false,
},
{
name: "minStep is auto and ds scrape interval 30s and time range 1 hour",
queryInterval: "",
dsScrapeInterval: "30s",
intervalMs: 30000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(1 * time.Hour),
},
Interval: 30 * time.Second,
MaxDataPoints: 1613,
},
want: 30 * time.Second,
wantErr: false,
},
{
name: "minStep is auto and ds scrape interval 15s and time range 5 minutes",
queryInterval: "",
dsScrapeInterval: "15s",
intervalMs: 15000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(5 * time.Minute),
},
Interval: 15 * time.Second,
MaxDataPoints: 1055,
},
want: 15 * time.Second,
wantErr: false,
},
// Additional test cases for better coverage
{
name: "with $__interval_ms variable",
queryInterval: "$__interval_ms",
dsScrapeInterval: "15s",
intervalMs: 60000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(48 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 120 * time.Second,
wantErr: false,
},
{
name: "with ${__interval_ms} variable",
queryInterval: "${__interval_ms}",
dsScrapeInterval: "15s",
intervalMs: 60000,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(48 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: 120 * time.Second,
wantErr: false,
},
{
name: "with MaxDataPoints zero",
queryInterval: "",
dsScrapeInterval: "15s",
intervalMs: 0,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(1 * time.Hour),
},
Interval: 1 * time.Minute,
MaxDataPoints: 0,
},
want: 15 * time.Second,
wantErr: false,
},
{
name: "with negative intervalFactor",
queryInterval: "",
dsScrapeInterval: "15s",
intervalMs: 0,
intervalFactor: -5,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(48 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: -10 * time.Minute,
wantErr: false,
},
{
name: "with invalid interval string that fails parsing",
queryInterval: "invalid-interval",
dsScrapeInterval: "15s",
intervalMs: 0,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(48 * time.Hour),
},
Interval: 1 * time.Minute,
},
want: time.Duration(0),
wantErr: true,
},
{
name: "with very small MaxDataPoints",
queryInterval: "",
dsScrapeInterval: "15s",
intervalMs: 0,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(1 * time.Hour),
},
Interval: 1 * time.Minute,
MaxDataPoints: 10,
},
want: 5 * time.Minute,
wantErr: false,
},
{
name: "when safeInterval is larger than calculatedInterval",
queryInterval: "",
dsScrapeInterval: "15s",
intervalMs: 0,
intervalFactor: 1,
query: backend.DataQuery{
TimeRange: backend.TimeRange{
From: testNow,
To: testNow.Add(1 * time.Hour),
},
Interval: 1 * time.Minute,
MaxDataPoints: 10000,
},
want: 15 * time.Second,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := calculatePrometheusInterval(
tt.queryInterval,
tt.dsScrapeInterval,
tt.intervalMs,
tt.intervalFactor,
tt.query,
testIntervalCalculator,
)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tt.want, got)
})
}
}
+126 -42
View File
@@ -92,7 +92,6 @@ const (
)
// Internal interval and range variables with {} syntax
// Repetitive code, we should have functionality to unify these
const (
varIntervalAlt = "${__interval}"
varIntervalMsAlt = "${__interval_ms}"
@@ -112,8 +111,16 @@ const (
UnknownQueryType TimeSeriesQueryType = "unknown"
)
// safeResolution is the maximum number of data points to prevent excessive resolution.
// This ensures queries don't exceed reasonable data point limits, improving performance
// and preventing potential memory issues. The value of 11000 provides a good balance
// between resolution and performance for most use cases.
var safeResolution = 11000
// rateIntervalMultiplier is the minimum multiplier for rate interval calculation.
// Rate intervals should be at least 4x the scrape interval to ensure accurate rate calculations.
const rateIntervalMultiplier = 4
// QueryModel includes both the common and specific values
// NOTE: this struct may have issues when decoding JSON that requires the special handling
// registered in https://github.com/grafana/grafana-plugin-sdk-go/blob/v0.228.0/experimental/apis/data/v0alpha1/query.go#L298
@@ -154,7 +161,7 @@ type Query struct {
// may be either a string or DataSourceRef
type internalQueryModel struct {
PrometheusQueryProperties `json:",inline"`
//sdkapi.CommonQueryProperties `json:",inline"`
// sdkapi.CommonQueryProperties `json:",inline"`
IntervalMS float64 `json:"intervalMs,omitempty"`
// The following properties may be part of the request payload, however they are not saved in panel JSON
@@ -272,44 +279,121 @@ func (query *Query) TimeRange() TimeRange {
}
}
// isRateIntervalVariable checks if the interval string is a rate interval variable
// ($__rate_interval, ${__rate_interval}, $__rate_interval_ms, or ${__rate_interval_ms})
func isRateIntervalVariable(interval string) bool {
return interval == varRateInterval ||
interval == varRateIntervalAlt ||
interval == varRateIntervalMs ||
interval == varRateIntervalMsAlt
}
// replaceVariable replaces both $__variable and ${__variable} formats in the expression
func replaceVariable(expr, dollarFormat, altFormat, replacement string) string {
expr = strings.ReplaceAll(expr, dollarFormat, replacement)
expr = strings.ReplaceAll(expr, altFormat, replacement)
return expr
}
// isManualIntervalOverride checks if the interval is a manually specified non-variable value
// that should override the calculated interval
func isManualIntervalOverride(interval string) bool {
return interval != "" &&
interval != varInterval &&
interval != varIntervalAlt &&
interval != varIntervalMs &&
interval != varIntervalMsAlt
}
// maxDuration returns the maximum of two durations
func maxDuration(a, b time.Duration) time.Duration {
if a > b {
return a
}
return b
}
// normalizeIntervalFactor ensures intervalFactor is at least 1
func normalizeIntervalFactor(factor int64) int64 {
if factor == 0 {
return 1
}
return factor
}
// calculatePrometheusInterval calculates the optimal step interval for a Prometheus query.
//
// The function determines the query step interval by considering multiple factors:
// - The minimum step specified in the query (queryInterval)
// - The data source scrape interval (dsScrapeInterval)
// - The requested interval in milliseconds (intervalMs)
// - The time range and maximum data points from the query
// - The interval factor multiplier
//
// Special handling:
// - Variable intervals ($__interval, $__rate_interval, etc.) are replaced with calculated values
// - Rate interval variables ($__rate_interval, ${__rate_interval}) use calculateRateInterval for proper rate() function support
// - Manual interval overrides (non-variable strings) take precedence over calculated values
// - The final interval ensures safe resolution limits are not exceeded
//
// Parameters:
// - queryInterval: The minimum step interval string (may contain variables like $__interval or $__rate_interval)
// - dsScrapeInterval: The data source scrape interval (e.g., "15s", "30s")
// - intervalMs: The requested interval in milliseconds
// - intervalFactor: Multiplier for the calculated interval (defaults to 1 if 0)
// - query: The backend data query containing time range and max data points
// - intervalCalculator: Calculator for determining optimal intervals
//
// Returns:
// - The calculated step interval as a time.Duration
// - An error if the interval cannot be calculated (e.g., invalid interval string)
func calculatePrometheusInterval(
queryInterval, dsScrapeInterval string,
intervalMs, intervalFactor int64,
query backend.DataQuery,
intervalCalculator intervalv2.Calculator,
) (time.Duration, error) {
// we need to compare the original query model after it is overwritten below to variables so that we can
// calculate the rateInterval if it is equal to $__rate_interval or ${__rate_interval}
// Preserve the original interval for later comparison, as it may be modified below
originalQueryInterval := queryInterval
// If we are using variable for interval/step, we will replace it with calculated interval
// If we are using a variable for minStep, replace it with empty string
// so that the interval calculation proceeds with the default logic
if isVariableInterval(queryInterval) {
queryInterval = ""
}
// Get the minimum interval from various sources (dsScrapeInterval, queryInterval, intervalMs)
minInterval, err := gtime.GetIntervalFrom(dsScrapeInterval, queryInterval, intervalMs, 15*time.Second)
if err != nil {
return time.Duration(0), err
}
// Calculate the optimal interval based on time range and max data points
calculatedInterval := intervalCalculator.Calculate(query.TimeRange, minInterval, query.MaxDataPoints)
// Calculate the safe interval to prevent too many data points
safeInterval := intervalCalculator.CalculateSafeInterval(query.TimeRange, int64(safeResolution))
adjustedInterval := safeInterval.Value
if calculatedInterval.Value > safeInterval.Value {
adjustedInterval = calculatedInterval.Value
}
// Use the larger of calculated or safe interval to ensure we don't exceed resolution limits
adjustedInterval := maxDuration(calculatedInterval.Value, safeInterval.Value)
// here is where we compare for $__rate_interval or ${__rate_interval}
if originalQueryInterval == varRateInterval || originalQueryInterval == varRateIntervalAlt {
// Handle rate interval variables: these require special calculation
if isRateIntervalVariable(originalQueryInterval) {
// Rate interval is final and is not affected by resolution
return calculateRateInterval(adjustedInterval, dsScrapeInterval), nil
} else {
queryIntervalFactor := intervalFactor
if queryIntervalFactor == 0 {
queryIntervalFactor = 1
}
return time.Duration(int64(adjustedInterval) * queryIntervalFactor), nil
}
// Handle manual interval override: if user specified a non-variable interval,
// it takes precedence over calculated values
if isManualIntervalOverride(originalQueryInterval) {
if parsedInterval, err := gtime.ParseIntervalStringToTimeDuration(originalQueryInterval); err == nil {
return parsedInterval, nil
}
// If parsing fails, fall through to calculated interval with factor
}
// Apply interval factor to the adjusted interval
normalizedFactor := normalizeIntervalFactor(intervalFactor)
return time.Duration(int64(adjustedInterval) * normalizedFactor), nil
}
// calculateRateInterval calculates the $__rate_interval value
@@ -331,7 +415,8 @@ func calculateRateInterval(
return time.Duration(0)
}
rateInterval := time.Duration(int64(math.Max(float64(queryInterval+scrapeIntervalDuration), float64(4)*float64(scrapeIntervalDuration))))
minRateInterval := rateIntervalMultiplier * scrapeIntervalDuration
rateInterval := maxDuration(queryInterval+scrapeIntervalDuration, minRateInterval)
return rateInterval
}
@@ -366,34 +451,33 @@ func InterpolateVariables(
rateInterval = calculateRateInterval(queryInterval, requestedMinStep)
}
expr = strings.ReplaceAll(expr, varIntervalMs, strconv.FormatInt(int64(calculatedStep/time.Millisecond), 10))
expr = strings.ReplaceAll(expr, varInterval, gtime.FormatInterval(calculatedStep))
expr = strings.ReplaceAll(expr, varRangeMs, strconv.FormatInt(rangeMs, 10))
expr = strings.ReplaceAll(expr, varRangeS, strconv.FormatInt(rangeSRounded, 10))
expr = strings.ReplaceAll(expr, varRange, strconv.FormatInt(rangeSRounded, 10)+"s")
expr = strings.ReplaceAll(expr, varRateIntervalMs, strconv.FormatInt(int64(rateInterval/time.Millisecond), 10))
expr = strings.ReplaceAll(expr, varRateInterval, rateInterval.String())
// Replace interval variables (both $__var and ${__var} formats)
expr = replaceVariable(expr, varIntervalMs, varIntervalMsAlt, strconv.FormatInt(int64(calculatedStep/time.Millisecond), 10))
expr = replaceVariable(expr, varInterval, varIntervalAlt, gtime.FormatInterval(calculatedStep))
// Replace range variables (both $__var and ${__var} formats)
expr = replaceVariable(expr, varRangeMs, varRangeMsAlt, strconv.FormatInt(rangeMs, 10))
expr = replaceVariable(expr, varRangeS, varRangeSAlt, strconv.FormatInt(rangeSRounded, 10))
expr = replaceVariable(expr, varRange, varRangeAlt, strconv.FormatInt(rangeSRounded, 10)+"s")
// Replace rate interval variables (both $__var and ${__var} formats)
expr = replaceVariable(expr, varRateIntervalMs, varRateIntervalMsAlt, strconv.FormatInt(int64(rateInterval/time.Millisecond), 10))
expr = replaceVariable(expr, varRateInterval, varRateIntervalAlt, rateInterval.String())
// Repetitive code, we should have functionality to unify these
expr = strings.ReplaceAll(expr, varIntervalMsAlt, strconv.FormatInt(int64(calculatedStep/time.Millisecond), 10))
expr = strings.ReplaceAll(expr, varIntervalAlt, gtime.FormatInterval(calculatedStep))
expr = strings.ReplaceAll(expr, varRangeMsAlt, strconv.FormatInt(rangeMs, 10))
expr = strings.ReplaceAll(expr, varRangeSAlt, strconv.FormatInt(rangeSRounded, 10))
expr = strings.ReplaceAll(expr, varRangeAlt, strconv.FormatInt(rangeSRounded, 10)+"s")
expr = strings.ReplaceAll(expr, varRateIntervalMsAlt, strconv.FormatInt(int64(rateInterval/time.Millisecond), 10))
expr = strings.ReplaceAll(expr, varRateIntervalAlt, rateInterval.String())
return expr
}
// isVariableInterval checks if the interval string is a variable interval
// (any of $__interval, ${__interval}, $__interval_ms, ${__interval_ms}, $__rate_interval, ${__rate_interval}, etc.)
func isVariableInterval(interval string) bool {
if interval == varInterval || interval == varIntervalMs || interval == varRateInterval || interval == varRateIntervalMs {
return true
}
// Repetitive code, we should have functionality to unify these
if interval == varIntervalAlt || interval == varIntervalMsAlt || interval == varRateIntervalAlt || interval == varRateIntervalMsAlt {
return true
}
return false
return interval == varInterval ||
interval == varIntervalAlt ||
interval == varIntervalMs ||
interval == varIntervalMsAlt ||
interval == varRateInterval ||
interval == varRateIntervalAlt ||
interval == varRateIntervalMs ||
interval == varRateIntervalMsAlt
}
// AlignTimeRange aligns query range to step and handles the time offset.
@@ -410,7 +494,7 @@ func AlignTimeRange(t time.Time, step time.Duration, offset int64) time.Time {
//go:embed query.types.json
var f embed.FS
// QueryTypeDefinitionsJSON returns the query type definitions
// QueryTypeDefinitionListJSON returns the query type definitions
func QueryTypeDefinitionListJSON() (json.RawMessage, error) {
return f.ReadFile("query.types.json")
}
+2 -322
View File
@@ -2,7 +2,6 @@ package models_test
import (
"context"
"fmt"
"reflect"
"testing"
"time"
@@ -14,6 +13,7 @@ import (
"go.opentelemetry.io/otel"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana/pkg/promlib/intervalv2"
"github.com/grafana/grafana/pkg/promlib/models"
)
@@ -50,95 +50,6 @@ func TestParse(t *testing.T) {
require.Equal(t, false, res.ExemplarQuery)
})
t.Run("parsing query model with step", func(t *testing.T) {
timeRange := backend.TimeRange{
From: now,
To: now.Add(12 * time.Hour),
}
q := queryContext(`{
"expr": "go_goroutines",
"format": "time_series",
"refId": "A"
}`, timeRange, time.Duration(1)*time.Minute)
res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false)
require.NoError(t, err)
require.Equal(t, time.Second*30, res.Step)
})
t.Run("parsing query model without step parameter", func(t *testing.T) {
timeRange := backend.TimeRange{
From: now,
To: now.Add(1 * time.Hour),
}
q := queryContext(`{
"expr": "go_goroutines",
"format": "time_series",
"intervalFactor": 1,
"refId": "A"
}`, timeRange, time.Duration(1)*time.Minute)
res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false)
require.NoError(t, err)
require.Equal(t, time.Second*15, res.Step)
})
t.Run("parsing query model with high intervalFactor", func(t *testing.T) {
timeRange := backend.TimeRange{
From: now,
To: now.Add(48 * time.Hour),
}
q := queryContext(`{
"expr": "go_goroutines",
"format": "time_series",
"intervalFactor": 10,
"refId": "A"
}`, timeRange, time.Duration(1)*time.Minute)
res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false)
require.NoError(t, err)
require.Equal(t, time.Minute*20, res.Step)
})
t.Run("parsing query model with low intervalFactor", func(t *testing.T) {
timeRange := backend.TimeRange{
From: now,
To: now.Add(48 * time.Hour),
}
q := queryContext(`{
"expr": "go_goroutines",
"format": "time_series",
"intervalFactor": 1,
"refId": "A"
}`, timeRange, time.Duration(1)*time.Minute)
res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false)
require.NoError(t, err)
require.Equal(t, time.Minute*2, res.Step)
})
t.Run("parsing query model specified scrape-interval in the data source", func(t *testing.T) {
timeRange := backend.TimeRange{
From: now,
To: now.Add(48 * time.Hour),
}
q := queryContext(`{
"expr": "go_goroutines",
"format": "time_series",
"intervalFactor": 1,
"refId": "A"
}`, timeRange, time.Duration(1)*time.Minute)
res, err := models.Parse(context.Background(), log.New(), span, q, "240s", intervalCalculator, false)
require.NoError(t, err)
require.Equal(t, time.Minute*4, res.Step)
})
t.Run("parsing query model with $__interval variable", func(t *testing.T) {
timeRange := backend.TimeRange{
From: now,
@@ -176,7 +87,7 @@ func TestParse(t *testing.T) {
res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false)
require.NoError(t, err)
require.Equal(t, "rate(ALERTS{job=\"test\" [2m]})", res.Expr)
require.Equal(t, "rate(ALERTS{job=\"test\" [1m]})", res.Expr)
})
t.Run("parsing query model with $__interval_ms variable", func(t *testing.T) {
@@ -533,232 +444,6 @@ func TestParse(t *testing.T) {
})
}
func TestRateInterval(t *testing.T) {
_, span := tracer.Start(context.Background(), "operation")
defer span.End()
type args struct {
expr string
interval string
intervalMs int64
dsScrapeInterval string
timeRange *backend.TimeRange
}
tests := []struct {
name string
args args
want *models.Query
}{
{
name: "intervalMs 100s, minStep override 150s and scrape interval 30s",
args: args{
expr: "rate(rpc_durations_seconds_count[$__rate_interval])",
interval: "150s",
intervalMs: 100000,
dsScrapeInterval: "30s",
},
want: &models.Query{
Expr: "rate(rpc_durations_seconds_count[10m0s])",
Step: time.Second * 150,
},
},
{
name: "intervalMs 120s, minStep override 150s and ds scrape interval 30s",
args: args{
expr: "rate(rpc_durations_seconds_count[$__rate_interval])",
interval: "150s",
intervalMs: 120000,
dsScrapeInterval: "30s",
},
want: &models.Query{
Expr: "rate(rpc_durations_seconds_count[10m0s])",
Step: time.Second * 150,
},
},
{
name: "intervalMs 120s, minStep auto (interval not overridden) and ds scrape interval 30s",
args: args{
expr: "rate(rpc_durations_seconds_count[$__rate_interval])",
interval: "120s",
intervalMs: 120000,
dsScrapeInterval: "30s",
},
want: &models.Query{
Expr: "rate(rpc_durations_seconds_count[8m0s])",
Step: time.Second * 120,
},
},
{
name: "interval and minStep are automatically calculated and ds scrape interval 30s and time range 1 hour",
args: args{
expr: "rate(rpc_durations_seconds_count[$__rate_interval])",
interval: "30s",
intervalMs: 30000,
dsScrapeInterval: "30s",
timeRange: &backend.TimeRange{
From: now,
To: now.Add(1 * time.Hour),
},
},
want: &models.Query{
Expr: "rate(rpc_durations_seconds_count[2m0s])",
Step: time.Second * 30,
},
},
{
name: "minStep is $__rate_interval and ds scrape interval 30s and time range 1 hour",
args: args{
expr: "rate(rpc_durations_seconds_count[$__rate_interval])",
interval: "$__rate_interval",
intervalMs: 30000,
dsScrapeInterval: "30s",
timeRange: &backend.TimeRange{
From: now,
To: now.Add(1 * time.Hour),
},
},
want: &models.Query{
Expr: "rate(rpc_durations_seconds_count[2m0s])",
Step: time.Minute * 2,
},
},
{
name: "minStep is $__rate_interval and ds scrape interval 30s and time range 2 days",
args: args{
expr: "rate(rpc_durations_seconds_count[$__rate_interval])",
interval: "$__rate_interval",
intervalMs: 120000,
dsScrapeInterval: "30s",
timeRange: &backend.TimeRange{
From: now,
To: now.Add(2 * 24 * time.Hour),
},
},
want: &models.Query{
Expr: "rate(rpc_durations_seconds_count[2m30s])",
Step: time.Second * 150,
},
},
{
name: "minStep is $__rate_interval and ds scrape interval 15s and time range 2 days",
args: args{
expr: "rate(rpc_durations_seconds_count[$__rate_interval])",
interval: "$__interval",
intervalMs: 120000,
dsScrapeInterval: "15s",
timeRange: &backend.TimeRange{
From: now,
To: now.Add(2 * 24 * time.Hour),
},
},
want: &models.Query{
Expr: "rate(rpc_durations_seconds_count[8m0s])",
Step: time.Second * 120,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
q := mockQuery(tt.args.expr, tt.args.interval, tt.args.intervalMs, tt.args.timeRange)
q.MaxDataPoints = 12384
res, err := models.Parse(context.Background(), log.New(), span, q, tt.args.dsScrapeInterval, intervalCalculator, false)
require.NoError(t, err)
require.Equal(t, tt.want.Expr, res.Expr)
require.Equal(t, tt.want.Step, res.Step)
})
}
t.Run("minStep is auto and ds scrape interval 30s and time range 1 hour", func(t *testing.T) {
query := backend.DataQuery{
RefID: "G",
QueryType: "",
MaxDataPoints: 1613,
Interval: 30 * time.Second,
TimeRange: backend.TimeRange{
From: now,
To: now.Add(1 * time.Hour),
},
JSON: []byte(`{
"datasource":{"type":"prometheus","uid":"zxS5e5W4k"},
"datasourceId":38,
"editorMode":"code",
"exemplar":false,
"expr":"sum(rate(process_cpu_seconds_total[$__rate_interval]))",
"instant":false,
"interval":"",
"intervalMs":30000,
"key":"Q-f96b6729-c47a-4ea8-8f71-a79774cf9bd5-0",
"legendFormat":"__auto",
"maxDataPoints":1613,
"range":true,
"refId":"G",
"requestId":"1G",
"utcOffsetSec":3600
}`),
}
res, err := models.Parse(context.Background(), log.New(), span, query, "30s", intervalCalculator, false)
require.NoError(t, err)
require.Equal(t, "sum(rate(process_cpu_seconds_total[2m0s]))", res.Expr)
require.Equal(t, 30*time.Second, res.Step)
})
t.Run("minStep is auto and ds scrape interval 15s and time range 5 minutes", func(t *testing.T) {
query := backend.DataQuery{
RefID: "A",
QueryType: "",
MaxDataPoints: 1055,
Interval: 15 * time.Second,
TimeRange: backend.TimeRange{
From: now,
To: now.Add(5 * time.Minute),
},
JSON: []byte(`{
"datasource": {
"type": "prometheus",
"uid": "2z9d6ElGk"
},
"editorMode": "code",
"expr": "sum(rate(cache_requests_total[$__rate_interval]))",
"legendFormat": "__auto",
"range": true,
"refId": "A",
"exemplar": false,
"requestId": "1A",
"utcOffsetSec": 0,
"interval": "",
"datasourceId": 508,
"intervalMs": 15000,
"maxDataPoints": 1055
}`),
}
res, err := models.Parse(context.Background(), log.New(), span, query, "15s", intervalCalculator, false)
require.NoError(t, err)
require.Equal(t, "sum(rate(cache_requests_total[1m0s]))", res.Expr)
require.Equal(t, 15*time.Second, res.Step)
})
}
func mockQuery(expr string, interval string, intervalMs int64, timeRange *backend.TimeRange) backend.DataQuery {
if timeRange == nil {
timeRange = &backend.TimeRange{
From: now,
To: now.Add(1 * time.Hour),
}
}
return backend.DataQuery{
Interval: time.Duration(intervalMs) * time.Millisecond,
JSON: []byte(fmt.Sprintf(`{
"expr": "%s",
"format": "time_series",
"interval": "%s",
"intervalMs": %v,
"intervalFactor": 1,
"refId": "A"
}`, expr, interval, intervalMs)),
TimeRange: *timeRange,
RefID: "A",
}
}
func queryContext(json string, timeRange backend.TimeRange, queryInterval time.Duration) backend.DataQuery {
return backend.DataQuery{
Interval: queryInterval,
@@ -768,11 +453,6 @@ func queryContext(json string, timeRange backend.TimeRange, queryInterval time.D
}
}
// AlignTimeRange aligns query range to step and handles the time offset.
// It rounds start and end down to a multiple of step.
// Prometheus caching is dependent on the range being aligned with the step.
// Rounding to the step can significantly change the start and end of the range for larger steps, i.e. a week.
// In rounding the range to a 1w step the range will always start on a Thursday.
func TestAlignTimeRange(t *testing.T) {
type args struct {
t time.Time
+96
View File
@@ -381,6 +381,102 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) {
})
}
func TestPrometheus_executedQueryString(t *testing.T) {
t.Run("executedQueryString should match expected format with intervalMs 300_000", func(t *testing.T) {
values := []p.SamplePair{
{Value: 1, Timestamp: 1000},
{Value: 2, Timestamp: 2000},
}
result := queryResult{
Type: p.ValMatrix,
Result: p.Matrix{
&p.SampleStream{
Metric: p.Metric{"app": "Application"},
Values: values,
},
},
}
queryJSON := `{
"expr": "test_metric",
"format": "time_series",
"intervalFactor": 1,
"interval": "2m",
"intervalMs": 300000,
"maxDataPoints": 761,
"refId": "A",
"range": true
}`
now := time.Now()
query := backend.DataQuery{
RefID: "A",
MaxDataPoints: 761,
Interval: 300000 * time.Millisecond,
TimeRange: backend.TimeRange{
From: now,
To: now.Add(48 * time.Hour),
},
JSON: []byte(queryJSON),
}
tctx, err := setup()
require.NoError(t, err)
res, err := execute(tctx, query, result, nil)
require.NoError(t, err)
require.Len(t, res, 1)
require.NotNil(t, res[0].Meta)
require.Equal(t, "Expr: test_metric\nStep: 2m0s", res[0].Meta.ExecutedQueryString)
})
t.Run("executedQueryString should match expected format with intervalMs 900_000", func(t *testing.T) {
values := []p.SamplePair{
{Value: 1, Timestamp: 1000},
{Value: 2, Timestamp: 2000},
}
result := queryResult{
Type: p.ValMatrix,
Result: p.Matrix{
&p.SampleStream{
Metric: p.Metric{"app": "Application"},
Values: values,
},
},
}
queryJSON := `{
"expr": "test_metric",
"format": "time_series",
"intervalFactor": 1,
"interval": "2m",
"intervalMs": 900000,
"maxDataPoints": 175,
"refId": "A",
"range": true
}`
now := time.Now()
query := backend.DataQuery{
RefID: "A",
MaxDataPoints: 175,
Interval: 900000 * time.Millisecond,
TimeRange: backend.TimeRange{
From: now,
To: now.Add(48 * time.Hour),
},
JSON: []byte(queryJSON),
}
tctx, err := setup()
require.NoError(t, err)
res, err := execute(tctx, query, result, nil)
require.NoError(t, err)
require.Len(t, res, 1)
require.NotNil(t, res[0].Meta)
require.Equal(t, "Expr: test_metric\nStep: 2m0s", res[0].Meta.ExecutedQueryString)
})
}
type queryResult struct {
Type p.ValueType `json:"resultType"`
Result any `json:"result"`
@@ -689,9 +689,6 @@ func validateListHistoryRequest(req *resourcepb.ListRequest) error {
if key.Namespace == "" {
return fmt.Errorf("namespace is required")
}
if key.Name == "" {
return fmt.Errorf("name is required")
}
return nil
}
+40 -150
View File
@@ -4132,7 +4132,7 @@ __metadata:
slate: "npm:0.47.9"
slate-plain-serializer: "npm:0.7.13"
slate-react: "npm:0.22.10"
storybook: "npm:^8.6.15"
storybook: "npm:^10.0.0"
style-loader: "npm:4.0.0"
tinycolor2: "npm:1.6.0"
tslib: "npm:2.8.1"
@@ -8500,54 +8500,6 @@ __metadata:
languageName: node
linkType: hard
"@storybook/core@npm:8.6.15":
version: 8.6.15
resolution: "@storybook/core@npm:8.6.15"
dependencies:
"@storybook/theming": "npm:8.6.15"
better-opn: "npm:^3.0.2"
browser-assert: "npm:^1.2.1"
esbuild: "npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0"
esbuild-register: "npm:^3.5.0"
jsdoc-type-pratt-parser: "npm:^4.0.0"
process: "npm:^0.11.10"
recast: "npm:^0.23.5"
semver: "npm:^7.6.2"
util: "npm:^0.12.5"
ws: "npm:^8.2.3"
peerDependencies:
prettier: ^2 || ^3
peerDependenciesMeta:
prettier:
optional: true
checksum: 10/d268d6fa00c38b35e5c363ee33779c2e087ab8e4681e0e205baa2fdb2780ea9feda3c9f6db35d60092778878d2782b2093c744bdf1af173c5688c3e1e0e960ac
languageName: node
linkType: hard
"@storybook/core@patch:@storybook/core@npm%3A8.6.15#~/.yarn/patches/@storybook-core-npm-8.6.15-a468a35170.patch":
version: 8.6.15
resolution: "@storybook/core@patch:@storybook/core@npm%3A8.6.15#~/.yarn/patches/@storybook-core-npm-8.6.15-a468a35170.patch::version=8.6.15&hash=c479fb"
dependencies:
"@storybook/theming": "npm:8.6.15"
better-opn: "npm:^3.0.2"
browser-assert: "npm:^1.2.1"
esbuild: "npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0"
esbuild-register: "npm:^3.5.0"
jsdoc-type-pratt-parser: "npm:^4.0.0"
process: "npm:^0.11.10"
recast: "npm:^0.23.5"
semver: "npm:^7.6.2"
util: "npm:^0.12.5"
ws: "npm:^8.2.3"
peerDependencies:
prettier: ^2 || ^3
peerDependenciesMeta:
prettier:
optional: true
checksum: 10/fd635098effe4ae87122ac706394dc89a26b3cae74d7d126897852a9da1960d617c76dc1be6d6d8a4c2d02c19e694c7222a3cfe7a6beb7daa605b30ffbf41644
languageName: node
linkType: hard
"@storybook/csf-plugin@npm:8.6.15":
version: 8.6.15
resolution: "@storybook/csf-plugin@npm:8.6.15"
@@ -8566,7 +8518,7 @@ __metadata:
languageName: node
linkType: hard
"@storybook/icons@npm:^1.2.12, @storybook/icons@npm:^1.6.0":
"@storybook/icons@npm:^1.2.12":
version: 1.6.0
resolution: "@storybook/icons@npm:1.6.0"
peerDependencies:
@@ -8576,6 +8528,16 @@ __metadata:
languageName: node
linkType: hard
"@storybook/icons@npm:^2.0.0":
version: 2.0.1
resolution: "@storybook/icons@npm:2.0.1"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
checksum: 10/04ffa285f4defc611def51f82492688bc49f6f4e8ce4e7ba5c99a1c1410b7e8820b5da65c33610a497df2409de7b48fae399052c5cacab6a4a4a9b48a36ebfd5
languageName: node
linkType: hard
"@storybook/instrumenter@npm:8.6.15":
version: 8.6.15
resolution: "@storybook/instrumenter@npm:8.6.15"
@@ -11868,25 +11830,6 @@ __metadata:
languageName: node
linkType: hard
"@vitest/mocker@npm:3.2.4":
version: 3.2.4
resolution: "@vitest/mocker@npm:3.2.4"
dependencies:
"@vitest/spy": "npm:3.2.4"
estree-walker: "npm:^3.0.3"
magic-string: "npm:^0.30.17"
peerDependencies:
msw: ^2.4.9
vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
peerDependenciesMeta:
msw:
optional: true
vite:
optional: true
checksum: 10/5e92431b6ed9fc1679060e4caef3e4623f4750542a5d7cd944774f8217c4d231e273202e8aea00bab33260a5a9222ecb7005d80da0348c3c829bd37d123071a8
languageName: node
linkType: hard
"@vitest/pretty-format@npm:2.0.5":
version: 2.0.5
resolution: "@vitest/pretty-format@npm:2.0.5"
@@ -13385,15 +13328,6 @@ __metadata:
languageName: node
linkType: hard
"better-opn@npm:^3.0.2":
version: 3.0.2
resolution: "better-opn@npm:3.0.2"
dependencies:
open: "npm:^8.0.4"
checksum: 10/24668e5a837d0d2c0edf17ad5ebcfeb00a8a5578a5eb09f7a409e1a60617cdfea40b8ebfc95e5f12d9568157930d033e6805788fcf0780413ac982c95d3745d1
languageName: node
linkType: hard
"bezier-easing@npm:^2.1.0":
version: 2.1.0
resolution: "bezier-easing@npm:2.1.0"
@@ -16459,9 +16393,9 @@ __metadata:
linkType: hard
"diff@npm:^8.0.0":
version: 8.0.3
resolution: "diff@npm:8.0.3"
checksum: 10/52f957e1fa53db4616ff5f4811b92b22b97a160c12a2f86f22debd4181227b0f6751aa8fd711d6a8fcf4618acb13b86bc702e6d9d6d6ed82acfd00c9cb26ace2
version: 8.0.2
resolution: "diff@npm:8.0.2"
checksum: 10/82a2120d3418f97822e17a6044ccd4b99a91e26e145e8698353673d7146bd2d092bbebb79c112aae7badc7b9c526f9098cbe342f96174feb6beabdd2587b3c42
languageName: node
linkType: hard
@@ -17314,18 +17248,7 @@ __metadata:
languageName: node
linkType: hard
"esbuild-register@npm:^3.5.0":
version: 3.5.0
resolution: "esbuild-register@npm:3.5.0"
dependencies:
debug: "npm:^4.3.4"
peerDependencies:
esbuild: ">=0.12 <1"
checksum: 10/af6874ce9b5fcdb0974c9d9e9f16530a5b9bd80c699b2ba9d7ace33439c1af1be6948535c775d9a6439e2bf23fb31cfd54ac882cfa38308a3f182039f4b98a01
languageName: node
linkType: hard
"esbuild@npm:0.25.8, esbuild@npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0, esbuild@npm:^0.25.0":
"esbuild@npm:0.25.8, esbuild@npm:^0.25.0":
version: 0.25.8
resolution: "esbuild@npm:0.25.8"
dependencies:
@@ -17414,7 +17337,7 @@ __metadata:
languageName: node
linkType: hard
"esbuild@npm:~0.27.0":
"esbuild@npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0, esbuild@npm:~0.27.0":
version: 0.27.2
resolution: "esbuild@npm:0.27.2"
dependencies:
@@ -19088,7 +19011,7 @@ __metadata:
languageName: node
linkType: hard
"fsevents@npm:2.3.2, fsevents@npm:^2.3.2, fsevents@npm:~2.3.2":
"fsevents@npm:2.3.2":
version: 2.3.2
resolution: "fsevents@npm:2.3.2"
dependencies:
@@ -19098,7 +19021,7 @@ __metadata:
languageName: node
linkType: hard
"fsevents@npm:~2.3.3":
"fsevents@npm:^2.3.2, fsevents@npm:~2.3.2, fsevents@npm:~2.3.3":
version: 2.3.3
resolution: "fsevents@npm:2.3.3"
dependencies:
@@ -19108,7 +19031,7 @@ __metadata:
languageName: node
linkType: hard
"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin<compat/fsevents>":
"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin<compat/fsevents>":
version: 2.3.2
resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin<compat/fsevents>::version=2.3.2&hash=df0bf1"
dependencies:
@@ -19117,7 +19040,7 @@ __metadata:
languageName: node
linkType: hard
"fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin<compat/fsevents>":
"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin<compat/fsevents>":
version: 2.3.3
resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin<compat/fsevents>::version=2.3.3&hash=df0bf1"
dependencies:
@@ -19331,16 +19254,7 @@ __metadata:
languageName: node
linkType: hard
"get-tsconfig@npm:^4.10.0, get-tsconfig@npm:^4.7.0":
version: 4.10.0
resolution: "get-tsconfig@npm:4.10.0"
dependencies:
resolve-pkg-maps: "npm:^1.0.0"
checksum: 10/5259b5c99a1957114337d9d0603b4a305ec9e29fa6cac7d2fbf634ba6754a0cc88bfd281a02416ce64e604b637d3cb239185381a79a5842b17fb55c097b38c4b
languageName: node
linkType: hard
"get-tsconfig@npm:^4.7.5":
"get-tsconfig@npm:^4.10.0, get-tsconfig@npm:^4.7.0, get-tsconfig@npm:^4.7.5":
version: 4.13.0
resolution: "get-tsconfig@npm:4.13.0"
dependencies:
@@ -23075,7 +22989,7 @@ __metadata:
languageName: node
linkType: hard
"jsdoc-type-pratt-parser@npm:^4.0.0, jsdoc-type-pratt-parser@npm:~4.1.0":
"jsdoc-type-pratt-parser@npm:~4.1.0":
version: 4.1.0
resolution: "jsdoc-type-pratt-parser@npm:4.1.0"
checksum: 10/30d88f95f6cbb4a1aa6d4b0d0ae46eb1096e606235ecaf9bab7a3ed5da860516b5d1cd967182765002f292c627526db918f3e56d34637bcf810e6ef84d403f3f
@@ -24205,7 +24119,7 @@ __metadata:
languageName: node
linkType: hard
"magic-string@npm:^0.30.17, magic-string@npm:^0.30.3, magic-string@npm:^0.30.5":
"magic-string@npm:^0.30.3, magic-string@npm:^0.30.5":
version: 0.30.21
resolution: "magic-string@npm:0.30.21"
dependencies:
@@ -26362,7 +26276,7 @@ __metadata:
languageName: node
linkType: hard
"open@npm:^8.0.4, open@npm:^8.4.0, open@npm:^8.4.2":
"open@npm:^8.4.0, open@npm:^8.4.2":
version: 8.4.2
resolution: "open@npm:8.4.2"
dependencies:
@@ -31732,20 +31646,21 @@ __metadata:
languageName: node
linkType: hard
"storybook@npm:^10.0.8":
version: 10.0.8
resolution: "storybook@npm:10.0.8"
"storybook@npm:^10.0.0, storybook@npm:^10.0.8":
version: 10.1.11
resolution: "storybook@npm:10.1.11"
dependencies:
"@storybook/global": "npm:^5.0.0"
"@storybook/icons": "npm:^1.6.0"
"@storybook/icons": "npm:^2.0.0"
"@testing-library/jest-dom": "npm:^6.6.3"
"@testing-library/user-event": "npm:^14.6.1"
"@vitest/expect": "npm:3.2.4"
"@vitest/mocker": "npm:3.2.4"
"@vitest/spy": "npm:3.2.4"
esbuild: "npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0"
esbuild: "npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0"
open: "npm:^10.2.0"
recast: "npm:^0.23.5"
semver: "npm:^7.6.2"
use-sync-external-store: "npm:^1.5.0"
ws: "npm:^8.18.0"
peerDependencies:
prettier: ^2 || ^3
@@ -31754,25 +31669,7 @@ __metadata:
optional: true
bin:
storybook: ./dist/bin/dispatcher.js
checksum: 10/a30b54248d69d7406f78e1871e075092ed9ae366d1c7cec56dc10b31b72a5196ef9e751453c9a7546797228c4fe0f01893c6fa541d24cf51037124d4f0e1566e
languageName: node
linkType: hard
"storybook@npm:^8.6.15":
version: 8.6.15
resolution: "storybook@npm:8.6.15"
dependencies:
"@storybook/core": "npm:8.6.15"
peerDependencies:
prettier: ^2 || ^3
peerDependenciesMeta:
prettier:
optional: true
bin:
getstorybook: ./bin/index.cjs
sb: ./bin/index.cjs
storybook: ./bin/index.cjs
checksum: 10/15762c79ec8444a46bc14cddfadbdd54dfd379828acd38555887a246c01e7c9ebb61e4eafafe04efb3ddf6278fb47035216e7f7d9f94fc205da148870173abdf
checksum: 10/1a5ac3d1c26b524aa92a44b1c513d3421a42c8ff4deca56c0698a8410a3e067cc6ecc3b57d2c10c79811dc488762de1f8b24230c1d4a198598eda20b7d376ede
languageName: node
linkType: hard
@@ -33874,12 +33771,12 @@ __metadata:
languageName: node
linkType: hard
"use-sync-external-store@npm:^1.0.0, use-sync-external-store@npm:^1.4.0":
version: 1.4.0
resolution: "use-sync-external-store@npm:1.4.0"
"use-sync-external-store@npm:^1.0.0, use-sync-external-store@npm:^1.4.0, use-sync-external-store@npm:^1.5.0":
version: 1.6.0
resolution: "use-sync-external-store@npm:1.6.0"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
checksum: 10/08bf581a8a2effaefc355e9d18ed025d436230f4cc973db2f593166df357cf63e47b9097b6e5089b594758bde322e1737754ad64905e030d70f8ff7ee671fd01
checksum: 10/b40ad2847ba220695bff2d4ba4f4d60391c0fb4fb012faa7a4c18eb38b69181936f5edc55a522c4d20a788d1a879b73c3810952c9d0fd128d01cb3f22042c09e
languageName: node
linkType: hard
@@ -33899,7 +33796,7 @@ __metadata:
languageName: node
linkType: hard
"util@npm:^0.12.0, util@npm:^0.12.4, util@npm:^0.12.5":
"util@npm:^0.12.0, util@npm:^0.12.4":
version: 0.12.5
resolution: "util@npm:0.12.5"
dependencies:
@@ -34938,7 +34835,7 @@ __metadata:
languageName: node
linkType: hard
"ws@npm:^8.18.0, ws@npm:^8.18.3, ws@npm:^8.2.3, ws@npm:^8.9.0":
"ws@npm:^8.18.0, ws@npm:^8.18.3, ws@npm:^8.9.0":
version: 8.18.3
resolution: "ws@npm:8.18.3"
peerDependencies:
@@ -35265,14 +35162,7 @@ __metadata:
languageName: node
linkType: hard
"zod@npm:^3.25 || ^4.0":
version: 4.1.13
resolution: "zod@npm:4.1.13"
checksum: 10/0679190318928f69fcb07751063719de232c663b13955fcdb55db59839569d39f3f29b955cb0cba7af0b724233f88c06b3e84c550397ad4e68f8088fa6799d88
languageName: node
linkType: hard
"zod@npm:^4.3.0":
"zod@npm:^3.25 || ^4.0, zod@npm:^4.3.0":
version: 4.3.5
resolution: "zod@npm:4.3.5"
checksum: 10/3148bd52e56ab7c1641ec397e6be6eddbb1d8f5db71e95baab9bb9622a0ea49d8a385885fc1c22b90fa6d8c5234e051f4ef5d469cfe3fb90198d5a91402fd89c