Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
69a337a
feat: Extend DashboardContainer schema with tabs and display options
alex-fedotyev Apr 3, 2026
33ea2bb
feat: Add @dnd-kit infrastructure for container reordering
alex-fedotyev Apr 3, 2026
cc4d62f
feat: Add GroupContainer, replace SectionHeader
alex-fedotyev Apr 3, 2026
7742b48
feat: Add useDashboardContainers and useTileSelection hooks
alex-fedotyev Apr 3, 2026
bcce771
feat: Integrate groups, tabs, and DnD into dashboard page
alex-fedotyev Apr 6, 2026
c244076
test: Add GroupContainer and dashboard container tests
alex-fedotyev Apr 6, 2026
b018011
Merge branch 'main' into feat/unified-group
alex-fedotyev Apr 6, 2026
e501956
fix: use semantic CSS tokens per review feedback
alex-fedotyev Apr 7, 2026
09831a0
fix: Replace remaining Mantine tokens with semantic tokens
alex-fedotyev Apr 7, 2026
dbec2c1
fix: Address PR review feedback — Mantine components, decomposition, …
alex-fedotyev Apr 10, 2026
649bea4
fix: Tab delete offers choice — delete tiles or move to another tab
alex-fedotyev Apr 15, 2026
3787fab
Merge remote-tracking branch 'origin/main' into feat/unified-group
alex-fedotyev Apr 21, 2026
08593ae
chore: remove redundant WHAT-style comments per review feedback
alex-fedotyev Apr 21, 2026
7110037
feat: auto-delete emptied groups after Cmd+G regrouping
alex-fedotyev Apr 21, 2026
9c39efa
refactor: rename GroupContainer → DashboardContainer
alex-fedotyev Apr 21, 2026
dc153b9
refactor: move activeTabId to URL state (per-viewer)
alex-fedotyev Apr 21, 2026
9c95f42
fix: take viewer to new tab after Add Tab + harden stale activeTabId …
alex-fedotyev Apr 21, 2026
b845595
feat: 3-option prompt for group delete (Cancel / Ungroup Tiles / Delete)
alex-fedotyev Apr 21, 2026
f059991
fix: address 3 issues from latest code review
alex-fedotyev Apr 21, 2026
c01ae19
feat: show group-level alert dot on expanded plain (no-tab) containers
alex-fedotyev Apr 21, 2026
931d80b
fix: address pulpdrew review round (bug fix + design simplifications …
alex-fedotyev Apr 22, 2026
1518b9e
docs: surface dashboard tile selection shortcuts in KeyboardShortcuts…
alex-fedotyev Apr 22, 2026
4cf5bc6
Merge remote-tracking branch 'origin/main' into feat/unified-group
alex-fedotyev Apr 22, 2026
4394704
Merge remote-tracking branch 'origin/main' into feat/unified-group
alex-fedotyev Apr 23, 2026
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
6 changes: 6 additions & 0 deletions .changeset/unified-group-containers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@hyperdx/app": patch
"@hyperdx/common-utils": patch
---

refactor: Unify section/group into single Group with collapsible/bordered options
100 changes: 100 additions & 0 deletions packages/app/src/components/DashboardDndComponents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React from 'react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Box, Button } from '@mantine/core';
import { IconPlus } from '@tabler/icons-react';

import { type DragData, type DragHandleProps } from './DashboardDndContext';

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

export function EmptyContainerPlaceholder({
containerId,
children,
isEmpty,
onAddTile,
}: {
containerId: string;
children?: React.ReactNode;
isEmpty?: boolean;
onAddTile?: () => void;
}) {
return (
<div
data-testid={`container-placeholder-${containerId}`}
style={{
minHeight: isEmpty ? 80 : undefined,
borderRadius: 4,
border: isEmpty
? '2px dashed var(--mantine-color-default-border)'
: undefined,
Comment thread
alex-fedotyev marked this conversation as resolved.
Outdated
position: 'relative',
}}
>
Comment thread
alex-fedotyev marked this conversation as resolved.
Outdated
{isEmpty && (
<Box
style={{
position: 'absolute',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '0 16px',
}}
Comment thread
alex-fedotyev marked this conversation as resolved.
Outdated
>
<Button
variant="secondary"
fw={400}
w="100%"
leftSection={<IconPlus size={16} />}
onClick={onAddTile}
>
Add
</Button>
</Box>
)}
{children}
</div>
);
}

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

export function SortableContainerWrapper({
containerId,
containerTitle,
children,
}: {
containerId: string;
containerTitle: string;
children: (dragHandleProps: DragHandleProps) => React.ReactNode;
}) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({
id: `container-sort-${containerId}`,
data: {
type: 'container',
containerId,
containerTitle,
} satisfies DragData,
});

const style: React.CSSProperties = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
};

return (
<div ref={setNodeRef} style={style}>
{children({ ...attributes, ...listeners })}
</div>
);
}
116 changes: 116 additions & 0 deletions packages/app/src/components/DashboardDndContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import React, { useCallback, useMemo, useState } from 'react';
import {
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
SortableContext,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { DashboardContainer } from '@hyperdx/common-utils/dist/types';
import { Box, Text } from '@mantine/core';

// --- Types ---

export type DragHandleProps = React.HTMLAttributes<HTMLElement>;

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

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

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

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

const mouseSensor = useSensor(MouseSensor, {
activationConstraint: { distance: 8 },
});
const touchSensor = useSensor(TouchSensor, {
activationConstraint: { delay: 200, tolerance: 5 },
});
const sensors = useSensors(mouseSensor, touchSensor);

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

const handleDragStart = useCallback((event: DragStartEvent) => {
setActiveDrag((event.active.data.current as DragData) ?? null);
}, []);

const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
setActiveDrag(null);
if (!over) return;

const activeData = active.data.current as DragData | undefined;
if (!activeData) return;

// Container reorder via sortable
const overData = over.data.current as DragData | undefined;
if (
overData?.type === 'container' &&
activeData.containerId !== overData.containerId
) {
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, onReorderContainers],
);

return (
<DndContext
sensors={sensors}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<SortableContext
items={containerSortableIds}
strategy={verticalListSortingStrategy}
>
{children}
</SortableContext>
<DragOverlay dropAnimation={null}>
{activeDrag && (
<Box
px="sm"
py={4}
style={{
background: 'var(--mantine-color-body)',
border: '1px solid var(--mantine-color-default-border)',
Comment thread
alex-fedotyev marked this conversation as resolved.
Outdated
borderRadius: 4,
opacity: 0.85,
}}
>
<Text size="sm" fw={500}>
{activeDrag.containerTitle}
</Text>
</Box>
)}
</DragOverlay>
</DndContext>
);
}
Loading