Fix lint errors in Sparkline hover implementation

- Remove type assertions (as any) in favor of type guards
- Fix emotion CSS to use object notation instead of template literal
- Rename 'component' to 'view' in test files (testing-library convention)
- Add eslint-disable comments for necessary any types in tests
- Add proper uPlot typing for test mock objects
This commit is contained in:
Cedric Ziel
2025-11-28 11:44:15 +01:00
parent 27a3503e61
commit e0f2bdbcc1
2 changed files with 46 additions and 31 deletions
@@ -1,4 +1,5 @@
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import uPlot from 'uplot';
import { createTheme, FieldConfig, FieldSparkline, FieldType } from '@grafana/data'; import { createTheme, FieldConfig, FieldSparkline, FieldType } from '@grafana/data';
import { GraphFieldConfig } from '@grafana/schema'; import { GraphFieldConfig } from '@grafana/schema';
@@ -36,10 +37,10 @@ describe('Sparkline', () => {
const config: FieldConfig<GraphFieldConfig> = { const config: FieldConfig<GraphFieldConfig> = {
custom: { custom: {
interactionEnabled: true, interactionEnabled: true,
} as any, } as GraphFieldConfig & { interactionEnabled?: boolean },
}; };
const component = render( const view = render(
<Sparkline <Sparkline
width={800} width={800}
height={600} height={600}
@@ -51,7 +52,9 @@ describe('Sparkline', () => {
); );
// Get the Sparkline instance to access the config builder // Get the Sparkline instance to access the config builder
const instance = (component.container.firstChild as any)?.__reactFiber$?.return?.stateNode; // eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const instance = (view.container.firstChild as any)?.__reactFiber$?.return?.stateNode;
if (instance?.state?.configBuilder) { if (instance?.state?.configBuilder) {
const builder = instance.state.configBuilder; const builder = instance.state.configBuilder;
const hooks = builder.getConfig().hooks; const hooks = builder.getConfig().hooks;
@@ -63,7 +66,9 @@ describe('Sparkline', () => {
const mockUPlot = { const mockUPlot = {
cursor: { idxs: [2, 2] }, cursor: { idxs: [2, 2] },
data: [mockSparkline.x!.values, mockSparkline.y.values], data: [mockSparkline.x!.values, mockSparkline.y.values],
}; } as Partial<uPlot>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setLegendHook(mockUPlot as any); setLegendHook(mockUPlot as any);
expect(onHover).toHaveBeenCalledWith(3, 2); expect(onHover).toHaveBeenCalledWith(3, 2);
@@ -76,10 +81,10 @@ describe('Sparkline', () => {
const config: FieldConfig<GraphFieldConfig> = { const config: FieldConfig<GraphFieldConfig> = {
custom: { custom: {
interactionEnabled: true, interactionEnabled: true,
} as any, } as GraphFieldConfig & { interactionEnabled?: boolean },
}; };
const component = render( const view = render(
<Sparkline <Sparkline
width={800} width={800}
height={600} height={600}
@@ -90,7 +95,8 @@ describe('Sparkline', () => {
/> />
); );
const instance = (component.container.firstChild as any)?.__reactFiber$?.return?.stateNode; // eslint-disable-next-line @typescript-eslint/no-explicit-any
const instance = (view.container.firstChild as any)?.__reactFiber$?.return?.stateNode;
if (instance?.state?.configBuilder) { if (instance?.state?.configBuilder) {
const builder = instance.state.configBuilder; const builder = instance.state.configBuilder;
const hooks = builder.getConfig().hooks; const hooks = builder.getConfig().hooks;
@@ -102,6 +108,7 @@ describe('Sparkline', () => {
cursor: { idxs: [null, null] }, cursor: { idxs: [null, null] },
data: [mockSparkline.x!.values, mockSparkline.y.values], data: [mockSparkline.x!.values, mockSparkline.y.values],
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setLegendHook(mockUPlot as any); setLegendHook(mockUPlot as any);
expect(onHover).toHaveBeenCalledWith(null, null); expect(onHover).toHaveBeenCalledWith(null, null);
@@ -114,10 +121,10 @@ describe('Sparkline', () => {
const config: FieldConfig<GraphFieldConfig> = { const config: FieldConfig<GraphFieldConfig> = {
custom: { custom: {
interactionEnabled: false, interactionEnabled: false,
} as any, } as GraphFieldConfig & { interactionEnabled?: boolean },
}; };
const component = render( const view = render(
<Sparkline <Sparkline
width={800} width={800}
height={600} height={600}
@@ -128,7 +135,8 @@ describe('Sparkline', () => {
/> />
); );
const instance = (component.container.firstChild as any)?.__reactFiber$?.return?.stateNode; // eslint-disable-next-line @typescript-eslint/no-explicit-any
const instance = (view.container.firstChild as any)?.__reactFiber$?.return?.stateNode;
if (instance?.state?.configBuilder) { if (instance?.state?.configBuilder) {
const builder = instance.state.configBuilder; const builder = instance.state.configBuilder;
const hooks = builder.getConfig().hooks; const hooks = builder.getConfig().hooks;
@@ -142,10 +150,10 @@ describe('Sparkline', () => {
const config: FieldConfig<GraphFieldConfig> = { const config: FieldConfig<GraphFieldConfig> = {
custom: { custom: {
interactionEnabled: true, interactionEnabled: true,
} as any, } as GraphFieldConfig & { interactionEnabled?: boolean },
}; };
const component = render( const view = render(
<Sparkline <Sparkline
width={800} width={800}
height={600} height={600}
@@ -155,7 +163,8 @@ describe('Sparkline', () => {
/> />
); );
const instance = (component.container.firstChild as any)?.__reactFiber$?.return?.stateNode; // eslint-disable-next-line @typescript-eslint/no-explicit-any
const instance = (view.container.firstChild as any)?.__reactFiber$?.return?.stateNode;
if (instance?.state?.configBuilder) { if (instance?.state?.configBuilder) {
const builder = instance.state.configBuilder; const builder = instance.state.configBuilder;
const hooks = builder.getConfig().hooks; const hooks = builder.getConfig().hooks;
@@ -168,10 +177,10 @@ describe('Sparkline', () => {
it('should enable interaction by default when not explicitly configured', () => { it('should enable interaction by default when not explicitly configured', () => {
const onHover = jest.fn(); const onHover = jest.fn();
const config: FieldConfig<GraphFieldConfig> = { const config: FieldConfig<GraphFieldConfig> = {
custom: {} as any, custom: {} as GraphFieldConfig & { interactionEnabled?: boolean },
}; };
const component = render( const view = render(
<Sparkline <Sparkline
width={800} width={800}
height={600} height={600}
@@ -182,7 +191,8 @@ describe('Sparkline', () => {
/> />
); );
const instance = (component.container.firstChild as any)?.__reactFiber$?.return?.stateNode; // eslint-disable-next-line @typescript-eslint/no-explicit-any
const instance = (view.container.firstChild as any)?.__reactFiber$?.return?.stateNode;
if (instance?.state?.configBuilder) { if (instance?.state?.configBuilder) {
const builder = instance.state.configBuilder; const builder = instance.state.configBuilder;
const hooks = builder.getConfig().hooks; const hooks = builder.getConfig().hooks;
@@ -198,7 +208,7 @@ describe('Sparkline', () => {
const config: FieldConfig<GraphFieldConfig> = { const config: FieldConfig<GraphFieldConfig> = {
custom: { custom: {
interactionEnabled: true, interactionEnabled: true,
} as any, } as GraphFieldConfig & { interactionEnabled?: boolean },
}; };
const sparklineWithNaN: FieldSparkline = { const sparklineWithNaN: FieldSparkline = {
@@ -209,7 +219,7 @@ describe('Sparkline', () => {
}, },
}; };
const component = render( const view = render(
<Sparkline <Sparkline
width={800} width={800}
height={600} height={600}
@@ -220,7 +230,8 @@ describe('Sparkline', () => {
/> />
); );
const instance = (component.container.firstChild as any)?.__reactFiber$?.return?.stateNode; // eslint-disable-next-line @typescript-eslint/no-explicit-any
const instance = (view.container.firstChild as any)?.__reactFiber$?.return?.stateNode;
if (instance?.state?.configBuilder) { if (instance?.state?.configBuilder) {
const builder = instance.state.configBuilder; const builder = instance.state.configBuilder;
const hooks = builder.getConfig().hooks; const hooks = builder.getConfig().hooks;
@@ -231,7 +242,8 @@ describe('Sparkline', () => {
const mockUPlot1 = { const mockUPlot1 = {
cursor: { idxs: [1, 1] }, cursor: { idxs: [1, 1] },
data: [sparklineWithNaN.x!.values, sparklineWithNaN.y.values], data: [sparklineWithNaN.x!.values, sparklineWithNaN.y.values],
}; } as Partial<uPlot>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setLegendHook(mockUPlot1 as any); setLegendHook(mockUPlot1 as any);
expect(onHover).toHaveBeenCalledWith(null, null); expect(onHover).toHaveBeenCalledWith(null, null);
@@ -241,7 +253,8 @@ describe('Sparkline', () => {
const mockUPlot3 = { const mockUPlot3 = {
cursor: { idxs: [3, 3] }, cursor: { idxs: [3, 3] },
data: [sparklineWithNaN.x!.values, sparklineWithNaN.y.values], data: [sparklineWithNaN.x!.values, sparklineWithNaN.y.values],
}; } as Partial<uPlot>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setLegendHook(mockUPlot3 as any); setLegendHook(mockUPlot3 as any);
expect(onHover).toHaveBeenCalledWith(null, null); expect(onHover).toHaveBeenCalledWith(null, null);
} }
@@ -112,8 +112,10 @@ export class Sparkline extends PureComponent<SparklineProps, State> {
const builder = new UPlotConfigBuilder(); const builder = new UPlotConfigBuilder();
// Check if interaction is enabled (default to true) // Check if interaction is enabled (default to true)
// Use type assertion since interactionEnabled is on TableSparklineCellOptions // interactionEnabled is on TableSparklineCellOptions which extends GraphFieldConfig
const interactionEnabled = (config?.custom as any)?.interactionEnabled ?? true; const customConfig = config?.custom;
const interactionEnabled =
customConfig && 'interactionEnabled' in customConfig ? customConfig.interactionEnabled : true;
// X is the first field in the alligned frame // X is the first field in the alligned frame
const xField = data.fields[0]; const xField = data.fields[0];
@@ -214,11 +216,11 @@ export class Sparkline extends PureComponent<SparklineProps, State> {
focus: { focus: {
prox: 30, // proximity in CSS pixels for hover detection prox: 30, // proximity in CSS pixels for hover detection
}, },
} as any); // Type assertion needed for cursor styling properties });
// Track cursor position and call onHover with the value at that position // Track cursor position and call onHover with the value at that position
// Using setLegend hook which fires on hover (not just drag-to-select like setSelect) // Using setLegend hook which fires on hover (not just drag-to-select like setSelect)
builder.addHook('setLegend', (u) => { builder.addHook('setLegend', (u: uPlot) => {
const dataIdx = u.cursor.idxs?.[1]; // Get the data index from the cursor const dataIdx = u.cursor.idxs?.[1]; // Get the data index from the cursor
if (dataIdx != null) { if (dataIdx != null) {
const yData = u.data[1]; // Y-axis data (values) const yData = u.data[1]; // Y-axis data (values)
@@ -250,12 +252,12 @@ export class Sparkline extends PureComponent<SparklineProps, State> {
const { width, height } = this.props; const { width, height } = this.props;
// Style the vertical cursor bar to be more visible on small sparklines // Style the vertical cursor bar to be more visible on small sparklines
const cursorStyles = css` const cursorStyles = css({
.u-cursor-x { '.u-cursor-x': {
border-left: 2px solid !important; borderLeft: '2px solid !important',
opacity: 1 !important; opacity: '1 !important',
} },
`; });
return ( return (
<div className={cursorStyles}> <div className={cursorStyles}>