Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/tidy-tile-menu-groups.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperdx/app': patch
---

fix: Keep the dashboard tile menu on screen when a dashboard has many groups, by moving the group list into a scrollable "Move to group" submenu
179 changes: 24 additions & 155 deletions packages/app/src/DBDashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ import {
IconChevronsDown,
IconChevronsUp,
IconCopy,
IconCornerDownRight,
IconDeviceFloppy,
IconDotsVertical,
IconDownload,
Expand All @@ -125,6 +124,11 @@ import {
import { IsolatedChartSyncProvider } from '@/chartSync';
import { mergeAnnotations } from '@/components/charts/chartAnnotations';
import { ContactSupportText } from '@/components/ContactSupportText';
import {
buildMoveTargets,
MoveTarget,
MoveToGroupSubmenu,
} from '@/components/dashboard/MoveToGroupSubmenu';
import SnapGridLayout from '@/components/dashboard/SnapGridLayout';
import DashboardContainer from '@/components/DashboardContainer';
import {
Expand Down Expand Up @@ -376,14 +380,6 @@ function HeatmapTile({
);
}

type MoveTarget = {
containerId: string;
tabId?: string;
label: string;
// For tabs: all tabs in order with the target tab ID
allTabs?: { id: string; title: string }[];
};

const tileToLayoutItem = (chart: Tile): RGL.Layout => ({
i: chart.id,
x: chart.x,
Expand Down Expand Up @@ -990,64 +986,12 @@ const Tile = ({
Edit
</Menu.Item>
{canMoveToGroup && (
<>
<Menu.Divider />
<Menu.Label>Move to Group</Menu.Label>
{chart.containerId && (
<Menu.Item
leftSection={<IconCornerDownRight size={14} />}
onClick={() => onMoveToGroup(undefined)}
>
(Ungrouped)
</Menu.Item>
)}
{moveTargets
.filter(
t =>
!(
t.containerId === chart.containerId &&
t.tabId === chart.tabId
),
)
.map(t => (
<Menu.Item
key={`${t.containerId}-${t.tabId ?? ''}`}
leftSection={<IconCornerDownRight size={14} />}
onClick={() => onMoveToGroup(t.containerId, t.tabId)}
>
{t.allTabs ? (
<span>
{t.allTabs.map((tab, i) => (
<span key={tab.id}>
{i > 0 && (
<span
style={{
color: 'var(--mantine-color-dimmed)',
}}
>
{' | '}
</span>
)}
<span
style={
tab.id !== t.tabId
? {
color: 'var(--mantine-color-dimmed)',
}
: undefined
}
>
{tab.title}
</span>
</span>
))}
</span>
) : (
t.label
)}
</Menu.Item>
))}
</>
<MoveToGroupSubmenu
moveTargets={moveTargets}
containerId={chart.containerId}
tabId={chart.tabId}
onMoveToGroup={onMoveToGroup}
/>
)}
<Menu.Divider />
<Menu.Item
Expand Down Expand Up @@ -1080,9 +1024,9 @@ const Tile = ({
readOnly,
]);

// Flat Menu.Item list for the collapsed (narrow-tile) toolbar.
// Merges the alert action + all kebab items into a single flat list
// so ChartContainer can render them without nested menus.
// Menu items for the collapsed (narrow-tile) toolbar.
// Merges the alert action + all kebab items into a single list
// for ChartContainer's menu.
const collapsedMenuItems = useMemo(() => {
if (readOnly) return null;

Expand Down Expand Up @@ -1137,64 +1081,12 @@ const Tile = ({
Edit
</Menu.Item>
{canMoveToGroup && (
<>
<Menu.Divider />
<Menu.Label>Move to Group</Menu.Label>
{chart.containerId && (
<Menu.Item
leftSection={<IconCornerDownRight size={14} />}
onClick={() => onMoveToGroup(undefined)}
>
(Ungrouped)
</Menu.Item>
)}
{moveTargets
.filter(
t =>
!(
t.containerId === chart.containerId &&
t.tabId === chart.tabId
),
)
.map(t => (
<Menu.Item
key={`collapsed-${t.containerId}-${t.tabId ?? ''}`}
leftSection={<IconCornerDownRight size={14} />}
onClick={() => onMoveToGroup(t.containerId, t.tabId)}
>
{t.allTabs ? (
<span>
{t.allTabs.map((tab, i) => (
<span key={tab.id}>
{i > 0 && (
<span
style={{
color: 'var(--mantine-color-dimmed)',
}}
>
{' | '}
</span>
)}
<span
style={
tab.id !== t.tabId
? {
color: 'var(--mantine-color-dimmed)',
}
: undefined
}
>
{tab.title}
</span>
</span>
))}
</span>
) : (
t.label
)}
</Menu.Item>
))}
</>
<MoveToGroupSubmenu
moveTargets={moveTargets}
containerId={chart.containerId}
tabId={chart.tabId}
onMoveToGroup={onMoveToGroup}
/>
)}
<Menu.Divider />
<Menu.Item
Expand Down Expand Up @@ -2359,33 +2251,10 @@ function DBDashboardPage({
setUrlActiveTabs,
]);

// Valid move targets: groups and individual tabs within groups
const moveTargetContainers = useMemo<MoveTarget[]>(() => {
const targets: MoveTarget[] = [];
for (const c of containers) {
const cTabs = c.tabs ?? [];
if (cTabs.length >= 2) {
for (const tab of cTabs) {
targets.push({
containerId: c.id,
tabId: tab.id,
label: tab.title,
allTabs: cTabs.map(t => ({ id: t.id, title: t.title })),
});
}
} else if (cTabs.length === 1) {
// 1-tab group: show just the group name, target the single tab
targets.push({
containerId: c.id,
tabId: cTabs[0].id,
label: cTabs[0].title,
});
} else {
targets.push({ containerId: c.id, label: c.title });
}
}
return targets;
}, [containers]);
const moveTargetContainers = useMemo(
() => buildMoveTargets(containers),
[containers],
);

const hasContainers = containers.length > 0;
const allTiles = useMemo(() => dashboard?.tiles ?? [], [dashboard?.tiles]);
Expand Down
87 changes: 87 additions & 0 deletions packages/app/src/components/dashboard/MoveToGroupSubmenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Fragment } from 'react';
import { DashboardContainer } from '@hyperdx/common-utils/dist/types';
import { Menu } from '@mantine/core';
import { IconCornerDownRight, IconFolderSymlink } from '@tabler/icons-react';

export type MoveTarget = {
containerId: string;
tabId?: string;
label: string;
// Heading for the tabs of a multi-tab group
group?: string;
};

export function buildMoveTargets(
containers: DashboardContainer[],
): MoveTarget[] {
return containers.flatMap(c => {
const tabs = c.tabs ?? [];
if (tabs.length === 0) return [{ containerId: c.id, label: c.title }];
if (tabs.length === 1)
return [{ containerId: c.id, tabId: tabs[0].id, label: tabs[0].title }];
// A multi-tab group is named by its first tab, matching its header
return tabs.map(tab => ({
containerId: c.id,
tabId: tab.id,
label: tab.title,
group: tabs[0].title,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — Group display name is worked out a third time instead of reusing a shared helper

DashboardContainer.tsx:93 (firstTab?.title ?? container.title) and DashboardTableOfContents.tsx:92 already compute the group's display name. Move that into one exported helper (e.g. getContainerDisplayTitle(container)) and call it here and at both existing sites, so the submenu heading cannot drift from the group header the PR says it matches.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

}));
});
}

// A submenu keeps the tile menu short however many groups the dashboard has.
export function MoveToGroupSubmenu({
moveTargets,
containerId,
tabId,
onMoveToGroup,
}: {
moveTargets: MoveTarget[];
containerId?: string;
tabId?: string;
onMoveToGroup: (containerId: string | undefined, tabId?: string) => void;
}) {
const targets = moveTargets.filter(
t => !(t.containerId === containerId && t.tabId === tabId),
);

return (
<Menu.Sub>
<Menu.Sub.Target>
<Menu.Sub.Item leftSection={<IconFolderSymlink size={14} />}>
Move to group
</Menu.Sub.Item>
</Menu.Sub.Target>
<Menu.Sub.Dropdown mah={320} style={{ overflowY: 'auto' }}>
{containerId && (
<Menu.Item
leftSection={<IconCornerDownRight size={14} />}
onClick={() => onMoveToGroup(undefined)}
>
(Ungrouped)
</Menu.Item>
)}
{containerId && targets.length > 0 && <Menu.Divider role="separator" />}
{targets.map((t, i) => {
const prev = targets[i - 1];
// Sections key on container, since default group names repeat
const newContainer = prev?.containerId !== t.containerId;
return (
<Fragment key={`${t.containerId}-${t.tabId ?? ''}`}>
{prev && newContainer && (prev.group || t.group) && (
Comment thread
jordan-simonovski marked this conversation as resolved.
Resolved
<Menu.Divider role="separator" />
)}
{t.group && newContainer && <Menu.Label>{t.group}</Menu.Label>}
<Menu.Item
leftSection={<IconCornerDownRight size={14} />}
onClick={() => onMoveToGroup(t.containerId, t.tabId)}
>
{t.label}
</Menu.Item>
</Fragment>
);
})}
</Menu.Sub.Dropdown>
</Menu.Sub>
);
}
Loading
Loading