Files
2026-07-13 13:32:57 +08:00

197 lines
5.5 KiB
TypeScript

import { IconChartHistogram } from "@tabler/icons-react";
import { GripVerticalIcon } from "lucide-react";
import ReactGridLayout from "react-grid-layout";
import { AIMetricsIcon } from "~/assets/icons/AIMetricsIcon";
import { ChartArrowIcon } from "~/assets/icons/ChartArrowIcon";
import { type MatchedOrganization, useCustomDashboards } from "~/hooks/useOrganizations";
import { type UserWithDashboardPreferences } from "~/models/user.server";
import { type RenderIcon } from "~/components/primitives/Icon";
import { v3BuiltInDashboardPath, v3CustomDashboardPath } from "~/utils/pathBuilder";
import { type SideMenuEnvironment, type SideMenuProject } from "./SideMenu";
import { SideMenuItem } from "./SideMenuItem";
import { TreeConnectorBranch, TreeConnectorEnd } from "./TreeConnectors";
import { useReorderableList } from "./useReorderableList";
type SideMenuUser = Pick<UserWithDashboardPreferences, "dashboardPreferences"> & {
isImpersonating: boolean;
};
/**
* Unified item for the reorderable dashboards-section list. Combines the two
* built-in dashboards (Runs, Agents) and any user-created custom dashboards so
* they can be reordered together under the Dashboards parent.
*/
type DashboardChild =
| {
key: string;
kind: "builtin";
label: string;
path: string;
collapsedIcon: RenderIcon;
activeColor: string;
}
| {
key: string;
kind: "custom";
label: string;
path: string;
};
export function DashboardList({
organization,
project,
environment,
isCollapsed,
user,
}: {
organization: MatchedOrganization;
project: SideMenuProject;
environment: SideMenuEnvironment;
isCollapsed: boolean;
user: SideMenuUser;
}) {
const customDashboards = useCustomDashboards();
const items: DashboardChild[] = [
{
key: "builtin:overview",
kind: "builtin",
label: "Run metrics",
path: v3BuiltInDashboardPath(organization, project, environment, "overview"),
collapsedIcon: ChartArrowIcon,
activeColor: "text-runs",
},
{
key: "builtin:llm",
kind: "builtin",
label: "AI metrics",
path: v3BuiltInDashboardPath(organization, project, environment, "llm"),
collapsedIcon: AIMetricsIcon,
activeColor: "text-aiMetrics",
},
...customDashboards.map(
(d): DashboardChild => ({
key: `custom:${d.friendlyId}`,
kind: "custom",
label: d.title,
path: v3CustomDashboardPath(organization, project, environment, d),
})
),
];
const initialOrder =
user.dashboardPreferences.sideMenu?.organizations?.[organization.id]?.orderedItems?.[
"dashboardChildren"
];
const {
orderedItems,
layout,
containerRef,
gridWidth,
canReorder,
handleDrag,
handleDragStop,
getIsLast,
} = useReorderableList({
organizationId: organization.id,
listId: "dashboardChildren",
items,
itemKey: (item) => item.key,
initialOrder,
isImpersonating: user.isImpersonating,
});
return (
<div ref={containerRef}>
{canReorder ? (
<ReactGridLayout
layout={layout}
width={gridWidth}
gridConfig={{
cols: 1,
rowHeight: 32,
margin: [0, 0] as const,
containerPadding: [0, 0] as const,
}}
resizeConfig={{ enabled: false }}
dragConfig={{ enabled: !isCollapsed, handle: ".sidebar-drag-handle" }}
onDrag={handleDrag}
onDragStop={handleDragStop}
className="sidebar-reorder-grid"
autoSize
>
{orderedItems.map((item, index) => {
const isLast = getIsLast(item.key, index);
return (
<div key={item.key}>
<DashboardChildMenuItem
item={item}
isCollapsed={isCollapsed}
isLast={isLast}
showDragHandle
/>
</div>
);
})}
</ReactGridLayout>
) : (
orderedItems.map((item, index) => (
<DashboardChildMenuItem
key={item.key}
item={item}
isCollapsed={isCollapsed}
isLast={index === orderedItems.length - 1}
/>
))
)}
</div>
);
}
function DashboardChildMenuItem({
item,
isCollapsed,
isLast,
showDragHandle = false,
}: {
item: DashboardChild;
isCollapsed: boolean;
isLast: boolean;
showDragHandle?: boolean;
}) {
const collapsedIcon: RenderIcon =
item.kind === "builtin" ? item.collapsedIcon : IconChartHistogram;
const expandedIcon: RenderIcon = isLast ? TreeConnectorEnd : TreeConnectorBranch;
const activeIconColor =
item.kind === "builtin"
? isCollapsed
? item.activeColor
: undefined
: isCollapsed
? "text-text-bright"
: undefined;
const inactiveIconColor = isCollapsed ? "text-text-dimmed" : "text-tertiary";
return (
<SideMenuItem
name={item.label}
icon={isCollapsed ? collapsedIcon : expandedIcon}
activeIconColor={activeIconColor}
inactiveIconColor={inactiveIconColor}
to={item.path}
isCollapsed={isCollapsed}
disableIconHover
action={
showDragHandle ? (
<div className="sidebar-drag-handle flex h-full w-full cursor-grab items-center justify-center rounded text-text-dimmed opacity-0 group-hover/menuitem:opacity-100 hover:text-text-bright active:cursor-grabbing">
<GripVerticalIcon className="size-3.5" />
</div>
) : undefined
}
/>
);
}