import { XMarkIcon } from "@heroicons/react/20/solid"; import { type LoaderFunctionArgs } from "@remix-run/node"; import { Form } from "@remix-run/react"; import type { TaskTriggerSource } from "@trigger.dev/database"; import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react"; import ReactGridLayout from "react-grid-layout"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { PageBody, PageContainer } from "~/components/layout/AppLayout"; import { LogsTaskFilter } from "~/components/logs/LogsTaskFilter"; import { ModelsFilter, type ModelOption } from "~/components/metrics/ModelsFilter"; import { OperationsFilter } from "~/components/metrics/OperationsFilter"; import { PromptsFilter } from "~/components/metrics/PromptsFilter"; import { ProvidersFilter } from "~/components/metrics/ProvidersFilter"; import { type WidgetData } from "~/components/metrics/QueryWidget"; import { QueuesFilter } from "~/components/metrics/QueuesFilter"; import { ScopeFilter } from "~/components/metrics/ScopeFilter"; import { TitleWidget } from "~/components/metrics/TitleWidget"; import { CreateDashboardPageButton } from "~/components/navigation/DashboardDialogs"; import { Button } from "~/components/primitives/Buttons"; import { NavBar, PageTitle } from "~/components/primitives/PageHeader"; import { TimeFilter } from "~/components/runs/v3/SharedFilters"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { useSearchParams } from "~/hooks/useSearchParam"; import { findProjectBySlug } from "~/models/project.server"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { getTaskIdentifiers } from "~/models/task.server"; import { type BuiltInDashboardFilter, type LayoutItem, type Widget, MetricDashboardPresenter, } from "~/presenters/v3/MetricDashboardPresenter.server"; import { PromptPresenter } from "~/presenters/v3/PromptPresenter.server"; import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server"; import { requireUser } from "~/services/session.server"; import { cn } from "~/utils/cn"; import { EnvironmentParamSchema } from "~/utils/pathBuilder"; import { QueryScopeSchema } from "~/v3/querySchemas"; import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route"; import { MetricWidget } from "../resources.metric"; const ParamSchema = EnvironmentParamSchema.extend({ dashboardKey: z.string(), }); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const user = await requireUser(request); const { projectParam, organizationSlug, envParam, dashboardKey } = ParamSchema.parse(params); const project = await findProjectBySlug(organizationSlug, projectParam, user.id); if (!project) { throw new Response(undefined, { status: 404, statusText: "Project not found", }); } const environment = await findEnvironmentBySlug(project.id, envParam, user.id); if (!environment) { throw new Response(undefined, { status: 404, statusText: "Environment not found", }); } const presenter = new MetricDashboardPresenter(); const [dashboard, possibleTasks] = await Promise.all([ presenter.builtInDashboard({ organizationId: project.organizationId, key: dashboardKey, }), getTaskIdentifiers(environment.id), ]); const filters = dashboard.filters ?? ["tasks", "queues"]; const clickhouse = await clickhouseFactory.getClickhouseForOrganization( project.organizationId, "standard" ); // Load distinct models from ClickHouse if the dashboard has a models filter let possibleModels: { model: string; system: string }[] = []; if (filters.includes("models")) { const queryFn = clickhouse.reader.query({ name: "getDistinctModels", query: `SELECT response_model, any(gen_ai_system) AS gen_ai_system FROM trigger_dev.llm_metrics_v1 WHERE organization_id = {organizationId: String} AND project_id = {projectId: String} AND environment_id = {environmentId: String} AND response_model != '' GROUP BY response_model ORDER BY response_model`, params: z.object({ organizationId: z.string(), projectId: z.string(), environmentId: z.string(), }), schema: z.object({ response_model: z.string(), gen_ai_system: z.string() }), }); const [error, rows] = await queryFn({ organizationId: project.organizationId, projectId: project.id, environmentId: environment.id, }); if (!error) { possibleModels = rows.map((r) => ({ model: r.response_model, system: r.gen_ai_system })); } } const promptPresenter = new PromptPresenter(clickhouse); const [possiblePrompts, possibleOperations, possibleProviders] = await Promise.all([ filters.includes("prompts") ? promptPresenter.getDistinctPromptSlugs(project.organizationId, project.id, environment.id) : ([] as string[]), filters.includes("operations") ? promptPresenter.getDistinctOperations(project.organizationId, project.id, environment.id) : ([] as string[]), filters.includes("providers") ? promptPresenter.getDistinctProviders(project.organizationId, project.id, environment.id) : ([] as string[]), ]); return typedjson({ ...dashboard, filters, possibleTasks, possibleModels, possiblePrompts, possibleOperations, possibleProviders, }); }; export default function Page() { const { key, title, layout: dashboardLayout, defaultPeriod, filters, possibleTasks, possibleModels, possiblePrompts, possibleOperations, possibleProviders, } = useTypedLoaderData(); const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); return (
} />
); } export function MetricDashboard({ layout, widgets, defaultPeriod, editable, filters: filterConfig, possibleTasks, possibleModels, possiblePrompts, possibleOperations, possibleProviders, onLayoutChange, onEditWidget, onRenameWidget, onDeleteWidget, onDuplicateWidget, filterAccessories, }: { /** The layout items (positions/sizes) - fully controlled from parent */ layout: LayoutItem[]; /** The widget configurations keyed by widget ID - fully controlled from parent */ widgets: Record; defaultPeriod: string; editable: boolean; /** Which filters to show. Defaults to ["tasks", "queues"]. */ filters?: BuiltInDashboardFilter[]; /** Possible tasks for filtering */ possibleTasks?: { slug: string; triggerSource: TaskTriggerSource; isInLatestDeployment: boolean; }[]; /** Possible models for filtering */ possibleModels?: ModelOption[]; /** Possible prompt slugs for filtering */ possiblePrompts?: string[]; /** Possible operations for filtering */ possibleOperations?: string[]; /** Possible providers for filtering */ possibleProviders?: string[]; onLayoutChange?: (layout: LayoutItem[]) => void; onEditWidget?: (widgetId: string, widget: WidgetData) => void; onRenameWidget?: (widgetId: string, newTitle: string) => void; onDeleteWidget?: (widgetId: string) => void; onDuplicateWidget?: (widgetId: string, widget: WidgetData) => void; filterAccessories?: ReactNode; }) { const { value, values } = useSearchParams(); const { width, containerRef, mounted } = useContainerWidth(); const [resizingItemId, setResizingItemId] = useState(null); const [isDragging, setIsDragging] = useState(false); const isInteracting = resizingItemId !== null || isDragging; const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); const plan = useCurrentPlan(); const maxPeriodDays = plan?.v3Subscription?.plan?.limits?.queryPeriodDays?.number; const period = value("period"); const from = value("from"); const to = value("to"); const parsedScope = QueryScopeSchema.safeParse(value("scope") ?? "environment"); const scope = parsedScope.success ? parsedScope.data : "environment"; const tasks = values("tasks").filter((v) => v !== ""); const queues = values("queues").filter((v) => v !== ""); const models = values("models").filter((v) => v !== ""); const prompts = values("prompts").filter((v) => v !== ""); const operations = values("operations").filter((v) => v !== ""); const providers = values("providers").filter((v) => v !== ""); const activeFilters = filterConfig ?? ["tasks", "queues"]; const hasAppliedFilters = tasks.length > 0 || queues.length > 0 || models.length > 0 || prompts.length > 0 || operations.length > 0 || providers.length > 0; const handleLayoutChange = useCallback( (newLayout: readonly LayoutItem[]) => { const mutableLayout = [...newLayout]; onLayoutChange?.(mutableLayout); }, [onLayoutChange] ); // Apply constraints for title widgets: fixed height of 2, allow horizontal resize only const constrainedLayout = useMemo( () => layout.map((item) => { const widget = widgets[item.i]; if (widget?.display.type === "title") { return { ...item, h: 2, minH: 2, maxH: 2 }; } return item; }), [layout, widgets] ); return (
{activeFilters.includes("tasks") && ( )} {activeFilters.includes("queues") && } {activeFilters.includes("models") && ( )} {activeFilters.includes("prompts") && ( )} {activeFilters.includes("operations") && ( )} {activeFilters.includes("providers") && ( )} {hasAppliedFilters && (
{filterAccessories &&
{filterAccessories}
}
{mounted && ( setResizingItemId(oldItem?.i ?? null)} onResizeStop={() => setResizingItemId(null)} onDragStart={() => setIsDragging(true)} onDragStop={() => setIsDragging(false)} > {Object.entries(widgets).map(([key, widget]) => (
{widget.display.type === "title" ? ( onRenameWidget(key, newTitle) : undefined } onDelete={onDeleteWidget ? () => onDeleteWidget(key) : undefined} /> ) : ( 0 ? tasks : undefined} queues={queues.length > 0 ? queues : undefined} responseModels={models.length > 0 ? models : undefined} promptSlugs={prompts.length > 0 ? prompts : undefined} operations={operations.length > 0 ? operations : undefined} providers={providers.length > 0 ? providers : undefined} config={widget.display} organizationId={organization.id} projectId={project.id} environmentId={environment.id} refreshIntervalMs={60_000} isResizing={resizingItemId === key} isDraggable={editable} onEdit={ onEditWidget ? (resultData) => onEditWidget(key, { ...widget, resultData }) : undefined } onRename={ onRenameWidget ? (newTitle) => onRenameWidget(key, newTitle) : undefined } onDelete={onDeleteWidget ? () => onDeleteWidget(key) : undefined} onDuplicate={ onDuplicateWidget ? (resultData) => onDuplicateWidget(key, { ...widget, resultData }) : undefined } /> )}
))}
)}
); } function useContainerWidth(initialWidth = 1280) { const containerRef = useRef(null); const [width, setWidth] = useState(initialWidth); const [mounted, setMounted] = useState(false); const measureWidth = useCallback(() => { if (containerRef.current) { setWidth(containerRef.current.offsetWidth); } }, []); useEffect(() => { measureWidth(); setMounted(true); const element = containerRef.current; if (!element) return; const resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { setWidth(entry.contentRect.width); } }); resizeObserver.observe(element); return () => resizeObserver.disconnect(); }, [measureWidth]); return { width, containerRef, mounted }; }