197 lines
5.5 KiB
TypeScript
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
|
|
}
|
|
/>
|
|
);
|
|
}
|