|
| 1 | +import { sceneGraph, SceneQueryRunner, type VizPanel } from '@grafana/scenes'; |
| 2 | + |
| 3 | +import pluginJson from '../../../plugin.json'; |
| 4 | +import { getDataSource, getDatasourceVariable, getTraceExplorationScene } from 'utils/utils'; |
| 5 | +import { |
| 6 | + ADD_TO_DASHBOARD_COMPONENT_ID, |
| 7 | + EventOpenAddToDashboard, |
| 8 | + getPanelData, |
| 9 | + type PanelDataRequestPayload, |
| 10 | +} from './addToDashboard'; |
| 11 | + |
| 12 | +jest.mock('utils/utils', () => ({ |
| 13 | + getTraceExplorationScene: jest.fn(), |
| 14 | + getDataSource: jest.fn(), |
| 15 | + getDatasourceVariable: jest.fn(), |
| 16 | +})); |
| 17 | + |
| 18 | +describe('addToDashboard constants', () => { |
| 19 | + it('declares the add-to-dashboard exposed component in plugin.json dependencies', () => { |
| 20 | + expect(pluginJson.dependencies?.extensions?.exposedComponents).toContain(ADD_TO_DASHBOARD_COMPONENT_ID); |
| 21 | + }); |
| 22 | + |
| 23 | + it('exposes the add-to-dashboard extension component id', () => { |
| 24 | + expect(ADD_TO_DASHBOARD_COMPONENT_ID).toBe('grafana/add-to-dashboard-form/v1'); |
| 25 | + }); |
| 26 | +}); |
| 27 | + |
| 28 | +describe('EventOpenAddToDashboard', () => { |
| 29 | + it('uses a fixed event type', () => { |
| 30 | + expect(EventOpenAddToDashboard.type).toBe('open-add-to-dashboard'); |
| 31 | + }); |
| 32 | + |
| 33 | + it('carries panel data on the payload', () => { |
| 34 | + const panelData = { |
| 35 | + panel: { type: 'timeseries', title: 'T', targets: [] }, |
| 36 | + range: { from: 'now-1h', to: 'now', raw: { from: 'now-1h', to: 'now' } }, |
| 37 | + } as unknown as PanelDataRequestPayload; |
| 38 | + |
| 39 | + const evt = new EventOpenAddToDashboard({ panelData }); |
| 40 | + |
| 41 | + expect(evt.payload.panelData).toBe(panelData); |
| 42 | + }); |
| 43 | +}); |
| 44 | + |
| 45 | +describe('getPanelData', () => { |
| 46 | + const mockRange = { from: 'now-6h', to: 'now', raw: { from: 'now-6h', to: 'now' } }; |
| 47 | + |
| 48 | + const baseVizPanel = (): VizPanel => |
| 49 | + ({ |
| 50 | + state: { |
| 51 | + pluginId: 'timeseries', |
| 52 | + title: '{{interval}} — RED', |
| 53 | + options: { legend: { displayMode: 'list' } }, |
| 54 | + fieldConfig: { defaults: {}, overrides: [] }, |
| 55 | + }, |
| 56 | + }) as unknown as VizPanel; |
| 57 | + |
| 58 | + beforeEach(() => { |
| 59 | + jest.spyOn(sceneGraph, 'getTimeRange').mockReturnValue({ |
| 60 | + state: { value: mockRange }, |
| 61 | + } as unknown as ReturnType<typeof sceneGraph.getTimeRange>); |
| 62 | + |
| 63 | + jest.spyOn(sceneGraph, 'interpolate').mockImplementation((_scene, value) => |
| 64 | + typeof value === 'string' ? `[interp:${value}]` : String(value ?? '') |
| 65 | + ); |
| 66 | + |
| 67 | + jest.spyOn(sceneGraph, 'findDescendents').mockReturnValue([]); |
| 68 | + }); |
| 69 | + |
| 70 | + afterEach(() => { |
| 71 | + jest.restoreAllMocks(); |
| 72 | + }); |
| 73 | + |
| 74 | + it('returns time range from the scene graph', () => { |
| 75 | + const vizPanel = baseVizPanel(); |
| 76 | + const dataRef = {}; |
| 77 | + jest.spyOn(sceneGraph, 'getData').mockReturnValue(dataRef as ReturnType<typeof sceneGraph.getData>); |
| 78 | + jest.spyOn(sceneGraph, 'findObject').mockReturnValue(null); |
| 79 | + |
| 80 | + const { range } = getPanelData(vizPanel); |
| 81 | + |
| 82 | + expect(range).toBe(mockRange); |
| 83 | + expect(sceneGraph.getTimeRange).toHaveBeenCalledWith(vizPanel); |
| 84 | + }); |
| 85 | + |
| 86 | + it('maps viz panel state onto the exported panel and clears targets when no query runner is found', () => { |
| 87 | + const vizPanel = baseVizPanel(); |
| 88 | + jest.spyOn(sceneGraph, 'getData').mockReturnValue({} as ReturnType<typeof sceneGraph.getData>); |
| 89 | + jest.spyOn(sceneGraph, 'findObject').mockReturnValue(null); |
| 90 | + |
| 91 | + const { panel } = getPanelData(vizPanel); |
| 92 | + |
| 93 | + expect(panel.type).toBe('timeseries'); |
| 94 | + expect(panel.title).toBe('[interp:{{interval}} — RED]'); |
| 95 | + expect(panel.targets).toEqual([]); |
| 96 | + expect(panel.options).toEqual(vizPanel.state.options); |
| 97 | + expect(panel.fieldConfig).toEqual(vizPanel.state.fieldConfig); |
| 98 | + expect(sceneGraph.findObject).toHaveBeenCalled(); |
| 99 | + }); |
| 100 | + |
| 101 | + it('includes optional description when present', () => { |
| 102 | + const vizPanel = { |
| 103 | + ...baseVizPanel(), |
| 104 | + state: { |
| 105 | + ...baseVizPanel().state, |
| 106 | + description: 'Panel help text', |
| 107 | + }, |
| 108 | + } as unknown as VizPanel; |
| 109 | + |
| 110 | + jest.spyOn(sceneGraph, 'getData').mockReturnValue({} as ReturnType<typeof sceneGraph.getData>); |
| 111 | + jest.spyOn(sceneGraph, 'findObject').mockReturnValue(null); |
| 112 | + |
| 113 | + expect(getPanelData(vizPanel).panel.description).toBe('Panel help text'); |
| 114 | + }); |
| 115 | + |
| 116 | + it('interpolates TraceQL targets and resolves datasource from exploration variable', () => { |
| 117 | + const vizPanel = baseVizPanel(); |
| 118 | + const exploration = {}; |
| 119 | + const runner = new SceneQueryRunner({ |
| 120 | + datasource: { uid: '${ds}', type: 'tempo' }, |
| 121 | + queries: [ |
| 122 | + { |
| 123 | + refId: 'A', |
| 124 | + query: '{ resource.service.name =~ "$service" }', |
| 125 | + datasource: { uid: '${ds}', type: 'tempo' }, |
| 126 | + }, |
| 127 | + ], |
| 128 | + maxDataPoints: 240, |
| 129 | + }); |
| 130 | + |
| 131 | + jest.mocked(getTraceExplorationScene).mockReturnValue(exploration as ReturnType<typeof getTraceExplorationScene>); |
| 132 | + jest.mocked(getDatasourceVariable).mockReturnValue({ |
| 133 | + getValue: () => 'tempo-uid-from-var', |
| 134 | + } as ReturnType<typeof getDatasourceVariable>); |
| 135 | + |
| 136 | + const dataRef = {}; |
| 137 | + jest.spyOn(sceneGraph, 'getData').mockReturnValue(dataRef as ReturnType<typeof sceneGraph.getData>); |
| 138 | + jest.spyOn(sceneGraph, 'findObject').mockImplementation((_root, predicate) => { |
| 139 | + expect(predicate(runner)).toBe(true); |
| 140 | + return runner; |
| 141 | + }); |
| 142 | + jest.spyOn(sceneGraph, 'findDescendents').mockReturnValue([]); |
| 143 | + |
| 144 | + const { panel } = getPanelData(vizPanel); |
| 145 | + |
| 146 | + expect(panel.targets).toEqual([ |
| 147 | + { |
| 148 | + refId: 'A', |
| 149 | + query: '[interp:{ resource.service.name =~ "$service" }]', |
| 150 | + }, |
| 151 | + ]); |
| 152 | + expect(panel.datasource).toEqual({ |
| 153 | + uid: 'tempo-uid-from-var', |
| 154 | + type: 'tempo', |
| 155 | + }); |
| 156 | + expect(panel.maxDataPoints).toBe(240); |
| 157 | + }); |
| 158 | + |
| 159 | + it('leaves an empty query string unmodified', () => { |
| 160 | + const vizPanel = baseVizPanel(); |
| 161 | + const runner = new SceneQueryRunner({ |
| 162 | + datasource: { uid: 'fixed-uid', type: 'tempo' }, |
| 163 | + queries: [{ refId: 'A', query: '' }], |
| 164 | + }); |
| 165 | + |
| 166 | + jest.spyOn(sceneGraph, 'getData').mockReturnValue({} as ReturnType<typeof sceneGraph.getData>); |
| 167 | + jest.spyOn(sceneGraph, 'findObject').mockReturnValue(runner); |
| 168 | + |
| 169 | + const { panel } = getPanelData(vizPanel); |
| 170 | + |
| 171 | + expect(panel.targets?.[0]?.query).toBe(''); |
| 172 | + expect(sceneGraph.interpolate).not.toHaveBeenCalledWith(vizPanel, ''); |
| 173 | + }); |
| 174 | + |
| 175 | + it('prefers explicit alertTargets over layout query runner (breakdown tile scope)', () => { |
| 176 | + const vizPanel = baseVizPanel(); |
| 177 | + const exploration = {}; |
| 178 | + const runner = new SceneQueryRunner({ |
| 179 | + datasource: { uid: 'tempo-runner', type: 'tempo' }, |
| 180 | + queries: [{ refId: 'A', query: '{ aggregate }' }], |
| 181 | + maxDataPoints: 64, |
| 182 | + }); |
| 183 | + |
| 184 | + jest.mocked(getTraceExplorationScene).mockReturnValue(exploration as ReturnType<typeof getTraceExplorationScene>); |
| 185 | + jest.mocked(getDatasourceVariable).mockReturnValue({ |
| 186 | + getValue: () => 'resolved-tempo-uid', |
| 187 | + } as ReturnType<typeof getDatasourceVariable>); |
| 188 | + jest.mocked(getDataSource).mockReturnValue('resolved-tempo-uid'); |
| 189 | + jest.spyOn(sceneGraph, 'getData').mockReturnValue({} as ReturnType<typeof sceneGraph.getData>); |
| 190 | + jest.spyOn(sceneGraph, 'findObject').mockReturnValue(runner); |
| 191 | + jest.spyOn(sceneGraph, 'interpolate').mockImplementation((_scene, value) => { |
| 192 | + if (typeof value === 'string') { |
| 193 | + return value.replace('${service}', 'checkout'); |
| 194 | + } |
| 195 | + return String(value ?? ''); |
| 196 | + }); |
| 197 | + |
| 198 | + const { panel } = getPanelData(vizPanel, [{ refId: 'A', query: '{ resource.service.name="${service}" }' }]); |
| 199 | + |
| 200 | + expect(getTraceExplorationScene).toHaveBeenCalledWith(vizPanel); |
| 201 | + expect(getDatasourceVariable).toHaveBeenCalledWith(exploration); |
| 202 | + expect(panel.targets).toEqual([{ refId: 'A', query: '{ resource.service.name="checkout" }' }]); |
| 203 | + expect(panel.datasource).toEqual({ uid: 'resolved-tempo-uid', type: 'tempo' }); |
| 204 | + expect(panel.maxDataPoints).toBe(64); |
| 205 | + }); |
| 206 | + |
| 207 | + it('does not add uid when query runner datasource omits it', () => { |
| 208 | + const vizPanel = baseVizPanel(); |
| 209 | + const runner = new SceneQueryRunner({ |
| 210 | + datasource: { type: 'tempo' }, |
| 211 | + queries: [{ refId: 'A', query: 'count()' }], |
| 212 | + }); |
| 213 | + |
| 214 | + jest.spyOn(sceneGraph, 'getData').mockReturnValue({} as ReturnType<typeof sceneGraph.getData>); |
| 215 | + jest.spyOn(sceneGraph, 'findObject').mockReturnValue(runner); |
| 216 | + |
| 217 | + const { panel } = getPanelData(vizPanel); |
| 218 | + |
| 219 | + expect(panel.datasource).toEqual({ type: 'tempo' }); |
| 220 | + }); |
| 221 | + |
| 222 | + it('omits panel datasource when there are no query targets', () => { |
| 223 | + const vizPanel = baseVizPanel(); |
| 224 | + |
| 225 | + jest.spyOn(sceneGraph, 'getData').mockReturnValue({} as ReturnType<typeof sceneGraph.getData>); |
| 226 | + jest.spyOn(sceneGraph, 'findObject').mockReturnValue(null); |
| 227 | + jest.spyOn(sceneGraph, 'findDescendents').mockReturnValue([]); |
| 228 | + |
| 229 | + const { panel } = getPanelData(vizPanel); |
| 230 | + |
| 231 | + expect(panel.datasource).toBeUndefined(); |
| 232 | + }); |
| 233 | +}); |
0 commit comments