diff --git a/packages/app/src/DBDashboardPage.tsx b/packages/app/src/DBDashboardPage.tsx
index a1d5b8c0..b5737650 100644
--- a/packages/app/src/DBDashboardPage.tsx
+++ b/packages/app/src/DBDashboardPage.tsx
@@ -936,7 +936,7 @@ function DashboardContainerRow({
onToggleDefaultCollapsed: () => void;
onToggleCollapsible: () => void;
onToggleBordered: () => void;
- onDeleteContainer: () => void;
+ onDeleteContainer: (action: 'ungroup' | 'delete') => void;
onAddTile: (containerId: string, tabId?: string) => void;
onAddTab: () => void;
onRenameTab: (tabId: string, newTitle: string) => void;
@@ -961,6 +961,7 @@ function DashboardContainerRow({
onToggleCollapsible={onToggleCollapsible}
onToggleBordered={onToggleBordered}
onDelete={onDeleteContainer}
+ tileCount={containerTiles.length}
onAddTile={() =>
onAddTile(container.id, hasTabs ? activeTabId : undefined)
}
@@ -1687,7 +1688,7 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
handleAddTab,
handleRenameTab,
handleDeleteTab,
- } = useDashboardContainers({ dashboard, setDashboard, confirm });
+ } = useDashboardContainers({ dashboard, setDashboard });
const onAddTile = (containerId?: string, tabId?: string) => {
// Auto-expand collapsed container via URL state so the new tile is visible
@@ -2239,8 +2240,8 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
onToggleBordered={() =>
handleToggleBordered(container.id)
}
- onDeleteContainer={() =>
- handleDeleteContainer(container.id)
+ onDeleteContainer={action =>
+ handleDeleteContainer(container.id, action)
}
onAddTile={onAddTile}
onAddTab={() => {
diff --git a/packages/app/src/__tests__/DashboardContainer.test.tsx b/packages/app/src/__tests__/DashboardContainer.test.tsx
index d7f4689e..9740a97e 100644
--- a/packages/app/src/__tests__/DashboardContainer.test.tsx
+++ b/packages/app/src/__tests__/DashboardContainer.test.tsx
@@ -258,6 +258,98 @@ describe('DashboardContainer', () => {
});
});
+ describe('group delete prompt', () => {
+ const baseContainer = {
+ id: 'g1',
+ title: 'My Group',
+ collapsed: false,
+ tabs: [{ id: 'tab-1', title: 'My Group' }],
+ };
+
+ it('opens the delete modal when "Delete Group" menu item is clicked', async () => {
+ renderDashboardContainer({
+ onDelete: jest.fn(),
+ tileCount: 2,
+ container: baseContainer,
+ });
+ fireEvent.click(screen.getByTestId('group-menu-g1'));
+ fireEvent.click(await screen.findByTestId('group-delete-g1'));
+ expect(
+ await screen.findByTestId('group-delete-modal'),
+ ).toBeInTheDocument();
+ });
+
+ it('offers Ungroup + Delete when tileCount > 0', async () => {
+ renderDashboardContainer({
+ onDelete: jest.fn(),
+ tileCount: 3,
+ container: baseContainer,
+ });
+ fireEvent.click(screen.getByTestId('group-menu-g1'));
+ fireEvent.click(await screen.findByTestId('group-delete-g1'));
+ expect(
+ await screen.findByTestId('group-delete-ungroup'),
+ ).toBeInTheDocument();
+ expect(screen.getByTestId('group-delete-confirm')).toBeInTheDocument();
+ expect(screen.getByTestId('group-delete-cancel')).toBeInTheDocument();
+ });
+
+ it('hides Ungroup option when tileCount is 0', async () => {
+ renderDashboardContainer({
+ onDelete: jest.fn(),
+ tileCount: 0,
+ container: baseContainer,
+ });
+ fireEvent.click(screen.getByTestId('group-menu-g1'));
+ fireEvent.click(await screen.findByTestId('group-delete-g1'));
+ expect(
+ screen.queryByTestId('group-delete-ungroup'),
+ ).not.toBeInTheDocument();
+ expect(
+ await screen.findByTestId('group-delete-confirm'),
+ ).toBeInTheDocument();
+ });
+
+ it('calls onDelete with "ungroup" when Ungroup Tiles is clicked', async () => {
+ const onDelete = jest.fn();
+ renderDashboardContainer({
+ onDelete,
+ tileCount: 2,
+ container: baseContainer,
+ });
+ fireEvent.click(screen.getByTestId('group-menu-g1'));
+ fireEvent.click(await screen.findByTestId('group-delete-g1'));
+ fireEvent.click(await screen.findByTestId('group-delete-ungroup'));
+ expect(onDelete).toHaveBeenCalledWith('ungroup');
+ });
+
+ it('calls onDelete with "delete" when Delete Group & Tiles is clicked', async () => {
+ const onDelete = jest.fn();
+ renderDashboardContainer({
+ onDelete,
+ tileCount: 2,
+ container: baseContainer,
+ });
+ fireEvent.click(screen.getByTestId('group-menu-g1'));
+ fireEvent.click(await screen.findByTestId('group-delete-g1'));
+ fireEvent.click(await screen.findByTestId('group-delete-confirm'));
+ expect(onDelete).toHaveBeenCalledWith('delete');
+ });
+
+ it('does not call onDelete when Cancel is clicked', async () => {
+ const onDelete = jest.fn();
+ renderDashboardContainer({
+ onDelete,
+ tileCount: 2,
+ container: baseContainer,
+ });
+ fireEvent.click(screen.getByTestId('group-menu-g1'));
+ fireEvent.click(await screen.findByTestId('group-delete-g1'));
+ fireEvent.click(await screen.findByTestId('group-delete-cancel'));
+ expect(onDelete).not.toHaveBeenCalled();
+ });
+ });
+
describe('alert indicators', () => {
it('shows alert dot on collapsed group header when alertingTabIds is non-empty', () => {
const { container: wrapper } = renderDashboardContainer({
diff --git a/packages/app/src/components/DashboardContainer.tsx b/packages/app/src/components/DashboardContainer.tsx
index a670f0cd..2859bfcc 100644
--- a/packages/app/src/components/DashboardContainer.tsx
+++ b/packages/app/src/components/DashboardContainer.tsx
@@ -3,8 +3,11 @@ import { DashboardContainer as DashboardContainerSchema } from '@hyperdx/common-
import {
ActionIcon,
Box,
+ Button,
Flex,
+ Group,
Menu,
+ Modal,
Tabs,
Text,
TextInput,
@@ -29,7 +32,9 @@ type DashboardContainerProps = {
onToggleDefaultCollapsed?: () => void;
onToggleCollapsible?: () => void;
onToggleBordered?: () => void;
- onDelete?: () => void;
+ onDelete?: (action: 'ungroup' | 'delete') => void;
+ /** Tile count inside this container — determines whether "Ungroup Tiles" is offered. */
+ tileCount?: number;
onAddTile?: () => void;
activeTabId?: string;
onTabChange?: (tabId: string) => void;
@@ -52,6 +57,7 @@ export default function DashboardContainer({
onToggleCollapsible,
onToggleBordered,
onDelete,
+ tileCount = 0,
onAddTile,
activeTabId,
onTabChange,
@@ -67,6 +73,7 @@ export default function DashboardContainer({
const [groupRenameValue, setGroupRenameValue] = useState(container.title);
const [hovered, setHovered] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
+ const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const tabs = container.tabs ?? [];
const hasTabs = tabs.length >= 2;
@@ -149,6 +156,7 @@ export default function DashboardContainer({
size="sm"
tabIndex={showControls ? 0 : -1}
style={hoverControlStyle}
+ data-testid={`group-menu-${container.id}`}
>