import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter, } from "@/src/components/ui/card"; import { api } from "@/src/utils/api"; import { metricAggregations, type QueryType, mapLegacyUiTableFilterToView, } from "@/src/features/query"; import React, { useState, useMemo, useEffect } from "react"; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from "@/src/components/ui/select"; import { WidgetPropertySelectItem } from "@/src/features/widgets/components/WidgetPropertySelectItem"; import { Label } from "@/src/components/ui/label"; import { viewDeclarations } from "@/src/features/query/dataModel"; import { type z } from "zod/v4"; import { views } from "@/src/features/query/types"; import { Input } from "@/src/components/ui/input"; import startCase from "lodash/startCase"; import { DatePickerWithRange } from "@/src/components/date-picker"; import { InlineFilterBuilder } from "@/src/features/filters/components/filter-builder"; import { useDashboardDateRange } from "@/src/hooks/useDashboardDateRange"; import { toAbsoluteTimeRange, type DashboardDateRangeOptions, } from "@/src/utils/date-range-utils"; import { type ColumnDefinition } from "@langfuse/shared"; import { Chart } from "@/src/features/widgets/chart-library/Chart"; import { type DataPoint } from "@/src/features/widgets/chart-library/chart-props"; import { Button } from "@/src/components/ui/button"; import { type DashboardWidgetChartType } from "@langfuse/shared/src/db"; import { showErrorToast } from "@/src/features/notifications/showErrorToast"; import { type FilterState } from "@langfuse/shared"; import { isTimeSeriesChart } from "@/src/features/widgets/chart-library/utils"; import { BarChart, PieChart, LineChart, BarChartHorizontal, Hash, BarChart3, Table, Plus, X, } from "lucide-react"; import { buildWidgetName, buildWidgetDescription, formatMetricName, } from "@/src/features/widgets/utils"; import { MAX_PIVOT_TABLE_DIMENSIONS, MAX_PIVOT_TABLE_METRICS, } from "@/src/features/widgets/utils/pivot-table-utils"; type ChartType = { group: "time-series" | "total-value"; name: string; value: DashboardWidgetChartType; icon: React.ElementType; supportsBreakdown: boolean; }; import { type WidgetChartConfig } from "@/src/features/widgets/utils"; type ChartConfig = WidgetChartConfig; const chartTypes: ChartType[] = [ { group: "total-value", name: "Big Number", value: "NUMBER", icon: Hash, supportsBreakdown: false, }, { group: "time-series", name: "Line Chart", value: "LINE_TIME_SERIES", icon: LineChart, supportsBreakdown: true, }, { group: "time-series", name: "Vertical Bar Chart", value: "BAR_TIME_SERIES", icon: BarChart, supportsBreakdown: true, }, { group: "total-value", name: "Horizontal Bar Chart", value: "HORIZONTAL_BAR", icon: BarChartHorizontal, supportsBreakdown: true, }, { group: "total-value", name: "Vertical Bar Chart", value: "VERTICAL_BAR", icon: BarChart, supportsBreakdown: true, }, { group: "total-value", name: "Histogram", value: "HISTOGRAM", icon: BarChart3, supportsBreakdown: false, }, { group: "total-value", name: "Pie Chart", value: "PIE", icon: PieChart, supportsBreakdown: true, }, { group: "total-value", name: "Pivot Table", value: "PIVOT_TABLE", icon: Table, supportsBreakdown: true, }, ]; /** * Interface for representing a selected metric combination * Combines measure and aggregation into a single selectable entity */ interface SelectedMetric { /** Unique identifier for this metric combination */ id: string; /** The measure field name (e.g., "count", "latency") */ measure: string; /** The aggregation method (e.g., "sum", "avg", "count") */ aggregation: z.infer; /** Display label for the metric */ label: string; } export function WidgetForm({ initialValues, projectId, onSave, widgetId, }: { initialValues: { name: string; description: string; view: z.infer; measure: string; aggregation: z.infer; dimension: string; filters?: FilterState; chartType: DashboardWidgetChartType; chartConfig?: ChartConfig; // Support for complete widget data (editing mode) metrics?: { measure: string; agg: string }[]; dimensions?: { field: string }[]; }; projectId: string; onSave: (widgetData: { name: string; description: string; view: string; dimensions: { field: string }[]; metrics: { measure: string; agg: string }[]; filters: any[]; chartType: DashboardWidgetChartType; chartConfig: ChartConfig; }) => void; widgetId?: string; }) { // State for form fields const [widgetName, setWidgetName] = useState(initialValues.name); const [widgetDescription, setWidgetDescription] = useState( initialValues.description, ); // Determine if this is an existing widget (editing mode) const isExistingWidget = Boolean(widgetId); // Disables further auto-updates once the user edits name or description const [autoLocked, setAutoLocked] = useState(isExistingWidget); const [selectedView, setSelectedView] = useState>( initialValues.view, ); // For regular charts: single metric selection const [selectedMeasure, setSelectedMeasure] = useState( initialValues.measure, ); const [selectedAggregation, setSelectedAggregation] = useState< z.infer >(initialValues.aggregation); // For pivot tables: multiple metrics selection const [selectedMetrics, setSelectedMetrics] = useState( initialValues.chartType === "PIVOT_TABLE" && initialValues.metrics?.length ? // Initialize from complete metrics data (editing mode) initialValues.metrics.map((metric) => ({ id: `${metric.agg}_${metric.measure}`, measure: metric.measure, aggregation: metric.agg as z.infer, label: `${startCase(metric.agg)} ${startCase(metric.measure)}`, })) : // Default to single metric (new widget) [ { id: `${initialValues.aggregation}_${initialValues.measure}`, measure: initialValues.measure, aggregation: initialValues.aggregation, label: `${startCase(initialValues.aggregation)} ${startCase(initialValues.measure)}`, }, ], ); const [selectedDimension, setSelectedDimension] = useState( initialValues.dimension, ); // Pivot table dimensions state (for PIVOT_TABLE chart type) const [pivotDimensions, setPivotDimensions] = useState( initialValues.chartType === "PIVOT_TABLE" && initialValues.dimensions?.length ? // Initialize from complete dimensions data (editing mode) initialValues.dimensions.map((dim) => dim.field) : // Default to empty array (new widget) [], ); const [selectedChartType, setSelectedChartType] = useState( initialValues.chartType, ); const [rowLimit, setRowLimit] = useState( initialValues.chartConfig?.row_limit ?? 100, ); const [histogramBins, setHistogramBins] = useState( initialValues.chartConfig?.bins ?? 10, ); // Default sort configuration for pivot tables const [defaultSortColumn, setDefaultSortColumn] = useState( initialValues.chartConfig?.defaultSort?.column ?? "none", ); const [defaultSortOrder, setDefaultSortOrder] = useState<"ASC" | "DESC">( initialValues.chartConfig?.defaultSort?.order ?? "DESC", ); // Filter state const { timeRange, setTimeRange } = useDashboardDateRange({ defaultRelativeAggregation: "last7Days", }); // Convert timeRange to absolute date range for compatibility const dateRange = useMemo(() => { return toAbsoluteTimeRange(timeRange) ?? undefined; }, [timeRange]); // Convert timeRange to legacy format for DatePickerWithRange compatibility const selectedOption = useMemo(() => { if ("range" in timeRange) { return timeRange.range; } return "custom" as const; }, [timeRange]); const setDateRangeAndOption = ( option: DashboardDateRangeOptions, range?: { from: Date; to: Date }, ) => { if (option === "custom") { if (range) { setTimeRange({ from: range.from, to: range.to, }); } } else { setTimeRange({ range: option }); } }; const [userFilterState, setUserFilterState] = useState( initialValues.filters?.map((filter) => { if (filter.column === "name") { // We need to map the generic `name` property to the correct column name for the selected view return { ...filter, column: initialValues.view === "traces" ? "traceName" : initialValues.view === "observations" ? "observationName" : "scoreName", }; } return filter; }) ?? [], ); // Static sort state for pivot table preview (non-interactive) const previewSortState = useMemo( () => selectedChartType === "PIVOT_TABLE" && defaultSortColumn && defaultSortColumn !== "none" ? { column: defaultSortColumn, order: defaultSortOrder } : null, [selectedChartType, defaultSortColumn, defaultSortOrder], ); // Helper function to update pivot table dimensions const updatePivotDimension = (index: number, value: string) => { const newDimensions = [...pivotDimensions]; if (value && value !== "none") { // Set the dimension at the specified index newDimensions[index] = value; } else { // Clear this dimension and all subsequent ones newDimensions.splice(index); } setPivotDimensions(newDimensions); }; // Helper function for updating pivot table metrics const updatePivotMetric = ( index: number, measure: string, aggregation?: z.infer, ) => { const newMetrics = [...selectedMetrics]; if (measure && measure !== "none") { let finalAggregation: z.infer; if (measure === "count") { finalAggregation = "count"; } else { // Get available aggregations for this measure at this index const availableAggregations = getAvailableAggregations(index, measure); if (aggregation && availableAggregations.includes(aggregation)) { // Use provided aggregation if it's available finalAggregation = aggregation as z.infer; } else { // Use the first available aggregation as default finalAggregation = availableAggregations.length > 0 ? availableAggregations[0] : ("sum" as z.infer); } } const newMetric: SelectedMetric = { id: `${finalAggregation}_${measure}`, measure: measure, aggregation: finalAggregation as z.infer, label: `${startCase(finalAggregation)} ${startCase(measure)}`, }; // Set the metric at the specified index newMetrics[index] = newMetric; } else { // Clear this metric and all subsequent ones newMetrics.splice(index); } setSelectedMetrics(newMetrics); }; // Add a new empty metric slot const addNewMetricSlot = () => { if (selectedMetrics.length < MAX_PIVOT_TABLE_METRICS) { const newMetrics = [...selectedMetrics]; newMetrics.push({ id: `temp_${selectedMetrics.length}`, measure: "", aggregation: "sum" as z.infer, label: "", }); setSelectedMetrics(newMetrics); } }; // Remove a metric slot and roll up subsequent ones const removeMetricSlot = (index: number) => { if (index > 0) { // Can't remove the first metric (it's required) const newMetrics = [...selectedMetrics]; newMetrics.splice(index, 1); // Remove only the metric at this index setSelectedMetrics(newMetrics); } }; const traceFilterOptions = api.traces.filterOptions.useQuery( { projectId, }, { trpc: { context: { skipBatch: true, }, }, refetchOnMount: false, refetchOnWindowFocus: false, refetchOnReconnect: false, staleTime: Infinity, }, ); const generationsFilterOptions = api.generations.filterOptions.useQuery( { projectId, }, { trpc: { context: { skipBatch: true, }, }, refetchOnMount: false, refetchOnWindowFocus: false, refetchOnReconnect: false, staleTime: Infinity, }, ); const environmentFilterOptions = api.projects.environmentFilterOptions.useQuery( { projectId, fromTimestamp: dateRange?.from, }, { trpc: { context: { skipBatch: true, }, }, refetchOnMount: false, refetchOnWindowFocus: false, refetchOnReconnect: false, staleTime: Infinity, }, ); const environmentOptions = environmentFilterOptions.data?.map((value) => ({ value: value.environment, })) || []; const nameOptions = traceFilterOptions.data?.name || []; const tagsOptions = traceFilterOptions.data?.tags || []; const modelOptions = generationsFilterOptions.data?.model || []; const toolNamesOptions = generationsFilterOptions.data?.toolNames || []; // Filter columns for PopoverFilterBuilder const filterColumns: ColumnDefinition[] = [ { name: "Environment", id: "environment", type: "stringOptions", options: environmentOptions, internal: "internalValue", }, { name: "Trace Name", id: "traceName", type: "stringOptions", options: nameOptions, internal: "internalValue", }, { name: "Observation Name", id: "observationName", type: "string", internal: "internalValue", }, { name: "Score Name", id: "scoreName", type: "string", internal: "internalValue", }, { name: "Tags", id: "tags", type: "arrayOptions", options: tagsOptions, internal: "internalValue", }, { name: "Tool Names", id: "toolNames", type: "arrayOptions", options: toolNamesOptions, internal: "internalValue", }, { name: "User", id: "user", type: "string", internal: "internalValue", }, { name: "Session", id: "session", type: "string", internal: "internalValue", }, { name: "Metadata", id: "metadata", type: "stringObject", internal: "internalValue", }, { name: "Release", id: "release", type: "string", internal: "internalValue", }, { name: "Version", id: "version", type: "string", internal: "internalValue", }, ]; if (selectedView === "observations") { filterColumns.push({ name: "Model", id: "providedModelName", type: "stringOptions", options: modelOptions, internal: "internalValue", }); } if (selectedView === "scores-categorical") { filterColumns.push({ name: "Score String Value", id: "stringValue", type: "string", internal: "internalValue", }); } if (selectedView === "scores-numeric") { filterColumns.push({ name: "Score Value", id: "value", type: "number", internal: "internalValue", }); } // When chart type does not support breakdown, wipe the breakdown dimension useEffect(() => { if ( chartTypes.find((c) => c.value === selectedChartType) ?.supportsBreakdown === false && selectedDimension !== "none" ) { setSelectedDimension("none"); } }, [selectedChartType, selectedDimension]); // Reset pivot dimensions when switching away from PIVOT_TABLE useEffect(() => { if (selectedChartType !== "PIVOT_TABLE" && pivotDimensions.length > 0) { setPivotDimensions([]); } }, [selectedChartType, pivotDimensions.length]); // Reset multiple metrics when switching away from PIVOT_TABLE useEffect(() => { if (selectedChartType !== "PIVOT_TABLE" && selectedMetrics.length > 1) { // Keep only the first metric for non-pivot charts setSelectedMetrics(selectedMetrics.slice(0, 1)); } }, [selectedChartType, selectedMetrics]); // When chart type does not support breakdown, wipe the breakdown dimension useEffect(() => { if ( chartTypes.find((c) => c.value === selectedChartType) ?.supportsBreakdown === false && selectedDimension !== "none" ) { setSelectedDimension("none"); } }, [selectedChartType, selectedDimension]); // Set aggregation based on chart type and metric, with histogram chart type taking priority useEffect(() => { // Histogram chart type always takes priority if ( selectedChartType === "HISTOGRAM" && selectedAggregation !== "histogram" ) { setSelectedAggregation("histogram"); } // If switching away from histogram chart type and aggregation is still histogram, reset to appropriate default else if ( selectedChartType !== "HISTOGRAM" && selectedAggregation === "histogram" ) { if (selectedMeasure === "count") { setSelectedAggregation("count"); } else { setSelectedAggregation("sum"); // Default aggregation for non-count metrics } } // Only set to "count" for count metric if not using histogram chart type else if ( selectedMeasure === "count" && selectedChartType !== "HISTOGRAM" && selectedAggregation !== "count" ) { setSelectedAggregation("count"); } }, [selectedMeasure, selectedAggregation, selectedChartType]); // Set aggregation based on chart type and metric, with histogram chart type taking priority useEffect(() => { // Histogram chart type always takes priority if ( selectedChartType === "HISTOGRAM" && selectedAggregation !== "histogram" ) { setSelectedAggregation("histogram"); } // If switching away from histogram chart type and aggregation is still histogram, reset to appropriate default else if ( selectedChartType !== "HISTOGRAM" && selectedAggregation === "histogram" ) { if (selectedMeasure === "count") { setSelectedAggregation("count"); } else { setSelectedAggregation("sum"); // Default aggregation for non-count metrics } } // Only set to "count" for count metric if not using histogram chart type else if ( selectedMeasure === "count" && selectedChartType !== "HISTOGRAM" && selectedAggregation !== "count" ) { setSelectedAggregation("count"); } }, [selectedMeasure, selectedAggregation, selectedChartType]); // Get available metrics for the selected view const availableMetrics = useMemo(() => { const viewDeclaration = viewDeclarations.v1[selectedView]; // For pivot tables, only show measures that still have available aggregations if (selectedChartType === "PIVOT_TABLE") { return Object.entries(viewDeclaration.measures) .filter(([measureKey]) => { // For count, there's only one aggregation option if (measureKey === "count") { return !selectedMetrics.some((m) => m.measure === "count"); } // For other measures, check if there are any aggregations left const selectedAggregationsForMeasure = selectedMetrics .filter((m) => m.measure === measureKey) .map((m) => m.aggregation); const availableAggregationsForMeasure = metricAggregations.options.filter( (agg) => agg !== "histogram" && !selectedAggregationsForMeasure.includes(agg), ); return availableAggregationsForMeasure.length > 0; }) .map(([key]) => ({ value: key, label: startCase(key), })) .sort((a, b) => a.label.localeCompare(b.label, "en", { sensitivity: "base" }), ); } // For regular charts, show all metrics return Object.entries(viewDeclaration.measures) .map(([key]) => ({ value: key, label: startCase(key), })) .sort((a, b) => a.label.localeCompare(b.label, "en", { sensitivity: "base" }), ); }, [selectedView, selectedChartType, selectedMetrics]); // Get available aggregations for a specific metric index in pivot tables const getAvailableAggregations = ( metricIndex: number, measureKey: string, ): z.infer[] => { if (selectedChartType === "PIVOT_TABLE" && measureKey) { return metricAggregations.options.filter( (agg) => !selectedMetrics.some( (m, idx) => idx !== metricIndex && m.measure === measureKey && m.aggregation === agg, ), ) as z.infer[]; } return metricAggregations.options as z.infer[]; }; // Get available metrics for a specific metric index in pivot tables const getAvailableMetrics = (metricIndex: number) => { if (selectedChartType === "PIVOT_TABLE") { const viewDeclaration = viewDeclarations.v1[selectedView]; return Object.entries(viewDeclaration.measures) .filter(([measureKey]) => { // For count, there's only one aggregation option if (measureKey === "count") { return !selectedMetrics.some( (m, idx) => idx !== metricIndex && m.measure === "count", ); } // For other measures, check if there are any aggregations left const selectedAggregationsForMeasure = selectedMetrics .filter((m, idx) => idx !== metricIndex && m.measure === measureKey) .map((m) => m.aggregation); const availableAggregationsForMeasure = metricAggregations.options.filter( (agg) => agg !== "histogram" && !selectedAggregationsForMeasure.includes(agg), ); return availableAggregationsForMeasure.length > 0; }) .map(([key]) => ({ value: key, label: startCase(key), })) .sort((a, b) => a.label.localeCompare(b.label, "en", { sensitivity: "base" }), ); } return availableMetrics; }; // Get available dimensions for the selected view const availableDimensions = useMemo(() => { const viewDeclaration = viewDeclarations.v1[selectedView]; return Object.entries(viewDeclaration.dimensions) .map(([key]) => ({ value: key, label: startCase(key), })) .sort((a, b) => a.label.localeCompare(b.label, "en", { sensitivity: "base" }), ); }, [selectedView]); // Create a dynamic query based on the selected view const query = useMemo(() => { // Calculate fromTimestamp and toTimestamp from dateRange const fromTimestamp = dateRange ? dateRange.from : new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000); // Default to last 7 days const toTimestamp = dateRange ? dateRange.to : new Date(); // Determine dimensions based on chart type const queryDimensions = selectedChartType === "PIVOT_TABLE" ? pivotDimensions.map((field) => ({ field })) : selectedDimension !== "none" ? [{ field: selectedDimension }] : []; // Determine metrics based on chart type const queryMetrics = selectedChartType === "PIVOT_TABLE" ? selectedMetrics .filter((metric) => metric.measure && metric.measure !== "") .map((metric) => ({ measure: metric.measure, aggregation: metric.aggregation, })) : [ { measure: selectedMeasure, aggregation: selectedAggregation, }, ]; return { view: selectedView, dimensions: queryDimensions, metrics: queryMetrics, filters: [...mapLegacyUiTableFilterToView(selectedView, userFilterState)], timeDimension: isTimeSeriesChart( selectedChartType as DashboardWidgetChartType, ) ? { granularity: "auto" } : null, fromTimestamp: fromTimestamp.toISOString(), toTimestamp: toTimestamp.toISOString(), orderBy: selectedChartType === "PIVOT_TABLE" && previewSortState ? [ { field: previewSortState.column, direction: previewSortState.order.toLowerCase() as | "asc" | "desc", }, ] : null, chartConfig: selectedChartType === "HISTOGRAM" ? { type: selectedChartType, bins: histogramBins } : selectedChartType === "PIVOT_TABLE" ? { type: selectedChartType, dimensions: pivotDimensions, row_limit: rowLimit, defaultSort: defaultSortColumn && defaultSortColumn !== "none" ? { column: defaultSortColumn, order: defaultSortOrder, } : undefined, } : { type: selectedChartType }, }; }, [ selectedView, selectedDimension, selectedAggregation, selectedMeasure, selectedMetrics, userFilterState, dateRange, selectedChartType, histogramBins, pivotDimensions, rowLimit, defaultSortColumn, defaultSortOrder, previewSortState, ]); const queryResult = api.dashboard.executeQuery.useQuery( { projectId, query, }, { trpc: { context: { skipBatch: true, }, }, }, ); // Transform the query results to a consistent format for charts const transformedData: DataPoint[] = useMemo( () => queryResult.data?.map((item: any) => { if (selectedChartType === "PIVOT_TABLE") { // For pivot tables, preserve all raw data fields // The PivotTable component will extract the appropriate metric fields return { dimension: pivotDimensions.length > 0 ? pivotDimensions[0] : "dimension", // Fallback for compatibility metric: 0, // Placeholder - not used for pivot tables time_dimension: item["time_dimension"], // Include all original query fields for pivot table processing ...item, }; } else { // Regular chart processing const metricField = `${selectedAggregation}_${selectedMeasure}`; const metric = item[metricField]; const dimensionField = selectedDimension; return { dimension: item[dimensionField] !== undefined && dimensionField !== "none" ? (() => { const val = item[dimensionField]; if (typeof val === "string") return val; if (val === null || val === undefined || val === "") return "n/a"; if (Array.isArray(val)) return val.join(", "); return String(val); })() : formatMetricName(metricField), metric: Array.isArray(metric) ? metric : Number(metric || 0), time_dimension: item["time_dimension"], }; } }) ?? [], [ queryResult.data, selectedAggregation, selectedDimension, selectedMeasure, selectedChartType, pivotDimensions, ], ); const handleSaveWidget = () => { if (!widgetName.trim()) { showErrorToast("Error", "Widget name is required"); return; } // Validate pivot table requirements const validMetrics = selectedMetrics.filter( (m) => m.measure && m.measure !== "", ); if (selectedChartType === "PIVOT_TABLE" && validMetrics.length === 0) { showErrorToast( "Error", "At least one metric is required for pivot tables", ); return; } onSave({ name: widgetName, description: widgetDescription, view: selectedView, dimensions: selectedChartType === "PIVOT_TABLE" ? pivotDimensions.map((field) => ({ field })) : selectedDimension !== "none" ? [{ field: selectedDimension }] : [], metrics: selectedChartType === "PIVOT_TABLE" ? validMetrics.map((metric) => ({ measure: metric.measure, agg: metric.aggregation, })) : [ { measure: selectedMeasure, agg: selectedAggregation, }, ], filters: mapLegacyUiTableFilterToView(selectedView, userFilterState), chartType: selectedChartType as DashboardWidgetChartType, chartConfig: isTimeSeriesChart( selectedChartType as DashboardWidgetChartType, ) ? { type: selectedChartType as DashboardWidgetChartType } : selectedChartType === "HISTOGRAM" ? { type: selectedChartType as DashboardWidgetChartType, bins: histogramBins, } : selectedChartType === "PIVOT_TABLE" ? { type: selectedChartType as DashboardWidgetChartType, row_limit: rowLimit, defaultSort: defaultSortColumn && defaultSortColumn !== "none" ? { column: defaultSortColumn, order: defaultSortOrder, } : undefined, } : { type: selectedChartType as DashboardWidgetChartType, row_limit: rowLimit, }, }); }; // Update widget name when selection changes, unless locked useEffect(() => { if (autoLocked) return; // For pivot tables, combine all dimensions, otherwise use regular dimension const dimensionForNaming = selectedChartType === "PIVOT_TABLE" && pivotDimensions.length > 0 ? pivotDimensions.map(startCase).join(" and ") : selectedDimension; // For pivot tables, extract actual metric names for the new formatting const isPivotTable = selectedChartType === "PIVOT_TABLE"; const validMetricsForNaming = selectedMetrics.filter( (m) => m.measure && m.measure !== "", ); const metricNames = isPivotTable && validMetricsForNaming.length > 0 ? validMetricsForNaming.map((m) => m.id) // Use the ID which is "${aggregation}_${measure}" : undefined; const suggested = buildWidgetName({ aggregation: isPivotTable ? "count" : selectedAggregation, measure: isPivotTable ? "count" : selectedMeasure, dimension: dimensionForNaming, view: selectedView, metrics: metricNames, isMultiMetric: isPivotTable && validMetricsForNaming.length > 0, }); setWidgetName(suggested); }, [ autoLocked, selectedAggregation, selectedMeasure, selectedMetrics, selectedDimension, selectedView, selectedChartType, pivotDimensions, ]); // Update widget description when selection or filters change, unless locked useEffect(() => { if (autoLocked) return; // For pivot tables, combine all dimensions, otherwise use regular dimension const dimensionForDescription = selectedChartType === "PIVOT_TABLE" && pivotDimensions.length > 0 ? pivotDimensions.map(startCase).join(" and ") : selectedDimension; // For pivot tables, extract actual metric names for the new formatting const isPivotTable = selectedChartType === "PIVOT_TABLE"; const validMetricsForDescription = selectedMetrics.filter( (m) => m.measure && m.measure !== "", ); const metricNames = isPivotTable && validMetricsForDescription.length > 0 ? validMetricsForDescription.map((m) => m.id) // Use the ID which is "${aggregation}_${measure}" : undefined; const suggested = buildWidgetDescription({ aggregation: isPivotTable ? "count" : selectedAggregation, measure: isPivotTable ? "count" : selectedMeasure, dimension: dimensionForDescription, view: selectedView, filters: userFilterState, metrics: metricNames, isMultiMetric: isPivotTable && validMetricsForDescription.length > 0, }); setWidgetDescription(suggested); }, [ autoLocked, selectedAggregation, selectedMeasure, selectedMetrics, selectedDimension, selectedView, userFilterState, selectedChartType, pivotDimensions, ]); return (
{/* Left column - Form */}
Widget Configuration Configure your widget by selecting data and visualization options {/* Data Selection Section */}

Data Selection

{/* View Selection */}
{/* Metrics Selection */}
{/* For pivot tables: multiple metrics selection */} {selectedChartType === "PIVOT_TABLE" ? (
{/* Metric selection dropdowns */} {Array.from( { length: Math.max(1, selectedMetrics.length) }, (_, index) => { const isEnabled = index === 0 || (selectedMetrics[index - 1] && selectedMetrics[index - 1].measure); const currentMetric = selectedMetrics[index]; const currentMeasure = currentMetric?.measure || ""; const currentAggregation = currentMetric?.aggregation || "sum"; const metricsForIndex = getAvailableMetrics(index); const aggregationsForIndex = getAvailableAggregations( index, currentMeasure, ); const canEdit = metricsForIndex.length > 0; return (
{index > 0 && ( )}
{currentMeasure && currentMeasure !== "count" && (
)}
); }, )} {/* Add new metric button */} {selectedMetrics.length < MAX_PIVOT_TABLE_METRICS && getAvailableMetrics(selectedMetrics.length).length > 0 && ( )}
) : ( /* For regular charts: single metric selection */
{selectedMeasure !== "count" && (
{selectedChartType === "HISTOGRAM" && (

Aggregation is automatically set to "histogram" for histogram charts

)}
)}
)}
{/* Filters Section */}
{/* Dimension Selection - Regular charts (Breakdown) */} {chartTypes.find((c) => c.value === selectedChartType) ?.supportsBreakdown && selectedChartType !== "PIVOT_TABLE" && (
)} {/* Pivot Table Dimension Selection */} {selectedChartType === "PIVOT_TABLE" && (

Row Dimensions

Configure up to {MAX_PIVOT_TABLE_DIMENSIONS} dimensions for pivot table rows. Each dimension creates groupings with subtotals.

{Array.from( { length: MAX_PIVOT_TABLE_DIMENSIONS }, (_, index) => { const isEnabled = index === 0 || pivotDimensions[index - 1]; // Enable if first or previous is selected const selectedDimensions = pivotDimensions.slice( 0, index, ); // Exclude current and later dimensions const currentValue = pivotDimensions[index] || ""; return (
); }, )}
)} {/* Pivot Table Default Sort Configuration */} {selectedChartType === "PIVOT_TABLE" && (

Default Sort Configuration

Configure the default sort order for the pivot table. This will be applied when the widget is first loaded.

)}
{/* Visualization Section */}

Visualization

{/* Widget Name */}
{ if (!autoLocked) setAutoLocked(true); setWidgetName(e.target.value); }} placeholder="Enter widget name" />
{/* Widget Description */}
{ if (!autoLocked) setAutoLocked(true); setWidgetDescription(e.target.value); }} placeholder="Enter widget description" />
{/* Chart Type Selection */}
{ if (option === "custom") { setDateRangeAndOption("custom", range); } else { setDateRangeAndOption(option, range); } }} selectedOption={ (selectedOption ?? "custom") as DashboardDateRangeOptions } className="w-full" />
{/* Histogram Bins Selection - Only shown for HISTOGRAM chart type */} {selectedChartType === "HISTOGRAM" && (
{ const value = parseInt(e.target.value); if (!isNaN(value) && value >= 1 && value <= 100) { setHistogramBins(value); } }} placeholder="Enter number of bins (1-100)" />
)} {/* Row Limit Selection - Only shown for non-time series charts that support breakdown */} {chartTypes.find((c) => c.value === selectedChartType) ?.supportsBreakdown && !isTimeSeriesChart( selectedChartType as DashboardWidgetChartType, ) && (
{ const value = parseInt(e.target.value); if (!isNaN(value) && value >= 0 && value <= 1000) { setRowLimit(value); } }} placeholder="Enter breakdown row limit (0-1000)" />
)}
{/* Right column - Chart */}
{widgetName} {widgetDescription} {queryResult.data ? ( metric.id), // Pass metric field names defaultSort: defaultSortColumn && defaultSortColumn !== "none" ? { column: defaultSortColumn, order: defaultSortOrder, } : undefined, } : selectedChartType === "HISTOGRAM" ? { type: selectedChartType as DashboardWidgetChartType, bins: histogramBins, } : { type: selectedChartType as DashboardWidgetChartType, row_limit: rowLimit, } } sortState={ selectedChartType === "PIVOT_TABLE" ? previewSortState : undefined } onSortChange={undefined} /> ) : (

Waiting for Input / Loading...

)}
); }