QueryVariable: Support preview and autocomplete of multi-props

This commit is contained in:
grafakus
2026-01-14 13:14:32 +01:00
parent c95e3da2d5
commit 2a9377ac02
7 changed files with 23 additions and 20 deletions
@@ -91,6 +91,7 @@ export interface VariableOption {
text: string | string[];
value: string | string[];
isNone?: boolean;
properties?: Record<string, any>;
}
export interface IntervalVariableModel extends VariableWithOptions {
@@ -284,6 +284,7 @@ function variableValueOptionsToVariableOptions(varState: MultiValueVariable['sta
value: String(o.value),
text: o.label,
selected: Array.isArray(varState.value) ? varState.value.includes(o.value) : varState.value === o.value,
...(o.properties && { properties: o.properties }),
}));
}
@@ -69,7 +69,6 @@ export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete
const isHasVariableOptions = hasVariableOptions(variable);
const optionsForSelect = isHasVariableOptions ? variable.getOptionsForSelect(false) : [];
const hasMultiProps = 'valuesFormat' in variable.state && variable.state.valuesFormat === 'json';
const onDeleteVariable = (hideModal: () => void) => () => {
reportInteraction('Delete variable');
@@ -125,7 +124,7 @@ export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete
{EditorToRender && <EditorToRender variable={variable} onRunQuery={onRunQuery} />}
{isHasVariableOptions && <VariableValuesPreview options={optionsForSelect} hasMultiProps={hasMultiProps} />}
{isHasVariableOptions && <VariableValuesPreview options={optionsForSelect} />}
<div className={styles.buttonContainer}>
<Stack gap={2}>
@@ -10,13 +10,16 @@ import { Button, InlineFieldRow, InlineLabel, InteractiveTable, Text, useStyles2
export interface Props {
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 hasOptions = options.length > 0;
const displayMultiPropsPreview = config.featureToggles.multiPropsVariables && hasMultiProps;
const displayMultiPropsPreview = config.featureToggles.multiPropsVariables && hasOptions && hasMultiProps(options);
return (
<div className={styles.previewContainer} style={{ gap: '8px' }}>
@@ -43,7 +46,8 @@ function VariableValuesWithPropsPreview({ options }: { options: VariableValueOpt
return {
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,
// see https://github.com/TanStack/table/issues/1671
header: unsanitizeKey(id),
@@ -62,7 +66,6 @@ function VariableValuesWithPropsPreview({ options }: { options: VariableValueOpt
/>
);
}
const sanitizeKey = (key: string) => key.replace(/\./g, '__dot__');
const unsanitizeKey = (key: string) => key.replace(/__dot__/g, '.');
@@ -69,7 +69,7 @@ function ModalEditorMultiProps(props: ModalEditorProps) {
{queryValidationError && <FieldValidationMessage>{queryValidationError.message}</FieldValidationMessage>}
</div>
<div>
<VariableValuesPreview options={options} hasMultiProps={valuesFormat === 'json'} />
<VariableValuesPreview options={options} />
</div>
</Stack>
<Modal.ButtonRow>
@@ -81,16 +81,16 @@ const buildLabelPath = (label: string) => {
};
const getVariableValueProperties = (variable: TypedVariableModel): string[] => {
if (!('valuesFormat' in variable) || variable.valuesFormat !== 'json') {
if (!('options' in variable) || !variable.options[0].properties) {
return [];
}
function collectFieldPaths(option: Record<string, string>, currentPath: string) {
function collectFieldPaths(properties: Record<string, any>, currentPath: string) {
let paths: string[] = [];
for (const field in option) {
if (option.hasOwnProperty(field)) {
for (const field in properties) {
if (properties.hasOwnProperty(field)) {
const newPath = `${currentPath}.${field}`;
const value = option[field];
const value = properties[field];
if (typeof value === 'object' && value !== null) {
paths = [...paths, ...collectFieldPaths(value, newPath)];
}
@@ -100,11 +100,7 @@ const getVariableValueProperties = (variable: TypedVariableModel): string[] => {
return paths;
}
try {
return collectFieldPaths(JSON.parse(variable.query)[0], variable.name);
} catch {
return [];
}
return collectFieldPaths(variable.options[0].properties, variable.name);
};
export const getPanelLinksVariableSuggestions = (): VariableSuggestion[] => [
@@ -503,13 +503,16 @@ describe('linkSrv', () => {
});
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', [
{
type: 'custom',
name: 'customServers',
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);