Compare commits

...

9 Commits

Author SHA1 Message Date
grafakus
7f584d143f shrug... 2026-01-14 15:15:51 +01:00
grafakus
e38240369b Update cue files++ 2026-01-14 14:58:36 +01:00
grafakus
26ac64cf1a Merge branch 'main' into grafakus/query-variable-support-multiprops 2026-01-14 14:20:50 +01:00
grafakus
d8777012cb Fix lint issues 2026-01-14 14:19:18 +01:00
grafakus
dfa07d8e10 Fix openapi tests 2026-01-14 14:17:43 +01:00
grafakus
dc63eb3314 Regen 2026-01-14 14:03:13 +01:00
grafakus
1e3c6fed55 Merge branch 'main' into grafakus/query-variable-support-multiprops
# Conflicts:
#	apps/dashboard/pkg/apis/dashboard_manifest.go
2026-01-14 13:50:54 +01:00
grafakus
d02201f564 Update schemas 2026-01-14 13:41:21 +01:00
grafakus
2a9377ac02 QueryVariable: Support preview and autocomplete of multi-props 2026-01-14 13:14:32 +01:00
24 changed files with 102 additions and 23 deletions

View File

@@ -800,6 +800,8 @@ VariableOption: {
text: string | [...string] text: string | [...string]
// Value of the option // Value of the option
value: string | [...string] value: string | [...string]
// Additional properties for multi-props variables
properties?: {[string]: string}
} }
// Query variable specification // Query variable specification

View File

@@ -804,6 +804,8 @@ VariableOption: {
text: string | [...string] text: string | [...string]
// Value of the option // Value of the option
value: string | [...string] value: string | [...string]
// Additional properties for multi-props variables
properties?: {[string]: string}
} }
// Query variable specification // Query variable specification

View File

@@ -241,6 +241,8 @@ lineage: schemas: [{
text: string | [...string] text: string | [...string]
// Value of the option // Value of the option
value: string | [...string] value: string | [...string]
// Additional properties for multi-props variables
properties?: {[string]: string}
} @cuetsy(kind="interface") } @cuetsy(kind="interface")
// Options to config when to refresh a variable // Options to config when to refresh a variable

View File

@@ -241,6 +241,8 @@ lineage: schemas: [{
text: string | [...string] text: string | [...string]
// Value of the option // Value of the option
value: string | [...string] value: string | [...string]
// Additional properties for multi-props variables
properties?: {[string]: string}
} @cuetsy(kind="interface") } @cuetsy(kind="interface")
// Options to config when to refresh a variable // Options to config when to refresh a variable

View File

@@ -804,6 +804,8 @@ VariableOption: {
text: string | [...string] text: string | [...string]
// Value of the option // Value of the option
value: string | [...string] value: string | [...string]
// Additional properties for multi-props variables
properties?: {[string]: string}
} }
// Query variable specification // Query variable specification

View File

@@ -1426,6 +1426,8 @@ type DashboardVariableOption struct {
Text DashboardStringOrArrayOfString `json:"text"` Text DashboardStringOrArrayOfString `json:"text"`
// Value of the option // Value of the option
Value DashboardStringOrArrayOfString `json:"value"` Value DashboardStringOrArrayOfString `json:"value"`
// Additional properties for multi-props variables
Properties map[string]string `json:"properties,omitempty"`
} }
// NewDashboardVariableOption creates a new DashboardVariableOption object. // NewDashboardVariableOption creates a new DashboardVariableOption object.

View File

@@ -5133,6 +5133,22 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardVariableOption(ref common.Refer
Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardStringOrArrayOfString"), Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardStringOrArrayOfString"),
}, },
}, },
"properties": {
SchemaProps: spec.SchemaProps{
Description: "Additional properties for multi-props variables",
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
},
},
}, },
Required: []string{"text", "value"}, Required: []string{"text", "value"},
}, },

View File

@@ -808,6 +808,8 @@ VariableOption: {
text: string | [...string] text: string | [...string]
// Value of the option // Value of the option
value: string | [...string] value: string | [...string]
// Additional properties for multi-props variables
properties?: {[string]: string}
} }
// Query variable specification // Query variable specification

View File

@@ -1429,6 +1429,8 @@ type DashboardVariableOption struct {
Text DashboardStringOrArrayOfString `json:"text"` Text DashboardStringOrArrayOfString `json:"text"`
// Value of the option // Value of the option
Value DashboardStringOrArrayOfString `json:"value"` Value DashboardStringOrArrayOfString `json:"value"`
// Additional properties for multi-props variables
Properties map[string]string `json:"properties,omitempty"`
} }
// NewDashboardVariableOption creates a new DashboardVariableOption object. // NewDashboardVariableOption creates a new DashboardVariableOption object.

View File

@@ -5196,6 +5196,22 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardVariableOption(ref common.Refere
Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardStringOrArrayOfString"), Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1.DashboardStringOrArrayOfString"),
}, },
}, },
"properties": {
SchemaProps: spec.SchemaProps{
Description: "Additional properties for multi-props variables",
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
},
},
}, },
Required: []string{"text", "value"}, Required: []string{"text", "value"},
}, },

File diff suppressed because one or more lines are too long

View File

@@ -237,6 +237,8 @@ lineage: schemas: [{
text: string | [...string] text: string | [...string]
// Value of the option // Value of the option
value: string | [...string] value: string | [...string]
// Additional properties for multi-props variables
properties?: {[string]: string}
} @cuetsy(kind="interface") } @cuetsy(kind="interface")
// Options to config when to refresh a variable // Options to config when to refresh a variable

View File

@@ -91,6 +91,8 @@ export interface VariableOption {
text: string | string[]; text: string | string[];
value: string | string[]; value: string | string[];
isNone?: boolean; isNone?: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
properties?: Record<string, any>;
} }
export interface IntervalVariableModel extends VariableWithOptions { export interface IntervalVariableModel extends VariableWithOptions {
@@ -118,6 +120,7 @@ export interface QueryVariableModel extends VariableWithMultiSupport {
definition: string; definition: string;
sort: VariableSort; sort: VariableSort;
queryValue?: string; queryValue?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
query: any; query: any;
regex: string; regex: string;
regexApplyTo?: VariableRegexApplyTo; regexApplyTo?: VariableRegexApplyTo;
@@ -193,6 +196,7 @@ export interface BaseVariableModel {
skipUrlSync: boolean; skipUrlSync: boolean;
index: number; index: number;
state: LoadingState; state: LoadingState;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error: any | null; error: any | null;
description: string | null; description: string | null;
usedInRepeat?: boolean; usedInRepeat?: boolean;

View File

@@ -231,6 +231,10 @@ export const defaultVariableModel: Partial<VariableModel> = {
* Option to be selected in a variable. * Option to be selected in a variable.
*/ */
export interface VariableOption { export interface VariableOption {
/**
* Additional properties for multi-props variables
*/
properties?: Record<string, string>;
/** /**
* Whether the option is selected or not * Whether the option is selected or not
*/ */

View File

@@ -715,7 +715,9 @@ VariableOption: {
// Text to be displayed for the option // Text to be displayed for the option
text: string | [...string] text: string | [...string]
// Value of the option // Value of the option
value: string | [...string] value: string | [...string]
// Additional properties for multi-props variables
properties?: {[string]: string}
} }
// Query variable specification // Query variable specification

View File

@@ -903,6 +903,8 @@ type VariableOption struct {
Text StringOrArrayOfString `json:"text"` Text StringOrArrayOfString `json:"text"`
// Value of the option // Value of the option
Value StringOrArrayOfString `json:"value"` Value StringOrArrayOfString `json:"value"`
// Additional properties for multi-props variables
Properties map[string]string `json:"properties,omitempty"`
} }
// NewVariableOption creates a new VariableOption object. // NewVariableOption creates a new VariableOption object.

View File

@@ -3912,6 +3912,13 @@
"value" "value"
], ],
"properties": { "properties": {
"properties": {
"description": "Additional properties for multi-props variables",
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"selected": { "selected": {
"description": "Whether the option is selected or not", "description": "Whether the option is selected or not",
"type": "boolean" "type": "boolean"

View File

@@ -3939,6 +3939,13 @@
"value" "value"
], ],
"properties": { "properties": {
"properties": {
"description": "Additional properties for multi-props variables",
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"selected": { "selected": {
"description": "Whether the option is selected or not", "description": "Whether the option is selected or not",
"type": "boolean" "type": "boolean"

View File

@@ -284,6 +284,7 @@ function variableValueOptionsToVariableOptions(varState: MultiValueVariable['sta
value: String(o.value), value: String(o.value),
text: o.label, text: o.label,
selected: Array.isArray(varState.value) ? varState.value.includes(o.value) : varState.value === o.value, selected: Array.isArray(varState.value) ? varState.value.includes(o.value) : varState.value === o.value,
...(o.properties && { properties: o.properties }),
})); }));
} }

View File

@@ -69,7 +69,6 @@ export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete
const isHasVariableOptions = hasVariableOptions(variable); const isHasVariableOptions = hasVariableOptions(variable);
const optionsForSelect = isHasVariableOptions ? variable.getOptionsForSelect(false) : []; const optionsForSelect = isHasVariableOptions ? variable.getOptionsForSelect(false) : [];
const hasMultiProps = 'valuesFormat' in variable.state && variable.state.valuesFormat === 'json';
const onDeleteVariable = (hideModal: () => void) => () => { const onDeleteVariable = (hideModal: () => void) => () => {
reportInteraction('Delete variable'); reportInteraction('Delete variable');
@@ -125,7 +124,7 @@ export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete
{EditorToRender && <EditorToRender variable={variable} onRunQuery={onRunQuery} />} {EditorToRender && <EditorToRender variable={variable} onRunQuery={onRunQuery} />}
{isHasVariableOptions && <VariableValuesPreview options={optionsForSelect} hasMultiProps={hasMultiProps} />} {isHasVariableOptions && <VariableValuesPreview options={optionsForSelect} />}
<div className={styles.buttonContainer}> <div className={styles.buttonContainer}>
<Stack gap={2}> <Stack gap={2}>

View File

@@ -10,13 +10,16 @@ import { Button, InlineFieldRow, InlineLabel, InteractiveTable, Text, useStyles2
export interface Props { export interface Props {
options: VariableValueOption[]; options: VariableValueOption[];
hasMultiProps?: boolean;
} }
export const VariableValuesPreview = ({ options, hasMultiProps }: Props) => { const hasMultiProps = (options: Props['options']) => {
return Object.keys(options[1]?.properties ?? options[0]?.properties ?? {}).length > 0;
};
export const VariableValuesPreview = ({ options }: Props) => {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const hasOptions = options.length > 0; const hasOptions = options.length > 0;
const displayMultiPropsPreview = config.featureToggles.multiPropsVariables && hasMultiProps; const displayMultiPropsPreview = config.featureToggles.multiPropsVariables && hasOptions && hasMultiProps(options);
return ( return (
<div className={styles.previewContainer} style={{ gap: '8px' }}> <div className={styles.previewContainer} style={{ gap: '8px' }}>
@@ -43,7 +46,8 @@ function VariableValuesWithPropsPreview({ options }: { options: VariableValueOpt
return { return {
data, data,
columns: Object.keys(data[0] ?? {}).map((id) => ({ // the option at index 0 can be "All" so we try to grab the column names from the 2nd option
columns: Object.keys(data[1] ?? data[0] ?? {}).map((id) => ({
id, id,
// see https://github.com/TanStack/table/issues/1671 // see https://github.com/TanStack/table/issues/1671
header: unsanitizeKey(id), header: unsanitizeKey(id),
@@ -62,7 +66,6 @@ function VariableValuesWithPropsPreview({ options }: { options: VariableValueOpt
/> />
); );
} }
const sanitizeKey = (key: string) => key.replace(/\./g, '__dot__'); const sanitizeKey = (key: string) => key.replace(/\./g, '__dot__');
const unsanitizeKey = (key: string) => key.replace(/__dot__/g, '.'); const unsanitizeKey = (key: string) => key.replace(/__dot__/g, '.');

View File

@@ -69,7 +69,7 @@ function ModalEditorMultiProps(props: ModalEditorProps) {
{queryValidationError && <FieldValidationMessage>{queryValidationError.message}</FieldValidationMessage>} {queryValidationError && <FieldValidationMessage>{queryValidationError.message}</FieldValidationMessage>}
</div> </div>
<div> <div>
<VariableValuesPreview options={options} hasMultiProps={valuesFormat === 'json'} /> <VariableValuesPreview options={options} />
</div> </div>
</Stack> </Stack>
<Modal.ButtonRow> <Modal.ButtonRow>

View File

@@ -81,16 +81,17 @@ const buildLabelPath = (label: string) => {
}; };
const getVariableValueProperties = (variable: TypedVariableModel): string[] => { const getVariableValueProperties = (variable: TypedVariableModel): string[] => {
if (!('valuesFormat' in variable) || variable.valuesFormat !== 'json') { if (!('options' in variable) || !variable.options[0].properties) {
return []; return [];
} }
function collectFieldPaths(option: Record<string, string>, currentPath: string) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
function collectFieldPaths(properties: Record<string, any>, currentPath: string) {
let paths: string[] = []; let paths: string[] = [];
for (const field in option) { for (const field in properties) {
if (option.hasOwnProperty(field)) { if (properties.hasOwnProperty(field)) {
const newPath = `${currentPath}.${field}`; const newPath = `${currentPath}.${field}`;
const value = option[field]; const value = properties[field];
if (typeof value === 'object' && value !== null) { if (typeof value === 'object' && value !== null) {
paths = [...paths, ...collectFieldPaths(value, newPath)]; paths = [...paths, ...collectFieldPaths(value, newPath)];
} }
@@ -100,11 +101,7 @@ const getVariableValueProperties = (variable: TypedVariableModel): string[] => {
return paths; return paths;
} }
try { return collectFieldPaths(variable.options[0].properties, variable.name);
return collectFieldPaths(JSON.parse(variable.query)[0], variable.name);
} catch {
return [];
}
}; };
export const getPanelLinksVariableSuggestions = (): VariableSuggestion[] => [ export const getPanelLinksVariableSuggestions = (): VariableSuggestion[] => [

View File

@@ -503,13 +503,16 @@ describe('linkSrv', () => {
}); });
describe('getPanelLinksVariableSuggestions', () => { describe('getPanelLinksVariableSuggestions', () => {
it('then it should return template variables, json properties and built-ins', () => { it('then it should return template variables, options properties and built-ins', () => {
const templateSrvWithJsonValues = initTemplateSrv('key', [ const templateSrvWithJsonValues = initTemplateSrv('key', [
{ {
type: 'custom', type: 'custom',
name: 'customServers', name: 'customServers',
valuesFormat: 'json', valuesFormat: 'json',
query: '[{"name":"web","ip":"192.168.0.100"},{"name":"ads","ip":"192.168.0.142"}]', options: [
{ text: 'web', value: 'web', properties: { name: 'web', ip: '192.168.0.100' } },
{ text: 'ads', value: 'ads', properties: { name: 'ads', ip: '192.168.0.142' } },
],
}, },
]); ]);
setTemplateSrv(templateSrvWithJsonValues); setTemplateSrv(templateSrvWithJsonValues);