QueryVariable: Add static options input (#107514)
* static options for query variable * add toggle * fix and add tests * run the hack codegen thing * more test fixes * make betterer happier * also make typecheck happy * make betterer happier * fix i18n key * tranalte static variables sort label * gen translations * update snapshot
This commit is contained in:
@@ -734,6 +734,8 @@ QueryVariableSpec: {
|
|||||||
allValue?: string
|
allValue?: string
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
allowCustomValue: bool | *true
|
allowCustomValue: bool | *true
|
||||||
|
staticOptions?: [...VariableOption]
|
||||||
|
staticOptionsOrder?: "before" | "after" | "sorted"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query variable kind
|
// Query variable kind
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ lineage: schemas: [{
|
|||||||
// Optional field, if you want to extract part of a series name or metric node segment.
|
// 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.
|
// Named capture groups can be used to separate the display text and value.
|
||||||
regex?: string
|
regex?: string
|
||||||
|
// Additional static options for query variable
|
||||||
|
staticOptions?: [...#VariableOption]
|
||||||
|
// Ordering of static options in relation to options returned from data source for query variable
|
||||||
|
staticOptionsOrder?: "before" | "after" | "sorted"
|
||||||
...
|
...
|
||||||
} @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview)
|
} @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview)
|
||||||
|
|
||||||
|
|||||||
@@ -219,6 +219,10 @@ lineage: schemas: [{
|
|||||||
// Optional field, if you want to extract part of a series name or metric node segment.
|
// 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.
|
// Named capture groups can be used to separate the display text and value.
|
||||||
regex?: string
|
regex?: string
|
||||||
|
// Additional static options for query variable
|
||||||
|
staticOptions?: [...#VariableOption]
|
||||||
|
// Ordering of static options in relation to options returned from data source for query variable
|
||||||
|
staticOptionsOrder?: "before" | "after" | "sorted"
|
||||||
...
|
...
|
||||||
} @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview)
|
} @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview)
|
||||||
|
|
||||||
|
|||||||
@@ -738,6 +738,8 @@ QueryVariableSpec: {
|
|||||||
allValue?: string
|
allValue?: string
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
allowCustomValue: bool | *true
|
allowCustomValue: bool | *true
|
||||||
|
staticOptions?: [...VariableOption]
|
||||||
|
staticOptionsOrder?: "before" | "after" | "sorted"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query variable kind
|
// Query variable kind
|
||||||
|
|||||||
@@ -1214,24 +1214,26 @@ func NewDashboardQueryVariableKind() *DashboardQueryVariableKind {
|
|||||||
// Query variable specification
|
// Query variable specification
|
||||||
// +k8s:openapi-gen=true
|
// +k8s:openapi-gen=true
|
||||||
type DashboardQueryVariableSpec struct {
|
type DashboardQueryVariableSpec struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Current DashboardVariableOption `json:"current"`
|
Current DashboardVariableOption `json:"current"`
|
||||||
Label *string `json:"label,omitempty"`
|
Label *string `json:"label,omitempty"`
|
||||||
Hide DashboardVariableHide `json:"hide"`
|
Hide DashboardVariableHide `json:"hide"`
|
||||||
Refresh DashboardVariableRefresh `json:"refresh"`
|
Refresh DashboardVariableRefresh `json:"refresh"`
|
||||||
SkipUrlSync bool `json:"skipUrlSync"`
|
SkipUrlSync bool `json:"skipUrlSync"`
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
Datasource *DashboardDataSourceRef `json:"datasource,omitempty"`
|
Datasource *DashboardDataSourceRef `json:"datasource,omitempty"`
|
||||||
Query DashboardDataQueryKind `json:"query"`
|
Query DashboardDataQueryKind `json:"query"`
|
||||||
Regex string `json:"regex"`
|
Regex string `json:"regex"`
|
||||||
Sort DashboardVariableSort `json:"sort"`
|
Sort DashboardVariableSort `json:"sort"`
|
||||||
Definition *string `json:"definition,omitempty"`
|
Definition *string `json:"definition,omitempty"`
|
||||||
Options []DashboardVariableOption `json:"options"`
|
Options []DashboardVariableOption `json:"options"`
|
||||||
Multi bool `json:"multi"`
|
Multi bool `json:"multi"`
|
||||||
IncludeAll bool `json:"includeAll"`
|
IncludeAll bool `json:"includeAll"`
|
||||||
AllValue *string `json:"allValue,omitempty"`
|
AllValue *string `json:"allValue,omitempty"`
|
||||||
Placeholder *string `json:"placeholder,omitempty"`
|
Placeholder *string `json:"placeholder,omitempty"`
|
||||||
AllowCustomValue bool `json:"allowCustomValue"`
|
AllowCustomValue bool `json:"allowCustomValue"`
|
||||||
|
StaticOptions []DashboardVariableOption `json:"staticOptions,omitempty"`
|
||||||
|
StaticOptionsOrder *DashboardQueryVariableSpecStaticOptionsOrder `json:"staticOptionsOrder,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDashboardQueryVariableSpec creates a new DashboardQueryVariableSpec object.
|
// NewDashboardQueryVariableSpec creates a new DashboardQueryVariableSpec object.
|
||||||
@@ -1880,6 +1882,15 @@ const (
|
|||||||
DashboardTimeSettingsSpecWeekStartSunday DashboardTimeSettingsSpecWeekStart = "sunday"
|
DashboardTimeSettingsSpecWeekStartSunday DashboardTimeSettingsSpecWeekStart = "sunday"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// +k8s:openapi-gen=true
|
||||||
|
type DashboardQueryVariableSpecStaticOptionsOrder string
|
||||||
|
|
||||||
|
const (
|
||||||
|
DashboardQueryVariableSpecStaticOptionsOrderBefore DashboardQueryVariableSpecStaticOptionsOrder = "before"
|
||||||
|
DashboardQueryVariableSpecStaticOptionsOrderAfter DashboardQueryVariableSpecStaticOptionsOrder = "after"
|
||||||
|
DashboardQueryVariableSpecStaticOptionsOrderSorted DashboardQueryVariableSpecStaticOptionsOrder = "sorted"
|
||||||
|
)
|
||||||
|
|
||||||
// +k8s:openapi-gen=true
|
// +k8s:openapi-gen=true
|
||||||
type DashboardPanelKindOrLibraryPanelKind struct {
|
type DashboardPanelKindOrLibraryPanelKind struct {
|
||||||
PanelKind *DashboardPanelKind `json:"PanelKind,omitempty"`
|
PanelKind *DashboardPanelKind `json:"PanelKind,omitempty"`
|
||||||
|
|||||||
@@ -3327,6 +3327,25 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardQueryVariableSpec(ref common.Re
|
|||||||
Format: "",
|
Format: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"staticOptions": {
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Type: []string{"array"},
|
||||||
|
Items: &spec.SchemaOrArray{
|
||||||
|
Schema: &spec.Schema{
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Default: map[string]interface{}{},
|
||||||
|
Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardVariableOption"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"staticOptionsOrder": {
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Type: []string{"string"},
|
||||||
|
Format: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Required: []string{"name", "current", "hide", "refresh", "skipUrlSync", "query", "regex", "sort", "options", "multi", "includeAll", "allowCustomValue"},
|
Required: []string{"name", "current", "hide", "refresh", "skipUrlSync", "query", "regex", "sort", "options", "multi", "includeAll", "allowCustomValue"},
|
||||||
},
|
},
|
||||||
|
|||||||
+1
@@ -20,6 +20,7 @@ API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/
|
|||||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryGroupSpec,Queries
|
API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryGroupSpec,Queries
|
||||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryGroupSpec,Transformations
|
API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryGroupSpec,Transformations
|
||||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableSpec,Options
|
API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableSpec,Options
|
||||||
|
API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardQueryVariableSpec,StaticOptions
|
||||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardRowsLayoutSpec,Rows
|
API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardRowsLayoutSpec,Rows
|
||||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardSpec,Annotations
|
API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardSpec,Annotations
|
||||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardSpec,Links
|
API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1,DashboardSpec,Links
|
||||||
|
|||||||
@@ -215,6 +215,10 @@ lineage: schemas: [{
|
|||||||
// Optional field, if you want to extract part of a series name or metric node segment.
|
// 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.
|
// Named capture groups can be used to separate the display text and value.
|
||||||
regex?: string
|
regex?: string
|
||||||
|
// Additional static options for query variable
|
||||||
|
staticOptions?: [...#VariableOption]
|
||||||
|
// Ordering of static options in relation to options returned from data source for query variable
|
||||||
|
staticOptionsOrder?: "before" | "after" | "sorted"
|
||||||
...
|
...
|
||||||
} @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview)
|
} @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview)
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -289,8 +289,8 @@
|
|||||||
"@grafana/plugin-ui": "0.10.7",
|
"@grafana/plugin-ui": "0.10.7",
|
||||||
"@grafana/prometheus": "workspace:*",
|
"@grafana/prometheus": "workspace:*",
|
||||||
"@grafana/runtime": "workspace:*",
|
"@grafana/runtime": "workspace:*",
|
||||||
"@grafana/scenes": "^6.27.0",
|
"@grafana/scenes": "^6.27.1",
|
||||||
"@grafana/scenes-react": "^6.27.0",
|
"@grafana/scenes-react": "^6.27.1",
|
||||||
"@grafana/schema": "workspace:*",
|
"@grafana/schema": "workspace:*",
|
||||||
"@grafana/sql": "workspace:*",
|
"@grafana/sql": "workspace:*",
|
||||||
"@grafana/ui": "workspace:*",
|
"@grafana/ui": "workspace:*",
|
||||||
|
|||||||
@@ -116,6 +116,8 @@ export interface QueryVariableModel extends VariableWithMultiSupport {
|
|||||||
query: any;
|
query: any;
|
||||||
regex: string;
|
regex: string;
|
||||||
refresh: VariableRefresh;
|
refresh: VariableRefresh;
|
||||||
|
staticOptions?: VariableOption[];
|
||||||
|
staticOptionsOrder?: 'before' | 'after' | 'sorted';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TextBoxVariableModel extends VariableWithOptions {
|
export interface TextBoxVariableModel extends VariableWithOptions {
|
||||||
|
|||||||
@@ -489,6 +489,27 @@ export const versionedPages = {
|
|||||||
queryOptionsQueryInput: {
|
queryOptionsQueryInput: {
|
||||||
'10.4.0': 'data-testid Variable editor Form Default Variable Query Editor textarea',
|
'10.4.0': 'data-testid Variable editor Form Default Variable Query Editor textarea',
|
||||||
},
|
},
|
||||||
|
queryOptionsStaticOptionsRow: {
|
||||||
|
[MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options row',
|
||||||
|
},
|
||||||
|
queryOptionsStaticOptionsToggle: {
|
||||||
|
[MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options toggle',
|
||||||
|
},
|
||||||
|
queryOptionsStaticOptionsLabelInput: {
|
||||||
|
[MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options Label input',
|
||||||
|
},
|
||||||
|
queryOptionsStaticOptionsValueInput: {
|
||||||
|
[MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options Value input',
|
||||||
|
},
|
||||||
|
queryOptionsStaticOptionsDeleteButton: {
|
||||||
|
[MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options Delete button',
|
||||||
|
},
|
||||||
|
queryOptionsStaticOptionsAddButton: {
|
||||||
|
[MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options Add button',
|
||||||
|
},
|
||||||
|
queryOptionsStaticOptionsOrderDropdown: {
|
||||||
|
[MIN_GRAFANA_VERSION]: 'Variable editor Form Query Static Options Order dropdown',
|
||||||
|
},
|
||||||
valueGroupsTagsEnabledSwitch: {
|
valueGroupsTagsEnabledSwitch: {
|
||||||
[MIN_GRAFANA_VERSION]: 'Variable editor Form Query UseTags switch',
|
[MIN_GRAFANA_VERSION]: 'Variable editor Form Query UseTags switch',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -191,6 +191,14 @@ export interface VariableModel {
|
|||||||
* Options sort order
|
* Options sort order
|
||||||
*/
|
*/
|
||||||
sort?: VariableSort;
|
sort?: VariableSort;
|
||||||
|
/**
|
||||||
|
* Additional static options for query variable
|
||||||
|
*/
|
||||||
|
staticOptions?: Array<VariableOption>;
|
||||||
|
/**
|
||||||
|
* Ordering of static options in relation to options returned from data source for query variable
|
||||||
|
*/
|
||||||
|
staticOptionsOrder?: ('before' | 'after' | 'sorted');
|
||||||
/**
|
/**
|
||||||
* Type of variable
|
* Type of variable
|
||||||
*/
|
*/
|
||||||
@@ -203,6 +211,7 @@ export const defaultVariableModel: Partial<VariableModel> = {
|
|||||||
multi: false,
|
multi: false,
|
||||||
options: [],
|
options: [],
|
||||||
skipUrlSync: false,
|
skipUrlSync: false,
|
||||||
|
staticOptions: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1005,6 +1005,8 @@ export interface QueryVariableSpec {
|
|||||||
allValue?: string;
|
allValue?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
allowCustomValue: boolean;
|
allowCustomValue: boolean;
|
||||||
|
staticOptions?: VariableOption[];
|
||||||
|
staticOptionsOrder?: "before" | "after" | "sorted";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultQueryVariableSpec = (): QueryVariableSpec => ({
|
export const defaultQueryVariableSpec = (): QueryVariableSpec => ({
|
||||||
|
|||||||
@@ -725,6 +725,10 @@ type VariableModel struct {
|
|||||||
// Optional field, if you want to extract part of a series name or metric node segment.
|
// 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.
|
// Named capture groups can be used to separate the display text and value.
|
||||||
Regex *string `json:"regex,omitempty"`
|
Regex *string `json:"regex,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
|
||||||
|
StaticOptionsOrder *VariableModelStaticOptionsOrder `json:"staticOptionsOrder,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewVariableModel creates a new VariableModel object.
|
// NewVariableModel creates a new VariableModel object.
|
||||||
@@ -1045,6 +1049,14 @@ const (
|
|||||||
DataTransformerConfigTopicAlertStates DataTransformerConfigTopic = "alertStates"
|
DataTransformerConfigTopicAlertStates DataTransformerConfigTopic = "alertStates"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type VariableModelStaticOptionsOrder string
|
||||||
|
|
||||||
|
const (
|
||||||
|
VariableModelStaticOptionsOrderBefore VariableModelStaticOptionsOrder = "before"
|
||||||
|
VariableModelStaticOptionsOrderAfter VariableModelStaticOptionsOrder = "after"
|
||||||
|
VariableModelStaticOptionsOrderSorted VariableModelStaticOptionsOrder = "sorted"
|
||||||
|
)
|
||||||
|
|
||||||
type ValueMapOrRangeMapOrRegexMapOrSpecialValueMap struct {
|
type ValueMapOrRangeMapOrRegexMapOrSpecialValueMap struct {
|
||||||
ValueMap *ValueMap `json:"ValueMap,omitempty"`
|
ValueMap *ValueMap `json:"ValueMap,omitempty"`
|
||||||
RangeMap *RangeMap `json:"RangeMap,omitempty"`
|
RangeMap *RangeMap `json:"RangeMap,omitempty"`
|
||||||
|
|||||||
@@ -3058,6 +3058,20 @@
|
|||||||
"sort": {
|
"sort": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": ""
|
"default": ""
|
||||||
|
},
|
||||||
|
"staticOptions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"default": {},
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardVariableOption"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"staticOptionsOrder": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+28
-1
@@ -101,6 +101,8 @@ describe('sceneVariablesSetToVariables', () => {
|
|||||||
allowCustomValue: true,
|
allowCustomValue: true,
|
||||||
allValue: 'test-all',
|
allValue: 'test-all',
|
||||||
isMulti: true,
|
isMulti: true,
|
||||||
|
staticOptions: [{ label: 'test', value: 'test' }],
|
||||||
|
staticOptionsOrder: 'after',
|
||||||
});
|
});
|
||||||
|
|
||||||
const set = new SceneVariableSet({
|
const set = new SceneVariableSet({
|
||||||
@@ -136,6 +138,13 @@ describe('sceneVariablesSetToVariables', () => {
|
|||||||
"query": "query",
|
"query": "query",
|
||||||
"refresh": 1,
|
"refresh": 1,
|
||||||
"regex": "",
|
"regex": "",
|
||||||
|
"staticOptions": [
|
||||||
|
{
|
||||||
|
"text": "test",
|
||||||
|
"value": "test",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"staticOptionsOrder": "after",
|
||||||
"type": "query",
|
"type": "query",
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
@@ -155,6 +164,8 @@ describe('sceneVariablesSetToVariables', () => {
|
|||||||
allValue: 'test-all',
|
allValue: 'test-all',
|
||||||
allowCustomValue: false,
|
allowCustomValue: false,
|
||||||
isMulti: true,
|
isMulti: true,
|
||||||
|
staticOptions: [{ label: 'test', value: 'test' }],
|
||||||
|
staticOptionsOrder: 'after',
|
||||||
});
|
});
|
||||||
const set = new SceneVariableSet({
|
const set = new SceneVariableSet({
|
||||||
variables: [variable],
|
variables: [variable],
|
||||||
@@ -189,6 +200,13 @@ describe('sceneVariablesSetToVariables', () => {
|
|||||||
"query": "query",
|
"query": "query",
|
||||||
"refresh": 1,
|
"refresh": 1,
|
||||||
"regex": "",
|
"regex": "",
|
||||||
|
"staticOptions": [
|
||||||
|
{
|
||||||
|
"text": "test",
|
||||||
|
"value": "test",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"staticOptionsOrder": "after",
|
||||||
"type": "query",
|
"type": "query",
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
@@ -221,7 +239,7 @@ describe('sceneVariablesSetToVariables', () => {
|
|||||||
expect(result[0].options).toEqual([]);
|
expect(result[0].options).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle Query variable when sceneVariablesSetToVariables shoudl keep options', () => {
|
it('should handle Query variable when sceneVariablesSetToVariables should keep options', () => {
|
||||||
const variable = new QueryVariable({
|
const variable = new QueryVariable({
|
||||||
name: 'test',
|
name: 'test',
|
||||||
label: 'test-label',
|
label: 'test-label',
|
||||||
@@ -846,6 +864,8 @@ describe('sceneVariablesSetToVariables', () => {
|
|||||||
includeAll: true,
|
includeAll: true,
|
||||||
allValue: 'test-all',
|
allValue: 'test-all',
|
||||||
isMulti: true,
|
isMulti: true,
|
||||||
|
staticOptions: [{ label: 'test', value: 'test' }],
|
||||||
|
staticOptionsOrder: 'after',
|
||||||
});
|
});
|
||||||
|
|
||||||
const set = new SceneVariableSet({
|
const set = new SceneVariableSet({
|
||||||
@@ -891,6 +911,13 @@ describe('sceneVariablesSetToVariables', () => {
|
|||||||
"regex": "",
|
"regex": "",
|
||||||
"skipUrlSync": false,
|
"skipUrlSync": false,
|
||||||
"sort": "disabled",
|
"sort": "disabled",
|
||||||
|
"staticOptions": [
|
||||||
|
{
|
||||||
|
"text": "test",
|
||||||
|
"value": "test",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"staticOptionsOrder": "after",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|||||||
@@ -86,6 +86,11 @@ export function sceneVariablesSetToVariables(set: SceneVariables, keepQueryOptio
|
|||||||
multi: variable.state.isMulti,
|
multi: variable.state.isMulti,
|
||||||
allowCustomValue: variable.state.allowCustomValue,
|
allowCustomValue: variable.state.allowCustomValue,
|
||||||
skipUrlSync: variable.state.skipUrlSync,
|
skipUrlSync: variable.state.skipUrlSync,
|
||||||
|
staticOptions: variable.state.staticOptions?.map((option) => ({
|
||||||
|
text: option.label,
|
||||||
|
value: String(option.value),
|
||||||
|
})),
|
||||||
|
staticOptionsOrder: variable.state.staticOptionsOrder,
|
||||||
});
|
});
|
||||||
} else if (sceneUtils.isCustomVariable(variable)) {
|
} else if (sceneUtils.isCustomVariable(variable)) {
|
||||||
variables.push({
|
variables.push({
|
||||||
@@ -324,6 +329,11 @@ export function sceneVariablesSetToSchemaV2Variables(
|
|||||||
multi: variable.state.isMulti || false,
|
multi: variable.state.isMulti || false,
|
||||||
skipUrlSync: variable.state.skipUrlSync || false,
|
skipUrlSync: variable.state.skipUrlSync || false,
|
||||||
allowCustomValue: variable.state.allowCustomValue ?? true,
|
allowCustomValue: variable.state.allowCustomValue ?? true,
|
||||||
|
staticOptions: variable.state.staticOptions?.map((option) => ({
|
||||||
|
text: option.label,
|
||||||
|
value: String(option.value),
|
||||||
|
})),
|
||||||
|
staticOptionsOrder: variable.state.staticOptionsOrder,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
variables.push(queryVariable);
|
variables.push(queryVariable);
|
||||||
|
|||||||
+174
@@ -77,6 +77,8 @@ describe('QueryVariableEditorForm', () => {
|
|||||||
const mockOnIncludeAllChange = jest.fn();
|
const mockOnIncludeAllChange = jest.fn();
|
||||||
const mockOnAllValueChange = jest.fn();
|
const mockOnAllValueChange = jest.fn();
|
||||||
const mockOnAllowCustomValueChange = jest.fn();
|
const mockOnAllowCustomValueChange = jest.fn();
|
||||||
|
const mockOnStaticOptionsChange = jest.fn();
|
||||||
|
const mockOnStaticOptionsOrderChange = jest.fn();
|
||||||
|
|
||||||
const defaultProps: React.ComponentProps<typeof QueryVariableEditorForm> = {
|
const defaultProps: React.ComponentProps<typeof QueryVariableEditorForm> = {
|
||||||
datasource: { uid: defaultDatasource.uid, type: defaultDatasource.type },
|
datasource: { uid: defaultDatasource.uid, type: defaultDatasource.type },
|
||||||
@@ -99,6 +101,8 @@ describe('QueryVariableEditorForm', () => {
|
|||||||
allValue: 'custom all value',
|
allValue: 'custom all value',
|
||||||
onAllValueChange: mockOnAllValueChange,
|
onAllValueChange: mockOnAllValueChange,
|
||||||
onAllowCustomValueChange: mockOnAllowCustomValueChange,
|
onAllowCustomValueChange: mockOnAllowCustomValueChange,
|
||||||
|
onStaticOptionsChange: mockOnStaticOptionsChange,
|
||||||
|
onStaticOptionsOrderChange: mockOnStaticOptionsOrderChange,
|
||||||
};
|
};
|
||||||
|
|
||||||
async function setup(props?: React.ComponentProps<typeof QueryVariableEditorForm>) {
|
async function setup(props?: React.ComponentProps<typeof QueryVariableEditorForm>) {
|
||||||
@@ -142,6 +146,10 @@ describe('QueryVariableEditorForm', () => {
|
|||||||
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch
|
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const staticOptionsToggle = getByTestId(
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsToggle
|
||||||
|
);
|
||||||
|
|
||||||
expect(dataSourcePicker).toBeInTheDocument();
|
expect(dataSourcePicker).toBeInTheDocument();
|
||||||
expect(dataSourcePicker.getAttribute('placeholder')).toBe('Default Test Data Source');
|
expect(dataSourcePicker.getAttribute('placeholder')).toBe('Default Test Data Source');
|
||||||
expect(regexInput).toBeInTheDocument();
|
expect(regexInput).toBeInTheDocument();
|
||||||
@@ -158,6 +166,7 @@ describe('QueryVariableEditorForm', () => {
|
|||||||
expect(includeAllSwitch).toBeChecked();
|
expect(includeAllSwitch).toBeChecked();
|
||||||
expect(allValueInput).toBeInTheDocument();
|
expect(allValueInput).toBeInTheDocument();
|
||||||
expect(allValueInput).toHaveValue('custom all value');
|
expect(allValueInput).toHaveValue('custom all value');
|
||||||
|
expect(staticOptionsToggle).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call onDataSourceChange when changing the datasource', async () => {
|
it('should call onDataSourceChange when changing the datasource', async () => {
|
||||||
@@ -293,4 +302,169 @@ describe('QueryVariableEditorForm', () => {
|
|||||||
((mockOnAllValueChange.mock.calls[0][0] as FormEvent<HTMLInputElement>).target as HTMLInputElement).value
|
((mockOnAllValueChange.mock.calls[0][0] as FormEvent<HTMLInputElement>).target as HTMLInputElement).value
|
||||||
).toBe('custom all value and another value');
|
).toBe('custom all value and another value');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should call onStaticOptionsOrderChange when changing the static options order', async () => {
|
||||||
|
const {
|
||||||
|
renderer: { getByTestId },
|
||||||
|
} = await setup();
|
||||||
|
|
||||||
|
// First enable static options
|
||||||
|
const staticOptionsToggle = getByTestId(
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsToggle
|
||||||
|
);
|
||||||
|
await userEvent.click(staticOptionsToggle);
|
||||||
|
|
||||||
|
// Then access the dropdown
|
||||||
|
const staticOptionsOrderDropdown = getByTestId(
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsOrderDropdown
|
||||||
|
);
|
||||||
|
await userEvent.click(staticOptionsOrderDropdown); // open the select
|
||||||
|
const anotherOption = await screen.getByText('After query values');
|
||||||
|
await userEvent.click(anotherOption);
|
||||||
|
|
||||||
|
expect(mockOnStaticOptionsOrderChange).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockOnStaticOptionsOrderChange.mock.calls[0][0]).toBe('after');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onStaticOptionsChange when adding a static option', async () => {
|
||||||
|
const {
|
||||||
|
renderer: { getByTestId, getAllByTestId },
|
||||||
|
} = await setup();
|
||||||
|
|
||||||
|
// First enable static options
|
||||||
|
const staticOptionsToggle = getByTestId(
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsToggle
|
||||||
|
);
|
||||||
|
await userEvent.click(staticOptionsToggle);
|
||||||
|
|
||||||
|
const addButton = getByTestId(
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsAddButton
|
||||||
|
);
|
||||||
|
await userEvent.click(addButton);
|
||||||
|
|
||||||
|
// Now enter label and value for the new option
|
||||||
|
const labelInputs = getAllByTestId(
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsLabelInput
|
||||||
|
);
|
||||||
|
const valueInputs = getAllByTestId(
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsValueInput
|
||||||
|
);
|
||||||
|
|
||||||
|
// Enter label for the new option (second input)
|
||||||
|
await userEvent.type(labelInputs[1], 'New Option Label');
|
||||||
|
await userEvent.type(valueInputs[1], 'new-option-value');
|
||||||
|
|
||||||
|
expect(mockOnStaticOptionsChange).toHaveBeenCalled();
|
||||||
|
expect(mockOnStaticOptionsChange.mock.lastCall[0]).toEqual([
|
||||||
|
{ value: 'new-option-value', label: 'New Option Label' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onStaticOptionsChange when removing a static option', async () => {
|
||||||
|
const {
|
||||||
|
renderer: { getAllByTestId },
|
||||||
|
} = await setup({
|
||||||
|
...defaultProps,
|
||||||
|
staticOptions: [
|
||||||
|
{ value: 'option1', label: 'Option 1' },
|
||||||
|
{ value: 'option2', label: 'Option 2' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteButtons = getAllByTestId(
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsDeleteButton
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove the first option
|
||||||
|
await userEvent.click(deleteButtons[0]);
|
||||||
|
|
||||||
|
expect(mockOnStaticOptionsChange).toHaveBeenCalledTimes(1);
|
||||||
|
// Should call with only the second option remaining
|
||||||
|
expect(mockOnStaticOptionsChange.mock.calls[0][0]).toEqual([{ value: 'option2', label: 'Option 2' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onStaticOptionsChange when editing a static option label', async () => {
|
||||||
|
const {
|
||||||
|
renderer: { getAllByTestId },
|
||||||
|
} = await setup({
|
||||||
|
...defaultProps,
|
||||||
|
staticOptions: [{ value: 'test', label: 'Test Label' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const labelInputs = getAllByTestId(
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsLabelInput
|
||||||
|
);
|
||||||
|
|
||||||
|
await userEvent.clear(labelInputs[0]);
|
||||||
|
await userEvent.type(labelInputs[0], 'Updated Label');
|
||||||
|
|
||||||
|
expect(mockOnStaticOptionsChange).toHaveBeenCalled();
|
||||||
|
expect(mockOnStaticOptionsChange.mock.lastCall[0]).toEqual([{ value: 'test', label: 'Updated Label' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onStaticOptionsChange when editing a static option value', async () => {
|
||||||
|
const {
|
||||||
|
renderer: { getAllByTestId },
|
||||||
|
} = await setup({
|
||||||
|
...defaultProps,
|
||||||
|
staticOptions: [{ value: 'old-value', label: 'Test Label' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const valueInputs = getAllByTestId(
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsValueInput
|
||||||
|
);
|
||||||
|
|
||||||
|
await userEvent.clear(valueInputs[0]);
|
||||||
|
await userEvent.type(valueInputs[0], 'new-value');
|
||||||
|
|
||||||
|
expect(mockOnStaticOptionsChange).toHaveBeenCalled();
|
||||||
|
expect(mockOnStaticOptionsChange.mock.lastCall[0]).toEqual([{ value: 'new-value', label: 'Test Label' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove static options and hide UI elements when static options switch is unchecked', async () => {
|
||||||
|
const {
|
||||||
|
renderer: { getByTestId, queryByTestId, getAllByTestId },
|
||||||
|
} = await setup({
|
||||||
|
...defaultProps,
|
||||||
|
staticOptions: [
|
||||||
|
{ value: 'option1', label: 'Option 1' },
|
||||||
|
{ value: 'option2', label: 'Option 2' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Static options should be visible initially
|
||||||
|
expect(
|
||||||
|
getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsToggle)
|
||||||
|
).toBeChecked();
|
||||||
|
expect(
|
||||||
|
getByTestId(
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsOrderDropdown
|
||||||
|
)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Option rows should be visible
|
||||||
|
expect(
|
||||||
|
getAllByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsRow)
|
||||||
|
).toHaveLength(2);
|
||||||
|
|
||||||
|
// Uncheck the static options switch
|
||||||
|
const staticOptionsToggle = getByTestId(
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsToggle
|
||||||
|
);
|
||||||
|
await userEvent.click(staticOptionsToggle);
|
||||||
|
|
||||||
|
// Should call onStaticOptionsChange to remove static options
|
||||||
|
expect(mockOnStaticOptionsChange).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockOnStaticOptionsChange).toHaveBeenCalledWith(undefined);
|
||||||
|
|
||||||
|
// Static options UI elements should be hidden
|
||||||
|
expect(
|
||||||
|
queryByTestId(
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsOrderDropdown
|
||||||
|
)
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
queryByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsRow)
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+22
@@ -14,6 +14,11 @@ import { DataSourcePicker } from 'app/features/datasources/components/picker/Dat
|
|||||||
import { getVariableQueryEditor } from 'app/features/variables/editor/getVariableQueryEditor';
|
import { getVariableQueryEditor } from 'app/features/variables/editor/getVariableQueryEditor';
|
||||||
import { QueryVariableRefreshSelect } from 'app/features/variables/query/QueryVariableRefreshSelect';
|
import { QueryVariableRefreshSelect } from 'app/features/variables/query/QueryVariableRefreshSelect';
|
||||||
import { QueryVariableSortSelect } from 'app/features/variables/query/QueryVariableSortSelect';
|
import { QueryVariableSortSelect } from 'app/features/variables/query/QueryVariableSortSelect';
|
||||||
|
import {
|
||||||
|
StaticOptionsOrderType,
|
||||||
|
StaticOptionsType,
|
||||||
|
QueryVariableStaticOptions,
|
||||||
|
} from 'app/features/variables/query/QueryVariableStaticOptions';
|
||||||
|
|
||||||
import { VariableLegend } from './VariableLegend';
|
import { VariableLegend } from './VariableLegend';
|
||||||
import { VariableTextAreaField } from './VariableTextAreaField';
|
import { VariableTextAreaField } from './VariableTextAreaField';
|
||||||
@@ -41,6 +46,10 @@ interface QueryVariableEditorFormProps {
|
|||||||
onIncludeAllChange: (event: FormEvent<HTMLInputElement>) => void;
|
onIncludeAllChange: (event: FormEvent<HTMLInputElement>) => void;
|
||||||
allValue: string;
|
allValue: string;
|
||||||
onAllValueChange: (event: FormEvent<HTMLInputElement>) => void;
|
onAllValueChange: (event: FormEvent<HTMLInputElement>) => void;
|
||||||
|
staticOptions?: StaticOptionsType;
|
||||||
|
staticOptionsOrder?: StaticOptionsOrderType;
|
||||||
|
onStaticOptionsChange?: (staticOptions: StaticOptionsType) => void;
|
||||||
|
onStaticOptionsOrderChange?: (staticOptionsOrder: StaticOptionsOrderType) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QueryVariableEditorForm({
|
export function QueryVariableEditorForm({
|
||||||
@@ -64,6 +73,10 @@ export function QueryVariableEditorForm({
|
|||||||
onIncludeAllChange,
|
onIncludeAllChange,
|
||||||
allValue,
|
allValue,
|
||||||
onAllValueChange,
|
onAllValueChange,
|
||||||
|
staticOptions,
|
||||||
|
staticOptionsOrder,
|
||||||
|
onStaticOptionsChange,
|
||||||
|
onStaticOptionsOrderChange,
|
||||||
}: QueryVariableEditorFormProps) {
|
}: QueryVariableEditorFormProps) {
|
||||||
const { value: dsConfig } = useAsync(async () => {
|
const { value: dsConfig } = useAsync(async () => {
|
||||||
const datasource = await getDataSourceSrv().get(datasourceRef ?? '');
|
const datasource = await getDataSourceSrv().get(datasourceRef ?? '');
|
||||||
@@ -144,6 +157,15 @@ export function QueryVariableEditorForm({
|
|||||||
refresh={refresh}
|
refresh={refresh}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{onStaticOptionsChange && onStaticOptionsOrderChange && (
|
||||||
|
<QueryVariableStaticOptions
|
||||||
|
staticOptions={staticOptions}
|
||||||
|
staticOptionsOrder={staticOptionsOrder}
|
||||||
|
onStaticOptionsChange={onStaticOptionsChange}
|
||||||
|
onStaticOptionsOrderChange={onStaticOptionsOrderChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<VariableLegend>
|
<VariableLegend>
|
||||||
<Trans i18nKey="dashboard-scene.query-variable-editor-form.selection-options">Selection options</Trans>
|
<Trans i18nKey="dashboard-scene.query-variable-editor-form.selection-options">Selection options</Trans>
|
||||||
</VariableLegend>
|
</VariableLegend>
|
||||||
|
|||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
import { t, Trans } from '@grafana/i18n';
|
||||||
|
import { VariableValueOption } from '@grafana/scenes';
|
||||||
|
import { Button, Input, Stack } from '@grafana/ui';
|
||||||
|
|
||||||
|
interface VariableOptionsFieldProps {
|
||||||
|
options: VariableValueOption[];
|
||||||
|
onChange: (options: VariableValueOption[]) => void;
|
||||||
|
width?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VariableOptionsInput({ options, onChange, width }: VariableOptionsFieldProps) {
|
||||||
|
const [optionsLocal, setOptionsLocal] = useState(options.length ? options : [{ value: '', label: '' }]);
|
||||||
|
|
||||||
|
const updateOptions = (newOptions: VariableValueOption[]) => {
|
||||||
|
setOptionsLocal(newOptions);
|
||||||
|
onChange(
|
||||||
|
newOptions
|
||||||
|
.map((option) => ({
|
||||||
|
label: option.label.trim(),
|
||||||
|
value: String(option.value).trim(),
|
||||||
|
}))
|
||||||
|
.filter((option) => !!option.label)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleValueChange = (index: number, value: string) => {
|
||||||
|
if (optionsLocal[index].value !== value) {
|
||||||
|
const newOptions = [...optionsLocal];
|
||||||
|
newOptions[index] = { ...newOptions[index], value };
|
||||||
|
updateOptions(newOptions);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLabelChange = (index: number, label: string) => {
|
||||||
|
if (optionsLocal[index].label !== label) {
|
||||||
|
const newOptions = [...optionsLocal];
|
||||||
|
newOptions[index] = { ...newOptions[index], label };
|
||||||
|
updateOptions(newOptions);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addOption = () => {
|
||||||
|
const newOption: VariableValueOption = { value: '', label: '' };
|
||||||
|
const newOptions = [...optionsLocal, newOption];
|
||||||
|
updateOptions(newOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeOption = (index: number) => {
|
||||||
|
const newOptions = optionsLocal.filter((_, i) => i !== index);
|
||||||
|
updateOptions(newOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack direction="column" gap={2} width={width}>
|
||||||
|
{optionsLocal.map((option, index) => (
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
key={index}
|
||||||
|
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsRow}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={option.label}
|
||||||
|
placeholder={t('variables.query-variable-static-options.label-placeholder', 'display label')}
|
||||||
|
onChange={(e) => handleLabelChange(index, e.currentTarget.value)}
|
||||||
|
data-testid={
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsLabelInput
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={String(option.value)}
|
||||||
|
placeholder={t('variables.query-variable-static-options.value-placeholder', 'value, default empty string')}
|
||||||
|
onChange={(e) => handleValueChange(index, e.currentTarget.value)}
|
||||||
|
data-testid={
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsValueInput
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon="times"
|
||||||
|
variant="secondary"
|
||||||
|
aria-label={t('variables.query-variable-static-options.remove-option-button-label', 'Remove option')}
|
||||||
|
onClick={() => removeOption(index)}
|
||||||
|
data-testid={
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsDeleteButton
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
))}
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
icon="plus"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={addOption}
|
||||||
|
data-testid={
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsAddButton
|
||||||
|
}
|
||||||
|
aria-label={t('variables.query-variable-static-options.add-option-button-label', 'Add option')}
|
||||||
|
>
|
||||||
|
<Trans i18nKey="variables.query-variable-static-options.add-option-button-label">Add option</Trans>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
+69
@@ -123,6 +123,10 @@ describe('QueryVariableEditor', () => {
|
|||||||
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch
|
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const staticOptionsToggle = renderer.getByTestId(
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsToggle
|
||||||
|
);
|
||||||
|
|
||||||
expect(dataSourcePicker).toBeInTheDocument();
|
expect(dataSourcePicker).toBeInTheDocument();
|
||||||
expect(dataSourcePicker.getAttribute('placeholder')).toBe('Default Test Data Source');
|
expect(dataSourcePicker.getAttribute('placeholder')).toBe('Default Test Data Source');
|
||||||
expect(queryEditor).toBeInTheDocument();
|
expect(queryEditor).toBeInTheDocument();
|
||||||
@@ -141,6 +145,7 @@ describe('QueryVariableEditor', () => {
|
|||||||
expect(includeAllSwitch).toBeChecked();
|
expect(includeAllSwitch).toBeChecked();
|
||||||
expect(allValueInput).toBeInTheDocument();
|
expect(allValueInput).toBeInTheDocument();
|
||||||
expect(allValueInput).toHaveValue('custom all value');
|
expect(allValueInput).toHaveValue('custom all value');
|
||||||
|
expect(staticOptionsToggle).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update the variable with default query for the selected DS', async () => {
|
it('should update the variable with default query for the selected DS', async () => {
|
||||||
@@ -362,6 +367,70 @@ describe('QueryVariableEditor', () => {
|
|||||||
expect(variable.state.allValue).toBe('custom all value and another value');
|
expect(variable.state.allValue).toBe('custom all value and another value');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should update the variable state when adding two static options', async () => {
|
||||||
|
const {
|
||||||
|
variable,
|
||||||
|
renderer: { getByTestId, getAllByTestId },
|
||||||
|
user,
|
||||||
|
} = await setup();
|
||||||
|
|
||||||
|
// Initially no static options
|
||||||
|
expect(variable.state.staticOptions).toBeUndefined();
|
||||||
|
|
||||||
|
// First enable static options
|
||||||
|
const staticOptionsToggle = getByTestId(
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsToggle
|
||||||
|
);
|
||||||
|
await userEvent.click(staticOptionsToggle);
|
||||||
|
|
||||||
|
// Add first static option
|
||||||
|
const addButton = getByTestId(
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsAddButton
|
||||||
|
);
|
||||||
|
await user.click(addButton);
|
||||||
|
|
||||||
|
// Enter label and value for first option
|
||||||
|
const labelInputs = getAllByTestId(
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsLabelInput
|
||||||
|
);
|
||||||
|
const valueInputs = getAllByTestId(
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsValueInput
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.type(labelInputs[0], 'First Option');
|
||||||
|
await user.type(valueInputs[0], 'first-value');
|
||||||
|
|
||||||
|
await waitFor(async () => {
|
||||||
|
await lastValueFrom(variable.validateAndUpdate());
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(variable.state.staticOptions).toEqual([{ label: 'First Option', value: 'first-value' }]);
|
||||||
|
|
||||||
|
// Add second static option
|
||||||
|
await user.click(addButton);
|
||||||
|
|
||||||
|
// Get updated inputs (now there should be 2 sets)
|
||||||
|
const updatedLabelInputs = getAllByTestId(
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsLabelInput
|
||||||
|
);
|
||||||
|
const updatedValueInputs = getAllByTestId(
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsValueInput
|
||||||
|
);
|
||||||
|
|
||||||
|
// Enter label and value for second option
|
||||||
|
await user.type(updatedLabelInputs[1], 'Second Option');
|
||||||
|
await user.type(updatedValueInputs[1], 'second-value');
|
||||||
|
|
||||||
|
await waitFor(async () => {
|
||||||
|
await lastValueFrom(variable.validateAndUpdate());
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(variable.state.staticOptions).toEqual([
|
||||||
|
{ label: 'First Option', value: 'first-value' },
|
||||||
|
{ label: 'Second Option', value: 'second-value' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('should return an empty array if variable is not a QueryVariable', () => {
|
it('should return an empty array if variable is not a QueryVariable', () => {
|
||||||
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
const variable = new TextBoxVariable({ name: 'test', value: 'test value' });
|
const variable = new TextBoxVariable({ name: 'test', value: 'test value' });
|
||||||
|
|||||||
+28
-2
@@ -14,6 +14,7 @@ import { DataSourcePicker } from 'app/features/datasources/components/picker/Dat
|
|||||||
import { getVariableQueryEditor } from 'app/features/variables/editor/getVariableQueryEditor';
|
import { getVariableQueryEditor } from 'app/features/variables/editor/getVariableQueryEditor';
|
||||||
import { QueryVariableRefreshSelect } from 'app/features/variables/query/QueryVariableRefreshSelect';
|
import { QueryVariableRefreshSelect } from 'app/features/variables/query/QueryVariableRefreshSelect';
|
||||||
import { QueryVariableSortSelect } from 'app/features/variables/query/QueryVariableSortSelect';
|
import { QueryVariableSortSelect } from 'app/features/variables/query/QueryVariableSortSelect';
|
||||||
|
import { StaticOptionsOrderType, StaticOptionsType } from 'app/features/variables/query/QueryVariableStaticOptions';
|
||||||
|
|
||||||
import { QueryVariableEditorForm } from '../components/QueryVariableForm';
|
import { QueryVariableEditorForm } from '../components/QueryVariableForm';
|
||||||
import { VariableTextAreaField } from '../components/VariableTextAreaField';
|
import { VariableTextAreaField } from '../components/VariableTextAreaField';
|
||||||
@@ -27,8 +28,19 @@ interface QueryVariableEditorProps {
|
|||||||
type VariableQueryType = QueryVariable['state']['query'];
|
type VariableQueryType = QueryVariable['state']['query'];
|
||||||
|
|
||||||
export function QueryVariableEditor({ variable, onRunQuery }: QueryVariableEditorProps) {
|
export function QueryVariableEditor({ variable, onRunQuery }: QueryVariableEditorProps) {
|
||||||
const { datasource, regex, sort, refresh, isMulti, includeAll, allValue, query, allowCustomValue } =
|
const {
|
||||||
variable.useState();
|
datasource,
|
||||||
|
regex,
|
||||||
|
sort,
|
||||||
|
refresh,
|
||||||
|
isMulti,
|
||||||
|
includeAll,
|
||||||
|
allValue,
|
||||||
|
query,
|
||||||
|
allowCustomValue,
|
||||||
|
staticOptions,
|
||||||
|
staticOptionsOrder,
|
||||||
|
} = variable.useState();
|
||||||
const { value: timeRange } = sceneGraph.getTimeRange(variable).useState();
|
const { value: timeRange } = sceneGraph.getTimeRange(variable).useState();
|
||||||
|
|
||||||
const onRegExChange = (event: React.FormEvent<HTMLTextAreaElement>) => {
|
const onRegExChange = (event: React.FormEvent<HTMLTextAreaElement>) => {
|
||||||
@@ -67,6 +79,16 @@ export function QueryVariableEditor({ variable, onRunQuery }: QueryVariableEdito
|
|||||||
onRunQuery();
|
onRunQuery();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onStaticOptionsChange = (staticOptions: StaticOptionsType) => {
|
||||||
|
onRunQuery();
|
||||||
|
variable.setState({ staticOptions });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStaticOptionsOrderChange = (staticOptionsOrder: StaticOptionsOrderType) => {
|
||||||
|
onRunQuery();
|
||||||
|
variable.setState({ staticOptionsOrder });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryVariableEditorForm
|
<QueryVariableEditorForm
|
||||||
datasource={datasource ?? undefined}
|
datasource={datasource ?? undefined}
|
||||||
@@ -89,6 +111,10 @@ export function QueryVariableEditor({ variable, onRunQuery }: QueryVariableEdito
|
|||||||
onAllValueChange={onAllValueChange}
|
onAllValueChange={onAllValueChange}
|
||||||
allowCustomValue={allowCustomValue}
|
allowCustomValue={allowCustomValue}
|
||||||
onAllowCustomValueChange={onAllowCustomValueChange}
|
onAllowCustomValueChange={onAllowCustomValueChange}
|
||||||
|
staticOptions={staticOptions}
|
||||||
|
staticOptionsOrder={staticOptionsOrder}
|
||||||
|
onStaticOptionsChange={onStaticOptionsChange}
|
||||||
|
onStaticOptionsOrderChange={onStaticOptionsOrderChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,6 +190,11 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode
|
|||||||
hide: variable.hide,
|
hide: variable.hide,
|
||||||
definition: variable.definition,
|
definition: variable.definition,
|
||||||
allowCustomValue: variable.allowCustomValue,
|
allowCustomValue: variable.allowCustomValue,
|
||||||
|
staticOptions: variable.staticOptions?.map((option) => ({
|
||||||
|
label: String(option.text),
|
||||||
|
value: String(option.value),
|
||||||
|
})),
|
||||||
|
staticOptionsOrder: variable.staticOptionsOrder,
|
||||||
});
|
});
|
||||||
} else if (variable.type === 'datasource') {
|
} else if (variable.type === 'datasource') {
|
||||||
return new DataSourceVariable({
|
return new DataSourceVariable({
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
import { t, Trans } from '@grafana/i18n';
|
||||||
|
import { QueryVariable } from '@grafana/scenes';
|
||||||
|
import { Field, Stack, Switch } from '@grafana/ui';
|
||||||
|
import { VariableLegend } from 'app/features/dashboard-scene/settings/variables/components/VariableLegend';
|
||||||
|
import { VariableOptionsInput } from 'app/features/dashboard-scene/settings/variables/components/VariableOptionsInput';
|
||||||
|
import { VariableSelectField } from 'app/features/dashboard-scene/settings/variables/components/VariableSelectField';
|
||||||
|
|
||||||
|
export type StaticOptionsType = QueryVariable['state']['staticOptions'];
|
||||||
|
export type StaticOptionsOrderType = QueryVariable['state']['staticOptionsOrder'];
|
||||||
|
|
||||||
|
interface QueryVariableStaticOptionsProps {
|
||||||
|
staticOptions: StaticOptionsType;
|
||||||
|
staticOptionsOrder: StaticOptionsOrderType;
|
||||||
|
onStaticOptionsChange: (staticOptions: StaticOptionsType) => void;
|
||||||
|
onStaticOptionsOrderChange: (staticOptionsOrder: StaticOptionsOrderType) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SORT_OPTIONS = [
|
||||||
|
{ label: 'Before query values', value: 'before' },
|
||||||
|
{ label: 'After query values', value: 'after' },
|
||||||
|
{ label: 'Sorted with query values', value: 'sorted' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function QueryVariableStaticOptions(props: QueryVariableStaticOptionsProps) {
|
||||||
|
const { staticOptions, onStaticOptionsChange, staticOptionsOrder, onStaticOptionsOrderChange } = props;
|
||||||
|
|
||||||
|
const value = SORT_OPTIONS.find((o) => o.value === staticOptionsOrder) ?? SORT_OPTIONS[0];
|
||||||
|
|
||||||
|
const [areStaticOptionsEnabled, setAreStaticOptionsEnabled] = useState(!!staticOptions?.length);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<VariableLegend>
|
||||||
|
<Trans i18nKey="dashboard-scene.query-variable-editor-form.static-options-legend">Static options</Trans>
|
||||||
|
</VariableLegend>
|
||||||
|
<Stack direction="column" gap={2}>
|
||||||
|
<Field
|
||||||
|
noMargin
|
||||||
|
label={t('dashboard-scene.query-variable-editor-form.label-use-static-options', 'Use static options')}
|
||||||
|
description={t(
|
||||||
|
'variables.query-variable-static-options.description',
|
||||||
|
'Add custom options in addition to query results'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<Stack direction="column" gap={2}>
|
||||||
|
<Switch
|
||||||
|
data-testid={
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsToggle
|
||||||
|
}
|
||||||
|
value={areStaticOptionsEnabled}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.currentTarget.checked) {
|
||||||
|
setAreStaticOptionsEnabled(true);
|
||||||
|
} else {
|
||||||
|
setAreStaticOptionsEnabled(false);
|
||||||
|
if (!!staticOptions?.length) {
|
||||||
|
onStaticOptionsChange(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{areStaticOptionsEnabled && (
|
||||||
|
<VariableOptionsInput width={60} options={staticOptions ?? []} onChange={onStaticOptionsChange} />
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
{areStaticOptionsEnabled && (
|
||||||
|
<VariableSelectField
|
||||||
|
name={t('dashboard-scene.query-variable-editor-form.label-static-options-sort', 'Static options sort')}
|
||||||
|
description={t(
|
||||||
|
'variables.query-variable-static-options-sort-select.description-values-variable',
|
||||||
|
'How to sort static options with query results'
|
||||||
|
)}
|
||||||
|
value={value}
|
||||||
|
options={SORT_OPTIONS}
|
||||||
|
onChange={(opt) => onStaticOptionsOrderChange(opt.value)}
|
||||||
|
testId={
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsStaticOptionsOrderDropdown
|
||||||
|
}
|
||||||
|
width={25}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5837,10 +5837,13 @@
|
|||||||
"description-examples": "Named capture groups can be used to separate the display text and value (<1>see examples</1>).",
|
"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-optional": "Optional, if you want to extract part of a series name or metric node segment.",
|
||||||
"label-data-source": "Data source",
|
"label-data-source": "Data source",
|
||||||
|
"label-static-options-sort": "Static options sort",
|
||||||
"label-target-data-source": "Target data source",
|
"label-target-data-source": "Target data source",
|
||||||
|
"label-use-static-options": "Use static options",
|
||||||
"name-regex": "Regex",
|
"name-regex": "Regex",
|
||||||
"query-options": "Query options",
|
"query-options": "Query options",
|
||||||
"selection-options": "Selection options"
|
"selection-options": "Selection options",
|
||||||
|
"static-options-legend": "Static options"
|
||||||
},
|
},
|
||||||
"resource-export": {
|
"resource-export": {
|
||||||
"label": {
|
"label": {
|
||||||
@@ -13158,6 +13161,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"query-variable-static-options": {
|
||||||
|
"add-option-button-label": "Add option",
|
||||||
|
"description": "Add custom options in addition to query results",
|
||||||
|
"label-placeholder": "display label",
|
||||||
|
"remove-option-button-label": "Remove option",
|
||||||
|
"value-placeholder": "value, default empty string"
|
||||||
|
},
|
||||||
|
"query-variable-static-options-sort-select": {
|
||||||
|
"description-values-variable": "How to sort static options with query results"
|
||||||
|
},
|
||||||
"text-box-variable-editor": {
|
"text-box-variable-editor": {
|
||||||
"name-default-value": "Default value",
|
"name-default-value": "Default value",
|
||||||
"placeholder-default-value-if-any": "default value, if any",
|
"placeholder-default-value-if-any": "default value, if any",
|
||||||
|
|||||||
@@ -3549,7 +3549,7 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
"@grafana/scenes-react@npm:^6.27.0":
|
"@grafana/scenes-react@npm:^6.27.1":
|
||||||
version: 6.27.2
|
version: 6.27.2
|
||||||
resolution: "@grafana/scenes-react@npm:6.27.2"
|
resolution: "@grafana/scenes-react@npm:6.27.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3569,7 +3569,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@grafana/scenes@npm:6.27.2, @grafana/scenes@npm:^6.27.0":
|
"@grafana/scenes@npm:6.27.2, @grafana/scenes@npm:^6.27.1":
|
||||||
version: 6.27.2
|
version: 6.27.2
|
||||||
resolution: "@grafana/scenes@npm:6.27.2"
|
resolution: "@grafana/scenes@npm:6.27.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -18159,8 +18159,8 @@ __metadata:
|
|||||||
"@grafana/plugin-ui": "npm:0.10.7"
|
"@grafana/plugin-ui": "npm:0.10.7"
|
||||||
"@grafana/prometheus": "workspace:*"
|
"@grafana/prometheus": "workspace:*"
|
||||||
"@grafana/runtime": "workspace:*"
|
"@grafana/runtime": "workspace:*"
|
||||||
"@grafana/scenes": "npm:^6.27.0"
|
"@grafana/scenes": "npm:^6.27.1"
|
||||||
"@grafana/scenes-react": "npm:^6.27.0"
|
"@grafana/scenes-react": "npm:^6.27.1"
|
||||||
"@grafana/schema": "workspace:*"
|
"@grafana/schema": "workspace:*"
|
||||||
"@grafana/sql": "workspace:*"
|
"@grafana/sql": "workspace:*"
|
||||||
"@grafana/test-utils": "workspace:*"
|
"@grafana/test-utils": "workspace:*"
|
||||||
|
|||||||
Reference in New Issue
Block a user