mirror of https://github.com/grafana/grafana
RowsLayout: Rethinking row repeats (#104312)
* Something is working * Update * Update * working * clear repeated rows * Update * Update * Outline via function * Update * Update * Update * Progress * Update * Udpate sum * Update * Update public/app/features/dashboard-scene/scene/types/EditableDashboardElement.ts Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com> * Update public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com> * Update public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com> * Update public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.tsx Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com> * Update public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com> * Update public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManagerRenderer.tsx Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com> * Update * more tests * Update * Update * Removed old behavior * Update * Update * Update * fix outline for default grid * Update --------- Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com>pull/107375/head
parent
249be02010
commit
c63a52958d
@ -0,0 +1,165 @@ |
||||
import { act, screen, waitFor } from '@testing-library/react'; |
||||
import { render } from 'test/test-utils'; |
||||
|
||||
import { VariableRefresh } from '@grafana/data'; |
||||
import { getPanelPlugin } from '@grafana/data/test'; |
||||
import { setPluginImportUtils } from '@grafana/runtime'; |
||||
import { SceneTimeRange, SceneVariableSet, TestVariable, VariableValueOption, PanelBuilders } from '@grafana/scenes'; |
||||
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from 'app/features/variables/constants'; |
||||
import { TextMode } from 'app/plugins/panel/text/panelcfg.gen'; |
||||
|
||||
import { DashboardScene } from '../DashboardScene'; |
||||
import { AutoGridItem } from '../layout-auto-grid/AutoGridItem'; |
||||
import { AutoGridLayout } from '../layout-auto-grid/AutoGridLayout'; |
||||
import { AutoGridLayoutManager } from '../layout-auto-grid/AutoGridLayoutManager'; |
||||
|
||||
import { RowItem } from './RowItem'; |
||||
import { RowsLayoutManager } from './RowsLayoutManager'; |
||||
|
||||
jest.mock('@grafana/runtime', () => ({ |
||||
...jest.requireActual('@grafana/runtime'), |
||||
setPluginExtensionGetter: jest.fn(), |
||||
getPluginLinkExtensions: jest.fn().mockReturnValue({ extensions: [] }), |
||||
})); |
||||
|
||||
setPluginImportUtils({ |
||||
importPanelPlugin: () => Promise.resolve(getPanelPlugin({})), |
||||
getPanelPluginFromCache: () => undefined, |
||||
}); |
||||
|
||||
describe('RowItemRepeater', () => { |
||||
describe('Given scene with variable with 3 values', () => { |
||||
it('Should repeat row', async () => { |
||||
const { rowToRepeat } = renderScene({ variableQueryTime: 0 }); |
||||
|
||||
await waitFor(() => { |
||||
expect(screen.queryByText('Row A')).toBeInTheDocument(); |
||||
expect(screen.queryByText('Row B')).toBeInTheDocument(); |
||||
expect(screen.queryByText('Row C')).toBeInTheDocument(); |
||||
}); |
||||
|
||||
expect(rowToRepeat.state.key).toBe('row-1-clone-0'); |
||||
expect(rowToRepeat.state.repeatedRows!.length).toBe(2); |
||||
expect(rowToRepeat.state.repeatedRows![0].state.key).toBe('row-1-clone-1'); |
||||
}); |
||||
|
||||
it('Should update repeats when variable value changes', async () => { |
||||
const { repeatByVariable, rowToRepeat } = renderScene({ variableQueryTime: 0 }); |
||||
|
||||
await waitFor(() => { |
||||
expect(screen.queryByText('Row C')).toBeInTheDocument(); |
||||
}); |
||||
|
||||
act(() => { |
||||
repeatByVariable.changeValueTo(['C', 'D']); |
||||
}); |
||||
|
||||
await waitFor(() => { |
||||
expect(screen.queryByText('Row A')).not.toBeInTheDocument(); |
||||
expect(screen.queryByText('Row D')).toBeInTheDocument(); |
||||
}); |
||||
|
||||
expect(rowToRepeat.state.repeatedRows!.length).toBe(1); |
||||
}); |
||||
|
||||
it('Should skip update repeats when variable values the same', async () => { |
||||
const { repeatByVariable, rowToRepeat } = renderScene({ variableQueryTime: 0 }); |
||||
let stateUpdates = 0; |
||||
|
||||
rowToRepeat.subscribeToState((s) => stateUpdates++); |
||||
|
||||
await waitFor(() => { |
||||
expect(screen.queryByText('Row C')).toBeInTheDocument(); |
||||
}); |
||||
|
||||
act(() => { |
||||
repeatByVariable.changeValueTo(['A1', 'B1', 'C1']); |
||||
}); |
||||
|
||||
expect(stateUpdates).toBe(1); |
||||
}); |
||||
|
||||
it('Should handle removing repeats', async () => { |
||||
const { rowToRepeat } = renderScene({ variableQueryTime: 0 }); |
||||
|
||||
await waitFor(() => { |
||||
expect(screen.queryByText('Row C')).toBeInTheDocument(); |
||||
}); |
||||
|
||||
act(() => { |
||||
rowToRepeat.onChangeRepeat(undefined); |
||||
}); |
||||
|
||||
expect(screen.queryByText('Row C')).not.toBeInTheDocument(); |
||||
expect(rowToRepeat.state.$variables).toBe(undefined); |
||||
expect(rowToRepeat.state.repeatedRows).toBe(undefined); |
||||
expect(rowToRepeat.state.repeatByVariable).toBe(undefined); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
interface SceneOptions { |
||||
variableQueryTime: number; |
||||
variableRefresh?: VariableRefresh; |
||||
} |
||||
|
||||
function buildTextPanel(key: string, content: string) { |
||||
const panel = PanelBuilders.text().setOption('content', content).setOption('mode', TextMode.Markdown).build(); |
||||
panel.setState({ key }); |
||||
return panel; |
||||
} |
||||
|
||||
function renderScene( |
||||
options: SceneOptions, |
||||
variableOptions?: VariableValueOption[], |
||||
variableStateOverrides?: { isMulti: boolean } |
||||
) { |
||||
const rows = [ |
||||
new RowItem({ |
||||
key: 'row-1', |
||||
title: 'Row $server', |
||||
repeatByVariable: 'server', |
||||
layout: new AutoGridLayoutManager({ |
||||
layout: new AutoGridLayout({ |
||||
children: [ |
||||
new AutoGridItem({ |
||||
body: buildTextPanel('text-1', 'Panel inside repeated row, server = $server'), |
||||
}), |
||||
], |
||||
}), |
||||
}), |
||||
}), |
||||
]; |
||||
|
||||
const layout = new RowsLayoutManager({ rows }); |
||||
const repeatByVariable = new TestVariable({ |
||||
name: 'server', |
||||
query: 'A.*', |
||||
value: ALL_VARIABLE_VALUE, |
||||
text: ALL_VARIABLE_TEXT, |
||||
isMulti: true, |
||||
includeAll: true, |
||||
delayMs: options.variableQueryTime, |
||||
refresh: options.variableRefresh, |
||||
optionsToReturn: variableOptions ?? [ |
||||
{ label: 'A', value: 'A1' }, |
||||
{ label: 'B', value: 'B1' }, |
||||
{ label: 'C', value: 'C1' }, |
||||
], |
||||
...variableStateOverrides, |
||||
}); |
||||
|
||||
const scene = new DashboardScene({ |
||||
$timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }), |
||||
$variables: new SceneVariableSet({ |
||||
variables: [repeatByVariable], |
||||
}), |
||||
body: layout, |
||||
}); |
||||
|
||||
const rowToRepeat = rows[0]; |
||||
|
||||
render(<scene.Component model={scene} />); |
||||
|
||||
return { scene, layout, rows, rowToRepeat, repeatByVariable }; |
||||
} |
@ -0,0 +1,160 @@ |
||||
import { isEqual } from 'lodash'; |
||||
import { useEffect } from 'react'; |
||||
|
||||
import { |
||||
MultiValueVariable, |
||||
SceneVariableSet, |
||||
LocalValueVariable, |
||||
sceneGraph, |
||||
VariableValueSingle, |
||||
} from '@grafana/scenes'; |
||||
import { Spinner } from '@grafana/ui'; |
||||
|
||||
import { DashboardStateChangedEvent } from '../../edit-pane/shared'; |
||||
import { getCloneKey } from '../../utils/clone'; |
||||
import { dashboardLog, getMultiVariableValues } from '../../utils/utils'; |
||||
import { DashboardRepeatsProcessedEvent } from '../types/DashboardRepeatsProcessedEvent'; |
||||
|
||||
import { RowItem } from './RowItem'; |
||||
import { RowsLayoutManager } from './RowsLayoutManager'; |
||||
|
||||
export interface Props { |
||||
row: RowItem; |
||||
manager: RowsLayoutManager; |
||||
variable: MultiValueVariable; |
||||
} |
||||
|
||||
export function RowItemRepeater({ |
||||
row, |
||||
variable, |
||||
}: { |
||||
row: RowItem; |
||||
manager: RowsLayoutManager; |
||||
variable: MultiValueVariable; |
||||
}) { |
||||
const { repeatedRows } = row.useState(); |
||||
|
||||
// Subscribe to variable state changes and perform repeats when the variable changes
|
||||
useEffect(() => { |
||||
performRowRepeats(variable, row, false); |
||||
|
||||
const variableChangeSub = variable.subscribeToState((state) => performRowRepeats(variable, row, false)); |
||||
const editEventSub = row.subscribeToEvent(DashboardStateChangedEvent, (e) => |
||||
performRowRepeats(variable, row, true) |
||||
); |
||||
|
||||
return () => { |
||||
editEventSub.unsubscribe(); |
||||
variableChangeSub.unsubscribe(); |
||||
}; |
||||
}, [variable, row]); |
||||
|
||||
if ( |
||||
repeatedRows === undefined || |
||||
sceneGraph.hasVariableDependencyInLoadingState(variable) || |
||||
variable.state.loading |
||||
) { |
||||
dashboardLog.logger('RowItemRepeater', false, 'Variable is loading, showing spinner'); |
||||
return <Spinner />; |
||||
} |
||||
|
||||
return ( |
||||
<> |
||||
<row.Component model={row} key={row.state.key!} /> |
||||
{repeatedRows?.map((rowClone) => <rowClone.Component model={rowClone} key={rowClone.state.key!} />)} |
||||
</> |
||||
); |
||||
} |
||||
|
||||
export function performRowRepeats(variable: MultiValueVariable, row: RowItem, contentChanged: boolean) { |
||||
if (sceneGraph.hasVariableDependencyInLoadingState(variable)) { |
||||
dashboardLog.logger('RowItemRepeater', false, 'Skipped dependency in loading state'); |
||||
return; |
||||
} |
||||
|
||||
if (variable.state.loading) { |
||||
dashboardLog.logger('RowItemRepeater', false, 'Skipped, variable is loading'); |
||||
return; |
||||
} |
||||
|
||||
const { values, texts } = getMultiVariableValues(variable); |
||||
const prevValues = getPrevRepeatValues(row, variable.state.name); |
||||
|
||||
if (!contentChanged && isEqual(prevValues, values)) { |
||||
dashboardLog.logger('RowItemRepeater', false, 'Skipped, values the same'); |
||||
return; |
||||
} |
||||
|
||||
if (contentChanged) { |
||||
dashboardLog.logger('RowItemRepeater', false, 'Performing repeats, contentChanged'); |
||||
} else { |
||||
dashboardLog.logger('RowItemRepeater', false, 'Performing repeats, variable values changed', values); |
||||
} |
||||
|
||||
const variableValues = values.length ? values : ['']; |
||||
const variableTexts = texts.length ? texts : variable.hasAllValue() ? ['All'] : ['None']; |
||||
const clonedRows: RowItem[] = []; |
||||
|
||||
// Loop through variable values and create repeats
|
||||
for (let rowIndex = 0; rowIndex < variableValues.length; rowIndex++) { |
||||
const isSourceRow = rowIndex === 0; |
||||
const rowCloneKey = getCloneKey(row.state.key!, rowIndex); |
||||
const rowClone = isSourceRow |
||||
? row |
||||
: row.clone({ repeatByVariable: undefined, repeatedRows: undefined, layout: undefined }); |
||||
|
||||
const layout = isSourceRow ? row.getLayout() : row.getLayout().cloneLayout(rowCloneKey, false); |
||||
|
||||
rowClone.setState({ |
||||
key: rowCloneKey, |
||||
$variables: new SceneVariableSet({ |
||||
variables: [ |
||||
new LocalValueVariable({ |
||||
name: variable.state.name, |
||||
value: variableValues[rowIndex], |
||||
text: String(variableTexts[rowIndex]), |
||||
isMulti: variable.state.isMulti, |
||||
includeAll: variable.state.includeAll, |
||||
}), |
||||
], |
||||
}), |
||||
layout, |
||||
}); |
||||
|
||||
if (!isSourceRow) { |
||||
clonedRows.push(rowClone); |
||||
} |
||||
} |
||||
|
||||
row.setState({ repeatedRows: clonedRows }); |
||||
row.publishEvent(new DashboardRepeatsProcessedEvent({ source: row }), true); |
||||
} |
||||
|
||||
/** |
||||
* Get previous variable values given the current repeated state |
||||
*/ |
||||
function getPrevRepeatValues(mainRow: RowItem, varName: string): VariableValueSingle[] { |
||||
const values: VariableValueSingle[] = []; |
||||
|
||||
if (!mainRow.state.repeatedRows) { |
||||
return []; |
||||
} |
||||
|
||||
function collectVariableValue(row: RowItem) { |
||||
const variable = sceneGraph.lookupVariable(varName, row); |
||||
if (variable) { |
||||
const value = variable.getValue(); |
||||
if (value != null && !Array.isArray(value)) { |
||||
values.push(value); |
||||
} |
||||
} |
||||
} |
||||
|
||||
collectVariableValue(mainRow); |
||||
|
||||
for (const row of mainRow.state.repeatedRows) { |
||||
collectVariableValue(row); |
||||
} |
||||
|
||||
return values; |
||||
} |
@ -1,272 +0,0 @@ |
||||
import { VariableRefresh } from '@grafana/data'; |
||||
import { getPanelPlugin } from '@grafana/data/test'; |
||||
import { setPluginImportUtils } from '@grafana/runtime'; |
||||
import { |
||||
SceneGridRow, |
||||
SceneTimeRange, |
||||
SceneVariableSet, |
||||
TestVariable, |
||||
VariableValueOption, |
||||
PanelBuilders, |
||||
} from '@grafana/scenes'; |
||||
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from 'app/features/variables/constants'; |
||||
import { TextMode } from 'app/plugins/panel/text/panelcfg.gen'; |
||||
|
||||
import { getCloneKey, isInCloneChain, joinCloneKeys } from '../../utils/clone'; |
||||
import { activateFullSceneTree } from '../../utils/test-utils'; |
||||
import { DashboardScene } from '../DashboardScene'; |
||||
import { DashboardGridItem } from '../layout-default/DashboardGridItem'; |
||||
import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager'; |
||||
|
||||
import { RowItem } from './RowItem'; |
||||
import { RowItemRepeaterBehavior } from './RowItemRepeaterBehavior'; |
||||
import { RowsLayoutManager } from './RowsLayoutManager'; |
||||
|
||||
jest.mock('@grafana/runtime', () => ({ |
||||
...jest.requireActual('@grafana/runtime'), |
||||
setPluginExtensionGetter: jest.fn(), |
||||
getPluginLinkExtensions: jest.fn().mockReturnValue({ extensions: [] }), |
||||
})); |
||||
|
||||
setPluginImportUtils({ |
||||
importPanelPlugin: () => Promise.resolve(getPanelPlugin({})), |
||||
getPanelPluginFromCache: () => undefined, |
||||
}); |
||||
|
||||
describe('RowItemRepeaterBehavior', () => { |
||||
describe('Given scene with variable with 5 values', () => { |
||||
let scene: DashboardScene, layout: RowsLayoutManager, repeatBehavior: RowItemRepeaterBehavior; |
||||
let layoutStateUpdates: unknown[]; |
||||
|
||||
beforeEach(async () => { |
||||
({ scene, layout, repeatBehavior } = buildScene({ variableQueryTime: 0 })); |
||||
|
||||
layoutStateUpdates = []; |
||||
layout.subscribeToState((state) => layoutStateUpdates.push(state)); |
||||
|
||||
activateFullSceneTree(scene); |
||||
await new Promise((r) => setTimeout(r, 1)); |
||||
}); |
||||
|
||||
it('Should repeat row', () => { |
||||
// Verify that first row still has repeat behavior
|
||||
const row1 = layout.state.rows[0]; |
||||
expect(row1.state.key).toBe(getCloneKey('row-1', 0)); |
||||
expect(row1.state.$behaviors?.[0]).toBeInstanceOf(RowItemRepeaterBehavior); |
||||
expect(row1.state.$variables!.state.variables[0].getValue()).toBe('A1'); |
||||
|
||||
const row1Children = getRowChildren(row1); |
||||
expect(row1Children[0].state.key!).toBe(joinCloneKeys(row1.state.key!, 'grid-item-0')); |
||||
expect(row1Children[0].state.body?.state.key).toBe(joinCloneKeys(row1Children[0].state.key!, 'panel-0')); |
||||
|
||||
const row2 = layout.state.rows[1]; |
||||
expect(row2.state.key).toBe(getCloneKey('row-1', 1)); |
||||
expect(row2.state.$behaviors).toEqual([]); |
||||
expect(row2.state.$variables!.state.variables[0].getValueText?.()).toBe('B'); |
||||
|
||||
const row2Children = getRowChildren(row2); |
||||
expect(row2Children[0].state.key!).toBe(joinCloneKeys(row2.state.key!, 'grid-item-0')); |
||||
expect(row2Children[0].state.body?.state.key).toBe(joinCloneKeys(row2Children[0].state.key!, 'panel-0')); |
||||
}); |
||||
|
||||
it('Repeated rows should be read only', () => { |
||||
const row1 = layout.state.rows[0]; |
||||
expect(isInCloneChain(row1.state.key!)).toBe(false); |
||||
|
||||
const row2 = layout.state.rows[1]; |
||||
expect(isInCloneChain(row2.state.key!)).toBe(true); |
||||
}); |
||||
|
||||
it('Should push row at the bottom down', () => { |
||||
// Should push row at the bottom down
|
||||
const rowAtTheBottom = layout.state.rows[5]; |
||||
expect(rowAtTheBottom.state.title).toBe('Row at the bottom'); |
||||
}); |
||||
|
||||
it('Should handle second repeat cycle and update remove old repeats', async () => { |
||||
// trigger another repeat cycle by changing the variable
|
||||
const variable = scene.state.$variables!.state.variables[0] as TestVariable; |
||||
variable.changeValueTo(['B1', 'C1']); |
||||
|
||||
await new Promise((r) => setTimeout(r, 1)); |
||||
|
||||
// should now only have 2 repeated rows (and the panel above + the row at the bottom)
|
||||
expect(layout.state.rows.length).toBe(3); |
||||
}); |
||||
|
||||
it('Should ignore repeat process if variable values are the same', async () => { |
||||
// trigger another repeat cycle by changing the variable
|
||||
repeatBehavior.performRepeat(); |
||||
|
||||
await new Promise((r) => setTimeout(r, 1)); |
||||
|
||||
expect(layoutStateUpdates.length).toBe(1); |
||||
}); |
||||
}); |
||||
|
||||
describe('Given scene with variable with 15 values', () => { |
||||
let scene: DashboardScene, layout: RowsLayoutManager; |
||||
let layoutStateUpdates: unknown[]; |
||||
|
||||
beforeEach(async () => { |
||||
({ scene, layout } = buildScene({ variableQueryTime: 0 }, [ |
||||
{ label: 'A', value: 'A1' }, |
||||
{ label: 'B', value: 'B1' }, |
||||
{ label: 'C', value: 'C1' }, |
||||
{ label: 'D', value: 'D1' }, |
||||
{ label: 'E', value: 'E1' }, |
||||
{ label: 'F', value: 'F1' }, |
||||
{ label: 'G', value: 'G1' }, |
||||
{ label: 'H', value: 'H1' }, |
||||
{ label: 'I', value: 'I1' }, |
||||
{ label: 'J', value: 'J1' }, |
||||
{ label: 'K', value: 'K1' }, |
||||
{ label: 'L', value: 'L1' }, |
||||
{ label: 'M', value: 'M1' }, |
||||
{ label: 'N', value: 'N1' }, |
||||
{ label: 'O', value: 'O1' }, |
||||
])); |
||||
|
||||
layoutStateUpdates = []; |
||||
layout.subscribeToState((state) => layoutStateUpdates.push(state)); |
||||
|
||||
activateFullSceneTree(scene); |
||||
await new Promise((r) => setTimeout(r, 1)); |
||||
}); |
||||
|
||||
it('Should handle second repeat cycle and update remove old repeats', async () => { |
||||
// should have 15 repeated rows (and the panel above)
|
||||
expect(layout.state.rows.length).toBe(16); |
||||
|
||||
// trigger another repeat cycle by changing the variable
|
||||
const variable = scene.state.$variables!.state.variables[0] as TestVariable; |
||||
variable.changeValueTo(['B1', 'C1']); |
||||
|
||||
await new Promise((r) => setTimeout(r, 1)); |
||||
|
||||
// should now only have 2 repeated rows (and the panel above)
|
||||
expect(layout.state.rows.length).toBe(3); |
||||
}); |
||||
}); |
||||
|
||||
describe('Given a scene with empty variable', () => { |
||||
it('Should preserve repeat row', async () => { |
||||
const { scene, layout } = buildScene({ variableQueryTime: 0 }, []); |
||||
activateFullSceneTree(scene); |
||||
await new Promise((r) => setTimeout(r, 1)); |
||||
|
||||
// Should have 2 rows, one without repeat and one with the dummy row
|
||||
expect(layout.state.rows.length).toBe(2); |
||||
expect(layout.state.rows[0].state.$behaviors?.[0]).toBeInstanceOf(RowItemRepeaterBehavior); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
interface SceneOptions { |
||||
variableQueryTime: number; |
||||
variableRefresh?: VariableRefresh; |
||||
} |
||||
|
||||
function buildTextPanel(key: string, content: string) { |
||||
const panel = PanelBuilders.text().setOption('content', content).setOption('mode', TextMode.Markdown).build(); |
||||
panel.setState({ key }); |
||||
return panel; |
||||
} |
||||
|
||||
function buildScene( |
||||
options: SceneOptions, |
||||
variableOptions?: VariableValueOption[], |
||||
variableStateOverrides?: { isMulti: boolean } |
||||
) { |
||||
const repeatBehavior = new RowItemRepeaterBehavior({ variableName: 'server' }); |
||||
|
||||
const rows = [ |
||||
new RowItem({ |
||||
key: 'row-1', |
||||
$behaviors: [repeatBehavior], |
||||
layout: DefaultGridLayoutManager.fromGridItems([ |
||||
new DashboardGridItem({ |
||||
key: 'grid-item-1', |
||||
x: 0, |
||||
y: 11, |
||||
width: 24, |
||||
height: 5, |
||||
body: buildTextPanel('text-1', 'Panel inside repeated row, server = $server'), |
||||
}), |
||||
]), |
||||
}), |
||||
new RowItem({ |
||||
key: 'row-2', |
||||
title: 'Row at the bottom', |
||||
layout: DefaultGridLayoutManager.fromGridItems([ |
||||
new DashboardGridItem({ |
||||
key: 'grid-item-2', |
||||
x: 0, |
||||
y: 17, |
||||
body: buildTextPanel('text-2', 'Panel inside row, server = $server'), |
||||
}), |
||||
new DashboardGridItem({ |
||||
key: 'grid-item-3', |
||||
x: 0, |
||||
y: 25, |
||||
body: buildTextPanel('text-3', 'Panel inside row, server = $server'), |
||||
}), |
||||
]), |
||||
}), |
||||
]; |
||||
|
||||
const layout = new RowsLayoutManager({ rows }); |
||||
|
||||
const scene = new DashboardScene({ |
||||
$timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }), |
||||
$variables: new SceneVariableSet({ |
||||
variables: [ |
||||
new TestVariable({ |
||||
name: 'server', |
||||
query: 'A.*', |
||||
value: ALL_VARIABLE_VALUE, |
||||
text: ALL_VARIABLE_TEXT, |
||||
isMulti: true, |
||||
includeAll: true, |
||||
delayMs: options.variableQueryTime, |
||||
refresh: options.variableRefresh, |
||||
optionsToReturn: variableOptions ?? [ |
||||
{ label: 'A', value: 'A1' }, |
||||
{ label: 'B', value: 'B1' }, |
||||
{ label: 'C', value: 'C1' }, |
||||
{ label: 'D', value: 'D1' }, |
||||
{ label: 'E', value: 'E1' }, |
||||
], |
||||
...variableStateOverrides, |
||||
}), |
||||
], |
||||
}), |
||||
body: layout, |
||||
}); |
||||
|
||||
const rowToRepeat = repeatBehavior.parent as SceneGridRow; |
||||
|
||||
return { scene, layout, rows, repeatBehavior, rowToRepeat }; |
||||
} |
||||
|
||||
function getRowLayout(row: RowItem): DefaultGridLayoutManager { |
||||
const layout = row.getLayout(); |
||||
|
||||
if (!(layout instanceof DefaultGridLayoutManager)) { |
||||
throw new Error('Invalid layout'); |
||||
} |
||||
|
||||
return layout; |
||||
} |
||||
|
||||
function getRowChildren(row: RowItem): DashboardGridItem[] { |
||||
const layout = getRowLayout(row); |
||||
|
||||
const filteredChildren = layout.state.grid.state.children.filter((child) => child instanceof DashboardGridItem); |
||||
|
||||
if (filteredChildren.length !== layout.state.grid.state.children.length) { |
||||
throw new Error('Invalid layout'); |
||||
} |
||||
|
||||
return filteredChildren; |
||||
} |
@ -1,161 +0,0 @@ |
||||
import { isEqual } from 'lodash'; |
||||
|
||||
import { |
||||
LocalValueVariable, |
||||
MultiValueVariable, |
||||
sceneGraph, |
||||
SceneObjectBase, |
||||
SceneObjectState, |
||||
SceneVariableSet, |
||||
VariableDependencyConfig, |
||||
VariableValueSingle, |
||||
} from '@grafana/scenes'; |
||||
|
||||
import { isClonedKeyOf, getCloneKey } from '../../utils/clone'; |
||||
import { getMultiVariableValues } from '../../utils/utils'; |
||||
import { DashboardRepeatsProcessedEvent } from '../types/DashboardRepeatsProcessedEvent'; |
||||
|
||||
import { RowItem } from './RowItem'; |
||||
import { RowsLayoutManager } from './RowsLayoutManager'; |
||||
|
||||
interface RowItemRepeaterBehaviorState extends SceneObjectState { |
||||
variableName: string; |
||||
} |
||||
|
||||
export class RowItemRepeaterBehavior extends SceneObjectBase<RowItemRepeaterBehaviorState> { |
||||
protected _variableDependency = new VariableDependencyConfig(this, { |
||||
variableNames: [this.state.variableName], |
||||
onVariableUpdateCompleted: () => this.performRepeat(), |
||||
}); |
||||
|
||||
private _prevRepeatValues?: VariableValueSingle[]; |
||||
private _clonedRows?: RowItem[]; |
||||
|
||||
public constructor(state: RowItemRepeaterBehaviorState) { |
||||
super(state); |
||||
|
||||
this.addActivationHandler(() => this._activationHandler()); |
||||
} |
||||
|
||||
private _activationHandler() { |
||||
this.performRepeat(); |
||||
} |
||||
|
||||
private _getRow(): RowItem { |
||||
if (!(this.parent instanceof RowItem)) { |
||||
throw new Error('RepeatedRowItemBehavior: Parent is not a RowItem'); |
||||
} |
||||
|
||||
return this.parent; |
||||
} |
||||
|
||||
private _getLayout(): RowsLayoutManager { |
||||
const layout = this._getRow().parent; |
||||
|
||||
if (!(layout instanceof RowsLayoutManager)) { |
||||
throw new Error('RepeatedRowItemBehavior: Layout is not a RowsLayoutManager'); |
||||
} |
||||
|
||||
return layout; |
||||
} |
||||
|
||||
public performRepeat(force = false) { |
||||
if (this._variableDependency.hasDependencyInLoadingState()) { |
||||
return; |
||||
} |
||||
|
||||
const variable = sceneGraph.lookupVariable(this.state.variableName, this.parent?.parent!); |
||||
|
||||
if (!variable) { |
||||
console.error('RepeatedRowItemBehavior: Variable not found'); |
||||
return; |
||||
} |
||||
|
||||
if (!(variable instanceof MultiValueVariable)) { |
||||
console.error('RepeatedRowItemBehavior: Variable is not a MultiValueVariable'); |
||||
return; |
||||
} |
||||
|
||||
const rowToRepeat = this._getRow(); |
||||
const layout = this._getLayout(); |
||||
const { values, texts } = getMultiVariableValues(variable); |
||||
|
||||
// Do nothing if values are the same
|
||||
if (isEqual(this._prevRepeatValues, values) && !force) { |
||||
return; |
||||
} |
||||
|
||||
this._prevRepeatValues = values; |
||||
|
||||
this._clonedRows = []; |
||||
|
||||
const rowContent = rowToRepeat.getLayout(); |
||||
|
||||
// when variable has no options (due to error or similar) it will not render any panels at all
|
||||
// adding a placeholder in this case so that there is at least empty panel that can display error
|
||||
const emptyVariablePlaceholderOption = { |
||||
values: [''], |
||||
texts: variable.hasAllValue() ? ['All'] : ['None'], |
||||
}; |
||||
|
||||
const variableValues = values.length ? values : emptyVariablePlaceholderOption.values; |
||||
const variableTexts = texts.length ? texts : emptyVariablePlaceholderOption.texts; |
||||
|
||||
// Loop through variable values and create repeats
|
||||
for (let rowIndex = 0; rowIndex < variableValues.length; rowIndex++) { |
||||
const isSourceRow = rowIndex === 0; |
||||
const rowClone = isSourceRow ? rowToRepeat : rowToRepeat.clone({ $behaviors: [] }); |
||||
|
||||
const rowCloneKey = getCloneKey(rowToRepeat.state.key!, rowIndex); |
||||
|
||||
rowClone.setState({ |
||||
key: rowCloneKey, |
||||
$variables: new SceneVariableSet({ |
||||
variables: [ |
||||
new LocalValueVariable({ |
||||
name: this.state.variableName, |
||||
value: variableValues[rowIndex], |
||||
text: String(variableTexts[rowIndex]), |
||||
isMulti: variable.state.isMulti, |
||||
includeAll: variable.state.includeAll, |
||||
}), |
||||
], |
||||
}), |
||||
layout: rowContent.cloneLayout?.(rowCloneKey, isSourceRow), |
||||
}); |
||||
|
||||
this._clonedRows.push(rowClone); |
||||
} |
||||
|
||||
updateLayout(layout, this._clonedRows, rowToRepeat.state.key!); |
||||
|
||||
// Used from dashboard url sync
|
||||
this.publishEvent(new DashboardRepeatsProcessedEvent({ source: this }), true); |
||||
} |
||||
|
||||
public removeBehavior() { |
||||
const row = this._getRow(); |
||||
const layout = this._getLayout(); |
||||
const rows = getRowsFilterOutRepeatClones(layout, row.state.key!); |
||||
|
||||
layout.setState({ rows }); |
||||
|
||||
// Remove behavior and the scoped local variable
|
||||
row.setState({ $behaviors: row.state.$behaviors!.filter((b) => b !== this), $variables: undefined }); |
||||
} |
||||
} |
||||
|
||||
function updateLayout(layout: RowsLayoutManager, rows: RowItem[], rowKey: string) { |
||||
const allRows = getRowsFilterOutRepeatClones(layout, rowKey); |
||||
const index = allRows.findIndex((row) => row.state.key!.includes(rowKey)); |
||||
|
||||
if (index === -1) { |
||||
throw new Error('RowItemRepeaterBehavior: Row not found in layout'); |
||||
} |
||||
|
||||
layout.setState({ rows: [...allRows.slice(0, index), ...rows, ...allRows.slice(index + 1)] }); |
||||
} |
||||
|
||||
function getRowsFilterOutRepeatClones(layout: RowsLayoutManager, rowKey: string) { |
||||
return layout.state.rows.filter((rows) => !isClonedKeyOf(rows.state.key!, rowKey)); |
||||
} |
Loading…
Reference in new issue