mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Plugins: Set migration handler to definne its own PanelMigrationModel and add tests (#97743)
* Add custom panel model type for the setMigrationHandler * Add tests
This commit is contained in:
parent
fccb7816b5
commit
089111f702
@ -5,6 +5,7 @@ import {
|
|||||||
standardFieldConfigEditorRegistry,
|
standardFieldConfigEditorRegistry,
|
||||||
} from '../field/standardFieldConfigEditorRegistry';
|
} from '../field/standardFieldConfigEditorRegistry';
|
||||||
import { FieldConfigProperty, FieldConfigPropertyItem } from '../types/fieldOverrides';
|
import { FieldConfigProperty, FieldConfigPropertyItem } from '../types/fieldOverrides';
|
||||||
|
import { PanelMigrationModel } from '../types/panel';
|
||||||
import { PanelOptionsEditorBuilder } from '../utils/OptionsUIBuilders';
|
import { PanelOptionsEditorBuilder } from '../utils/OptionsUIBuilders';
|
||||||
|
|
||||||
import { PanelPlugin } from './PanelPlugin';
|
import { PanelPlugin } from './PanelPlugin';
|
||||||
@ -308,4 +309,178 @@ describe('PanelPlugin', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('setMigrationHandler', () => {
|
||||||
|
it('should handle synchronous migrations', () => {
|
||||||
|
const panel = new PanelPlugin(() => <div>Panel</div>);
|
||||||
|
const mockMigration = () => ({
|
||||||
|
newOption: 'migrated',
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.setMigrationHandler(mockMigration);
|
||||||
|
|
||||||
|
const migrationModel: PanelMigrationModel = {
|
||||||
|
id: 1,
|
||||||
|
type: 'test-panel',
|
||||||
|
options: { oldOption: 'value' },
|
||||||
|
fieldConfig: { defaults: {}, overrides: [] },
|
||||||
|
pluginVersion: '1.0.0',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(panel.onPanelMigration).toBeDefined();
|
||||||
|
expect(panel.onPanelMigration!(migrationModel)).toEqual({
|
||||||
|
newOption: 'migrated',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle async migrations', async () => {
|
||||||
|
const panel = new PanelPlugin(() => <div>Panel</div>);
|
||||||
|
const mockAsyncMigration = async () => {
|
||||||
|
return Promise.resolve({
|
||||||
|
newOption: 'async-migrated',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
panel.setMigrationHandler(mockAsyncMigration);
|
||||||
|
|
||||||
|
const migrationModel: PanelMigrationModel = {
|
||||||
|
id: 1,
|
||||||
|
type: 'test-panel',
|
||||||
|
options: { oldOption: 'value' },
|
||||||
|
fieldConfig: { defaults: {}, overrides: [] },
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await panel.onPanelMigration!(migrationModel);
|
||||||
|
expect(result).toEqual({
|
||||||
|
newOption: 'async-migrated',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex panel migrations with advanced transformations', () => {
|
||||||
|
const panel = new PanelPlugin(() => <div>Panel</div>);
|
||||||
|
|
||||||
|
const mockMigration = (model: PanelMigrationModel) => {
|
||||||
|
const { options, fieldConfig, title, id, type, pluginVersion } = model;
|
||||||
|
|
||||||
|
//notice many of these migrations don't make sense in real code but are here
|
||||||
|
//to make sure the attributes of the PanelMigrationModel are used and tested
|
||||||
|
const baseMigration = {
|
||||||
|
...options,
|
||||||
|
display: {
|
||||||
|
...options.display,
|
||||||
|
mode: options.display?.type === 'legacy' ? 'modern' : options.display?.mode,
|
||||||
|
title: title?.toLowerCase() ?? 'untitled',
|
||||||
|
panelId: `${type}-${id}`,
|
||||||
|
},
|
||||||
|
thresholds: options.thresholds?.map((t: { value: string | number; color: string }) => ({
|
||||||
|
...t,
|
||||||
|
value: typeof t.value === 'string' ? parseInt(t.value, 10) : t.value,
|
||||||
|
// Use fieldConfig defaults for threshold colors if available
|
||||||
|
color: fieldConfig.defaults?.color ?? t.color,
|
||||||
|
})),
|
||||||
|
metadata: {
|
||||||
|
migrationVersion: pluginVersion ? `${pluginVersion} -> 2.0.0` : '2.0.0',
|
||||||
|
migratedFields: Object.keys(fieldConfig.defaults ?? {}),
|
||||||
|
overrideCount: fieldConfig.overrides?.length ?? 0,
|
||||||
|
},
|
||||||
|
// Merge custom field defaults into options
|
||||||
|
customDefaults: fieldConfig.defaults?.custom ?? {},
|
||||||
|
// Transform overrides into a map
|
||||||
|
overrideMap: fieldConfig.overrides?.reduce(
|
||||||
|
(acc, override) => ({
|
||||||
|
...acc,
|
||||||
|
[override.matcher.id]: override.properties,
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply panel type specific migrations
|
||||||
|
if (type.includes('visualization')) {
|
||||||
|
return {
|
||||||
|
...baseMigration,
|
||||||
|
visualizationSpecific: {
|
||||||
|
enhanced: true,
|
||||||
|
legacyFormat: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseMigration;
|
||||||
|
};
|
||||||
|
|
||||||
|
panel.setMigrationHandler(mockMigration);
|
||||||
|
|
||||||
|
const complexModel: PanelMigrationModel = {
|
||||||
|
id: 123,
|
||||||
|
type: 'visualization-panel',
|
||||||
|
title: 'Complex METRICS',
|
||||||
|
pluginVersion: '1.0.0',
|
||||||
|
options: {
|
||||||
|
display: {
|
||||||
|
type: 'legacy',
|
||||||
|
showHeader: true,
|
||||||
|
},
|
||||||
|
thresholds: [
|
||||||
|
{ value: '90', color: 'red' },
|
||||||
|
{ value: '50', color: 'yellow' },
|
||||||
|
],
|
||||||
|
queries: ['A', 'B'],
|
||||||
|
},
|
||||||
|
fieldConfig: {
|
||||||
|
defaults: {
|
||||||
|
color: { mode: 'thresholds' },
|
||||||
|
custom: {
|
||||||
|
lineWidth: 1,
|
||||||
|
fillOpacity: 0.5,
|
||||||
|
},
|
||||||
|
mappings: [],
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
matcher: { id: 'byName', options: 'cpu' },
|
||||||
|
properties: [{ id: 'color', value: 'red' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
matcher: { id: 'byValue', options: 'memory' },
|
||||||
|
properties: [{ id: 'unit', value: 'bytes' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = panel.onPanelMigration!(complexModel);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
display: {
|
||||||
|
mode: 'modern',
|
||||||
|
showHeader: true,
|
||||||
|
title: 'complex metrics',
|
||||||
|
panelId: 'visualization-panel-123',
|
||||||
|
},
|
||||||
|
thresholds: [
|
||||||
|
{ value: 90, color: { mode: 'thresholds' } },
|
||||||
|
{ value: 50, color: { mode: 'thresholds' } },
|
||||||
|
],
|
||||||
|
queries: ['A', 'B'],
|
||||||
|
metadata: {
|
||||||
|
migrationVersion: '1.0.0 -> 2.0.0',
|
||||||
|
migratedFields: ['color', 'custom', 'mappings'],
|
||||||
|
overrideCount: 2,
|
||||||
|
},
|
||||||
|
customDefaults: {
|
||||||
|
lineWidth: 1,
|
||||||
|
fillOpacity: 0.5,
|
||||||
|
},
|
||||||
|
overrideMap: {
|
||||||
|
byName: [{ id: 'color', value: 'red' }],
|
||||||
|
byValue: [{ id: 'unit', value: 'bytes' }],
|
||||||
|
},
|
||||||
|
visualizationSpecific: {
|
||||||
|
enhanced: true,
|
||||||
|
legacyFormat: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -134,12 +134,32 @@ export interface PanelEditorProps<T = any> {
|
|||||||
data?: PanelData;
|
data?: PanelData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This type mirrors the required properties from PanelModel<TOptions> needed for migration handlers.
|
||||||
|
*
|
||||||
|
* By maintaining a separate type definition, we ensure that changes to PanelModel
|
||||||
|
* that would break third-party migration handlers are caught at compile time,
|
||||||
|
* rather than failing silently when third-party code attempts to use an incompatible panel.
|
||||||
|
*
|
||||||
|
* TOptions must be any to follow the same pattern as PanelModel<TOptions>
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export interface PanelMigrationModel<TOptions = any> {
|
||||||
|
id: number;
|
||||||
|
type: string;
|
||||||
|
title?: string;
|
||||||
|
options: TOptions;
|
||||||
|
fieldConfig: PanelModel<TOptions>['fieldConfig'];
|
||||||
|
pluginVersion?: string;
|
||||||
|
targets?: PanelModel<TOptions>['targets'];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when a panel is first loaded with current panel model to migrate panel options if needed.
|
* Called when a panel is first loaded with current panel model to migrate panel options if needed.
|
||||||
* Can return panel options, or a Promise that resolves to panel options for async migrations
|
* Can return panel options, or a Promise that resolves to panel options for async migrations
|
||||||
*/
|
*/
|
||||||
export type PanelMigrationHandler<TOptions = any> = (
|
export type PanelMigrationHandler<TOptions = any> = (
|
||||||
panel: PanelModel<TOptions>
|
panel: PanelMigrationModel<TOptions>
|
||||||
) => Partial<TOptions> | Promise<Partial<TOptions>>;
|
) => Partial<TOptions> | Promise<Partial<TOptions>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user