Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
328 changes: 133 additions & 195 deletions packages/app/src/DBDashboardPage.tsx

Large diffs are not rendered by default.

70 changes: 49 additions & 21 deletions packages/app/src/__tests__/dashboardSections.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,60 @@ import {
} from '@hyperdx/common-utils/dist/types';

describe('DashboardContainer schema', () => {
it('validates a valid section', () => {
it('validates a valid group', () => {
const result = DashboardContainerSchema.safeParse({
id: 'section-1',
type: 'section',
id: 'group-1',
type: 'group',
title: 'Infrastructure',
collapsed: false,
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.type).toBe('group');
}
});

it('validates a collapsed section', () => {
it('accepts legacy section type for backward compatibility', () => {
const result = DashboardContainerSchema.safeParse({
id: 'section-2',
id: 'section-1',
type: 'section',
title: 'Legacy Section',
collapsed: false,
});
expect(result.success).toBe(true);
});

it('validates a collapsed group', () => {
const result = DashboardContainerSchema.safeParse({
id: 'group-2',
type: 'group',
title: 'Database Metrics',
collapsed: true,
});
expect(result.success).toBe(true);
});

it('rejects a section missing required fields', () => {
it('rejects a container missing required fields', () => {
const result = DashboardContainerSchema.safeParse({
id: 'section-3',
id: 'group-3',
// missing title and collapsed
});
expect(result.success).toBe(false);
});

it('rejects a section with empty id or title', () => {
it('rejects a container with empty id or title', () => {
expect(
DashboardContainerSchema.safeParse({
id: '',
type: 'section',
type: 'group',
title: 'Valid',
collapsed: false,
}).success,
).toBe(false);
expect(
DashboardContainerSchema.safeParse({
id: 'valid',
type: 'section',
type: 'group',
title: '',
collapsed: false,
}).success,
Expand Down Expand Up @@ -206,7 +219,7 @@ describe('Tile schema with containerId and tabId', () => {
});
});

describe('Dashboard schema with sections', () => {
describe('Dashboard schema with containers', () => {
const baseDashboard = {
id: 'dash-1',
name: 'My Dashboard',
Expand All @@ -233,28 +246,28 @@ describe('Dashboard schema with sections', () => {
}
});

it('rejects duplicate section IDs', () => {
it('rejects duplicate container IDs', () => {
const result = DashboardSchema.safeParse({
...baseDashboard,
containers: [
{ id: 's1', type: 'section', title: 'Section A', collapsed: false },
{ id: 's1', type: 'section', title: 'Section B', collapsed: true },
{ id: 's1', type: 'group', title: 'Group A', collapsed: false },
{ id: 's1', type: 'group', title: 'Group B', collapsed: true },
],
});
expect(result.success).toBe(false);
});

it('validates a dashboard with sections', () => {
it('validates a dashboard with groups', () => {
const result = DashboardSchema.safeParse({
...baseDashboard,
containers: [
{
id: 's1',
type: 'section',
type: 'group',
title: 'Infrastructure',
collapsed: false,
},
{ id: 's2', type: 'section', title: 'Application', collapsed: true },
{ id: 's2', type: 'group', title: 'Application', collapsed: true },
],
});
expect(result.success).toBe(true);
Expand All @@ -265,7 +278,22 @@ describe('Dashboard schema with sections', () => {
}
});

it('validates a full dashboard with sections and tiles referencing them', () => {
it('accepts legacy section type in dashboard containers', () => {
const result = DashboardSchema.safeParse({
...baseDashboard,
containers: [
{
id: 's1',
type: 'section',
title: 'Legacy',
collapsed: false,
},
],
});
expect(result.success).toBe(true);
});

it('validates a full dashboard with groups and tiles referencing them', () => {
const tile = {
id: 'tile-1',
x: 0,
Expand Down Expand Up @@ -293,7 +321,7 @@ describe('Dashboard schema with sections', () => {
containers: [
{
id: 's1',
type: 'section',
type: 'group',
title: 'Infrastructure',
collapsed: false,
},
Expand Down Expand Up @@ -354,7 +382,7 @@ describe('Dashboard schema with sections', () => {
});
});

describe('section tile grouping logic', () => {
describe('container tile grouping logic', () => {
// Test the grouping logic used in DBDashboardPage
type SimpleTile = { id: string; containerId?: string; tabId?: string };
type SimpleSection = { id: string; title: string; collapsed: boolean };
Expand Down Expand Up @@ -518,7 +546,7 @@ describe('section tile grouping logic', () => {
});
});

describe('section authoring operations', () => {
describe('container authoring operations', () => {
type SimpleTile = { id: string; containerId?: string; tabId?: string };
type SimpleSection = { id: string; title: string; collapsed: boolean };
type SimpleDashboard = {
Expand Down
28 changes: 14 additions & 14 deletions packages/app/src/components/DashboardDndComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,22 @@ import { IconPlus } from '@tabler/icons-react';
import { type DragData, type DragHandleProps } from './DashboardDndContext';

// --- Empty container placeholder ---
// Visual placeholder for empty sections/groups/tabs with optional add-tile click.
// Visual placeholder for empty groups/tabs with optional add-tile click.

export function EmptyContainerPlaceholder({
sectionId,
containerId,
children,
isEmpty,
onAddTile,
}: {
sectionId: string;
containerId: string;
children?: React.ReactNode;
isEmpty?: boolean;
onAddTile?: () => void;
}) {
return (
<div
data-testid={`container-placeholder-${sectionId}`}
data-testid={`container-placeholder-${containerId}`}
style={{
minHeight: isEmpty ? 80 : undefined,
borderRadius: 4,
Expand Down Expand Up @@ -59,15 +59,15 @@ export function EmptyContainerPlaceholder({
);
}

// --- Sortable section wrapper (for container reordering) ---
// --- Sortable container wrapper (for container reordering) ---

export function SortableSectionWrapper({
sectionId,
sectionTitle,
export function SortableContainerWrapper({
containerId,
containerTitle,
children,
}: {
sectionId: string;
sectionTitle: string;
containerId: string;
containerTitle: string;
children: (dragHandleProps: DragHandleProps) => React.ReactNode;
}) {
const {
Expand All @@ -78,11 +78,11 @@ export function SortableSectionWrapper({
transition,
isDragging,
} = useSortable({
id: `section-sort-${sectionId}`,
id: `container-sort-${containerId}`,
data: {
type: 'section',
sectionId,
sectionTitle,
type: 'container',
containerId,
containerTitle,
} satisfies DragData,
});

Expand Down
34 changes: 17 additions & 17 deletions packages/app/src/components/DashboardDndContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,23 @@ import { Box, Text } from '@mantine/core';
export type DragHandleProps = React.HTMLAttributes<HTMLElement>;

export type DragData = {
type: 'section';
sectionId: string;
sectionTitle: string;
type: 'container';
containerId: string;
containerTitle: string;
};

type Props = {
children: React.ReactNode;
containers: DashboardContainer[];
onReorderSections: (fromIndex: number, toIndex: number) => void;
onReorderContainers: (fromIndex: number, toIndex: number) => void;
};

// --- Provider (section reorder only) ---
// --- Provider (container reorder only) ---

export function DashboardDndProvider({
children,
containers,
onReorderSections,
onReorderContainers,
}: Props) {
const [activeDrag, setActiveDrag] = useState<DragData | null>(null);

Expand All @@ -49,8 +49,8 @@ export function DashboardDndProvider({
});
const sensors = useSensors(mouseSensor, touchSensor);

const sectionSortableIds = useMemo(
() => containers.map(c => `section-sort-${c.id}`),
const containerSortableIds = useMemo(
() => containers.map(c => `container-sort-${c.id}`),
[containers],
);

Expand All @@ -67,18 +67,18 @@ export function DashboardDndProvider({
const activeData = active.data.current as DragData | undefined;
if (!activeData) return;

// Section reorder via sortable
// Container reorder via sortable
const overData = over.data.current as DragData | undefined;
if (
overData?.type === 'section' &&
activeData.sectionId !== overData.sectionId
overData?.type === 'container' &&
activeData.containerId !== overData.containerId
) {
const from = containers.findIndex(c => c.id === activeData.sectionId);
const to = containers.findIndex(c => c.id === overData.sectionId);
if (from !== -1 && to !== -1) onReorderSections(from, to);
const from = containers.findIndex(c => c.id === activeData.containerId);
const to = containers.findIndex(c => c.id === overData.containerId);
if (from !== -1 && to !== -1) onReorderContainers(from, to);
}
},
[containers, onReorderSections],
[containers, onReorderContainers],
);

return (
Expand All @@ -88,7 +88,7 @@ export function DashboardDndProvider({
onDragEnd={handleDragEnd}
>
<SortableContext
items={sectionSortableIds}
items={containerSortableIds}
strategy={verticalListSortingStrategy}
>
{children}
Expand All @@ -106,7 +106,7 @@ export function DashboardDndProvider({
}}
>
<Text size="sm" fw={500}>
{activeDrag.sectionTitle}
{activeDrag.containerTitle}
</Text>
</Box>
)}
Expand Down
Loading
Loading