Merge remote-tracking branch 'origin' into short-url-default-never-expire

This commit is contained in:
nmarrs
2025-12-09 09:31:44 -08:00
64 changed files with 579 additions and 153 deletions
@@ -768,6 +768,10 @@ VariableRefresh: *"never" | "onDashboardLoad" | "onTimeRangeChanged"
// Accepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing).
VariableHide: *"dontHide" | "hideLabel" | "hideVariable"
// Determine whether regex applies to variable value or display text
// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users)
VariableRegexApplyTo: *"value" | "text"
// Determine the origin of the adhoc variable filter
FilterOrigin: "dashboard"
@@ -803,6 +807,7 @@ QueryVariableSpec: {
datasource?: DataSourceRef
query: DataQueryKind
regex: string | *""
regexApplyTo?: VariableRegexApplyTo
sort: VariableSort
definition?: string
options: [...VariableOption] | *[]
@@ -772,6 +772,10 @@ VariableRefresh: *"never" | "onDashboardLoad" | "onTimeRangeChanged"
// Accepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing), `inControlsMenu` (show in a drop-down menu).
VariableHide: *"dontHide" | "hideLabel" | "hideVariable" | "inControlsMenu"
// Determine whether regex applies to variable value or display text
// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users)
VariableRegexApplyTo: *"value" | "text"
// Determine the origin of the adhoc variable filter
FilterOrigin: "dashboard"
@@ -806,6 +810,7 @@ QueryVariableSpec: {
description?: string
query: DataQueryKind
regex: string | *""
regexApplyTo?: VariableRegexApplyTo
sort: VariableSort
definition?: string
options: [...VariableOption] | *[]
@@ -222,6 +222,8 @@ lineage: schemas: [{
// Optional field, if you want to extract part of a series name or metric node segment.
// Named capture groups can be used to separate the display text and value.
regex?: string
// Determine whether regex applies to variable value or display text
regexApplyTo?: #VariableRegexApplyTo
// Additional static options for query variable
staticOptions?: [...#VariableOption]
// Ordering of static options in relation to options returned from data source for query variable
@@ -249,6 +251,10 @@ lineage: schemas: [{
// Accepted values are 0 (show label and value), 1 (show value only), 2 (show nothing), 3 (show under the controls dropdown menu).
#VariableHide: 0 | 1 | 2 | 3 @cuetsy(kind="enum",memberNames="dontHide|hideLabel|hideVariable|inControlsMenu") @grafana(TSVeneer="type")
// Determine whether regex applies to variable value or display text
// Accepted values are "value" (apply to value used in queries) or "text" (apply to display text shown to users)
#VariableRegexApplyTo: "value" | "text" @cuetsy(kind="type")
// Sort variable options
// Accepted values are:
// `0`: No sorting
@@ -222,6 +222,8 @@ lineage: schemas: [{
// Optional field, if you want to extract part of a series name or metric node segment.
// Named capture groups can be used to separate the display text and value.
regex?: string
// Determine whether regex applies to variable value or display text
regexApplyTo?: #VariableRegexApplyTo
// Additional static options for query variable
staticOptions?: [...#VariableOption]
// Ordering of static options in relation to options returned from data source for query variable
@@ -249,6 +251,10 @@ lineage: schemas: [{
// Accepted values are 0 (show label and value), 1 (show value only), 2 (show nothing), 3 (show under the controls dropdown menu).
#VariableHide: 0 | 1 | 2 | 3 @cuetsy(kind="enum",memberNames="dontHide|hideLabel|hideVariable|inControlsMenu") @grafana(TSVeneer="type")
// Determine whether regex applies to variable value or display text
// Accepted values are "value" (apply to value used in queries) or "text" (apply to display text shown to users)
#VariableRegexApplyTo: "value" | "text" @cuetsy(kind="type")
// Sort variable options
// Accepted values are:
// `0`: No sorting
@@ -772,6 +772,10 @@ VariableRefresh: *"never" | "onDashboardLoad" | "onTimeRangeChanged"
// Accepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing).
VariableHide: *"dontHide" | "hideLabel" | "hideVariable"
// Determine whether regex applies to variable value or display text
// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users)
VariableRegexApplyTo: *"value" | "text"
// Determine the origin of the adhoc variable filter
FilterOrigin: "dashboard"
@@ -807,6 +811,7 @@ QueryVariableSpec: {
datasource?: DataSourceRef
query: DataQueryKind
regex: string | *""
regexApplyTo?: VariableRegexApplyTo
sort: VariableSort
definition?: string
options: [...VariableOption] | *[]
@@ -1364,6 +1364,7 @@ type DashboardQueryVariableSpec struct {
Datasource *DashboardDataSourceRef `json:"datasource,omitempty"`
Query DashboardDataQueryKind `json:"query"`
Regex string `json:"regex"`
RegexApplyTo *DashboardVariableRegexApplyTo `json:"regexApplyTo,omitempty"`
Sort DashboardVariableSort `json:"sort"`
Definition *string `json:"definition,omitempty"`
Options []DashboardVariableOption `json:"options"`
@@ -1393,6 +1394,7 @@ func NewDashboardQueryVariableSpec() *DashboardQueryVariableSpec {
SkipUrlSync: false,
Query: *NewDashboardDataQueryKind(),
Regex: "",
RegexApplyTo: (func(input DashboardVariableRegexApplyTo) *DashboardVariableRegexApplyTo { return &input })(DashboardVariableRegexApplyToValue),
Options: []DashboardVariableOption{},
Multi: false,
IncludeAll: false,
@@ -1443,6 +1445,16 @@ const (
DashboardVariableRefreshOnTimeRangeChanged DashboardVariableRefresh = "onTimeRangeChanged"
)
// Determine whether regex applies to variable value or display text
// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users)
// +k8s:openapi-gen=true
type DashboardVariableRegexApplyTo string
const (
DashboardVariableRegexApplyToValue DashboardVariableRegexApplyTo = "value"
DashboardVariableRegexApplyToText DashboardVariableRegexApplyTo = "text"
)
// Sort variable options
// Accepted values are:
// `disabled`: No sorting
@@ -3646,6 +3646,12 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableSpec(ref common.Re
Format: "",
},
},
"regexApplyTo": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
"sort": {
SchemaProps: spec.SchemaProps{
Default: "",
@@ -776,6 +776,10 @@ VariableRefresh: *"never" | "onDashboardLoad" | "onTimeRangeChanged"
// Accepted values are `dontHide` (show label and value), `hideLabel` (show value only), `hideVariable` (show nothing), `inControlsMenu` (show in a drop-down menu).
VariableHide: *"dontHide" | "hideLabel" | "hideVariable" | "inControlsMenu"
// Determine whether regex applies to variable value or display text
// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users)
VariableRegexApplyTo: *"value" | "text"
// Determine the origin of the adhoc variable filter
FilterOrigin: "dashboard"
@@ -810,6 +814,7 @@ QueryVariableSpec: {
description?: string
query: DataQueryKind
regex: string | *""
regexApplyTo?: VariableRegexApplyTo
sort: VariableSort
definition?: string
options: [...VariableOption] | *[]
@@ -1367,6 +1367,7 @@ type DashboardQueryVariableSpec struct {
Description *string `json:"description,omitempty"`
Query DashboardDataQueryKind `json:"query"`
Regex string `json:"regex"`
RegexApplyTo *DashboardVariableRegexApplyTo `json:"regexApplyTo,omitempty"`
Sort DashboardVariableSort `json:"sort"`
Definition *string `json:"definition,omitempty"`
Options []DashboardVariableOption `json:"options"`
@@ -1396,6 +1397,7 @@ func NewDashboardQueryVariableSpec() *DashboardQueryVariableSpec {
SkipUrlSync: false,
Query: *NewDashboardDataQueryKind(),
Regex: "",
RegexApplyTo: (func(input DashboardVariableRegexApplyTo) *DashboardVariableRegexApplyTo { return &input })(DashboardVariableRegexApplyToValue),
Options: []DashboardVariableOption{},
Multi: false,
IncludeAll: false,
@@ -1447,6 +1449,16 @@ const (
DashboardVariableRefreshOnTimeRangeChanged DashboardVariableRefresh = "onTimeRangeChanged"
)
// Determine whether regex applies to variable value or display text
// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users)
// +k8s:openapi-gen=true
type DashboardVariableRegexApplyTo string
const (
DashboardVariableRegexApplyToValue DashboardVariableRegexApplyTo = "value"
DashboardVariableRegexApplyToText DashboardVariableRegexApplyTo = "text"
)
// Sort variable options
// Accepted values are:
// `disabled`: No sorting
@@ -3656,6 +3656,12 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardQueryVariableSpec(ref common.Ref
Format: "",
},
},
"regexApplyTo": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
"sort": {
SchemaProps: spec.SchemaProps{
Default: "",
File diff suppressed because one or more lines are too long
@@ -12,13 +12,6 @@ import (
)
func RegisterConversions(s *runtime.Scheme, dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) error {
// Wrap the provider once with 10s caching for all conversions.
// This prevents repeated DB queries across multiple conversion calls while allowing
// the cache to refresh periodically, making it suitable for long-lived singleton usage.
dsIndexProvider = schemaversion.WrapIndexProviderWithCache(dsIndexProvider)
// Wrap library element provider with caching as well
leIndexProvider = schemaversion.WrapLibraryElementProviderWithCache(leIndexProvider)
// v0 conversions
if err := s.AddConversionFunc((*dashv0.Dashboard)(nil), (*dashv1.Dashboard)(nil),
withConversionMetrics(dashv0.APIVERSION, dashv1.APIVERSION, func(a, b interface{}, scope conversion.Scope) error {
@@ -42,7 +42,7 @@
"regex": "",
"skipUrlSync": false,
"refresh": 1
},
},
{
"name": "query_var",
"type": "query",
@@ -81,6 +81,7 @@
"allValue": ".*",
"multi": true,
"regex": "/.*9090.*/",
"regexApplyTo": "text",
"skipUrlSync": false,
"refresh": 2,
"sort": 1,
@@ -107,7 +108,7 @@
},
{
"selected": false,
"text": "staging",
"text": "staging",
"value": "staging"
},
{
@@ -335,6 +336,7 @@
"allValue": "*",
"multi": true,
"regex": "/host[0-9]+/",
"regexApplyTo": "value",
"skipUrlSync": false,
"refresh": 1,
"sort": 2,
@@ -354,4 +356,4 @@
},
"links": []
}
}
}
@@ -94,6 +94,7 @@
"query": "label_values(up, instance)",
"refresh": 2,
"regex": "/.*9090.*/",
"regexApplyTo": "text",
"skipUrlSync": false,
"sort": 1,
"tagValuesQuery": "",
@@ -362,6 +363,7 @@
},
"refresh": 1,
"regex": "/host[0-9]+/",
"regexApplyTo": "value",
"skipUrlSync": false,
"sort": 2,
"tagValuesQuery": "",
@@ -110,6 +110,7 @@
}
},
"regex": "/.*9090.*/",
"regexApplyTo": "text",
"sort": "alphabeticalAsc",
"definition": "label_values(up, instance)",
"options": [
@@ -401,6 +402,7 @@
}
},
"regex": "/host[0-9]+/",
"regexApplyTo": "value",
"sort": "alphabeticalDesc",
"definition": "terms field:@host size:100",
"options": [],
@@ -111,6 +111,7 @@
}
},
"regex": "/.*9090.*/",
"regexApplyTo": "text",
"sort": "alphabeticalAsc",
"definition": "label_values(up, instance)",
"options": [
@@ -404,6 +405,7 @@
}
},
"regex": "/host[0-9]+/",
"regexApplyTo": "value",
"sort": "alphabeticalDesc",
"definition": "terms field:@host size:100",
"options": [],
@@ -229,6 +229,16 @@ func getBoolField(m map[string]interface{}, key string, defaultValue bool) bool
return defaultValue
}
func getUnionField[T ~string](m map[string]interface{}, key string) *T {
if val, ok := m[key]; ok {
if str, ok := val.(string); ok && str != "" {
result := T(str)
return &result
}
}
return nil
}
// Helper function to create int64 pointer
func int64Ptr(i int64) *int64 {
return &i
@@ -1195,6 +1205,7 @@ func buildQueryVariable(ctx context.Context, varMap map[string]interface{}, comm
Refresh: transformVariableRefreshToEnum(varMap["refresh"]),
Sort: transformVariableSortToEnum(varMap["sort"]),
Regex: schemaversion.GetStringValue(varMap, "regex"),
RegexApplyTo: getUnionField[dashv2alpha1.DashboardVariableRegexApplyTo](varMap, "regexApplyTo"),
Query: buildDataQueryKindForVariable(varMap["query"], datasourceType),
AllowCustomValue: getBoolField(varMap, "allowCustomValue", true),
},
@@ -1312,6 +1312,9 @@ func convertQueryVariableToV1(variable *dashv2alpha1.DashboardQueryVariableKind)
if spec.Definition != nil {
varMap["definition"] = *spec.Definition
}
if spec.RegexApplyTo != nil {
varMap["regexApplyTo"] = string(*spec.RegexApplyTo)
}
varMap["allowCustomValue"] = spec.AllowCustomValue
// Convert query - handle LEGACY_STRING_VALUE_KEY
@@ -767,6 +767,7 @@ func convertQueryVariableSpec_V2alpha1_to_V2beta1(in *dashv2alpha1.DashboardQuer
out.SkipUrlSync = in.SkipUrlSync
out.Description = in.Description
out.Regex = in.Regex
out.RegexApplyTo = (*dashv2beta1.DashboardVariableRegexApplyTo)(in.RegexApplyTo)
out.Sort = dashv2beta1.DashboardVariableSort(in.Sort)
out.Definition = in.Definition
out.Options = convertVariableOptions_V2alpha1_to_V2beta1(in.Options)
@@ -806,6 +806,7 @@ func convertQueryVariableSpec_V2beta1_to_V2alpha1(in *dashv2beta1.DashboardQuery
out.SkipUrlSync = in.SkipUrlSync
out.Description = in.Description
out.Regex = in.Regex
out.RegexApplyTo = (*dashv2alpha1.DashboardVariableRegexApplyTo)(in.RegexApplyTo)
out.Sort = dashv2alpha1.DashboardVariableSort(in.Sort)
out.Definition = in.Definition
out.Options = convertVariableOptions_V2beta1_to_V2alpha1(in.Options)
+7 -3
View File
@@ -61,9 +61,13 @@ type migrator struct {
func (m *migrator) init(dsIndexProvider schemaversion.DataSourceIndexProvider, leIndexProvider schemaversion.LibraryElementIndexProvider) {
initOnce.Do(func() {
m.dsIndexProvider = dsIndexProvider
m.leIndexProvider = leIndexProvider
m.migrations = schemaversion.GetMigrations(dsIndexProvider, leIndexProvider)
// Wrap the provider once with 10s caching for all conversions.
// This prevents repeated DB queries across multiple conversion calls while allowing
// the cache to refresh periodically, making it suitable for long-lived singleton usage.
m.dsIndexProvider = schemaversion.WrapIndexProviderWithCache(dsIndexProvider)
// Wrap library element provider with caching as well
m.leIndexProvider = schemaversion.WrapLibraryElementProviderWithCache(leIndexProvider)
m.migrations = schemaversion.GetMigrations(m.dsIndexProvider, m.leIndexProvider)
close(m.ready)
})
}
@@ -171,6 +171,7 @@ Query expressions are different for each data source. For more information, refe
- If you need more room in a single input field query editor, then hover your cursor over the lines in the lower right corner of the field and drag downward to expand.
1. (Optional) In the **Regex** field, type a regular expression to filter or capture specific parts of the names returned by your data source query. To see examples, refer to [Filter variables with a regular expression](#filter-variables-with-regex).
1. Under **Apply regex to**, select **Variable value** or **Display text** to choose where the regex pattern is applied. The default is **Variable value**.
1. In the **Sort** drop-down list, select the sort order for values to be displayed in the dropdown list. The default option, **Disabled**, means that the order of options returned by your data source query is used.
1. Under **Refresh**, select when the variable should update options:
- **On dashboard load** - Queries the data source every time the dashboard loads. This slows down dashboard loading, because the variable query needs to be completed before dashboard can be initialized.
@@ -79,6 +79,16 @@ test.describe(
await expect(regexInput).toHaveAttribute('placeholder', '/.*-(?<text>.*)-(?<value>.*)-.*/');
await expect(regexInput).toHaveValue('');
// Check regex apply to field - should default to "Variable value"
const regexApplyToField = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExApplyToSelectV2
);
await expect(regexApplyToField).toBeVisible();
const variableValueRadio = page.getByRole('radio', { name: 'Variable value' });
await expect(variableValueRadio).toBeChecked();
const displayTextRadio = page.getByRole('radio', { name: 'Display text' });
await expect(displayTextRadio).not.toBeChecked();
const sortSelect = dashboardPage.getByGrafanaSelector(
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2
);
-5
View File
@@ -2001,11 +2001,6 @@
"count": 1
}
},
"public/app/features/dashboard-scene/settings/variables/components/VariableTextAreaField.tsx": {
"no-restricted-syntax": {
"count": 1
}
},
"public/app/features/dashboard-scene/settings/variables/components/VariableTextField.tsx": {
"no-restricted-syntax": {
"count": 1
+6
View File
@@ -218,6 +218,8 @@ lineage: schemas: [{
// Optional field, if you want to extract part of a series name or metric node segment.
// Named capture groups can be used to separate the display text and value.
regex?: string
// Determine whether regex applies to variable value or display text
regexApplyTo?: #VariableRegexApplyTo
// Additional static options for query variable
staticOptions?: [...#VariableOption]
// Ordering of static options in relation to options returned from data source for query variable
@@ -245,6 +247,10 @@ lineage: schemas: [{
// Accepted values are 0 (show label and value), 1 (show value only), 2 (show nothing), 3 (show under the controls dropdown menu).
#VariableHide: 0 | 1 | 2 | 3 @cuetsy(kind="enum",memberNames="dontHide|hideLabel|hideVariable|inControlsMenu") @grafana(TSVeneer="type")
// Determine whether regex applies to variable value or display text
// Accepted values are "value" (apply to value used in queries) or "text" (apply to display text shown to users)
#VariableRegexApplyTo: "value" | "text" @cuetsy(kind="type")
// Sort variable options
// Accepted values are:
// `0`: No sorting
+2 -2
View File
@@ -296,8 +296,8 @@
"@grafana/plugin-ui": "^0.11.1",
"@grafana/prometheus": "workspace:*",
"@grafana/runtime": "workspace:*",
"@grafana/scenes": "6.47.1",
"@grafana/scenes-react": "6.47.1",
"@grafana/scenes": "6.49.0",
"@grafana/scenes-react": "6.49.0",
"@grafana/schema": "workspace:*",
"@grafana/sql": "workspace:*",
"@grafana/ui": "workspace:*",
+1
View File
@@ -521,6 +521,7 @@ export {
VariableRefresh,
VariableSort,
VariableHide,
type VariableRegexApplyTo,
type VariableType,
type VariableModel,
type TypedVariableModel,
+4
View File
@@ -1224,4 +1224,8 @@ export interface FeatureToggles {
* @default false
*/
useMTPlugins?: boolean;
/**
* Enables support for variables whose values can have multiple properties
*/
multiPropsVariables?: boolean;
}
@@ -32,6 +32,8 @@ export enum VariableRefresh {
onTimeRangeChanged,
}
export type VariableRegexApplyTo = 'value' | 'text';
export enum VariableSort {
disabled,
alphabeticalAsc,
@@ -117,6 +119,7 @@ export interface QueryVariableModel extends VariableWithMultiSupport {
queryValue?: string;
query: any;
regex: string;
regexApplyTo?: VariableRegexApplyTo;
refresh: VariableRefresh;
staticOptions?: VariableOption[];
staticOptionsOrder?: 'before' | 'after' | 'sorted';
@@ -508,6 +508,9 @@ export const versionedPages = {
queryOptionsRegExInputV2: {
[MIN_GRAFANA_VERSION]: 'data-testid Variable editor Form Query RegEx field',
},
queryOptionsRegExApplyToSelectV2: {
[MIN_GRAFANA_VERSION]: 'data-testid Variable editor Form Query RegExApplyTo select',
},
queryOptionsSortSelect: {
[MIN_GRAFANA_VERSION]: 'Variable editor Form Query Sort select',
},
+1
View File
@@ -12,6 +12,7 @@ export type {
AnnotationTarget,
AnnotationPanelFilter,
VariableOption,
VariableRegexApplyTo,
DashboardLink,
DashboardLinkType,
DashboardLinkPlacement,
@@ -187,6 +187,10 @@ export interface VariableModel {
* Named capture groups can be used to separate the display text and value.
*/
regex?: string;
/**
* Determine whether regex applies to variable value or display text
*/
regexApplyTo?: VariableRegexApplyTo;
/**
* Whether the variable value should be managed by URL query params or not
*/
@@ -259,6 +263,12 @@ export enum VariableHide {
inControlsMenu = 3,
}
/**
* Determine whether regex applies to variable value or display text
* Accepted values are "value" (apply to value used in queries) or "text" (apply to display text shown to users)
*/
export type VariableRegexApplyTo = ('value' | 'text');
/**
* Sort variable options
* Accepted values are:
@@ -293,6 +293,7 @@ export const handyTestingSchema: Spec = {
},
refresh: 'onDashboardLoad',
regex: 'regex1',
regexApplyTo: 'value',
skipUrlSync: false,
sort: 'disabled',
allowCustomValue: true,
@@ -1105,6 +1105,7 @@ export interface QueryVariableSpec {
datasource?: DataSourceRef;
query: DataQueryKind;
regex: string;
regexApplyTo?: VariableRegexApplyTo;
sort: VariableSort;
definition?: string;
options: VariableOption[];
@@ -1125,6 +1126,7 @@ export const defaultQueryVariableSpec = (): QueryVariableSpec => ({
skipUrlSync: false,
query: defaultDataQueryKind(),
regex: "",
regexApplyTo: "value",
sort: "disabled",
options: [],
multi: false,
@@ -1161,6 +1163,12 @@ export type VariableRefresh = "never" | "onDashboardLoad" | "onTimeRangeChanged"
export const defaultVariableRefresh = (): VariableRefresh => ("never");
// Determine whether regex applies to variable value or display text
// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users)
export type VariableRegexApplyTo = "value" | "text";
export const defaultVariableRegexApplyTo = (): VariableRegexApplyTo => ("value");
// Sort variable options
// Accepted values are:
// `disabled`: No sorting
@@ -1111,6 +1111,7 @@ export interface QueryVariableSpec {
description?: string;
query: DataQueryKind;
regex: string;
regexApplyTo?: VariableRegexApplyTo;
sort: VariableSort;
definition?: string;
options: VariableOption[];
@@ -1131,6 +1132,7 @@ export const defaultQueryVariableSpec = (): QueryVariableSpec => ({
skipUrlSync: false,
query: defaultDataQueryKind(),
regex: "",
regexApplyTo: "value",
sort: "disabled",
options: [],
multi: false,
@@ -1167,6 +1169,12 @@ export type VariableRefresh = "never" | "onDashboardLoad" | "onTimeRangeChanged"
export const defaultVariableRefresh = (): VariableRefresh => ("never");
// Determine whether regex applies to variable value or display text
// Accepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users)
export type VariableRegexApplyTo = "value" | "text";
export const defaultVariableRegexApplyTo = (): VariableRegexApplyTo => ("value");
// Sort variable options
// Accepted values are:
// `disabled`: No sorting
+11
View File
@@ -834,6 +834,8 @@ type VariableModel struct {
// Optional field, if you want to extract part of a series name or metric node segment.
// Named capture groups can be used to separate the display text and value.
Regex *string `json:"regex,omitempty"`
// Determine whether regex applies to variable value or display text
RegexApplyTo *VariableRegexApplyTo `json:"regexApplyTo,omitempty"`
// Additional static options for query variable
StaticOptions []VariableOption `json:"staticOptions,omitempty"`
// Ordering of static options in relation to options returned from data source for query variable
@@ -942,6 +944,15 @@ const (
VariableSortNaturalDesc VariableSort = 8
)
// Determine whether regex applies to variable value or display text
// Accepted values are "value" (apply to value used in queries) or "text" (apply to display text shown to users)
type VariableRegexApplyTo string
const (
VariableRegexApplyToValue VariableRegexApplyTo = "value"
VariableRegexApplyToText VariableRegexApplyTo = "text"
)
// Contains the list of annotations that are associated with the dashboard.
// Annotations are used to overlay event markers and overlay event tags on graphs.
// Grafana comes with a native annotation store and the ability to add annotation events directly from the graph panel or via the HTTP API.
+7
View File
@@ -2022,6 +2022,13 @@ var (
FrontendOnly: true,
Expression: "false",
},
{
Name: "multiPropsVariables",
Description: "Enables support for variables whose values can have multiple properties",
Stage: FeatureStageExperimental,
FrontendOnly: true,
Owner: grafanaDashboardsSquad,
},
}
)
@@ -244,7 +244,6 @@ addFieldFromCalculationStatFunctions,2023-11-03T14:39:58Z,2025-11-17T15:58:43Z,6
pdfTables,2023-11-06T13:39:22Z,,95b48339f89c7d267bce7d894404d26ccd75d0e3,Agnès Toulet
newVizTooltips,2023-11-06T16:35:59Z,2024-04-03T00:32:01Z,6b4b7127544865b78f712d907e6f1719595f4232,Adela Almasan
ssoSettingsApi,2023-11-08T09:50:01Z,2025-07-03T08:53:33Z,5285e9503be5702680acb2b52a6bda0632f4603d,Misi
logsInfiniteScrolling,2023-11-09T10:54:03Z,,174c2ab45a2af912519153c5c3e671f04396d7d7,Matias Chomicki
flameGraphItemCollapsing,2023-11-09T14:31:07Z,2024-07-15T12:45:41Z,494a07b522df4e3ff9512b47767745a94c15f080,Andrej Ocenas
alertingDetailsViewV2,2023-11-09T17:35:03Z,2024-03-14T14:18:01Z,323ee7c38ceb18b8e71c780d797fabac7041a673,Gilles De Mey
alertingSimplifiedRouting,2023-11-10T13:14:39Z,2025-05-09T13:30:56Z,68e37c3925080cf64a5e7570eabb042f19ca2dbf,Sonia Aguilar
1 #name created deleted hash author
244 pdfTables 2023-11-06T13:39:22Z 95b48339f89c7d267bce7d894404d26ccd75d0e3 Agnès Toulet
245 newVizTooltips 2023-11-06T16:35:59Z 2024-04-03T00:32:01Z 6b4b7127544865b78f712d907e6f1719595f4232 Adela Almasan
246 ssoSettingsApi 2023-11-08T09:50:01Z 2025-07-03T08:53:33Z 5285e9503be5702680acb2b52a6bda0632f4603d Misi
logsInfiniteScrolling 2023-11-09T10:54:03Z 174c2ab45a2af912519153c5c3e671f04396d7d7 Matias Chomicki
247 flameGraphItemCollapsing 2023-11-09T14:31:07Z 2024-07-15T12:45:41Z 494a07b522df4e3ff9512b47767745a94c15f080 Andrej Ocenas
248 alertingDetailsViewV2 2023-11-09T17:35:03Z 2024-03-14T14:18:01Z 323ee7c38ceb18b8e71c780d797fabac7041a673 Gilles De Mey
249 alertingSimplifiedRouting 2023-11-10T13:14:39Z 2025-05-09T13:30:56Z 68e37c3925080cf64a5e7570eabb042f19ca2dbf Sonia Aguilar
+1
View File
@@ -274,3 +274,4 @@ lokiQueryLimitsContext,experimental,@grafana/observability-logs,false,false,true
rudderstackUpgrade,experimental,@grafana/grafana-frontend-platform,false,false,true
kubernetesAlertingHistorian,experimental,@grafana/alerting-squad,false,true,false
useMTPlugins,experimental,@grafana/plugins-platform-backend,false,false,true
multiPropsVariables,experimental,@grafana/dashboards-squad,false,false,true
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
274 rudderstackUpgrade experimental @grafana/grafana-frontend-platform false false true
275 kubernetesAlertingHistorian experimental @grafana/alerting-squad false true false
276 useMTPlugins experimental @grafana/plugins-platform-backend false false true
277 multiPropsVariables experimental @grafana/dashboards-squad false false true
+13
View File
@@ -2272,6 +2272,19 @@
"codeowner": "@grafana/alerting-squad"
}
},
{
"metadata": {
"name": "multiPropsVariables",
"resourceVersion": "1765293230587",
"creationTimestamp": "2025-12-09T15:13:50Z"
},
"spec": {
"description": "Enables support for variables whose values can have multiple properties",
"stage": "experimental",
"codeowner": "@grafana/dashboards-squad",
"frontend": true
}
},
{
"metadata": {
"name": "multiTenantTempCredentials",
@@ -3020,6 +3020,9 @@
"type": "string",
"default": ""
},
"regexApplyTo": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableRegexApplyTo"
},
"skipUrlSync": {
"type": "boolean",
"default": false
@@ -3930,6 +3933,14 @@
"onTimeRangeChanged"
]
},
"com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableRegexApplyTo": {
"description": "Determine whether regex applies to variable value or display text\nAccepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users)",
"type": "string",
"enum": [
"value",
"text"
]
},
"com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableSort": {
"description": "Sort variable options\nAccepted values are:\n`disabled`: No sorting\n`alphabeticalAsc`: Alphabetical ASC\n`alphabeticalDesc`: Alphabetical DESC\n`numericalAsc`: Numerical ASC\n`numericalDesc`: Numerical DESC\n`alphabeticalCaseInsensitiveAsc`: Alphabetical Case Insensitive ASC\n`alphabeticalCaseInsensitiveDesc`: Alphabetical Case Insensitive DESC\n`naturalAsc`: Natural ASC\n`naturalDesc`: Natural DESC\nVariableSort enum with default value",
"type": "string",
@@ -3047,6 +3047,9 @@
"type": "string",
"default": ""
},
"regexApplyTo": {
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableRegexApplyTo"
},
"skipUrlSync": {
"type": "boolean",
"default": false
@@ -3957,6 +3960,14 @@
"onTimeRangeChanged"
]
},
"com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableRegexApplyTo": {
"description": "Determine whether regex applies to variable value or display text\nAccepted values are `value` (apply to value used in queries) or `text` (apply to display text shown to users)",
"type": "string",
"enum": [
"value",
"text"
]
},
"com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2beta1.DashboardVariableSort": {
"description": "Sort variable options\nAccepted values are:\n`disabled`: No sorting\n`alphabeticalAsc`: Alphabetical ASC\n`alphabeticalDesc`: Alphabetical DESC\n`numericalAsc`: Numerical ASC\n`numericalDesc`: Numerical DESC\n`alphabeticalCaseInsensitiveAsc`: Alphabetical Case Insensitive ASC\n`alphabeticalCaseInsensitiveDesc`: Alphabetical Case Insensitive DESC\n`naturalAsc`: Natural ASC\n`naturalDesc`: Natural DESC\nVariableSort enum with default value",
"type": "string",
@@ -323,6 +323,7 @@ exports[`Given a scene with custom quick ranges should save quick ranges to save
},
"refresh": 1,
"regex": "",
"regexApplyTo": "value",
"type": "query",
},
{
@@ -1049,6 +1050,7 @@ exports[`transformSceneToSaveModel Given a simple scene with custom settings Sho
},
"refresh": 1,
"regex": "",
"regexApplyTo": "value",
"type": "query",
},
{
@@ -1408,6 +1410,7 @@ exports[`transformSceneToSaveModel Given a simple scene with variables Should tr
},
"refresh": 1,
"regex": "",
"regexApplyTo": "value",
"type": "query",
},
{
@@ -173,6 +173,7 @@ exports[`transformSceneToSaveModelSchemaV2 should transform scene to save model
},
"refresh": "onDashboardLoad",
"regex": "regex1",
"regexApplyTo": "value",
"skipUrlSync": false,
"sort": "alphabeticalDesc",
},
@@ -141,6 +141,7 @@ describe('sceneVariablesSetToVariables', () => {
"query": "query",
"refresh": 1,
"regex": "",
"regexApplyTo": "value",
"staticOptions": [
{
"text": "test",
@@ -205,6 +206,7 @@ describe('sceneVariablesSetToVariables', () => {
"query": "query",
"refresh": 1,
"regex": "",
"regexApplyTo": "value",
"staticOptions": [
{
"text": "test",
@@ -1084,6 +1086,7 @@ describe('sceneVariablesSetToVariables', () => {
},
"refresh": "onDashboardLoad",
"regex": "",
"regexApplyTo": "value",
"skipUrlSync": false,
"sort": "disabled",
"staticOptions": [
@@ -84,6 +84,7 @@ export function sceneVariablesSetToVariables(set: SceneVariables, keepQueryOptio
sort: variable.state.sort,
refresh: variable.state.refresh,
regex: variable.state.regex,
regexApplyTo: variable.state.regexApplyTo,
allValue: variable.state.allValue,
includeAll: variable.state.includeAll,
multi: variable.state.isMulti,
@@ -375,6 +376,7 @@ export function sceneVariablesSetToSchemaV2Variables(
sort: transformSortVariableToEnum(variable.state.sort),
refresh: transformVariableRefreshToEnum(variable.state.refresh),
regex: variable.state.regex ?? '',
regexApplyTo: variable.state.regexApplyTo ?? 'value',
allValue: variable.state.allValue,
includeAll: variable.state.includeAll || false,
multi: variable.state.isMulti || false,
@@ -366,6 +366,7 @@ function createSceneVariableFromVariableModel(variable: TypedVariableModelV2): S
sort: transformSortVariableToEnumV1(variable.spec.sort),
refresh: transformVariableRefreshToEnumV1(variable.spec.refresh),
regex: variable.spec.regex,
regexApplyTo: variable.spec.regexApplyTo,
allValue: variable.spec.allValue || undefined,
includeAll: variable.spec.includeAll,
defaultToAll: Boolean(variable.spec.includeAll),
@@ -283,6 +283,7 @@ describe('transformSceneToSaveModelSchemaV2', () => {
sort: VariableSortV1.alphabeticalDesc,
refresh: VariableRefresh.onDashboardLoad,
regex: 'regex1',
regexApplyTo: 'value',
allValue: '*',
includeAll: true,
isMulti: true,
@@ -71,6 +71,7 @@ describe('QueryVariableEditorForm', () => {
const mockOnQueryChange = jest.fn();
const mockOnLegacyQueryChange = jest.fn();
const mockOnRegExChange = jest.fn();
const mockOnRegexApplyToChange = jest.fn();
const mockOnSortChange = jest.fn();
const mockOnRefreshChange = jest.fn();
const mockOnMultiChange = jest.fn();
@@ -89,6 +90,8 @@ describe('QueryVariableEditorForm', () => {
timeRange: getDefaultTimeRange(),
regex: '.*',
onRegExChange: mockOnRegExChange,
regexApplyTo: 'value',
onRegexApplyToChange: mockOnRegexApplyToChange,
sort: VariableSort.alphabeticalAsc,
onSortChange: mockOnSortChange,
refresh: VariableRefresh.onDashboardLoad,
@@ -126,6 +129,9 @@ describe('QueryVariableEditorForm', () => {
const regexInput = getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2
);
const regexApplyToSelect = getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExApplyToSelectV2
);
const sortSelect = getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2
);
@@ -154,6 +160,8 @@ describe('QueryVariableEditorForm', () => {
expect(dataSourcePicker.getAttribute('placeholder')).toBe('Default Test Data Source');
expect(regexInput).toBeInTheDocument();
expect(regexInput).toHaveValue('.*');
expect(regexApplyToSelect).toBeInTheDocument();
expect(getByRole('radio', { name: 'Variable value' })).toBeChecked();
expect(sortSelect).toBeInTheDocument();
expect(sortSelect).toHaveTextContent('Alphabetical (asc)');
expect(refreshSelect).toBeInTheDocument();
@@ -213,6 +221,21 @@ describe('QueryVariableEditorForm', () => {
).toBe('.?');
});
it('should call onRegexApplyToChange when selecting the regex apply to option', async () => {
const {
renderer: { getByTestId },
} = await setup();
const regexApplyToSelect = getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExApplyToSelectV2
);
await userEvent.click(regexApplyToSelect);
const anotherOption = screen.getByText('Display text');
await userEvent.click(anotherOption);
expect(mockOnRegexApplyToChange).toHaveBeenCalledTimes(1);
expect(mockOnRegexApplyToChange).toHaveBeenCalledWith('text');
});
it('should call onSortChange when changing the sort', async () => {
const {
renderer: { getByTestId },
@@ -1,14 +1,15 @@
import { FormEvent, useCallback } from 'react';
import { useAsync } from 'react-use';
import { DataSourceInstanceSettings, SelectableValue, TimeRange } from '@grafana/data';
import { DataSourceInstanceSettings, SelectableValue, TimeRange, VariableRegexApplyTo } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { getDataSourceSrv } from '@grafana/runtime';
import { QueryVariable } from '@grafana/scenes';
import { DataSourceRef, VariableRefresh, VariableSort } from '@grafana/schema';
import { Field, TextLink } from '@grafana/ui';
import { Field } from '@grafana/ui';
import { QueryEditor } from 'app/features/dashboard-scene/settings/variables/components/QueryEditor';
import { QueryVariableRegexForm } from 'app/features/dashboard-scene/settings/variables/components/QueryVariableRegexForm';
import { SelectionOptionsForm } from 'app/features/dashboard-scene/settings/variables/components/SelectionOptionsForm';
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
import { getVariableQueryEditor } from 'app/features/variables/editor/getVariableQueryEditor';
@@ -21,7 +22,6 @@ import {
} from 'app/features/variables/query/QueryVariableStaticOptions';
import { VariableLegend } from './VariableLegend';
import { VariableTextAreaField } from './VariableTextAreaField';
type VariableQueryType = QueryVariable['state']['query'];
@@ -34,6 +34,8 @@ interface QueryVariableEditorFormProps {
timeRange: TimeRange;
regex: string | null;
onRegExChange: (event: FormEvent<HTMLTextAreaElement>) => void;
regexApplyTo?: VariableRegexApplyTo;
onRegexApplyToChange?: (event: VariableRegexApplyTo) => void;
sort: VariableSort;
onSortChange: (option: SelectableValue<VariableSort>) => void;
refresh: VariableRefresh;
@@ -61,6 +63,8 @@ export function QueryVariableEditorForm({
timeRange,
regex,
onRegExChange,
regexApplyTo,
onRegexApplyToChange,
sort,
onSortChange,
refresh,
@@ -131,32 +135,11 @@ export function QueryVariableEditorForm({
/>
)}
<VariableTextAreaField
defaultValue={regex ?? ''}
name={t('dashboard-scene.query-variable-editor-form.name-regex', 'Regex')}
description={
<div>
<Trans i18nKey="dashboard-scene.query-variable-editor-form.description-optional">
Optional, if you want to extract part of a series name or metric node segment.
</Trans>
<br />
<Trans i18nKey="dashboard-scene.query-variable-editor-form.description-examples">
Named capture groups can be used to separate the display text and value (
<TextLink
href="https://grafana.com/docs/grafana/latest/variables/filter-variables-with-regex#filter-and-modify-using-named-text-and-value-capture-groups"
external
>
see examples
</TextLink>
).
</Trans>
</div>
}
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
placeholder="/.*-(?<text>.*)-(?<value>.*)-.*/"
onBlur={onRegExChange}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2}
width={52}
<QueryVariableRegexForm
regex={regex}
regexApplyTo={regexApplyTo}
onRegExChange={onRegExChange}
onRegexApplyToChange={onRegexApplyToChange}
/>
<QueryVariableSortSelect
@@ -0,0 +1,96 @@
import { render, fireEvent } from '@testing-library/react';
import { selectors } from '@grafana/e2e-selectors';
import { QueryVariableRegexForm } from './QueryVariableRegexForm';
describe('QueryVariableRegexForm', () => {
const onRegExChange = jest.fn();
const onRegexApplyToChange = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
it('should render the form fields correctly', () => {
const { getByTestId, getByRole } = render(
<QueryVariableRegexForm
regex=".*test.*"
regexApplyTo="value"
onRegExChange={onRegExChange}
onRegexApplyToChange={onRegexApplyToChange}
/>
);
const regexInput = getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2
);
const regexApplyToField = getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExApplyToSelectV2
);
expect(regexInput).toBeInTheDocument();
expect(regexInput).toHaveValue('.*test.*');
expect(regexApplyToField).toBeInTheDocument();
expect(getByRole('radio', { name: 'Variable value' })).toBeChecked();
expect(getByRole('radio', { name: 'Display text' })).not.toBeChecked();
});
it('should render with "Display text" option selected', () => {
const { getByRole } = render(
<QueryVariableRegexForm
regex=""
regexApplyTo="text"
onRegExChange={onRegExChange}
onRegexApplyToChange={onRegexApplyToChange}
/>
);
expect(getByRole('radio', { name: 'Display text' })).toBeChecked();
expect(getByRole('radio', { name: 'Variable value' })).not.toBeChecked();
});
it('should default to "Variable value" when regexApplyTo is not provided', () => {
const { getByRole } = render(
<QueryVariableRegexForm regex="" onRegExChange={onRegExChange} onRegexApplyToChange={onRegexApplyToChange} />
);
expect(getByRole('radio', { name: 'Variable value' })).toBeChecked();
});
it('should call onRegExChange when regex input is blurred', () => {
const { getByTestId } = render(
<QueryVariableRegexForm
regex=".*"
regexApplyTo="value"
onRegExChange={onRegExChange}
onRegexApplyToChange={onRegexApplyToChange}
/>
);
const regexInput = getByTestId(
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2
);
fireEvent.blur(regexInput);
expect(onRegExChange).toHaveBeenCalledTimes(1);
});
it('should call onRegexApplyToChange when radio option is changed', () => {
const { getByRole } = render(
<QueryVariableRegexForm
regex=""
regexApplyTo="value"
onRegExChange={onRegExChange}
onRegexApplyToChange={onRegexApplyToChange}
/>
);
const displayTextOption = getByRole('radio', { name: 'Display text' });
fireEvent.click(displayTextOption);
expect(onRegexApplyToChange).toHaveBeenCalledTimes(1);
expect(onRegexApplyToChange).toHaveBeenCalledWith('text');
});
});
@@ -0,0 +1,91 @@
import { useMemo, FormEvent } from 'react';
import { VariableRegexApplyTo, SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { Field, Stack, TextLink, RadioButtonGroup, Box } from '@grafana/ui';
import { VariableTextAreaField } from '../components/VariableTextAreaField';
interface Props {
regex: string | null;
onRegExChange: (event: FormEvent<HTMLTextAreaElement>) => void;
regexApplyTo?: VariableRegexApplyTo;
onRegexApplyToChange?: (option: VariableRegexApplyTo) => void;
}
export function QueryVariableRegexForm({ regex, regexApplyTo, onRegExChange, onRegexApplyToChange }: Props) {
const APPLY_REGEX_TO_OPTIONS: Array<SelectableValue<VariableRegexApplyTo>> = useMemo(
() => [
{
label: t('dashboard-scene.query-variable-editor-form.regex-apply-to-options.label.value', 'Variable value'),
value: 'value',
},
{
label: t('dashboard-scene.query-variable-editor-form.regex-apply-to-options.label.text', 'Display text'),
value: 'text',
},
],
[]
);
const regexApplyToValue = useMemo(
() => APPLY_REGEX_TO_OPTIONS.find((o) => o.value === regexApplyTo)?.value ?? APPLY_REGEX_TO_OPTIONS[0].value,
[regexApplyTo, APPLY_REGEX_TO_OPTIONS]
);
return (
<Box marginBottom={2}>
<Stack direction="column" gap={2}>
<VariableTextAreaField
defaultValue={regex ?? ''}
name={t('dashboard-scene.query-variable-editor-form.name-regex', 'Regex')}
description={
<div>
<Trans i18nKey="dashboard-scene.query-variable-editor-form.description-optional">
Optional, if you want to extract part of a series name or metric node segment.
</Trans>
<br />
<Trans i18nKey="dashboard-scene.query-variable-editor-form.description-examples">
Named capture groups can be used to separate the display text and value (
<TextLink
href="https://grafana.com/docs/grafana/latest/variables/filter-variables-with-regex#filter-and-modify-using-named-text-and-value-capture-groups"
external
>
see examples
</TextLink>
).
</Trans>
</div>
}
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
placeholder="/.*-(?<text>.*)-(?<value>.*)-.*/"
onBlur={onRegExChange}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2}
width={52}
noMargin
/>
{onRegexApplyToChange && (
<Field
label={t('dashboard-scene.query-variable-editor-form.label-regex-apply-to', 'Apply regex to')}
description={t(
'dashboard-scene.query-variable-editor-form.description-regex-apply-to',
'Choose whether to apply the regex pattern to the variable value or display text'
)}
data-testid={
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExApplyToSelectV2
}
noMargin
>
<RadioButtonGroup
options={APPLY_REGEX_TO_OPTIONS}
onChange={onRegexApplyToChange}
value={regexApplyToValue}
/>
</Field>
)}
</Stack>
</Box>
);
}
@@ -1,7 +1,6 @@
import { css } from '@emotion/css';
import { useId } from '@react-aria/utils';
import { FormEvent, PropsWithChildren, ReactElement } from 'react';
import * as React from 'react';
import { FormEvent, PropsWithChildren, ReactElement, ReactNode } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Field, TextArea, useStyles2 } from '@grafana/ui';
@@ -17,7 +16,8 @@ interface VariableTextAreaFieldProps {
required?: boolean;
testId?: string;
onBlur?: (event: FormEvent<HTMLTextAreaElement>) => void;
description?: React.ReactNode;
description?: ReactNode;
noMargin?: boolean;
}
export function VariableTextAreaField({
@@ -31,13 +31,14 @@ export function VariableTextAreaField({
ariaLabel,
required,
width,
noMargin,
testId,
}: PropsWithChildren<VariableTextAreaFieldProps>): ReactElement {
const styles = useStyles2(getStyles);
const id = useId();
return (
<Field label={name} description={description} htmlFor={id}>
<Field label={name} description={description} htmlFor={id} noMargin={noMargin}>
<TextArea
id={id}
rows={2}
@@ -73,6 +73,7 @@ describe('QueryVariableEditor', () => {
},
query: 'my-query',
regex: '.*',
regexApplyTo: 'value',
sort: VariableSort.alphabeticalAsc,
refresh: VariableRefresh.onDashboardLoad,
isMulti: true,
@@ -1,15 +1,16 @@
import { useState, FormEvent } from 'react';
import { useAsync } from 'react-use';
import { SelectableValue, DataSourceInstanceSettings, getDataSourceRef } from '@grafana/data';
import { SelectableValue, DataSourceInstanceSettings, getDataSourceRef, VariableRegexApplyTo } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { getDataSourceSrv } from '@grafana/runtime';
import { QueryVariable, sceneGraph, SceneVariable } from '@grafana/scenes';
import { VariableRefresh, VariableSort } from '@grafana/schema';
import { Box, Button, Field, Modal, TextLink } from '@grafana/ui';
import { Box, Button, Field, Modal } from '@grafana/ui';
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
import { QueryEditor } from 'app/features/dashboard-scene/settings/variables/components/QueryEditor';
import { QueryVariableRegexForm } from 'app/features/dashboard-scene/settings/variables/components/QueryVariableRegexForm';
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
import { getVariableQueryEditor } from 'app/features/variables/editor/getVariableQueryEditor';
import { QueryVariableRefreshSelect } from 'app/features/variables/query/QueryVariableRefreshSelect';
@@ -21,7 +22,6 @@ import {
} from 'app/features/variables/query/QueryVariableStaticOptions';
import { QueryVariableEditorForm } from '../components/QueryVariableForm';
import { VariableTextAreaField } from '../components/VariableTextAreaField';
import { VariableValuesPreview } from '../components/VariableValuesPreview';
import { hasVariableOptions } from '../utils';
@@ -35,6 +35,7 @@ export function QueryVariableEditor({ variable, onRunQuery }: QueryVariableEdito
const {
datasource,
regex,
regexApplyTo,
sort,
refresh,
isMulti,
@@ -50,6 +51,9 @@ export function QueryVariableEditor({ variable, onRunQuery }: QueryVariableEdito
const onRegExChange = (event: React.FormEvent<HTMLTextAreaElement>) => {
variable.setState({ regex: event.currentTarget.value });
};
const onRegexApplyToChange = (event: VariableRegexApplyTo) => {
variable.setState({ regexApplyTo: event });
};
const onSortChange = (sort: SelectableValue<VariableSort>) => {
variable.setState({ sort: sort.value });
};
@@ -102,7 +106,9 @@ export function QueryVariableEditor({ variable, onRunQuery }: QueryVariableEdito
onLegacyQueryChange={onQueryChange}
timeRange={timeRange}
regex={regex}
regexApplyTo={regexApplyTo}
onRegExChange={onRegExChange}
onRegexApplyToChange={onRegexApplyToChange}
sort={sort}
onSortChange={onSortChange}
refresh={refresh}
@@ -196,6 +202,7 @@ export function Editor({ variable }: { variable: QueryVariable }) {
refresh,
query,
regex,
regexApplyTo,
staticOptions,
staticOptionsOrder,
} = variable.useState();
@@ -230,11 +237,12 @@ export function Editor({ variable }: { variable: QueryVariable }) {
const onQueryChange = (query: VariableQueryType) => {
variable.setState({ query, definition: getQueryDef(query) });
};
const onRegExChange = (event: React.FormEvent<HTMLTextAreaElement>) => {
variable.setState({ regex: event.currentTarget.value });
};
const onRegexApplyToChange = (event: VariableRegexApplyTo) => {
variable.setState({ regexApplyTo: event });
};
const onSortChange = (sort: SelectableValue<VariableSort>) => {
variable.setState({ sort: sort.value });
};
@@ -271,32 +279,11 @@ export function Editor({ variable }: { variable: QueryVariable }) {
/>
)}
<VariableTextAreaField
defaultValue={regex ?? ''}
name={t('dashboard-scene.query-variable-editor-form.name-regex', 'Regex')}
description={
<div>
<Trans i18nKey="dashboard-scene.query-variable-editor-form.description-optional">
Optional, if you want to extract part of a series name or metric node segment.
</Trans>
<br />
<Trans i18nKey="dashboard-scene.query-variable-editor-form.description-examples">
Named capture groups can be used to separate the display text and value (
<TextLink
href="https://grafana.com/docs/grafana/latest/variables/filter-variables-with-regex#filter-and-modify-using-named-text-and-value-capture-groups"
external
>
see examples
</TextLink>
).
</Trans>
</div>
}
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
placeholder="/.*-(?<text>.*)-(?<value>.*)-.*/"
onBlur={onRegExChange}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2}
width={52}
<QueryVariableRegexForm
regex={regex}
regexApplyTo={regexApplyTo}
onRegExChange={onRegExChange}
onRegexApplyToChange={onRegexApplyToChange}
/>
<QueryVariableSortSelect
@@ -104,6 +104,7 @@ describe('when creating variables objects', () => {
type: 'custom',
value: 'a',
hide: 0,
valuesFormat: 'csv',
});
});
@@ -184,6 +185,7 @@ describe('when creating variables objects', () => {
query: 'SHOW TAG VALUES WITH KEY = "datacenter" ',
refresh: 1,
regex: '',
regexApplyTo: 'value',
skipUrlSync: false,
sort: 0,
text: 'America',
@@ -189,12 +189,12 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode
...commonProperties,
value: variable.current?.value ?? '',
text: variable.current?.text ?? '',
query: variable.query ?? {},
datasource: variable.datasource,
sort: variable.sort,
refresh: variable.refresh,
regex: variable.regex,
...(variable.regexApplyTo && { regexApplyTo: variable.regexApplyTo }),
allValue: variable.allValue || undefined,
includeAll: variable.includeAll,
defaultToAll: Boolean(variable.includeAll),
@@ -654,6 +654,7 @@ function getVariables(vars: TypedVariableModel[]): DashboardV2Spec['variables']
...(v.definition && { definition: v.definition }),
refresh: transformVariableRefreshToEnum(v.refresh),
regex: v.regex ?? '',
...(v.regexApplyTo && { regexApplyTo: v.regexApplyTo }),
sort: v.sort ? transformSortVariableToEnum(v.sort) : 'disabled',
query: {
kind: 'DataQuery',
@@ -919,6 +920,7 @@ function getVariablesV1(vars: DashboardV2Spec['variables']): VariableModel[] {
sort: transformSortVariableToEnumV1(v.spec.sort),
refresh: transformVariableRefreshToEnumV1(v.spec.refresh),
regex: v.spec.regex,
regexApplyTo: v.spec.regexApplyTo,
allValue: v.spec.allValue,
includeAll: v.spec.includeAll,
multi: v.spec.multi,
@@ -334,14 +334,14 @@ const Log = memo(
<>
{showTime && (
<span className={`${styles.timestamp} level-${log.logLevel} field`}>
{timestampResolution === 'ms' ? log.timestamp : log.timestampNs}
{timestampResolution === 'ms' ? log.timestamp : log.timestampNs}{' '}
</span>
)}
{
// When logs are unwrapped, we want an empty column space to align with other log lines.
}
{(log.displayLevel || !wrapLogMessage) && (
<span className={`${styles.level} level-${log.logLevel} field`}>{log.displayLevel}</span>
<span className={`${styles.level} level-${log.logLevel} field`}>{log.displayLevel} </span>
)}
{showUniqueLabels && log.uniqueLabels && (
<span className="field">
@@ -395,7 +395,7 @@ const DisplayedFields = ({
if (field === OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME && syntaxHighlighting) {
return (
<span className="field log-syntax-highlight" title={getNormalizedFieldName(field)} key={field}>
<HighlightedLogRenderer tokens={log.highlightedLogAttributesTokens} />
<HighlightedLogRenderer tokens={log.highlightedLogAttributesTokens} />{' '}
</span>
);
}
@@ -410,7 +410,7 @@ const DisplayedFields = ({
/>
) : (
log.getDisplayedFieldValue(field)
)}
)}{' '}
</span>
);
});
@@ -434,7 +434,7 @@ const LogLineBody = ({ log, styles }: { log: LogListModel; styles: LogLineStyles
if (log.hasAnsi) {
return (
<span className="field no-highlighting">
<LogMessageAnsi value={log.body} highlight={highlight} />
<LogMessageAnsi value={log.body} highlight={highlight} />{' '}
</span>
);
}
@@ -448,13 +448,13 @@ const LogLineBody = ({ log, styles }: { log: LogListModel; styles: LogLineStyles
highlightClassName={styles.matchHighLight}
/>
) : (
<span className="field no-highlighting">{log.body}</span>
<span className="field no-highlighting">{log.body} </span>
);
}
return (
<span className="field log-syntax-highlight">
<HighlightedLogRenderer tokens={log.highlightedBodyTokens} />
<HighlightedLogRenderer tokens={log.highlightedBodyTokens} />{' '}
</span>
);
};
@@ -267,54 +267,56 @@ export const LogLineDetailsField = ({
<div className={styles.row}>
{!disableActions && (
<div className={styles.actions}>
{onClickFilterLabel && fieldSupportsFilters && (
<AsyncIconButton
name="search-plus"
onClick={filterLabel}
// We purposely want to pass a new function on every render to allow the active state to be updated when log details remains open between updates.
isActive={labelFilterActive}
tooltipSuffix={refIdTooltip}
/>
)}
{onClickFilterOutLabel && fieldSupportsFilters && (
<div className={styles.actionIcons}>
{onClickFilterLabel && fieldSupportsFilters && (
<AsyncIconButton
name="search-plus"
onClick={filterLabel}
// We purposely want to pass a new function on every render to allow the active state to be updated when log details remains open between updates.
isActive={labelFilterActive}
tooltipSuffix={refIdTooltip}
/>
)}
{onClickFilterOutLabel && fieldSupportsFilters && (
<IconButton
name="search-minus"
tooltip={
app === CoreApp.Explore && log.dataFrame?.refId
? t('logs.log-line-details.fields.filter-out-query', 'Filter out value in query {{query}}', {
query: log.dataFrame?.refId,
})
: t('logs.log-line-details.fields.filter-out', 'Filter out value')
}
onClick={filterOutLabel}
/>
)}
{singleKey && displayedFields.includes(keys[0]) && (
<IconButton
variant="primary"
tooltip={t('logs.log-line-details.fields.toggle-field-button.hide-this-field', 'Hide this field')}
name="eye"
onClick={hideField}
/>
)}
{singleKey && !displayedFields.includes(keys[0]) && (
<IconButton
tooltip={t(
'logs.log-line-details.fields.toggle-field-button.field-instead-message',
'Show this field instead of the message'
)}
name="eye"
onClick={showField}
/>
)}
<IconButton
name="search-minus"
tooltip={
app === CoreApp.Explore && log.dataFrame?.refId
? t('logs.log-line-details.fields.filter-out-query', 'Filter out value in query {{query}}', {
query: log.dataFrame?.refId,
})
: t('logs.log-line-details.fields.filter-out', 'Filter out value')
}
onClick={filterOutLabel}
variant={showFieldsStats ? 'primary' : 'secondary'}
name="signal"
tooltip={t('logs.log-line-details.fields.adhoc-statistics', 'Ad-hoc statistics')}
className={styles.statsIcon}
disabled={!singleKey}
onClick={showStats}
/>
)}
{singleKey && displayedFields.includes(keys[0]) && (
<IconButton
variant="primary"
tooltip={t('logs.log-line-details.fields.toggle-field-button.hide-this-field', 'Hide this field')}
name="eye"
onClick={hideField}
/>
)}
{singleKey && !displayedFields.includes(keys[0]) && (
<IconButton
tooltip={t(
'logs.log-line-details.fields.toggle-field-button.field-instead-message',
'Show this field instead of the message'
)}
name="eye"
onClick={showField}
/>
)}
<IconButton
variant={showFieldsStats ? 'primary' : 'secondary'}
name="signal"
tooltip={t('logs.log-line-details.fields.adhoc-statistics', 'Ad-hoc statistics')}
className="stats-button"
disabled={!singleKey}
onClick={showStats}
/>
</div>
</div>
)}
<div className={styles.label}>
@@ -388,6 +390,15 @@ const getFieldStyles = (theme: GrafanaTheme2) => ({
actions: css({
whiteSpace: 'nowrap',
}),
actionIcons: css({
display: 'flex',
justifyContent: 'space-between',
paddingRight: 2,
}),
statsIcon: css({
margin: 0,
paddingRight: 4,
}),
label: css({
paddingRight: theme.spacing(1),
overflowWrap: 'break-word',
@@ -235,6 +235,7 @@ export const LogLineDetailsHeader = ({ focusLogLine, log, search, onSearch }: Pr
tabIndex={0}
/>
)}
<div className={`${styles.divider} ${styles.dividerMargin}`} />
<IconButton
name={detailsMode === 'inline' ? 'web-section' : 'gf-layout-simple'}
tooltip={
@@ -244,9 +245,11 @@ export const LogLineDetailsHeader = ({ focusLogLine, log, search, onSearch }: Pr
}
onClick={toggleDetailsMode}
/>
<div className={styles.divider} />
<IconButton
name="times"
tooltip={t('logs.log-line-details.close', 'Close log details')}
variant="primary"
onClick={closeDetails}
/>
</div>
@@ -280,6 +283,7 @@ const getStyles = (theme: GrafanaTheme2, mode: LogLineDetailsMode, wrapLogMessag
display: 'flex',
gap: theme.spacing(1),
paddingLeft: theme.spacing(1),
alignContent: 'center',
}),
copyLogButton: css({
padding: 0,
@@ -293,4 +297,12 @@ const getStyles = (theme: GrafanaTheme2, mode: LogLineDetailsMode, wrapLogMessag
componentWrapper: css({
padding: theme.spacing(0, 1, 1, 1),
}),
divider: css({
width: 1,
borderRight: `solid 1px ${theme.colors.border.medium}`,
height: theme.spacing(2.25),
}),
dividerMargin: css({
marginRight: theme.spacing(0.5),
}),
});
@@ -1,5 +1,6 @@
import { useEffect } from 'react';
import { useLogDetailsContext } from './LogDetailsContext';
import { useLogListSearchContext } from './LogListSearchContext';
/**
@@ -11,6 +12,7 @@ import { useLogListSearchContext } from './LogListSearchContext';
export const useKeyBindings = () => {
const { hideSearch, searchVisible, showSearch } = useLogListSearchContext();
const { showDetails, detailsMode, closeDetails } = useLogDetailsContext();
useEffect(() => {
function handleToggleSearch(event: KeyboardEvent) {
@@ -24,10 +26,13 @@ export const useKeyBindings = () => {
if (event.key === 'Escape' && searchVisible) {
hideSearch();
}
if (event.key === 'Escape' && showDetails.length > 0 && detailsMode === 'sidebar') {
closeDetails();
}
}
document.addEventListener('keydown', handleToggleSearch);
return () => {
document.removeEventListener('keydown', handleToggleSearch);
};
});
}, [closeDetails, detailsMode, hideSearch, searchVisible, showDetails.length, showSearch]);
};
+8
View File
@@ -6271,12 +6271,20 @@
"query-variable-editor-form": {
"description-examples": "Named capture groups can be used to separate the display text and value (<1>see examples</1> ).",
"description-optional": "Optional, if you want to extract part of a series name or metric node segment.",
"description-regex-apply-to": "Choose whether to apply the regex pattern to the variable value or display text",
"label-data-source": "Data source",
"label-regex-apply-to": "Apply regex to",
"label-static-options-sort": "Static options sort",
"label-target-data-source": "Target data source",
"label-use-static-options": "Use static options",
"name-regex": "Regex",
"query-options": "Query options",
"regex-apply-to-options": {
"label": {
"text": "Display text",
"value": "Variable value"
}
},
"selection-options": "Selection options",
"static-options-legend": "Static options"
},
+11 -11
View File
@@ -3603,11 +3603,11 @@ __metadata:
languageName: unknown
linkType: soft
"@grafana/scenes-react@npm:6.47.1":
version: 6.47.1
resolution: "@grafana/scenes-react@npm:6.47.1"
"@grafana/scenes-react@npm:6.49.0":
version: 6.49.0
resolution: "@grafana/scenes-react@npm:6.49.0"
dependencies:
"@grafana/scenes": "npm:6.47.1"
"@grafana/scenes": "npm:6.49.0"
lru-cache: "npm:^10.2.2"
react-use: "npm:^17.4.0"
peerDependencies:
@@ -3619,7 +3619,7 @@ __metadata:
react: ^18.0.0
react-dom: ^18.0.0
react-router-dom: ^6.28.0
checksum: 10/dc20f9ee80eaf648665f7449e3ccb3b640a931f8f4a1be89599dce17eb0f52e763e3a603a4d491d9886b3e6cdf2ad3634124252c223315917206200f7cd6da16
checksum: 10/0f9ba2ccaf2c8a703f3c4867320852c07f640aea85491e6c1a35d7fd25b48e69740f7782b52280ab4d423a6e87659de41cf66904acc865c9ea1f934ded6c7e64
languageName: node
linkType: hard
@@ -3649,9 +3649,9 @@ __metadata:
languageName: node
linkType: hard
"@grafana/scenes@npm:6.47.1":
version: 6.47.1
resolution: "@grafana/scenes@npm:6.47.1"
"@grafana/scenes@npm:6.49.0":
version: 6.49.0
resolution: "@grafana/scenes@npm:6.49.0"
dependencies:
"@floating-ui/react": "npm:^0.26.16"
"@leeoniya/ufuzzy": "npm:^1.0.16"
@@ -3671,7 +3671,7 @@ __metadata:
react: ^18.0.0
react-dom: ^18.0.0
react-router-dom: ^6.28.0
checksum: 10/bc0c76258955058e7493b04e7cdd5d59dcc4159adf06da0837e992716ea15700b54f8403614df04326350363dc3344fb2602a2e8f7807724571659b4bd95aded
checksum: 10/0e873ceac0834879ade41df56ced5a3e8f6a10e5aba15a6f271327aa804729402bcf44845e54614dd0fb1541a28efbb9a5499acceae04cb8be3b11bcdc230347
languageName: node
linkType: hard
@@ -19441,8 +19441,8 @@ __metadata:
"@grafana/plugin-ui": "npm:^0.11.1"
"@grafana/prometheus": "workspace:*"
"@grafana/runtime": "workspace:*"
"@grafana/scenes": "npm:6.47.1"
"@grafana/scenes-react": "npm:6.47.1"
"@grafana/scenes": "npm:6.49.0"
"@grafana/scenes-react": "npm:6.49.0"
"@grafana/schema": "workspace:*"
"@grafana/sql": "workspace:*"
"@grafana/test-utils": "workspace:*"