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