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:
Esteban Beltran 2024-12-16 11:50:07 +01:00 committed by GitHub
parent fccb7816b5
commit 089111f702
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 196 additions and 1 deletions

View File

@ -5,6 +5,7 @@ import {
standardFieldConfigEditorRegistry,
} from '../field/standardFieldConfigEditorRegistry';
import { FieldConfigProperty, FieldConfigPropertyItem } from '../types/fieldOverrides';
import { PanelMigrationModel } from '../types/panel';
import { PanelOptionsEditorBuilder } from '../utils/OptionsUIBuilders';
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,
},
});
});
});
});

View File

@ -134,12 +134,32 @@ export interface PanelEditorProps<T = any> {
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.
* Can return panel options, or a Promise that resolves to panel options for async migrations
*/
export type PanelMigrationHandler<TOptions = any> = (
panel: PanelModel<TOptions>
panel: PanelMigrationModel<TOptions>
) => Partial<TOptions> | Promise<Partial<TOptions>>;
/**