Dashboards: Repeating with no clone keys (#109839)
* Dashboards: New pathId concept to find panels not based on scene object key * Minor fix * fix test * Fix test * adding unit tests for pathId functions * fix * Fix link sharing to use new path * fix * Update * No clone keys * Remove all the clone keys complexity * More changes * update e2e test * Progress * fix auto grid item * Finally working cannot move panel into clone row * adjust how we find vizpanel for keyboard shortcuts * Update * Fix shortcuts * Fix test * fixing tests * fix lint * fix tests * fix e2e * adjust how outliine children are collected for auto and custom grids * update * Upgrade scenes --------- Co-authored-by: oscarkilhed <oscar.kilhed@grafana.com> Co-authored-by: Sergej-Vlasov <sergej.s.vlasov@gmail.com>
This commit is contained in:
co-authored by
oscarkilhed
Sergej-Vlasov
parent
f6404b9589
commit
485831f0b2
@@ -35,12 +35,12 @@ test.describe(
|
||||
test('Can view solo repeated panel in scenes', async ({ page, selectors }) => {
|
||||
// open Panel Tests - Graph NG
|
||||
const soloPanelUrl = selectors.pages.SoloPanel.url(
|
||||
'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-2-clone-1&__feature.dashboardSceneSolo=true'
|
||||
'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=A$panel-2&__feature.dashboardSceneSolo=true'
|
||||
);
|
||||
await page.goto(soloPanelUrl);
|
||||
|
||||
// Check that the panel title exists
|
||||
const panelTitle = page.getByTestId(selectors.components.Panels.Panel.title('server=B'));
|
||||
const panelTitle = page.getByTestId(selectors.components.Panels.Panel.title('server=A'));
|
||||
await expect(panelTitle).toBeVisible();
|
||||
|
||||
// Check that uplot-main-div does not exist
|
||||
@@ -51,7 +51,7 @@ test.describe(
|
||||
test('Can view solo in repeated row and panel in scenes', async ({ page, selectors }) => {
|
||||
// open Panel Tests - Graph NG
|
||||
const soloPanelUrl = selectors.pages.SoloPanel.url(
|
||||
'Repeating-rows-uid/repeating-rows?orgId=1&var-server=A&var-server=B&var-server=D&var-pod=1&var-pod=2&var-pod=3&panelId=panel-16-clone-1/grid-item-2/panel-2-clone-1&__feature.dashboardSceneSolo=true'
|
||||
'Repeating-rows-uid/repeating-rows?orgId=1&var-server=A&var-server=B&var-server=D&var-pod=1&var-pod=2&var-pod=3&panelId=B$2$panel-2&__feature.dashboardSceneSolo=true'
|
||||
);
|
||||
await page.goto(soloPanelUrl);
|
||||
|
||||
|
||||
@@ -25,17 +25,17 @@ describe('Solo Route', () => {
|
||||
it('Can view solo repeated panel in scenes', () => {
|
||||
// open Panel Tests - Graph NG
|
||||
e2e.pages.SoloPanel.visit(
|
||||
'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-2-clone-1&__feature.dashboardSceneSolo=true'
|
||||
'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=A$panel-2&__feature.dashboardSceneSolo=true'
|
||||
);
|
||||
|
||||
e2e.components.Panels.Panel.title('server=B').should('exist');
|
||||
e2e.components.Panels.Panel.title('server=A').should('exist');
|
||||
cy.contains('uplot-main-div').should('not.exist');
|
||||
});
|
||||
|
||||
it('Can view solo in repeated row and panel in scenes', () => {
|
||||
// open Panel Tests - Graph NG
|
||||
e2e.pages.SoloPanel.visit(
|
||||
'Repeating-rows-uid/repeating-rows?orgId=1&var-server=A&var-server=B&var-server=D&var-pod=1&var-pod=2&var-pod=3&panelId=panel-16-clone-1/grid-item-2/panel-2-clone-1&__feature.dashboardSceneSolo=true'
|
||||
'Repeating-rows-uid/repeating-rows?orgId=1&var-server=A&var-server=B&var-server=D&var-pod=1&var-pod=2&var-pod=3&panelId=B$2$panel-2&__feature.dashboardSceneSolo=true'
|
||||
);
|
||||
|
||||
e2e.components.Panels.Panel.title('server = B, pod = Rob').should('exist');
|
||||
|
||||
@@ -25,17 +25,17 @@ describe('Solo Route', () => {
|
||||
it('Can view solo repeated panel in scenes', () => {
|
||||
// open Panel Tests - Graph NG
|
||||
e2e.pages.SoloPanel.visit(
|
||||
'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-2-clone-1&__feature.dashboardSceneSolo=true'
|
||||
'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=A$panel-2&__feature.dashboardSceneSolo=true'
|
||||
);
|
||||
|
||||
e2e.components.Panels.Panel.title('server=B').should('exist');
|
||||
e2e.components.Panels.Panel.title('server=A').should('exist');
|
||||
cy.contains('uplot-main-div').should('not.exist');
|
||||
});
|
||||
|
||||
it('Can view solo in repeated row and panel in scenes', () => {
|
||||
// open Panel Tests - Graph NG
|
||||
e2e.pages.SoloPanel.visit(
|
||||
'Repeating-rows-uid/repeating-rows?orgId=1&var-server=A&var-server=B&var-server=D&var-pod=1&var-pod=2&var-pod=3&panelId=panel-16-clone-1/grid-item-2/panel-2-clone-1&__feature.dashboardSceneSolo=true'
|
||||
'Repeating-rows-uid/repeating-rows?orgId=1&var-server=A&var-server=B&var-server=D&var-pod=1&var-pod=2&var-pod=3&panelId=B$2$panel-2&__feature.dashboardSceneSolo=true'
|
||||
);
|
||||
|
||||
e2e.components.Panels.Panel.title('server = B, pod = Rob').should('exist');
|
||||
|
||||
+2
-2
@@ -290,8 +290,8 @@
|
||||
"@grafana/plugin-ui": "0.10.9",
|
||||
"@grafana/prometheus": "workspace:*",
|
||||
"@grafana/runtime": "workspace:*",
|
||||
"@grafana/scenes": "6.29.7",
|
||||
"@grafana/scenes-react": "6.29.7",
|
||||
"@grafana/scenes": "^6.30.0",
|
||||
"@grafana/scenes-react": "^6.30.0",
|
||||
"@grafana/schema": "workspace:*",
|
||||
"@grafana/sql": "workspace:*",
|
||||
"@grafana/ui": "workspace:*",
|
||||
|
||||
@@ -99,7 +99,7 @@ export const getShareUrlParams = (
|
||||
const urlParamsUpdate: UrlQueryMap = {};
|
||||
|
||||
if (panel) {
|
||||
urlParamsUpdate.viewPanel = panel.state.key;
|
||||
urlParamsUpdate.viewPanel = panel.getPathId();
|
||||
}
|
||||
|
||||
if (opts.useAbsoluteTimeRange) {
|
||||
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
} from '@grafana/ui';
|
||||
|
||||
import { isDashboardLayoutItem } from '../scene/types/DashboardLayoutItem';
|
||||
import { containsCloneKey, getLastKeyFromClone, isInCloneChain } from '../utils/clone';
|
||||
import { findEditPanel, getDashboardSceneFor } from '../utils/utils';
|
||||
import { isRepeatCloneOrChildOf } from '../utils/clone';
|
||||
import { getDashboardSceneFor } from '../utils/utils';
|
||||
|
||||
import { ElementSelection } from './ElementSelection';
|
||||
import {
|
||||
@@ -183,24 +183,13 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
|
||||
}
|
||||
|
||||
private selectElement(element: ElementSelectionContextItem, options: ElementSelectionOnSelectOptions) {
|
||||
// We should not select clones
|
||||
if (isInCloneChain(element.id)) {
|
||||
if (options.multi) {
|
||||
let obj = sceneGraph.findByKey(this, element.id);
|
||||
if (obj) {
|
||||
// Do not select repeat clones or their children
|
||||
if (isRepeatCloneOrChildOf(obj)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
let obj = sceneGraph.findByKey(this, element.id);
|
||||
if (obj) {
|
||||
if (obj instanceof VizPanel && containsCloneKey(getLastKeyFromClone(element.id))) {
|
||||
const sourceVizPanel = findEditPanel(this, element.id);
|
||||
if (sourceVizPanel) {
|
||||
obj = sourceVizPanel;
|
||||
}
|
||||
}
|
||||
this.selectObject(obj, element.id, options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Trans, t } from '@grafana/i18n';
|
||||
import { SceneObject } from '@grafana/scenes';
|
||||
import { Box, Icon, Stack, Text, useElementSelection, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { isInCloneChain } from '../utils/clone';
|
||||
import { isRepeatCloneOrChildOf } from '../utils/clone';
|
||||
import { getDashboardSceneFor } from '../utils/utils';
|
||||
|
||||
import { DashboardEditPane } from './DashboardEditPane';
|
||||
@@ -39,7 +39,7 @@ function DashboardOutlineNode({ sceneObject, editPane, depth }: DashboardOutline
|
||||
const { key } = sceneObject.useState();
|
||||
const [isCollapsed, setIsCollapsed] = useState(depth > 0);
|
||||
const { isSelected, onSelect } = useElementSelection(key);
|
||||
const isCloned = useMemo(() => isInCloneChain(key!), [key]);
|
||||
const isCloned = useMemo(() => isRepeatCloneOrChildOf(sceneObject), [sceneObject]);
|
||||
const editableElement = useMemo(() => getEditableElementFor(sceneObject)!, [sceneObject]);
|
||||
|
||||
const noTitleText = t('dashboard.outline.tree-item.no-title', '<no title>');
|
||||
|
||||
@@ -5,7 +5,7 @@ import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/constan
|
||||
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
|
||||
|
||||
import {
|
||||
findOriginalVizPanelByKey,
|
||||
findVizPanelByKey,
|
||||
getDashboardSceneFor,
|
||||
getLibraryPanelBehavior,
|
||||
getQueryRunnerFor,
|
||||
@@ -54,7 +54,7 @@ export class DashboardDatasourceBehaviour extends SceneObjectBase<DashboardDatas
|
||||
const panelId = dashboardQuery.panelId;
|
||||
const vizKey = getVizPanelKeyForPanelId(panelId);
|
||||
// We're trying to find the original panel, not a cloned one, since `panelId` alone cannot resolve clones
|
||||
const sourcePanel = findOriginalVizPanelByKey(dashboard, vizKey);
|
||||
const sourcePanel = findVizPanelByKey(dashboard, vizKey);
|
||||
|
||||
if (!(sourcePanel instanceof VizPanel)) {
|
||||
return;
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
SceneGridRow,
|
||||
behaviors,
|
||||
SceneDataTransformer,
|
||||
LocalValueVariable,
|
||||
} from '@grafana/scenes';
|
||||
import { Dashboard, DashboardCursorSync, LibraryPanel } from '@grafana/schema';
|
||||
import appEvents from 'app/core/app_events';
|
||||
@@ -36,7 +37,6 @@ import { DecoratedRevisionModel } from '../settings/VersionsEditView';
|
||||
import { historySrv } from '../settings/version-history/HistorySrv';
|
||||
import { getCloneKey } from '../utils/clone';
|
||||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||
import { djb2Hash } from '../utils/djb2Hash';
|
||||
import { findVizPanelByKey, getLibraryPanelBehavior, isLibraryPanel } from '../utils/utils';
|
||||
|
||||
import { DashboardControls } from './DashboardControls';
|
||||
@@ -662,8 +662,7 @@ describe('DashboardScene', () => {
|
||||
|
||||
it('Should hash the key of the cloned panels and set it as panelId', () => {
|
||||
const queryRunner = sceneGraph.findObject(scene, (o) => o.state.key === 'data-query-runner2')!;
|
||||
const expectedPanelId = djb2Hash(getCloneKey('panel-2', 1));
|
||||
expect(scene.enrichDataRequest(queryRunner).panelId).toEqual(expectedPanelId);
|
||||
expect(scene.enrichDataRequest(queryRunner).panelId).toEqual(3670868617);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -974,6 +973,10 @@ function buildTestScene(overrides?: Partial<DashboardSceneState>) {
|
||||
body: new VizPanel({
|
||||
title: 'Panel B',
|
||||
key: getCloneKey('panel-2', 1),
|
||||
repeatSourceKey: 'panel-2',
|
||||
$variables: new SceneVariableSet({
|
||||
variables: [new LocalValueVariable({ name: 'a', value: 'A' })],
|
||||
}),
|
||||
pluginId: 'table',
|
||||
$data: new SceneQueryRunner({ key: 'data-query-runner2', queries: [{ refId: 'A' }] }),
|
||||
}),
|
||||
|
||||
@@ -61,7 +61,7 @@ import { DecoratedRevisionModel } from '../settings/VersionsEditView';
|
||||
import { DashboardEditView } from '../settings/utils';
|
||||
import { historySrv } from '../settings/version-history/HistorySrv';
|
||||
import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper';
|
||||
import { isInCloneChain } from '../utils/clone';
|
||||
import { isRepeatCloneOrChildOf } from '../utils/clone';
|
||||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||
import { djb2Hash } from '../utils/djb2Hash';
|
||||
import { getDashboardUrl } from '../utils/getDashboardUrl';
|
||||
@@ -660,9 +660,9 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
|
||||
let panelId = 0;
|
||||
|
||||
if (panel && panel.state.key) {
|
||||
if (isInCloneChain(panel.state.key)) {
|
||||
if (isRepeatCloneOrChildOf(panel)) {
|
||||
// We check if any of the panel ancestors are clones because we can't use the original panel ID in this case
|
||||
panelId = djb2Hash(panel?.state.key);
|
||||
panelId = djb2Hash(panel.getPathId());
|
||||
} else {
|
||||
// Otherwise, it's the absolute original panel, and we can use the key directly
|
||||
// getPanelIdForVizPanel extracts the panel ID from the key so we don't need to do it manually
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import { SceneQueryRunner, VizPanel } from '@grafana/scenes';
|
||||
import { LocalValueVariable, SceneQueryRunner, SceneVariableSet, VizPanel } from '@grafana/scenes';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { KioskMode } from 'app/types/dashboard';
|
||||
|
||||
import { getCloneKey } from '../utils/clone';
|
||||
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
import { DashboardGridItem } from './layout-default/DashboardGridItem';
|
||||
import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager';
|
||||
@@ -72,7 +70,7 @@ describe('DashboardSceneUrlSync', () => {
|
||||
let errorNotice = 0;
|
||||
appEvents.on(AppEvents.alertError, (evt) => errorNotice++);
|
||||
|
||||
scene.urlSync?.updateFromUrl({ viewPanel: getCloneKey('panel-1', 1) });
|
||||
scene.urlSync?.updateFromUrl({ viewPanel: 'A$panel-1' });
|
||||
|
||||
expect(scene.state.viewPanelScene).toBeUndefined();
|
||||
// Verify no error notice was shown
|
||||
@@ -87,8 +85,17 @@ describe('DashboardSceneUrlSync', () => {
|
||||
key: 'griditem-1',
|
||||
x: 0,
|
||||
body: new VizPanel({
|
||||
$variables: new SceneVariableSet({
|
||||
variables: [
|
||||
new LocalValueVariable({
|
||||
name: 'server',
|
||||
value: 'A',
|
||||
text: 'A',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
title: 'Clone Panel A',
|
||||
key: getCloneKey('panel-1', 1),
|
||||
key: 'panel-1',
|
||||
pluginId: 'table',
|
||||
}),
|
||||
}),
|
||||
@@ -97,7 +104,7 @@ describe('DashboardSceneUrlSync', () => {
|
||||
|
||||
// Verify it subscribes to DashboardRepeatsProcessedEvent
|
||||
scene.publishEvent(new DashboardRepeatsProcessedEvent({ source: scene }));
|
||||
expect(scene.state.viewPanelScene?.getUrlKey()).toBe(getCloneKey('panel-1', 1));
|
||||
expect(scene.state.viewPanelScene?.getUrlKey()).toBe('A$panel-1');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ import { buildPanelEditScene } from '../panel-edit/PanelEditor';
|
||||
import { createDashboardEditViewFor } from '../settings/utils';
|
||||
import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer';
|
||||
import { ShareModal } from '../sharing/ShareModal';
|
||||
import { containsCloneKey } from '../utils/clone';
|
||||
import { findEditPanel, findVizPanelByKey, getLibraryPanelBehavior } from '../utils/utils';
|
||||
import { containsPathIdSeparator, findVizPanelByPathId } from '../utils/pathId';
|
||||
import { findEditPanel, getLibraryPanelBehavior } from '../utils/utils';
|
||||
|
||||
import { DashboardScene, DashboardSceneState } from './DashboardScene';
|
||||
import { LibraryPanelBehavior } from './LibraryPanelBehavior';
|
||||
@@ -74,13 +74,13 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
|
||||
|
||||
// Handle view panel state
|
||||
if (typeof values.viewPanel === 'string') {
|
||||
const panel = findVizPanelByKey(this._scene, values.viewPanel);
|
||||
const panel = findVizPanelByPathId(this._scene, values.viewPanel);
|
||||
|
||||
if (!panel) {
|
||||
// If we are trying to view a repeat clone that can't be found it might be that the repeats have not been processed yet
|
||||
// Here we check if the key contains the clone key so we force the repeat processing
|
||||
// It doesn't matter if the element or the ancestors are clones or not, just that the key contains the clone key
|
||||
if (containsCloneKey(values.viewPanel)) {
|
||||
if (containsPathIdSeparator(values.viewPanel)) {
|
||||
this._handleViewRepeatClone(values.viewPanel);
|
||||
return;
|
||||
}
|
||||
@@ -162,7 +162,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
|
||||
private _handleViewRepeatClone(viewPanel: string) {
|
||||
if (!this._viewEventSub) {
|
||||
this._viewEventSub = this._scene.subscribeToEvent(DashboardRepeatsProcessedEvent, () => {
|
||||
const panel = findVizPanelByKey(this._scene, viewPanel);
|
||||
const panel = findVizPanelByPathId(this._scene, viewPanel);
|
||||
if (panel) {
|
||||
this._viewEventSub?.unsubscribe();
|
||||
this._scene.setState({ viewPanelScene: new ViewPanelScene({ panelRef: panel.getRef() }) });
|
||||
|
||||
@@ -76,7 +76,7 @@ describe('panelMenuBehavior', () => {
|
||||
|
||||
expect(menu.state.items?.length).toBe(6);
|
||||
// verify view panel url keeps url params and adds viewPanel=<panel-key>
|
||||
expect(menu.state.items?.[0].href).toBe('/d/dash-1?from=now-5m&to=now&viewPanel=panel-12');
|
||||
expect(menu.state.items?.[0].href).toBe('/d/dash-1?from=now-5m&to=now&viewPanel=a$panel-12');
|
||||
// verify edit url keeps url time range
|
||||
expect(menu.state.items?.[1].href).toBe('/d/dash-1?from=now-5m&to=now&editPanel=12');
|
||||
// verify share
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { LocalValueVariable, sceneGraph, SceneGridRow, VizPanel, VizPanelMenu } from '@grafana/scenes';
|
||||
import { LocalValueVariable, sceneGraph, VizPanel, VizPanelMenu } from '@grafana/scenes';
|
||||
import { DataQuery, OptionsWithLegend } from '@grafana/schema';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||
@@ -35,7 +35,7 @@ import { ShowConfirmModalEvent } from 'app/types/events';
|
||||
import { PanelInspectDrawer } from '../inspect/PanelInspectDrawer';
|
||||
import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer';
|
||||
import { ShareModal } from '../sharing/ShareModal';
|
||||
import { isInCloneChain } from '../utils/clone';
|
||||
import { isRepeatCloneOrChildOf } from '../utils/clone';
|
||||
import { DashboardInteractions } from '../utils/interactions';
|
||||
import { getEditPanelUrl, getViewPanelUrl, tryGetExploreUrlForPanel } from '../utils/urlBuilders';
|
||||
import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor, isLibraryPanel } from '../utils/utils';
|
||||
@@ -74,7 +74,7 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
|
||||
const dashboard = getDashboardSceneFor(panel);
|
||||
const { isEmbedded } = dashboard.state.meta;
|
||||
const exploreMenuItem = await getExploreMenuItem(panel);
|
||||
const isReadOnlyRepeat = isInCloneChain(panel.state.key!);
|
||||
const isReadOnlyRepeat = isRepeatCloneOrChildOf(panel);
|
||||
|
||||
// For embedded dashboards we only have explore action for now
|
||||
if (isEmbedded) {
|
||||
@@ -504,21 +504,6 @@ function createExtensionContext(panel: VizPanel, dashboard: DashboardScene): Plu
|
||||
});
|
||||
}
|
||||
|
||||
// Handle row repeats scenario
|
||||
if (panel.parent?.parent instanceof SceneGridRow) {
|
||||
const row = panel.parent.parent;
|
||||
if (row.state.$variables) {
|
||||
row.state.$variables.state.variables.forEach((variable) => {
|
||||
if (variable instanceof LocalValueVariable) {
|
||||
scopedVars = {
|
||||
...scopedVars,
|
||||
[variable.state.name]: { value: variable.getValue(), text: variable.getValueText() },
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
pluginId: panel.state.pluginId,
|
||||
|
||||
@@ -19,7 +19,7 @@ export class ViewPanelScene extends SceneObjectBase<ViewPanelSceneState> {
|
||||
}
|
||||
|
||||
public getUrlKey() {
|
||||
return this.state.panelRef.resolve().state.key;
|
||||
return this.state.panelRef.resolve().getPathId();
|
||||
}
|
||||
|
||||
public static Component = ({ model }: SceneComponentProps<ViewPanelScene>) => {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { PanelInspectDrawer } from '../inspect/PanelInspectDrawer';
|
||||
import { ShareDrawer } from '../sharing/ShareDrawer/ShareDrawer';
|
||||
import { ShareModal } from '../sharing/ShareModal';
|
||||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||
import { findVizPanelByPathId } from '../utils/pathId';
|
||||
import { getEditPanelUrl, getViewPanelUrl, tryGetExploreUrlForPanel } from '../utils/urlBuilders';
|
||||
import { getPanelIdForVizPanel } from '../utils/utils';
|
||||
|
||||
@@ -21,20 +22,24 @@ import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutMana
|
||||
|
||||
export function setupKeyboardShortcuts(scene: DashboardScene) {
|
||||
const keybindings = new KeybindingSet();
|
||||
let vizPanelKey: string | null = null;
|
||||
let vizPanelPathId: string | null = null;
|
||||
|
||||
const canEdit = scene.canEditDashboard();
|
||||
|
||||
const panelAttentionSubscription = appEvents.subscribe(SetPanelAttentionEvent, (event) => {
|
||||
if (typeof event.payload.panelId === 'string') {
|
||||
vizPanelKey = event.payload.panelId;
|
||||
vizPanelPathId = event.payload.panelId;
|
||||
}
|
||||
});
|
||||
|
||||
function withFocusedPanel(scene: DashboardScene, fn: (vizPanel: VizPanel) => void) {
|
||||
return () => {
|
||||
const vizPanel = sceneGraph.findObject(scene, (o) => o.state.key === vizPanelKey);
|
||||
if (vizPanel && vizPanel instanceof VizPanel) {
|
||||
if (vizPanelPathId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const vizPanel = findVizPanelByPathId(scene, vizPanelPathId);
|
||||
if (vizPanel) {
|
||||
fn(vizPanel);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -116,7 +116,10 @@ export class AutoGridItem extends SceneObjectBase<AutoGridItemState> implements
|
||||
const isSource = index === 0;
|
||||
const clone = isSource
|
||||
? panelToRepeat
|
||||
: panelToRepeat.clone({ key: getCloneKey(panelToRepeat.state.key!, index) });
|
||||
: panelToRepeat.clone({
|
||||
key: getCloneKey(panelToRepeat.state.key!, index),
|
||||
repeatSourceKey: panelToRepeat.state.key,
|
||||
});
|
||||
|
||||
clone.setState({ $variables: getLocalVariableValueSet(variable, variableValues[index], variableTexts[index]) });
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createRef, CSSProperties, PointerEvent as ReactPointerEvent } from 'rea
|
||||
|
||||
import { SceneLayout, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes';
|
||||
|
||||
import { isRepeatCloneOrChildOf } from '../../utils/clone';
|
||||
import { getLayoutOrchestratorFor } from '../../utils/utils';
|
||||
|
||||
import { AutoGridItem } from './AutoGridItem';
|
||||
@@ -92,6 +93,10 @@ export class AutoGridLayout extends SceneObjectBase<AutoGridLayoutState> impleme
|
||||
}
|
||||
|
||||
public isDraggable(): boolean {
|
||||
if (isRepeatCloneOrChildOf(this)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.state.isDraggable ?? false;
|
||||
}
|
||||
|
||||
|
||||
+24
-19
@@ -1,6 +1,6 @@
|
||||
import { SceneQueryRunner, VizPanel } from '@grafana/scenes';
|
||||
|
||||
import { findVizPanelByKey } from '../../utils/utils';
|
||||
import { DashboardEditActionEvent } from '../../edit-pane/shared';
|
||||
import { DashboardScene } from '../DashboardScene';
|
||||
|
||||
import { AutoGridItem } from './AutoGridItem';
|
||||
@@ -8,33 +8,38 @@ import { AutoGridLayout } from './AutoGridLayout';
|
||||
import { AutoGridLayoutManager } from './AutoGridLayoutManager';
|
||||
|
||||
describe('AutoGridLayoutManager', () => {
|
||||
it('Should clone the layout', () => {
|
||||
const { manager } = setup();
|
||||
const clone = manager.cloneLayout('foo', true) as AutoGridLayoutManager;
|
||||
it('can remove panel', () => {
|
||||
const { manager, panel1 } = setup();
|
||||
|
||||
expect(clone).not.toBe(manager);
|
||||
expect(clone.state.layout).not.toBe(manager.state.layout);
|
||||
expect(clone.state.layout.state.children).not.toBe(manager.state.layout.state.children);
|
||||
expect(clone.state.layout.state.children.length).toBe(manager.state.layout.state.children.length);
|
||||
manager.subscribeToEvent(DashboardEditActionEvent, (event) => {
|
||||
event.payload.perform();
|
||||
});
|
||||
|
||||
const panelA = findVizPanelByKey(clone, 'foo/grid-item-1/panel-1');
|
||||
expect(panelA?.state.title).toBe('Panel A');
|
||||
manager.removePanel(panel1);
|
||||
|
||||
const panelB = findVizPanelByKey(clone, 'foo/grid-item-2/panel-2');
|
||||
expect(panelB?.state.title).toBe('Panel B');
|
||||
expect(manager.state.layout.state.children.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
function setup() {
|
||||
const panel1 = new VizPanel({
|
||||
title: 'Panel A',
|
||||
key: 'panel-1',
|
||||
pluginId: 'table',
|
||||
$data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }),
|
||||
});
|
||||
|
||||
const panel2 = new VizPanel({
|
||||
title: 'Panel A',
|
||||
key: 'panel-1',
|
||||
pluginId: 'table',
|
||||
$data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }),
|
||||
});
|
||||
|
||||
const gridItems = [
|
||||
new AutoGridItem({
|
||||
key: 'grid-item-1',
|
||||
body: new VizPanel({
|
||||
title: 'Panel A',
|
||||
key: 'panel-1',
|
||||
pluginId: 'table',
|
||||
$data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }),
|
||||
}),
|
||||
body: panel1,
|
||||
}),
|
||||
new AutoGridItem({
|
||||
key: 'grid-item-2',
|
||||
@@ -50,5 +55,5 @@ function setup() {
|
||||
|
||||
new DashboardScene({ body: manager });
|
||||
|
||||
return { manager };
|
||||
return { manager, panel1, panel2 };
|
||||
}
|
||||
|
||||
+8
-24
@@ -7,13 +7,11 @@ import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/Pan
|
||||
|
||||
import { dashboardEditActions, NewObjectAddedToCanvasEvent } from '../../edit-pane/shared';
|
||||
import { serializeAutoGridLayout } from '../../serialization/layoutSerializers/AutoGridLayoutSerializer';
|
||||
import { joinCloneKeys } from '../../utils/clone';
|
||||
import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
|
||||
import {
|
||||
forceRenderChildren,
|
||||
getDashboardSceneFor,
|
||||
getGridItemKeyForPanelId,
|
||||
getPanelIdForVizPanel,
|
||||
getVizPanelKeyForPanelId,
|
||||
} from '../../utils/utils';
|
||||
import { DashboardGridItem } from '../layout-default/DashboardGridItem';
|
||||
@@ -90,8 +88,13 @@ export class AutoGridLayoutManager
|
||||
}
|
||||
|
||||
public getOutlineChildren(): SceneObject[] {
|
||||
const outlineChildren = this.state.layout.state.children.map((gridItem) => gridItem.state.body);
|
||||
return outlineChildren;
|
||||
const children: SceneObject[] = [];
|
||||
|
||||
for (const child of this.state.layout.state.children) {
|
||||
children.push(child.state.body, ...(child.state.repeatedPanels || []));
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
public addPanel(vizPanel: VizPanel) {
|
||||
@@ -245,26 +248,7 @@ export class AutoGridLayoutManager
|
||||
}
|
||||
|
||||
public cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager {
|
||||
return this.clone({
|
||||
layout: this.state.layout.clone({
|
||||
isDraggable: isSource && this.state.layout.state.isDraggable,
|
||||
children: this.state.layout.state.children.map((gridItem) => {
|
||||
if (gridItem instanceof AutoGridItem) {
|
||||
// Get the original panel ID from the gridItem's key
|
||||
const panelId = getPanelIdForVizPanel(gridItem.state.body);
|
||||
const gridItemKey = joinCloneKeys(ancestorKey, getGridItemKeyForPanelId(panelId));
|
||||
|
||||
return gridItem.clone({
|
||||
key: gridItemKey,
|
||||
body: gridItem.state.body.clone({
|
||||
key: joinCloneKeys(gridItemKey, getVizPanelKeyForPanelId(panelId)),
|
||||
}),
|
||||
});
|
||||
}
|
||||
throw new Error('Unexpected child type');
|
||||
}),
|
||||
}),
|
||||
});
|
||||
return this.clone({});
|
||||
}
|
||||
|
||||
public getOptions(): OptionsPaneItemDescriptor[] {
|
||||
|
||||
+2
-3
@@ -4,7 +4,7 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { SceneComponentProps, sceneGraph } from '@grafana/scenes';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { useHasClonedParents } from '../../utils/clone';
|
||||
import { isRepeatCloneOrChildOf } from '../../utils/clone';
|
||||
import { useDashboardState } from '../../utils/utils';
|
||||
import { CanvasGridAddActions } from '../layouts-shared/CanvasGridAddActions';
|
||||
import { dashboardCanvasAddButtonHoverStyles } from '../layouts-shared/styles';
|
||||
@@ -14,7 +14,6 @@ import { AutoGridLayoutManager } from './AutoGridLayoutManager';
|
||||
|
||||
export function AutoGridLayoutRenderer({ model }: SceneComponentProps<AutoGridLayout>) {
|
||||
const { children, isHidden } = model.useState();
|
||||
const hasClonedParents = useHasClonedParents(model);
|
||||
const styles = useStyles2(getStyles, model.state);
|
||||
const { layoutOrchestrator, isEditing } = useDashboardState(model);
|
||||
const layoutManager = sceneGraph.getAncestor(model, AutoGridLayoutManager);
|
||||
@@ -24,7 +23,7 @@ export function AutoGridLayoutRenderer({ model }: SceneComponentProps<AutoGridLa
|
||||
return null;
|
||||
}
|
||||
|
||||
const showCanvasActions = !hasClonedParents && isEditing;
|
||||
const showCanvasActions = !isRepeatCloneOrChildOf(model) && isEditing;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -3,7 +3,6 @@ import { setPluginImportUtils } from '@grafana/runtime';
|
||||
import { SceneGridLayout, SceneVariableSet, TestVariable, VizPanel } from '@grafana/scenes';
|
||||
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from 'app/features/variables/constants';
|
||||
|
||||
import { isInCloneChain } from '../../utils/clone';
|
||||
import { activateFullSceneTree, buildPanelRepeaterScene } from '../../utils/test-utils';
|
||||
import { DashboardScene } from '../DashboardScene';
|
||||
|
||||
@@ -42,8 +41,8 @@ describe('PanelRepeaterGridItem', () => {
|
||||
expect(panel1.state.$variables?.state.variables[0].getValueText?.()).toBe('A');
|
||||
expect(panel2.state.$variables?.state.variables[0].getValue()).toBe('2');
|
||||
|
||||
expect(panel1.state.key).toBe('panel-1');
|
||||
expect(isInCloneChain(panel2.state.key!)).toBe(true);
|
||||
expect(panel1.state.repeatSourceKey).toBe(undefined);
|
||||
expect(panel2.state.repeatSourceKey).toBe(repeater.state.body.state.key);
|
||||
});
|
||||
|
||||
it('Should wait for variable to load', async () => {
|
||||
|
||||
@@ -173,7 +173,10 @@ export class DashboardGridItem
|
||||
const isSource = index === 0;
|
||||
const clone = isSource
|
||||
? panelToRepeat
|
||||
: panelToRepeat.clone({ key: getCloneKey(panelToRepeat.state.key!, index) });
|
||||
: panelToRepeat.clone({
|
||||
key: getCloneKey(panelToRepeat.state.key!, index),
|
||||
repeatSourceKey: panelToRepeat.state.key,
|
||||
});
|
||||
|
||||
clone.setState({ $variables: getLocalVariableValueSet(variable, variableValues[index], variableTexts[index]) });
|
||||
|
||||
|
||||
-16
@@ -210,22 +210,6 @@ describe('DefaultGridLayoutManager', () => {
|
||||
|
||||
expect(gridRow.state.children.length).toBe(3);
|
||||
});
|
||||
|
||||
it('Should clone the layout correctly', () => {
|
||||
const { manager } = setup();
|
||||
const clone = manager.cloneLayout('foo', true) as DefaultGridLayoutManager;
|
||||
const panelA = findVizPanelByKey(clone, 'foo/grid-item-0/panel-0');
|
||||
expect(panelA?.state.title).toBe('Panel A');
|
||||
|
||||
const panelB = findVizPanelByKey(clone, 'foo/grid-item-1/panel-1');
|
||||
expect(panelB?.state.title).toBe('Panel B');
|
||||
|
||||
const panelC = findVizPanelByKey(clone, 'foo/panel-2/grid-item-3/panel-3');
|
||||
expect(panelC?.state.title).toBe('Panel C');
|
||||
|
||||
const panelD = findVizPanelByKey(clone, 'foo/panel-2/grid-item-4/panel-4');
|
||||
expect(panelD?.state.title).toBe('Panel D');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+4
-76
@@ -29,7 +29,7 @@ import {
|
||||
ObjectsReorderedOnCanvasEvent,
|
||||
} from '../../edit-pane/shared';
|
||||
import { serializeDefaultGridLayout } from '../../serialization/layoutSerializers/DefaultGridLayoutSerializer';
|
||||
import { isClonedKey, joinCloneKeys, useHasClonedParents } from '../../utils/clone';
|
||||
import { isRepeatCloneOrChildOf } from '../../utils/clone';
|
||||
import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
|
||||
import {
|
||||
forceRenderChildren,
|
||||
@@ -433,11 +433,7 @@ export class DefaultGridLayoutManager
|
||||
for (const child of this.state.grid.state.children) {
|
||||
// Flatten repeated grid items
|
||||
if (child instanceof DashboardGridItem) {
|
||||
if (child.state.repeatedPanels) {
|
||||
children.push(...child.state.repeatedPanels);
|
||||
} else {
|
||||
children.push(child.state.body);
|
||||
}
|
||||
children.push(child.state.body, ...(child.state.repeatedPanels || []));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,75 +441,7 @@ export class DefaultGridLayoutManager
|
||||
}
|
||||
|
||||
public cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager {
|
||||
return this.clone({
|
||||
grid: this.state.grid.clone({
|
||||
isResizable: isSource && this.state.grid.state.isResizable,
|
||||
isDraggable: isSource && this.state.grid.state.isDraggable,
|
||||
children: this.state.grid.state.children.reduce<{ panelId: number; children: SceneGridItemLike[] }>(
|
||||
(childrenAcc, child) => {
|
||||
if (child instanceof DashboardGridItem) {
|
||||
const gridItemKey = joinCloneKeys(ancestorKey, getGridItemKeyForPanelId(childrenAcc.panelId));
|
||||
|
||||
const gridItem = child.clone({
|
||||
key: gridItemKey,
|
||||
body: child.state.body.clone({
|
||||
key: joinCloneKeys(gridItemKey, getVizPanelKeyForPanelId(childrenAcc.panelId++)),
|
||||
}),
|
||||
isDraggable: isSource && child.state.isDraggable,
|
||||
isResizable: isSource && child.state.isResizable,
|
||||
});
|
||||
|
||||
childrenAcc.children.push(gridItem);
|
||||
|
||||
return childrenAcc;
|
||||
}
|
||||
|
||||
if (child instanceof SceneGridRow) {
|
||||
const rowKey = joinCloneKeys(ancestorKey, getVizPanelKeyForPanelId(childrenAcc.panelId++));
|
||||
|
||||
const row = child.clone({
|
||||
key: rowKey,
|
||||
children: child.state.children.reduce<SceneGridItemLike[]>((rowAcc, rowChild) => {
|
||||
if (isClonedKey(rowChild.state.key!)) {
|
||||
return rowAcc;
|
||||
}
|
||||
|
||||
if (!(rowChild instanceof DashboardGridItem)) {
|
||||
rowAcc.push(rowChild.clone());
|
||||
return rowAcc;
|
||||
}
|
||||
|
||||
const gridItemKey = joinCloneKeys(rowKey, getGridItemKeyForPanelId(childrenAcc.panelId));
|
||||
|
||||
const gridItem = rowChild.clone({
|
||||
key: gridItemKey,
|
||||
isDraggable: isSource && rowChild.state.isDraggable,
|
||||
isResizable: isSource && rowChild.state.isResizable,
|
||||
body: rowChild.state.body.clone({
|
||||
key: joinCloneKeys(gridItemKey, getVizPanelKeyForPanelId(childrenAcc.panelId++)),
|
||||
}),
|
||||
});
|
||||
|
||||
rowAcc.push(gridItem);
|
||||
return rowAcc;
|
||||
}, []),
|
||||
isDraggable: isSource && child.state.isDraggable,
|
||||
isResizable: isSource && child.state.isResizable,
|
||||
});
|
||||
|
||||
childrenAcc.children.push(row);
|
||||
|
||||
return childrenAcc;
|
||||
}
|
||||
|
||||
childrenAcc.children.push(child.clone());
|
||||
|
||||
return childrenAcc;
|
||||
},
|
||||
{ panelId: 0, children: [] }
|
||||
).children,
|
||||
}),
|
||||
});
|
||||
return this.clone({});
|
||||
}
|
||||
|
||||
public removeRow(row: SceneGridRow, removePanels = false) {
|
||||
@@ -634,7 +562,7 @@ function DefaultGridLayoutManagerRenderer({ model }: SceneComponentProps<Default
|
||||
const { children } = useSceneObjectState(model.state.grid, { shouldActivateOrKeepAlive: true });
|
||||
const dashboard = useDashboard(model);
|
||||
const { isEditing } = dashboard.useState();
|
||||
const hasClonedParents = useHasClonedParents(model);
|
||||
const hasClonedParents = isRepeatCloneOrChildOf(model);
|
||||
const styles = useStyles2(getStyles);
|
||||
const showCanvasActions = isEditing && config.featureToggles.dashboardNewLayouts && !hasClonedParents;
|
||||
|
||||
|
||||
+6
-44
@@ -13,7 +13,7 @@ import {
|
||||
} from '@grafana/scenes';
|
||||
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from 'app/features/variables/constants';
|
||||
|
||||
import { getCloneKey, isInCloneChain, joinCloneKeys } from '../../utils/clone';
|
||||
import { getCloneKey, isRepeatCloneOrChildOf } from '../../utils/clone';
|
||||
import { activateFullSceneTree } from '../../utils/test-utils';
|
||||
import { DashboardScene } from '../DashboardScene';
|
||||
|
||||
@@ -54,14 +54,13 @@ describe('RowRepeaterBehavior', () => {
|
||||
|
||||
// Verify that first row still has repeat behavior
|
||||
const row1 = grid.state.children[1] as SceneGridRow;
|
||||
expect(row1.state.key).toBe(getCloneKey('row-1', 0));
|
||||
expect(row1.state.key).toBe('row-1');
|
||||
expect(row1.state.$behaviors?.[0]).toBeInstanceOf(RowRepeaterBehavior);
|
||||
expect(row1.state.$variables!.state.variables[0].getValue()).toBe('A1');
|
||||
expect(row1.state.actions).toBeDefined();
|
||||
|
||||
const gridItemRow1 = row1.state.children[0] as SceneGridItem;
|
||||
expect(gridItemRow1.state.key!).toBe(joinCloneKeys(row1.state.key!, 'grid-item-1'));
|
||||
expect(gridItemRow1.state.body?.state.key).toBe('canvas-1');
|
||||
expect(gridItemRow1.state.key!).toBe('grid-item-1');
|
||||
|
||||
const row2 = grid.state.children[2] as SceneGridRow;
|
||||
expect(row2.state.key).toBe(getCloneKey('row-1', 1));
|
||||
@@ -70,51 +69,14 @@ describe('RowRepeaterBehavior', () => {
|
||||
expect(row2.state.actions).toBeUndefined();
|
||||
|
||||
const gridItemRow2 = row2.state.children[0] as SceneGridItem;
|
||||
expect(gridItemRow2.state.key!).toBe(joinCloneKeys(row2.state.key!, 'grid-item-1'));
|
||||
expect(gridItemRow2.state.body?.state.key).toBe(joinCloneKeys(gridItemRow2.state.key!, 'canvas-1'));
|
||||
expect(gridItemRow2.state.key!).toBe(row2.state.key! + 'grid-item-1');
|
||||
});
|
||||
|
||||
it('Repeated rows should be read only', () => {
|
||||
const row1 = grid.state.children[1] as SceneGridRow;
|
||||
const row2 = grid.state.children[2] as SceneGridRow;
|
||||
expect(isInCloneChain(row1.state.key!)).toBe(false);
|
||||
expect(isInCloneChain(row2.state.key!)).toBe(true);
|
||||
});
|
||||
|
||||
it('Should update all rows when a panel is added to a clone', async () => {
|
||||
const originalRow = grid.state.children[1] as SceneGridRow;
|
||||
const clone1 = grid.state.children[2] as SceneGridRow;
|
||||
const clone2 = grid.state.children[3] as SceneGridRow;
|
||||
|
||||
expect(originalRow.state.children.length).toBe(1);
|
||||
expect(clone1.state.children.length).toBe(1);
|
||||
expect(clone2.state.children.length).toBe(1);
|
||||
|
||||
clone1.setState({
|
||||
children: [
|
||||
...clone1.state.children,
|
||||
new SceneGridItem({
|
||||
x: 0,
|
||||
y: 16,
|
||||
width: 24,
|
||||
height: 5,
|
||||
key: 'grid-item-4',
|
||||
body: new SceneCanvasText({
|
||||
text: 'new panel',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
grid.forceRender();
|
||||
|
||||
// repeater has run so there are new clone row objects
|
||||
const newClone1 = grid.state.children[2] as SceneGridRow;
|
||||
const newClone2 = grid.state.children[3] as SceneGridRow;
|
||||
|
||||
expect(originalRow.state.children.length).toBe(2);
|
||||
expect(newClone1.state.children.length).toBe(2);
|
||||
expect(newClone2.state.children.length).toBe(2);
|
||||
expect(isRepeatCloneOrChildOf(row1)).toBe(false);
|
||||
expect(isRepeatCloneOrChildOf(row2)).toBe(true);
|
||||
});
|
||||
|
||||
it('Should push row at the bottom down', () => {
|
||||
|
||||
@@ -12,16 +12,7 @@ import {
|
||||
VariableValueSingle,
|
||||
} from '@grafana/scenes';
|
||||
|
||||
import {
|
||||
containsCloneKey,
|
||||
getLastKeyFromClone,
|
||||
isClonedKeyOf,
|
||||
joinCloneKeys,
|
||||
getCloneKey,
|
||||
isClonedKey,
|
||||
getOriginalKey,
|
||||
getLocalVariableValueSet,
|
||||
} from '../../utils/clone';
|
||||
import { getCloneKey, getLocalVariableValueSet } from '../../utils/clone';
|
||||
import { getMultiVariableValues } from '../../utils/utils';
|
||||
import { DashboardRepeatsProcessedEvent } from '../types/DashboardRepeatsProcessedEvent';
|
||||
|
||||
@@ -49,11 +40,10 @@ export class RowRepeaterBehavior extends SceneObjectBase<RowRepeaterBehaviorStat
|
||||
|
||||
const layout = this._getLayout();
|
||||
const originalRow = this._getRow();
|
||||
const originalRowNonClonedPanels = originalRow.state.children.filter((child) => !isClonedKey(child.state.key!));
|
||||
|
||||
const sub = layout.subscribeToState(() => {
|
||||
const repeatedRows = layout.state.children.filter((child) =>
|
||||
isClonedKeyOf(child.state.key!, originalRow.state.key!)
|
||||
const repeatedRows = layout.state.children.filter(
|
||||
(child) => child instanceof SceneGridRow && child.state.repeatSourceKey === originalRow.state.key
|
||||
);
|
||||
|
||||
// go through cloned rows, search for panels that are not clones
|
||||
@@ -62,31 +52,12 @@ export class RowRepeaterBehavior extends SceneObjectBase<RowRepeaterBehaviorStat
|
||||
continue;
|
||||
}
|
||||
|
||||
const rowNonClonedPanels = row.state.children.filter((child) => !isClonedKey(child.state.key!));
|
||||
|
||||
// if no differences in row children compared to original, then no new panel added to clone
|
||||
if (rowNonClonedPanels.length === originalRowNonClonedPanels.length) {
|
||||
if (row.state.children.length === originalRow.state.children.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// if there are differences, find the new panel, move it to the original and perform repeat
|
||||
const gridItem = rowNonClonedPanels.find((gridItem) => !containsCloneKey(gridItem.state.key!));
|
||||
|
||||
if (gridItem) {
|
||||
const newGridItem = gridItem.clone();
|
||||
|
||||
row.setState({ children: row.state.children.filter((item) => item !== gridItem) });
|
||||
|
||||
// if we are moving a panel from the origin row to a clone row, we just return
|
||||
// this means we are modifying the origin row, re-triggering the repeat and losing that panel
|
||||
if (originalRow.state.children.find((item) => item.state.key === newGridItem.state.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
originalRow.setState({ children: [...originalRow.state.children, newGridItem] });
|
||||
|
||||
this.performRepeat(true);
|
||||
}
|
||||
this.performRepeat(true);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -163,15 +134,14 @@ export class RowRepeaterBehavior extends SceneObjectBase<RowRepeaterBehaviorStat
|
||||
const rowClone = isSourceRow
|
||||
? rowToRepeat
|
||||
: rowToRepeat.clone({
|
||||
key: getCloneKey(rowToRepeat.state.key!, rowIndex),
|
||||
repeatSourceKey: rowToRepeat.state.key,
|
||||
y: (rowToRepeat.state.y ?? 0) + rowContentHeight * rowIndex + rowIndex,
|
||||
$behaviors: [],
|
||||
actions: undefined,
|
||||
});
|
||||
|
||||
const rowCloneKey = getCloneKey(rowToRepeat.state.key!, rowIndex);
|
||||
|
||||
rowClone.setState({
|
||||
key: rowCloneKey,
|
||||
$variables: getLocalVariableValueSet(variable, variableValues[rowIndex], variableTexts[rowIndex]),
|
||||
children: [],
|
||||
});
|
||||
@@ -180,24 +150,13 @@ export class RowRepeaterBehavior extends SceneObjectBase<RowRepeaterBehaviorStat
|
||||
|
||||
for (const sourceItem of rowContent) {
|
||||
const sourceItemY = sourceItem.state.y ?? 0;
|
||||
|
||||
const cloneItemKey = joinCloneKeys(rowCloneKey, getLastKeyFromClone(sourceItem.state.key!));
|
||||
const cloneItem = rowIndex > 0 ? sourceItem.clone() : sourceItem;
|
||||
const cloneItemY = sourceItemY + (rowContentHeight + 1) * rowIndex;
|
||||
const cloneItem =
|
||||
rowIndex > 0
|
||||
? sourceItem.clone({
|
||||
isDraggable: false,
|
||||
isResizable: false,
|
||||
})
|
||||
: sourceItem;
|
||||
|
||||
cloneItem.setState({
|
||||
key: cloneItemKey,
|
||||
y: cloneItemY,
|
||||
});
|
||||
|
||||
// Update grid item keys on clone rows (not needed on source row)
|
||||
// Needed to not have duplicate grid items keys in the same grid
|
||||
if (rowIndex > 0) {
|
||||
ensureUniqueKeys(cloneItem, cloneItemKey);
|
||||
cloneItem.setState({ y: cloneItemY, key: rowClone.state.key + sourceItem.state.key! });
|
||||
}
|
||||
|
||||
children.push(cloneItem);
|
||||
@@ -252,9 +211,7 @@ function getRowContentHeight(panels: SceneGridItemLike[]): number {
|
||||
|
||||
function updateLayout(layout: SceneGridLayout, rows: SceneGridRow[], maxYOfRows: number, rowKey: string) {
|
||||
const allChildren = getLayoutChildrenFilterOutRepeatClones(layout, rowKey);
|
||||
const index = allChildren.findIndex(
|
||||
(child) => child instanceof SceneGridRow && getOriginalKey(child.state.key!) === getOriginalKey(rowKey)
|
||||
);
|
||||
const index = allChildren.findIndex((child) => child instanceof SceneGridRow && child.state.key === rowKey);
|
||||
|
||||
if (index === -1) {
|
||||
throw new Error('RowRepeaterBehavior: Parent row not found in layout children');
|
||||
@@ -284,14 +241,6 @@ function updateLayout(layout: SceneGridLayout, rows: SceneGridRow[], maxYOfRows:
|
||||
|
||||
function getLayoutChildrenFilterOutRepeatClones(layout: SceneGridLayout, rowKey: string) {
|
||||
return layout.state.children.filter(
|
||||
(child) => !(child instanceof SceneGridRow) || !isClonedKeyOf(getLastKeyFromClone(child.state.key!), rowKey)
|
||||
(child) => !(child instanceof SceneGridRow) || child.state.repeatSourceKey !== rowKey
|
||||
);
|
||||
}
|
||||
|
||||
function ensureUniqueKeys(item: SceneGridItemLike, ancestors: string) {
|
||||
item.forEachChild((child) => {
|
||||
const key = joinCloneKeys(ancestors, child.state.key!);
|
||||
child.setState({ key });
|
||||
ensureUniqueKeys(child, key);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,6 +45,8 @@ export interface RowItemState extends SceneObjectState {
|
||||
conditionalRendering?: ConditionalRendering;
|
||||
repeatByVariable?: string;
|
||||
repeatedRows?: RowItem[];
|
||||
/** Marks object as a repeated object and a key pointer to source object */
|
||||
repeatSourceKey?: string;
|
||||
}
|
||||
|
||||
export class RowItem
|
||||
|
||||
@@ -9,7 +9,7 @@ import { SceneComponentProps } from '@grafana/scenes';
|
||||
import { clearButtonStyles, Icon, Tooltip, useElementSelection, usePointerDistance, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { useIsConditionallyHidden } from '../../conditional-rendering/useIsConditionallyHidden';
|
||||
import { useIsClone } from '../../utils/clone';
|
||||
import { isRepeatCloneOrChildOf } from '../../utils/clone';
|
||||
import { useDashboardState, useInterpolatedTitle } from '../../utils/utils';
|
||||
import { DashboardScene } from '../DashboardScene';
|
||||
|
||||
@@ -17,7 +17,7 @@ import { RowItem } from './RowItem';
|
||||
|
||||
export function RowItemRenderer({ model }: SceneComponentProps<RowItem>) {
|
||||
const { layout, collapse: isCollapsed, fillScreen, hideHeader: isHeaderHidden, isDropTarget, key } = model.useState();
|
||||
const isClone = useIsClone(model);
|
||||
const isClone = isRepeatCloneOrChildOf(model);
|
||||
const { isEditing } = useDashboardState(model);
|
||||
const [isConditionallyHidden, conditionalRenderingClass, conditionalRenderingOverlay] =
|
||||
useIsConditionallyHidden(model);
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('RowItemRepeater', () => {
|
||||
expect(screen.queryByText('Row C')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(rowToRepeat.state.key).toBe('row-1-clone-0');
|
||||
expect(rowToRepeat.state.key).toBe('row-1');
|
||||
expect(rowToRepeat.state.repeatedRows!.length).toBe(2);
|
||||
expect(rowToRepeat.state.repeatedRows![0].state.key).toBe('row-1-clone-1');
|
||||
});
|
||||
|
||||
@@ -97,12 +97,17 @@ export function performRowRepeats(variable: MultiValueVariable, row: RowItem, co
|
||||
const rowCloneKey = getCloneKey(row.state.key!, rowIndex);
|
||||
const rowClone = isSourceRow
|
||||
? row
|
||||
: row.clone({ repeatByVariable: undefined, repeatedRows: undefined, layout: undefined });
|
||||
: row.clone({
|
||||
key: rowCloneKey,
|
||||
repeatSourceKey: row.state.key,
|
||||
repeatByVariable: undefined,
|
||||
repeatedRows: undefined,
|
||||
layout: undefined,
|
||||
});
|
||||
|
||||
const layout = isSourceRow ? row.getLayout() : row.getLayout().cloneLayout(rowCloneKey, false);
|
||||
|
||||
rowClone.setState({
|
||||
key: rowCloneKey,
|
||||
$variables: getLocalVariableValueSet(variable, variableValues[rowIndex], variableTexts[rowIndex]),
|
||||
layout,
|
||||
});
|
||||
|
||||
@@ -12,7 +12,6 @@ import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboa
|
||||
|
||||
import { dashboardEditActions, ObjectsReorderedOnCanvasEvent } from '../../edit-pane/shared';
|
||||
import { serializeRowsLayout } from '../../serialization/layoutSerializers/RowsLayoutSerializer';
|
||||
import { isClonedKey, joinCloneKeys } from '../../utils/clone';
|
||||
import { getDashboardSceneFor } from '../../utils/utils';
|
||||
import { DashboardGridItem } from '../layout-default/DashboardGridItem';
|
||||
import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager';
|
||||
@@ -67,16 +66,7 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
|
||||
}
|
||||
|
||||
public cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager {
|
||||
return this.clone({
|
||||
rows: this.state.rows.map((row) => {
|
||||
const key = joinCloneKeys(ancestorKey, row.state.key!);
|
||||
|
||||
return row.clone({
|
||||
key,
|
||||
layout: row.state.layout.cloneLayout(key, isSource),
|
||||
});
|
||||
}),
|
||||
});
|
||||
return this.clone({});
|
||||
}
|
||||
|
||||
public duplicate(): DashboardLayoutManager {
|
||||
@@ -188,7 +178,7 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
|
||||
|
||||
if (layout instanceof TabsLayoutManager) {
|
||||
for (const tab of layout.state.tabs) {
|
||||
if (isClonedKey(tab.state.key!)) {
|
||||
if (tab.state.repeatSourceKey) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -221,21 +211,24 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
|
||||
}
|
||||
|
||||
if (child instanceof SceneGridRow) {
|
||||
if (!isClonedKey(child.state.key!)) {
|
||||
const behaviour = child.state.$behaviors?.find((b) => b instanceof RowRepeaterBehavior);
|
||||
|
||||
config.push({
|
||||
title: child.state.title,
|
||||
isCollapsed: !!child.state.isCollapsed,
|
||||
isDraggable: child.state.isDraggable,
|
||||
isResizable: child.state.isResizable,
|
||||
children: child.state.children,
|
||||
repeat: behaviour?.state.variableName,
|
||||
});
|
||||
|
||||
// Since we encountered a row item, any subsequent panels should be added to a new row
|
||||
children = undefined;
|
||||
// Skip repeated row clones
|
||||
if (child.state.repeatSourceKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const behaviour = child.state.$behaviors?.find((b) => b instanceof RowRepeaterBehavior);
|
||||
|
||||
config.push({
|
||||
title: child.state.title,
|
||||
isCollapsed: !!child.state.isCollapsed,
|
||||
isDraggable: child.state.isDraggable,
|
||||
isResizable: child.state.isResizable,
|
||||
children: child.state.children,
|
||||
repeat: behaviour?.state.variableName,
|
||||
});
|
||||
|
||||
// Since we encountered a row item, any subsequent panels should be added to a new row
|
||||
children = undefined;
|
||||
} else {
|
||||
if (!children) {
|
||||
children = [];
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Trans } from '@grafana/i18n';
|
||||
import { MultiValueVariable, SceneComponentProps, sceneGraph, useSceneObjectState } from '@grafana/scenes';
|
||||
import { Button, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { isInCloneChain } from '../../utils/clone';
|
||||
import { isRepeatCloneOrChildOf } from '../../utils/clone';
|
||||
import { useDashboardState } from '../../utils/utils';
|
||||
import { useClipboardState } from '../layouts-shared/useClipboardState';
|
||||
|
||||
@@ -21,7 +21,7 @@ export function RowLayoutManagerRenderer({ model }: SceneComponentProps<RowsLayo
|
||||
const styles = useStyles2(getStyles);
|
||||
const { hasCopiedRow } = useClipboardState();
|
||||
|
||||
const isClone = isInCloneChain(rows[0]?.state.key || '');
|
||||
const isClone = isRepeatCloneOrChildOf(model);
|
||||
|
||||
return (
|
||||
<DragDropContext
|
||||
|
||||
@@ -42,6 +42,8 @@ export interface TabItemState extends SceneObjectState {
|
||||
conditionalRendering?: ConditionalRendering;
|
||||
repeatByVariable?: string;
|
||||
repeatedTabs?: TabItem[];
|
||||
/** Marks object as a repeated object and a key pointer to source object */
|
||||
repeatSourceKey?: string;
|
||||
}
|
||||
|
||||
export class TabItem
|
||||
|
||||
@@ -8,7 +8,7 @@ import { SceneComponentProps, sceneGraph } from '@grafana/scenes';
|
||||
import { Box, Icon, Tab, Tooltip, useElementSelection, usePointerDistance, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { useIsConditionallyHidden } from '../../conditional-rendering/useIsConditionallyHidden';
|
||||
import { useIsClone } from '../../utils/clone';
|
||||
import { isRepeatCloneOrChildOf } from '../../utils/clone';
|
||||
import { useDashboardState } from '../../utils/utils';
|
||||
|
||||
import { TabItem } from './TabItem';
|
||||
@@ -29,7 +29,7 @@ export function TabItemRenderer({ model }: SceneComponentProps<TabItem>) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const pointerDistance = usePointerDistance();
|
||||
const [isConditionallyHidden] = useIsConditionallyHidden(model);
|
||||
const isClone = useIsClone(model);
|
||||
const isClone = isRepeatCloneOrChildOf(model);
|
||||
|
||||
const isDraggable = !isClone && isEditing;
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('TabItemRepeater', () => {
|
||||
expect(screen.queryByText('Tab C')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(tabToRepeat.state.key).toBe('tab-1-clone-0');
|
||||
expect(tabToRepeat.state.key).toBe('tab-1');
|
||||
expect(tabToRepeat.state.repeatedTabs!.length).toBe(2);
|
||||
expect(tabToRepeat.state.repeatedTabs![0].state.key).toBe('tab-1-clone-1');
|
||||
});
|
||||
|
||||
@@ -152,12 +152,17 @@ export function createTabRepeats({
|
||||
const tabCloneKey = getCloneKey(tab.state.key!, tabIndex);
|
||||
const tabClone = isSourceTab
|
||||
? tab
|
||||
: tab.clone({ repeatByVariable: undefined, repeatedTabs: undefined, layout: undefined });
|
||||
: tab.clone({
|
||||
key: tabCloneKey,
|
||||
repeatSourceKey: tab.state.key,
|
||||
repeatByVariable: undefined,
|
||||
repeatedTabs: undefined,
|
||||
layout: undefined,
|
||||
});
|
||||
|
||||
const layout = isSourceTab ? tab.getLayout() : tab.getLayout().cloneLayout(tabCloneKey, false);
|
||||
|
||||
tabClone.setState({
|
||||
key: tabCloneKey,
|
||||
$variables: getLocalVariableValueSet(variable, variableValues[tabIndex], variableTexts[tabIndex]),
|
||||
layout,
|
||||
});
|
||||
|
||||
@@ -12,15 +12,6 @@ import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboa
|
||||
|
||||
import { dashboardEditActions, ObjectsReorderedOnCanvasEvent } from '../../edit-pane/shared';
|
||||
import { serializeTabsLayout } from '../../serialization/layoutSerializers/TabsLayoutSerializer';
|
||||
import {
|
||||
containsCloneKey,
|
||||
getCloneKey,
|
||||
getLastKeyFromClone,
|
||||
getOriginalKey,
|
||||
isClonedKey,
|
||||
isClonedKeyOf,
|
||||
joinCloneKeys,
|
||||
} from '../../utils/clone';
|
||||
import { getDashboardSceneFor } from '../../utils/utils';
|
||||
import { RowItem } from '../layout-rows/RowItem';
|
||||
import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager';
|
||||
@@ -138,16 +129,7 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
|
||||
}
|
||||
|
||||
public cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager {
|
||||
return this.clone({
|
||||
tabs: this.state.tabs.map((tab) => {
|
||||
const key = joinCloneKeys(ancestorKey, tab.state.key!);
|
||||
|
||||
return tab.clone({
|
||||
key,
|
||||
layout: tab.state.layout.cloneLayout(key, isSource),
|
||||
});
|
||||
}),
|
||||
});
|
||||
return this.clone();
|
||||
}
|
||||
|
||||
public getOutlineChildren() {
|
||||
@@ -257,19 +239,19 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
|
||||
let destinationTab = allTabs[toIndex];
|
||||
let selectionIndex = toIndex;
|
||||
|
||||
if (containsCloneKey(getLastKeyFromClone(destinationTab.state.key!))) {
|
||||
if (isClonedKeyOf(destinationTab.state.key!, objectToMove.state.key!)) {
|
||||
if (destinationTab.state.repeatSourceKey) {
|
||||
if (destinationTab.state.repeatSourceKey === objectToMove.state.repeatSourceKey) {
|
||||
// moving tab between its clones
|
||||
return;
|
||||
}
|
||||
const originalTabKey = getCloneKey(getOriginalKey(destinationTab.state.key!), 0);
|
||||
const originalTabIndex = allTabs.findIndex((tab) => tab.state.key === originalTabKey);
|
||||
|
||||
if (originalTabIndex !== -1) {
|
||||
destinationTab = allTabs[originalTabIndex];
|
||||
const sourceTabIndx = allTabs.findIndex((tab) => tab.state.key === destinationTab.state.repeatSourceKey);
|
||||
|
||||
if (sourceTabIndx !== -1) {
|
||||
destinationTab = allTabs[sourceTabIndx];
|
||||
|
||||
const isMovingLeft = toIndex < fromIndex;
|
||||
selectionIndex = originalTabIndex + (isMovingLeft ? 0 : destinationTab.state.repeatedTabs?.length || 0);
|
||||
selectionIndex = sourceTabIndx + (isMovingLeft ? 0 : destinationTab.state.repeatedTabs?.length || 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,7 +301,7 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
|
||||
|
||||
if (layout instanceof RowsLayoutManager) {
|
||||
for (const row of layout.state.rows) {
|
||||
if (isClonedKey(row.state.key!)) {
|
||||
if (row.state.repeatSourceKey) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { MultiValueVariable, SceneComponentProps, sceneGraph, useSceneObjectStat
|
||||
import { Button, TabContent, TabsBar, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { useIsConditionallyHidden } from '../../conditional-rendering/useIsConditionallyHidden';
|
||||
import { isInCloneChain } from '../../utils/clone';
|
||||
import { isRepeatCloneOrChildOf } from '../../utils/clone';
|
||||
import { getDashboardSceneFor } from '../../utils/utils';
|
||||
import { dashboardCanvasAddButtonHoverStyles } from '../layouts-shared/styles';
|
||||
import { useClipboardState } from '../layouts-shared/useClipboardState';
|
||||
@@ -27,7 +27,7 @@ export function TabsLayoutManagerRenderer({ model }: SceneComponentProps<TabsLay
|
||||
const { hasCopiedTab } = useClipboardState();
|
||||
const [_, conditionalRenderingClass, conditionalRenderingOverlay] = useIsConditionallyHidden(currentTab);
|
||||
|
||||
const isClone = isInCloneChain(tabs[0]?.state.key || '');
|
||||
const isClone = isRepeatCloneOrChildOf(model);
|
||||
|
||||
return (
|
||||
<div className={styles.tabLayoutContainer}>
|
||||
|
||||
+1
-2
@@ -2,7 +2,6 @@ import { Spec as DashboardV2Spec, RowsLayoutRowKind } from '@grafana/schema/dist
|
||||
|
||||
import { RowItem } from '../../scene/layout-rows/RowItem';
|
||||
import { RowsLayoutManager } from '../../scene/layout-rows/RowsLayoutManager';
|
||||
import { isClonedKey } from '../../utils/clone';
|
||||
|
||||
import { layoutDeserializerRegistry } from './layoutSerializerRegistry';
|
||||
import { getConditionalRendering } from './utils';
|
||||
@@ -11,7 +10,7 @@ export function serializeRowsLayout(layoutManager: RowsLayoutManager): Dashboard
|
||||
return {
|
||||
kind: 'RowsLayout',
|
||||
spec: {
|
||||
rows: layoutManager.state.rows.filter((row) => !isClonedKey(row.state.key!)).map(serializeRow),
|
||||
rows: layoutManager.state.rows.filter((row) => !row.state.repeatSourceKey).map(serializeRow),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+1
-2
@@ -2,7 +2,6 @@ import { Spec as DashboardV2Spec, TabsLayoutTabKind } from '@grafana/schema/dist
|
||||
|
||||
import { TabItem } from '../../scene/layout-tabs/TabItem';
|
||||
import { TabsLayoutManager } from '../../scene/layout-tabs/TabsLayoutManager';
|
||||
import { isClonedKey } from '../../utils/clone';
|
||||
|
||||
import { layoutDeserializerRegistry } from './layoutSerializerRegistry';
|
||||
import { getConditionalRendering } from './utils';
|
||||
@@ -11,7 +10,7 @@ export function serializeTabsLayout(layoutManager: TabsLayoutManager): Dashboard
|
||||
return {
|
||||
kind: 'TabsLayout',
|
||||
spec: {
|
||||
tabs: layoutManager.state.tabs.filter((tab) => !isClonedKey(tab.state.key!)).map(serializeTab),
|
||||
tabs: layoutManager.state.tabs.filter((tab) => !tab.state.repeatSourceKey).map(serializeTab),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ import { PanelTimeRange } from '../scene/PanelTimeRange';
|
||||
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
|
||||
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
|
||||
import { RowRepeaterBehavior } from '../scene/layout-default/RowRepeaterBehavior';
|
||||
import { isClonedKey } from '../utils/clone';
|
||||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||
import { djb2Hash } from '../utils/djb2Hash';
|
||||
import {
|
||||
@@ -74,9 +73,10 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa
|
||||
|
||||
if (child instanceof SceneGridRow) {
|
||||
// Skip repeat clones or when generating a snapshot
|
||||
if (isClonedKey(child.state.key!) && !isSnapshot) {
|
||||
if (child.state.repeatSourceKey && !isSnapshot) {
|
||||
continue;
|
||||
}
|
||||
|
||||
gridRowToSaveModel(child, panels, isSnapshot);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { dateTime } from '@grafana/data';
|
||||
import { getPanelPlugin } from '@grafana/data/test';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { config, locationService, setPluginImportUtils } from '@grafana/runtime';
|
||||
import { SceneTimeRange, VizPanel } from '@grafana/scenes';
|
||||
import { LocalValueVariable, SceneTimeRange, SceneVariableSet, VizPanel } from '@grafana/scenes';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
|
||||
@@ -46,7 +46,7 @@ describe('ShareLinkTab', () => {
|
||||
buildAndRenderScenario({});
|
||||
|
||||
expect(await screen.findByRole('textbox', { name: 'Link URL' })).toHaveValue(
|
||||
'http://dashboards.grafana.com/grafana/d/dash-1?from=2019-02-11T13:00:00.000Z&to=2019-02-11T19:00:00.000Z&viewPanel=panel-12'
|
||||
'http://dashboards.grafana.com/grafana/d/dash-1?from=2019-02-11T13:00:00.000Z&to=2019-02-11T19:00:00.000Z&viewPanel=A$panel-12'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -57,7 +57,7 @@ describe('ShareLinkTab', () => {
|
||||
await act(() => tab.onToggleLockedTime());
|
||||
|
||||
expect(await screen.findByRole('textbox', { name: 'Link URL' })).toHaveValue(
|
||||
'http://dashboards.grafana.com/grafana/d/dash-1?from=now-6h&to=now&viewPanel=panel-12'
|
||||
'http://dashboards.grafana.com/grafana/d/dash-1?from=now-6h&to=now&viewPanel=A$panel-12'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -67,7 +67,7 @@ describe('ShareLinkTab', () => {
|
||||
await act(() => tab.onThemeChange('light'));
|
||||
|
||||
expect(await screen.findByRole('textbox', { name: 'Link URL' })).toHaveValue(
|
||||
'http://dashboards.grafana.com/grafana/d/dash-1?from=2019-02-11T13:00:00.000Z&to=2019-02-11T19:00:00.000Z&viewPanel=panel-12&theme=light'
|
||||
'http://dashboards.grafana.com/grafana/d/dash-1?from=2019-02-11T13:00:00.000Z&to=2019-02-11T19:00:00.000Z&viewPanel=A$panel-12&theme=light'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -88,7 +88,7 @@ describe('ShareLinkTab', () => {
|
||||
await screen.findByRole('link', { name: selectors.pages.SharePanelModal.linkToRenderedImage })
|
||||
).toHaveAttribute(
|
||||
'href',
|
||||
'http://dashboards.grafana.com/grafana/render/d-solo/dash-1?from=2019-02-11T13:00:00.000Z&to=2019-02-11T19:00:00.000Z&panelId=panel-12&__feature.dashboardSceneSolo=true&width=1000&height=500&tz=Pacific%2FEaster'
|
||||
'http://dashboards.grafana.com/grafana/render/d-solo/dash-1?from=2019-02-11T13:00:00.000Z&to=2019-02-11T19:00:00.000Z&panelId=A$panel-12&__feature.dashboardSceneSolo=true&width=1000&height=500&tz=Pacific%2FEaster'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -102,6 +102,15 @@ function buildAndRenderScenario(options: ScenarioOptions) {
|
||||
title: 'Panel A',
|
||||
pluginId: 'table',
|
||||
key: 'panel-12',
|
||||
$variables: new SceneVariableSet({
|
||||
variables: [
|
||||
new LocalValueVariable({
|
||||
name: 'server',
|
||||
value: 'A',
|
||||
text: 'A',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
const tab = new ShareLinkTab({ panelRef: panel.getRef() });
|
||||
const scene = new DashboardScene({
|
||||
|
||||
@@ -76,7 +76,7 @@ export class ShareLinkTab extends SceneObjectBase<ShareLinkTabState> implements
|
||||
let imageQueryParams = urlParamsUpdate;
|
||||
if (panel) {
|
||||
delete imageQueryParams.viewPanel;
|
||||
imageQueryParams.panelId = panel.state.key;
|
||||
imageQueryParams.panelId = panel.getPathId();
|
||||
// force solo route to use scenes
|
||||
imageQueryParams['__feature.dashboardSceneSolo'] = true;
|
||||
}
|
||||
@@ -84,7 +84,7 @@ export class ShareLinkTab extends SceneObjectBase<ShareLinkTabState> implements
|
||||
const imageUrl = getDashboardUrl({
|
||||
uid: dashboard.state.uid,
|
||||
currentQueryParams: window.location.search,
|
||||
updateQuery: { ...urlParamsUpdate, ...queryOptions, panelId: panel?.state.key },
|
||||
updateQuery: { ...urlParamsUpdate, ...queryOptions, panelId: panel?.getPathId() },
|
||||
absolute: true,
|
||||
soloRoute: true,
|
||||
render: true,
|
||||
|
||||
@@ -43,7 +43,7 @@ function SharePanelEmbedTabRenderer({ model }: SceneComponentProps<SharePanelEmb
|
||||
|
||||
return (
|
||||
<ShareEmbed
|
||||
panelId={p.state.key!}
|
||||
panelId={p.getPathId()}
|
||||
timeFrom={timeFrom}
|
||||
range={timeRangeState.state.value}
|
||||
dashboard={{ uid: dashUid ?? '', time: timeRangeState.state.value }}
|
||||
|
||||
@@ -46,7 +46,7 @@ describe('useSoloPanel', () => {
|
||||
|
||||
it('should return the cloned panel when panel is found', () => {
|
||||
const { dashboard } = setup();
|
||||
const { result } = renderHook(() => useSoloPanel(dashboard, 'panel-1-clone-1'));
|
||||
const { result } = renderHook(() => useSoloPanel(dashboard, 'A$panel-1'));
|
||||
const panel = findVizPanelByKey(dashboard, 'panel-1');
|
||||
|
||||
expect(result.current[0]).not.toBe(panel);
|
||||
|
||||
@@ -4,10 +4,9 @@ import { VizPanel, UrlSyncManager } from '@grafana/scenes';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { DashboardRepeatsProcessedEvent } from '../scene/types/DashboardRepeatsProcessedEvent';
|
||||
import { containsCloneKey } from '../utils/clone';
|
||||
import { findVizPanelByKey } from '../utils/utils';
|
||||
import { containsPathIdSeparator, findVizPanelByPathId } from '../utils/pathId';
|
||||
|
||||
export function useSoloPanel(dashboard: DashboardScene, panelId: string): [VizPanel | undefined, string | undefined] {
|
||||
export function useSoloPanel(dashboard: DashboardScene, pathId: string): [VizPanel | undefined, string | undefined] {
|
||||
const [panel, setPanel] = useState<VizPanel>();
|
||||
const [error, setError] = useState<string | undefined>();
|
||||
|
||||
@@ -19,7 +18,7 @@ export function useSoloPanel(dashboard: DashboardScene, panelId: string): [VizPa
|
||||
|
||||
let panel: VizPanel | null = null;
|
||||
try {
|
||||
panel = findVizPanelByKey(dashboard, panelId);
|
||||
panel = findVizPanelByPathId(dashboard, pathId);
|
||||
} catch (e) {
|
||||
// do nothing, just the panel is not found or not a VizPanel
|
||||
}
|
||||
@@ -27,8 +26,8 @@ export function useSoloPanel(dashboard: DashboardScene, panelId: string): [VizPa
|
||||
if (panel) {
|
||||
activateParents(panel);
|
||||
setPanel(panel);
|
||||
} else if (containsCloneKey(panelId)) {
|
||||
findRepeatClone(dashboard, panelId).then((panel) => {
|
||||
} else if (containsPathIdSeparator(pathId)) {
|
||||
findRepeatClone(dashboard, pathId).then((panel) => {
|
||||
if (panel) {
|
||||
setPanel(panel);
|
||||
} else {
|
||||
@@ -40,7 +39,7 @@ export function useSoloPanel(dashboard: DashboardScene, panelId: string): [VizPa
|
||||
}
|
||||
|
||||
return cleanUp;
|
||||
}, [dashboard, panelId]);
|
||||
}, [dashboard, pathId]);
|
||||
|
||||
return [panel, error];
|
||||
}
|
||||
@@ -54,10 +53,10 @@ function activateParents(panel: VizPanel) {
|
||||
}
|
||||
}
|
||||
|
||||
function findRepeatClone(dashboard: DashboardScene, panelId: string): Promise<VizPanel | undefined> {
|
||||
function findRepeatClone(dashboard: DashboardScene, pathId: string): Promise<VizPanel | undefined> {
|
||||
return new Promise((resolve) => {
|
||||
dashboard.subscribeToEvent(DashboardRepeatsProcessedEvent, () => {
|
||||
const panel = findVizPanelByKey(dashboard, panelId);
|
||||
const panel = findVizPanelByPathId(dashboard, pathId);
|
||||
if (panel) {
|
||||
resolve(panel);
|
||||
} else {
|
||||
|
||||
@@ -1,134 +1,10 @@
|
||||
import {
|
||||
getCloneKey,
|
||||
getOriginalKey,
|
||||
isInCloneChain,
|
||||
isClonedKey,
|
||||
joinCloneKeys,
|
||||
containsCloneKey,
|
||||
getLastKeyFromClone,
|
||||
isClonedKeyOf,
|
||||
} from './clone';
|
||||
import { getCloneKey } from './clone';
|
||||
|
||||
describe('clone', () => {
|
||||
describe('getCloneKey', () => {
|
||||
it('should return the clone key', () => {
|
||||
expect(getCloneKey('panel', 1)).toBe('panel-clone-1');
|
||||
expect(getCloneKey('panel-clone-2', 1)).toBe('panel-clone-1');
|
||||
});
|
||||
|
||||
it('should not alter ancestors', () => {
|
||||
expect(getCloneKey('row-clone-1/panel', 2)).toBe('row-clone-1/panel-clone-2');
|
||||
expect(getCloneKey('tab-clone-0/row-clone-1/panel', 2)).toBe('tab-clone-0/row-clone-1/panel-clone-2');
|
||||
expect(getCloneKey('row-clone-1/panel-clone-3', 2)).toBe('row-clone-1/panel-clone-2');
|
||||
expect(getCloneKey('tab-clone-0/row-clone-1/panel-clone-3', 2)).toBe('tab-clone-0/row-clone-1/panel-clone-2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOriginalKey', () => {
|
||||
it('should return the original key', () => {
|
||||
expect(getOriginalKey('panel')).toBe('panel');
|
||||
expect(getOriginalKey('panel-clone-1')).toBe('panel');
|
||||
expect(getOriginalKey('row-clone-1/panel-clone-2')).toBe('panel');
|
||||
expect(getOriginalKey('tab-clone-0/row-clone-1/panel-clone-2')).toBe('panel');
|
||||
expect(getOriginalKey('panel-2-clone-3')).toBe('panel-2');
|
||||
expect(getOriginalKey('panel-2')).toBe('panel-2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isClonedKey', () => {
|
||||
it('should return true for cloned keys', () => {
|
||||
expect(isClonedKey('tab-clone-0/row-clone-1/panel-clone-2')).toBe(true);
|
||||
expect(isClonedKey('row-clone-0/panel-clone-1')).toBe(true);
|
||||
expect(isClonedKey('panel-clone-1')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-cloned keys', () => {
|
||||
expect(isClonedKey('panel-clone-0')).toBe(false);
|
||||
expect(isClonedKey('tab-clone-1/row-clone-2/panel-clone-0')).toBe(false);
|
||||
expect(isClonedKey('row-clone-1/panel-clone-0')).toBe(false);
|
||||
expect(isClonedKey('panel')).toBe(false);
|
||||
expect(isClonedKey('tab-clone-1/row-clone-2/panel')).toBe(false);
|
||||
expect(isClonedKey('row-clone-1/panel')).toBe(false);
|
||||
});
|
||||
|
||||
it('should properly handle indexes containing 0', () => {
|
||||
expect(isClonedKey('tab-clone-0/row-clone-1/panel-clone-0')).toBe(false);
|
||||
expect(isClonedKey('row-clone-0/panel-clone-0')).toBe(false);
|
||||
expect(isClonedKey('panel-clone-0')).toBe(false);
|
||||
|
||||
expect(isClonedKey('tab-clone-0/row-clone-1/panel-clone-101')).toBe(true);
|
||||
expect(isClonedKey('row-clone-0/panel-clone-101')).toBe(true);
|
||||
expect(isClonedKey('panel-clone-1010')).toBe(true);
|
||||
|
||||
expect(isClonedKey('tab-clone-0/row-clone-1/panel-clone-10')).toBe(true);
|
||||
expect(isClonedKey('row-clone-0/panel-clone-100')).toBe(true);
|
||||
expect(isClonedKey('panel-clone-1000')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isClonedKeyOf', () => {
|
||||
it('should return true for cloned keys', () => {
|
||||
expect(isClonedKeyOf('tab-clone-0/row-clone-1/panel-clone-2', 'panel-clone-2')).toBe(true);
|
||||
expect(isClonedKeyOf('tab-clone-0/row-clone-1/panel-clone-2', 'panel')).toBe(true);
|
||||
expect(isClonedKeyOf('panel-clone-2', 'panel-clone-2')).toBe(true);
|
||||
expect(isClonedKeyOf('panel-clone-2', 'panel')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-cloned keys', () => {
|
||||
expect(isClonedKeyOf('tab-clone-0/row-clone-1/panel-clone-2', 'panel2-clone-2')).toBe(false);
|
||||
expect(isClonedKeyOf('tab-clone-0/row-clone-1/panel-clone-2', 'panel2')).toBe(false);
|
||||
expect(isClonedKeyOf('panel-clone-2', 'panel2-clone-2')).toBe(false);
|
||||
expect(isClonedKeyOf('panel-clone-2', 'panel2')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isInCloneChain', () => {
|
||||
it('should return true for keys with cloned ancestors', () => {
|
||||
expect(isInCloneChain('tab-clone-1/row-clone-0/panel-clone-0')).toBe(true);
|
||||
expect(isInCloneChain('row-clone-0/row-clone-1/panel-clone-0')).toBe(true);
|
||||
expect(isInCloneChain('row-clone-0/row-clone-0/panel-clone-1')).toBe(true);
|
||||
expect(isInCloneChain('panel-clone-1')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for keys without cloned ancestors', () => {
|
||||
expect(isInCloneChain('panel-clone-0')).toBe(false);
|
||||
expect(isInCloneChain('row-clone-0/panel-clone-0')).toBe(false);
|
||||
expect(isInCloneChain('tab-clone-0/row-clone-0/panel-clone-0')).toBe(false);
|
||||
expect(isInCloneChain('panel')).toBe(false);
|
||||
expect(isInCloneChain('tab-clone-0/row-clone-0/panel')).toBe(false);
|
||||
expect(isInCloneChain('tab-clone-0/row/panel')).toBe(false);
|
||||
expect(isInCloneChain('tab-clone-0/row/panel-0')).toBe(false);
|
||||
expect(isInCloneChain('tab/row-clone-0/panel-0')).toBe(false);
|
||||
expect(isInCloneChain('row-clone-0/panel')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLastKeyFromClone', () => {
|
||||
it('should return the last key', () => {
|
||||
expect(getLastKeyFromClone('tab-clone-1/row-clone-2/panel-clone-3')).toBe('panel-clone-3');
|
||||
expect(getLastKeyFromClone('row-clone-1/panel-clone-2')).toBe('panel-clone-2');
|
||||
expect(getLastKeyFromClone('row-clone-1/panel')).toBe('panel');
|
||||
expect(getLastKeyFromClone('panel')).toBe('panel');
|
||||
});
|
||||
});
|
||||
|
||||
describe('joinCloneKeys', () => {
|
||||
it('should join keys with a separator', () => {
|
||||
expect(joinCloneKeys('row', 'panel-clone-1')).toBe('row/panel-clone-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('containsCloneKey', () => {
|
||||
it('should return true for keys with clone key', () => {
|
||||
expect(containsCloneKey('row-clone-0/panel-clone-1')).toBe(true);
|
||||
expect(containsCloneKey('tab-clone-0/row-clone-1/panel-clone-2')).toBe(true);
|
||||
expect(containsCloneKey('panel-clone-1')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for keys without clone key', () => {
|
||||
expect(containsCloneKey('panel')).toBe(false);
|
||||
expect(containsCloneKey('tab-0/row-1/panel-2')).toBe(false);
|
||||
expect(containsCloneKey('row-1/panel-2')).toBe(false);
|
||||
expect(getCloneKey('panel-1', 1)).toBe('panel-1-clone-1');
|
||||
expect(getCloneKey('panel-22', 1)).toBe('panel-22-clone-1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,13 +7,7 @@ import {
|
||||
VariableValueSingle,
|
||||
} from '@grafana/scenes';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
|
||||
const CLONE_KEY = '-clone-';
|
||||
const CLONE_SEPARATOR = '/';
|
||||
|
||||
const CLONED_KEY_REGEX = new RegExp(`${CLONE_KEY}[1-9][0-9]*$`);
|
||||
const ORIGINAL_REGEX = new RegExp(`${CLONE_KEY}\\d+$`);
|
||||
|
||||
/**
|
||||
* Create or alter the last key for a key
|
||||
@@ -21,91 +15,21 @@ const ORIGINAL_REGEX = new RegExp(`${CLONE_KEY}\\d+$`);
|
||||
* @param index
|
||||
*/
|
||||
export function getCloneKey(key: string, index: number): string {
|
||||
const parts = key.split(CLONE_SEPARATOR).slice(0, -1);
|
||||
const lastKey = getOriginalKey(getLastKeyFromClone(key));
|
||||
return [...parts, `${lastKey}${CLONE_KEY}${index}`].join(CLONE_SEPARATOR);
|
||||
return `${key}${CLONE_KEY}${index}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the original key from a clone key
|
||||
* @param key
|
||||
*/
|
||||
export function getOriginalKey(key: string): string {
|
||||
return getLastKeyFromClone(key).replace(ORIGINAL_REGEX, '');
|
||||
}
|
||||
export function isRepeatCloneOrChildOf(scene: SceneObject): boolean {
|
||||
let obj: SceneObject | undefined = scene;
|
||||
|
||||
/**
|
||||
* Checks if the last key is a clone key
|
||||
* @param key
|
||||
*/
|
||||
export function isClonedKey(key: string): boolean {
|
||||
return CLONED_KEY_REGEX.test(getLastKeyFromClone(key));
|
||||
}
|
||||
do {
|
||||
if ('repeatSourceKey' in obj.state && obj.state.repeatSourceKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if key1 is a clone of key2
|
||||
* @param key1
|
||||
* @param key2
|
||||
*/
|
||||
export function isClonedKeyOf(key1: string, key2: string): boolean {
|
||||
return isClonedKey(key1) && getOriginalKey(key1) === getOriginalKey(key2);
|
||||
}
|
||||
obj = obj.parent;
|
||||
} while (obj);
|
||||
|
||||
/**
|
||||
* Checks if the key or any of its ancestors are cloned
|
||||
* @param key
|
||||
*/
|
||||
export function isInCloneChain(key: string): boolean {
|
||||
return key.split(CLONE_SEPARATOR).some(isClonedKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last key from a clone key
|
||||
* @param key
|
||||
*/
|
||||
export function getLastKeyFromClone(key: string): string {
|
||||
return key.split(CLONE_SEPARATOR).pop() ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Join clone keys
|
||||
* @param keys
|
||||
*/
|
||||
export function joinCloneKeys(...keys: string[]): string {
|
||||
return keys.filter(Boolean).join(CLONE_SEPARATOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a key contains the '-clone-' string
|
||||
* @param key
|
||||
*/
|
||||
export function containsCloneKey(key: string): boolean {
|
||||
return key.includes(CLONE_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Useful hook for checking of a scene is a clone
|
||||
* @param scene
|
||||
*/
|
||||
export function useIsClone(scene: SceneObject): boolean {
|
||||
const { key } = scene.useState();
|
||||
return isClonedKey(key!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Useful hook for checking if a scene is in a clone chain
|
||||
* @param scene
|
||||
*/
|
||||
export function useHasClonedParents(scene: SceneObject): boolean {
|
||||
if (isClonedKey(scene.state.key!)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!scene.parent || scene.parent instanceof DashboardScene) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return useHasClonedParents(scene.parent);
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getLocalVariableValueSet(
|
||||
|
||||
@@ -4,7 +4,6 @@ import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { VizPanelLinks } from '../scene/PanelLinks';
|
||||
|
||||
import { isClonedKey } from './clone';
|
||||
import { getDashboardSceneFor, getLayoutManagerFor, getPanelIdForVizPanel, getVizPanelKeyForPanelId } from './utils';
|
||||
|
||||
function getTimePicker(scene: DashboardScene) {
|
||||
@@ -37,12 +36,11 @@ export function getNextPanelId(scene: SceneObject): number {
|
||||
let max = 0;
|
||||
|
||||
sceneGraph
|
||||
.findAllObjects(scene.getRoot(), (obj) => obj instanceof VizPanel || obj instanceof SceneGridRow)
|
||||
.findAllObjects(
|
||||
scene.getRoot(),
|
||||
(obj) => (obj instanceof VizPanel || obj instanceof SceneGridRow) && !obj.state.repeatSourceKey
|
||||
)
|
||||
.forEach((panel) => {
|
||||
if (isClonedKey(panel.state.key!)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const panelId = getPanelIdForVizPanel(panel);
|
||||
if (panelId > max) {
|
||||
max = panelId;
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import {
|
||||
LocalValueVariable,
|
||||
SceneGridItem,
|
||||
SceneGridLayout,
|
||||
SceneGridRow,
|
||||
SceneVariableSet,
|
||||
VizPanel,
|
||||
} from '@grafana/scenes';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
|
||||
|
||||
import { findVizPanelByPathId } from './pathId';
|
||||
|
||||
describe('findVizPanelByPathId', () => {
|
||||
it('should find correct panel', () => {
|
||||
const { scene, panel1, repeatedPanel } = buildTestScene();
|
||||
|
||||
expect(findVizPanelByPathId(scene, 'panel-1')).toBe(panel1);
|
||||
expect(findVizPanelByPathId(scene, 'US$pod1$panel-2')).toBe(repeatedPanel);
|
||||
});
|
||||
|
||||
it('should find correct pane with legacy number only', () => {
|
||||
const { scene, panel1 } = buildTestScene();
|
||||
|
||||
expect(findVizPanelByPathId(scene, '1')).toBe(panel1);
|
||||
// This should not find anything
|
||||
expect(findVizPanelByPathId(scene, '1$panel-1')).toBe(null);
|
||||
});
|
||||
|
||||
it('should include local and parent local variable value', () => {
|
||||
const { repeatedPanel } = buildTestScene();
|
||||
|
||||
expect(repeatedPanel.getPathId()).toBe('US$pod1$panel-2');
|
||||
});
|
||||
});
|
||||
|
||||
function buildTestScene() {
|
||||
const panel1 = new VizPanel({
|
||||
title: 'Panel 1',
|
||||
pluginId: 'table',
|
||||
key: 'panel-1',
|
||||
});
|
||||
|
||||
const repeatedPanel = new VizPanel({
|
||||
title: 'Panel 2',
|
||||
pluginId: 'table',
|
||||
key: 'panel-2',
|
||||
$variables: new SceneVariableSet({
|
||||
variables: [new LocalValueVariable({ name: 'pod', value: 'pod1', text: 'pod1' })],
|
||||
}),
|
||||
});
|
||||
|
||||
const grid = new SceneGridLayout({
|
||||
children: [
|
||||
new SceneGridItem({
|
||||
key: 'grid-item-1',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 24,
|
||||
height: 10,
|
||||
body: panel1,
|
||||
}),
|
||||
new SceneGridRow({
|
||||
key: 'row-1',
|
||||
x: 0,
|
||||
y: 10,
|
||||
width: 24,
|
||||
height: 1,
|
||||
$variables: new SceneVariableSet({
|
||||
variables: [new LocalValueVariable({ name: 'datacenter', value: 'US', text: 'US' })],
|
||||
}),
|
||||
children: [
|
||||
new SceneGridItem({
|
||||
key: 'grid-item-2',
|
||||
x: 0,
|
||||
y: 11,
|
||||
width: 24,
|
||||
height: 5,
|
||||
body: repeatedPanel,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const scene = new DashboardScene({
|
||||
title: 'My dashboard',
|
||||
uid: 'dash-1',
|
||||
tags: ['database', 'panel'],
|
||||
meta: {
|
||||
canEdit: true,
|
||||
},
|
||||
body: new DefaultGridLayoutManager({ grid }),
|
||||
});
|
||||
|
||||
return { scene, panel1, repeatedPanel };
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { SceneObject, VizPanel, sceneGraph, PATH_ID_SEPARATOR } from '@grafana/scenes';
|
||||
|
||||
import { getVizPanelKeyForPanelId } from './utils';
|
||||
|
||||
export function findVizPanelByPathId(scene: SceneObject, pathId: string): VizPanel | null {
|
||||
// Check if pathId is just an old legacy panel id
|
||||
if (/^\d+$/.test(pathId)) {
|
||||
pathId = getVizPanelKeyForPanelId(parseInt(pathId, 10));
|
||||
}
|
||||
|
||||
const panel = sceneGraph.findObject(scene, (obj) => {
|
||||
if (!(obj instanceof VizPanel)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return pathId === obj.getPathId();
|
||||
});
|
||||
|
||||
if (panel) {
|
||||
if (panel instanceof VizPanel) {
|
||||
return panel;
|
||||
} else {
|
||||
throw new Error(`Found panel with key ${pathId} but it was not a VizPanel`);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function containsPathIdSeparator(key: string): boolean {
|
||||
return key.includes(PATH_ID_SEPARATOR);
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { getQueryRunnerFor } from './utils';
|
||||
|
||||
export function getViewPanelUrl(vizPanel: VizPanel) {
|
||||
return locationUtil.getUrlForPartial(locationService.getLocation(), {
|
||||
viewPanel: vizPanel.state.key,
|
||||
viewPanel: vizPanel.getPathId(),
|
||||
editPanel: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,8 +29,6 @@ import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
|
||||
import { setDashboardPanelContext } from '../scene/setDashboardPanelContext';
|
||||
import { DashboardLayoutManager, isDashboardLayoutManager } from '../scene/types/DashboardLayoutManager';
|
||||
|
||||
import { containsCloneKey, getLastKeyFromClone, getOriginalKey, isInCloneChain } from './clone';
|
||||
|
||||
export const NEW_PANEL_HEIGHT = 8;
|
||||
export const NEW_PANEL_WIDTH = 12;
|
||||
|
||||
@@ -44,7 +42,7 @@ export function getVizPanelKeyForPanelId(panelId: number) {
|
||||
}
|
||||
|
||||
export function getPanelIdForVizPanel(panel: SceneObject): number {
|
||||
return parseInt(getOriginalKey(panel.state.key!).replace('panel-', ''), 10);
|
||||
return parseInt(panel.state.key!.replace('panel-', ''), 10);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,102 +96,16 @@ function findVizPanelInternal(scene: SceneObject, key: string | undefined): VizP
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function findOriginalVizPanelByKey(scene: SceneObject, key: string | undefined): VizPanel | null {
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let panel: VizPanel | null = findOriginalVizPanelInternal(scene, key);
|
||||
|
||||
if (panel) {
|
||||
return panel;
|
||||
}
|
||||
|
||||
// Also try to find by panel id
|
||||
const id = parseInt(key, 10);
|
||||
if (isNaN(id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const panelId = getVizPanelKeyForPanelId(id);
|
||||
panel = findVizPanelInternal(scene, panelId);
|
||||
|
||||
if (panel) {
|
||||
return panel;
|
||||
}
|
||||
|
||||
panel = findOriginalVizPanelInternal(scene, panelId);
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
function findOriginalVizPanelInternal(scene: SceneObject, key: string | undefined): VizPanel | null {
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const panel = sceneGraph.findObject(scene, (obj) => {
|
||||
const objKey = obj.state.key!;
|
||||
|
||||
// Compare the original keys
|
||||
if (objKey === key || (!isInCloneChain(objKey) && getOriginalKey(objKey) === getOriginalKey(key))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(obj instanceof VizPanel)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (panel) {
|
||||
if (panel instanceof VizPanel) {
|
||||
return panel;
|
||||
} else {
|
||||
throw new Error(`Found panel with key ${key} but it was not a VizPanel`);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function findEditPanel(scene: SceneObject, key: string | undefined): VizPanel | null {
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// First we try to find the non-cloned panel
|
||||
// This means it is either in not in a repeat chain or every item in the chain is not a clone
|
||||
let panel: SceneObject | null = findOriginalVizPanelByKey(scene, key);
|
||||
let panel: SceneObject | null = findVizPanelByKey(scene, key);
|
||||
if (!panel || !panel.state.key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the actual panel key, without any of the ancestors
|
||||
const panelKey = getLastKeyFromClone(panel.state.key);
|
||||
|
||||
// If the panel contains clone in the key, this means it's a repeated panel, and we need to find the original panel
|
||||
if (containsCloneKey(panelKey)) {
|
||||
// Get the original key of the panel that we are looking for
|
||||
const originalPanelKey = getOriginalKey(panelKey);
|
||||
// Start the search from the parent to avoid unnecessary checks
|
||||
// The parent usually is the grid item where the referenced panel is also located
|
||||
panel = sceneGraph.findObject(panel.parent ?? scene, (sceneObject) => {
|
||||
if (!sceneObject.state.key || isInCloneChain(sceneObject.state.key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentLastKey = getLastKeyFromClone(sceneObject.state.key);
|
||||
if (containsCloneKey(currentLastKey)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return getOriginalKey(currentLastKey) === originalPanelKey;
|
||||
});
|
||||
}
|
||||
|
||||
if (!(panel instanceof VizPanel)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import { config } from '@grafana/runtime';
|
||||
import { SceneDataProvider, SceneDataTransformer, SceneObject } from '@grafana/scenes';
|
||||
import {
|
||||
activateSceneObjectAndParentTree,
|
||||
findOriginalVizPanelByKey,
|
||||
findVizPanelByKey,
|
||||
getVizPanelKeyForPanelId,
|
||||
} from 'app/features/dashboard-scene/utils/utils';
|
||||
|
||||
@@ -63,7 +63,7 @@ export class DashboardDatasource extends DataSourceApi<DashboardQuery> {
|
||||
return of({ data: [] });
|
||||
}
|
||||
|
||||
let sourcePanel = this.findSourcePanel(scene, panelId);
|
||||
let sourcePanel = findVizPanelByKey(scene, getVizPanelKeyForPanelId(panelId));
|
||||
|
||||
if (!sourcePanel) {
|
||||
return of({ data: [], error: { message: 'Could not find source panel' } });
|
||||
@@ -295,11 +295,6 @@ export class DashboardDatasource extends DataSourceApi<DashboardQuery> {
|
||||
};
|
||||
}
|
||||
|
||||
private findSourcePanel(scene: SceneObject, panelId: number) {
|
||||
// We're trying to find the original panel, not a cloned one, since `panelId` alone cannot resolve clones
|
||||
return findOriginalVizPanelByKey(scene, getVizPanelKeyForPanelId(panelId));
|
||||
}
|
||||
|
||||
private emitFirstLoadedDataIfMixedDS(
|
||||
requestId: string
|
||||
): (source: Observable<DataQueryResponse>) => Observable<DataQueryResponse> {
|
||||
|
||||
@@ -3586,11 +3586,11 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@grafana/scenes-react@npm:6.29.7":
|
||||
version: 6.29.7
|
||||
resolution: "@grafana/scenes-react@npm:6.29.7"
|
||||
"@grafana/scenes-react@npm:^6.30.0":
|
||||
version: 6.30.0
|
||||
resolution: "@grafana/scenes-react@npm:6.30.0"
|
||||
dependencies:
|
||||
"@grafana/scenes": "npm:6.29.7"
|
||||
"@grafana/scenes": "npm:6.30.0"
|
||||
lru-cache: "npm:^10.2.2"
|
||||
react-use: "npm:^17.4.0"
|
||||
peerDependencies:
|
||||
@@ -3602,13 +3602,13 @@ __metadata:
|
||||
react: ^18.0.0
|
||||
react-dom: ^18.0.0
|
||||
react-router-dom: ^6.28.0
|
||||
checksum: 10/d3db42b8face33a871cc2b983b00890b4a7b9cf685b06dd02df730036f9c13f1cdbc0dcb578453a0ce29381bdceb26d2a07ae4be77a8f753677dde1560eb491f
|
||||
checksum: 10/107b930aaf88945cbc51601443190357d9733bd3e9063fa7d2fd7496ad772e25f7cbddd34fd6a2adeb5ae85d39d03cecf8d2ad3d74a351e61fedabb17dfce82a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@grafana/scenes@npm:6.29.7":
|
||||
version: 6.29.7
|
||||
resolution: "@grafana/scenes@npm:6.29.7"
|
||||
"@grafana/scenes@npm:6.30.0, @grafana/scenes@npm:^6.30.0":
|
||||
version: 6.30.0
|
||||
resolution: "@grafana/scenes@npm:6.30.0"
|
||||
dependencies:
|
||||
"@floating-ui/react": "npm:^0.26.16"
|
||||
"@leeoniya/ufuzzy": "npm:^1.0.16"
|
||||
@@ -3628,7 +3628,7 @@ __metadata:
|
||||
react: ^18.0.0
|
||||
react-dom: ^18.0.0
|
||||
react-router-dom: ^6.28.0
|
||||
checksum: 10/a2bbda33c78cd568d6333e033e1b587917ee4ca00d8f69e3707197d6712beba1d1b9244f05259bc4cce7416b9ec67d24eddaa1e6073695e2b4a7af1a37253174
|
||||
checksum: 10/b1036a1d8c531b3e197c3de276c4fe4a7092fcd27e1fd350ff6530cabb8eb5436a6184840ef6a4e6747510326e58c9a73d54b0c36f8ee7d20f8fd6eaccea8d75
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -18313,8 +18313,8 @@ __metadata:
|
||||
"@grafana/plugin-ui": "npm:0.10.9"
|
||||
"@grafana/prometheus": "workspace:*"
|
||||
"@grafana/runtime": "workspace:*"
|
||||
"@grafana/scenes": "npm:6.29.7"
|
||||
"@grafana/scenes-react": "npm:6.29.7"
|
||||
"@grafana/scenes": "npm:^6.30.0"
|
||||
"@grafana/scenes-react": "npm:^6.30.0"
|
||||
"@grafana/schema": "workspace:*"
|
||||
"@grafana/sql": "workspace:*"
|
||||
"@grafana/test-utils": "workspace:*"
|
||||
|
||||
Reference in New Issue
Block a user