From d7771f7d21f13a2061be8633b3033404c84f97af Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Thu, 3 Apr 2025 10:44:09 +0200 Subject: [PATCH] Dynamic dashboards: Deduplicate titles of new rows and tabs (#103330) Deduplicate titles of new rows and tabs --- .../scene/layout-rows/RowsLayoutManager.tsx | 8 +++ .../scene/layout-tabs/TabsLayoutManager.tsx | 7 +++ .../scene/layouts-shared/utils.test.ts | 50 +++++++++++++++++++ .../scene/layouts-shared/utils.ts | 32 ++++++++++++ 4 files changed, 97 insertions(+) create mode 100644 public/app/features/dashboard-scene/scene/layouts-shared/utils.test.ts diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx index e3310aa9aef..d013c72d620 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx @@ -16,6 +16,7 @@ import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutMan import { RowRepeaterBehavior } from '../layout-default/RowRepeaterBehavior'; import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager'; import { getRowFromClipboard } from '../layouts-shared/paste'; +import { generateUniqueTitle } from '../layouts-shared/utils'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; import { LayoutRegistryItem } from '../types/LayoutRegistryItem'; @@ -95,6 +96,13 @@ export class RowsLayoutManager extends SceneObjectBase i public addNewRow(row?: RowItem): RowItem { const newRow = row ?? new RowItem({ isNew: true }); + const existingNames = new Set(this.state.rows.map((row) => row.state.title).filter((title) => title !== undefined)); + + const newTitle = generateUniqueTitle(newRow.state.title, existingNames); + if (newTitle !== newRow.state.title) { + newRow.setState({ title: newTitle }); + } + this.setState({ rows: [...this.state.rows, newRow] }); this.publishEvent(new NewObjectAddedToCanvasEvent(newRow), true); return newRow; diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx index fe59bf6411d..1033b86ef6e 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx @@ -18,6 +18,7 @@ import { getDashboardSceneFor } from '../../utils/utils'; import { RowItem } from '../layout-rows/RowItem'; import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager'; import { getTabFromClipboard } from '../layouts-shared/paste'; +import { generateUniqueTitle } from '../layouts-shared/utils'; import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; import { LayoutRegistryItem } from '../types/LayoutRegistryItem'; @@ -125,6 +126,12 @@ export class TabsLayoutManager extends SceneObjectBase i public addNewTab(tab?: TabItem) { const newTab = tab ?? new TabItem({ isNew: true }); + const existingNames = new Set(this.state.tabs.map((tab) => tab.state.title).filter((title) => title !== undefined)); + const newTitle = generateUniqueTitle(newTab.state.title, existingNames); + if (newTitle !== newTab.state.title) { + newTab.setState({ title: newTitle }); + } + this.setState({ tabs: [...this.state.tabs, newTab], currentTabIndex: this.state.tabs.length }); this.publishEvent(new NewObjectAddedToCanvasEvent(newTab), true); return newTab; diff --git a/public/app/features/dashboard-scene/scene/layouts-shared/utils.test.ts b/public/app/features/dashboard-scene/scene/layouts-shared/utils.test.ts new file mode 100644 index 00000000000..b262698f914 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/layouts-shared/utils.test.ts @@ -0,0 +1,50 @@ +import { generateUniqueTitle } from './utils'; + +describe('generateUniqueTitle', () => { + it('should return the original title if it is not in the existing titles', () => { + const title = 'My Title'; + const existingTitles = new Set(['Other Title', 'Another Title']); + expect(generateUniqueTitle(title, existingTitles)).toBe(title); + }); + + it('should handle undefined title by using empty string as base', () => { + const existingTitles = new Set(['Title 1', 'Title 2']); + expect(generateUniqueTitle(undefined, existingTitles)).toBe(''); + }); + + it('should append "1" to a title that does not end with a number', () => { + const title = 'My Title'; + const existingTitles = new Set(['My Title']); + expect(generateUniqueTitle(title, existingTitles)).toBe('My Title 1'); + }); + + it('should increment a number at the end of a title', () => { + const title = 'My Title 1'; + const existingTitles = new Set(['My Title 1', 'My Title 2']); + expect(generateUniqueTitle(title, existingTitles)).toBe('My Title 3'); + }); + + it('should handle multiple increments when needed', () => { + const title = 'My Title'; + const existingTitles = new Set(['My Title', 'My Title 1', 'My Title 2', 'My Title 3']); + expect(generateUniqueTitle(title, existingTitles)).toBe('My Title 4'); + }); + + it('should handle titles with multiple numbers', () => { + const title = 'My Title 123'; + const existingTitles = new Set(['My Title 123', 'My Title 124']); + expect(generateUniqueTitle(title, existingTitles)).toBe('My Title 125'); + }); + + it('should handle titles with spaces before the number', () => { + const title = 'My Title 1'; + const existingTitles = new Set(['My Title 1', 'My Title 2']); + expect(generateUniqueTitle(title, existingTitles)).toBe('My Title 3'); + }); + + it('should handle empty existing titles set', () => { + const title = 'My Title'; + const existingTitles = new Set(); + expect(generateUniqueTitle(title, existingTitles)).toBe(title); + }); +}); diff --git a/public/app/features/dashboard-scene/scene/layouts-shared/utils.ts b/public/app/features/dashboard-scene/scene/layouts-shared/utils.ts index e704e108c04..3d4b850d568 100644 --- a/public/app/features/dashboard-scene/scene/layouts-shared/utils.ts +++ b/public/app/features/dashboard-scene/scene/layouts-shared/utils.ts @@ -34,3 +34,35 @@ export function useEditPaneInputAutoFocus({ autoFocus }: EditPaneInputAutoFocusP return ref; } + +export function generateUniqueTitle(title: string | undefined, existingTitles: Set): string { + const baseTitle = title ?? ''; + + if (existingTitles.has(baseTitle)) { + const titleMatch = baseTitle.match(/^(.*?)(\d+)$/); + if (titleMatch) { + // If title ends with a number, increment it + const baseTitle = titleMatch[1]; + const currentNumber = parseInt(titleMatch[2], 10); + let newTitle = `${baseTitle}${currentNumber + 1}`; + + // Keep incrementing until we find an unused title + while (existingTitles.has(newTitle)) { + const nextNumber = parseInt(newTitle.match(/\d+$/)![0], 10) + 1; + newTitle = `${baseTitle}${nextNumber}`; + } + return newTitle; + } else { + // If title doesn't end with a number, append "1" + let i = 1; + let newTitle = `${baseTitle} ${i}`; + while (existingTitles.has(newTitle)) { + i++; + newTitle = `${baseTitle} ${i}`; + } + return newTitle; + } + } + + return baseTitle; +}