diff --git a/src/frontend/src/pages/Metrics.tsx b/src/frontend/src/pages/Metrics.tsx index 9e01000e..0c2522cd 100644 --- a/src/frontend/src/pages/Metrics.tsx +++ b/src/frontend/src/pages/Metrics.tsx @@ -8,7 +8,7 @@ interface Metric { metric_ID: string; metric_name: string; value_type: string; - source_type?: string; + source_type?: string | null; metric_key?: string | null; option_category?: string | null; rule?: string | null; @@ -118,11 +118,25 @@ const ExpandableText: React.FC<{ emptyText?: string; textStyle?: React.CSSProperties; preserveWhitespace?: boolean; -}> = ({ text, lines = 2, emptyText = "—", textStyle, preserveWhitespace = false }) => { + description?: string; + onToggle?: (isOpen: boolean) => void; +}> = ({ + text, + lines = 2, + emptyText = "—", + textStyle, + preserveWhitespace = false, + description, + onToggle, +}) => { const [open, setOpen] = useState(false); const [truncated, setTruncated] = useState(false); - const textRef = useRef(null); - const wrapRef = useRef(null); + + const wrapRef = React.useRef(null); + const textRef = React.useRef(null); + + const clampStyle = + lines === 4 ? clamp4Style : lines === 3 ? clamp3Style : clamp2Style; useLayoutEffect(() => { const el = textRef.current; @@ -138,35 +152,45 @@ const ExpandableText: React.FC<{ check(); window.addEventListener("resize", check); return () => window.removeEventListener("resize", check); - }, [text, lines]); + }, [text, lines, description]); useEffect(() => { if (!open) return; const onDocClick = (e: MouseEvent) => { if (!wrapRef.current) return; - if (!wrapRef.current.contains(e.target as Node)) setOpen(false); + if (!wrapRef.current.contains(e.target as Node)) { + setOpen(false); + onToggle?.(false); + } }; const onEsc = (e: KeyboardEvent) => { - if (e.key === "Escape") setOpen(false); + if (e.key === "Escape") { + setOpen(false); + onToggle?.(false); + } }; document.addEventListener("mousedown", onDocClick); document.addEventListener("keydown", onEsc); - return () => { document.removeEventListener("mousedown", onDocClick); document.removeEventListener("keydown", onEsc); }; - }, [open]); + }, [open, onToggle]); + + const showMoreButton = truncated || !!description; - if (!text) { + if (!text && !description) { return
{emptyText}
; } - const clampStyle = - lines === 4 ? clamp4Style : lines === 3 ? clamp3Style : clamp2Style; + const handleToggle = () => { + const newState = !open; + setOpen(newState); + onToggle?.(newState); + }; return (
- {text} + {text || emptyText}
- {truncated && ( + {showMoreButton && ( + {description ? ( + <> +
+ Value: +
+
{text || emptyText}
+
+ Description: +
+
{description}
+ + ) : ( + <> +
{text}
+ + + )} )} @@ -240,10 +278,10 @@ const MetricsPage: React.FC = () => { const navigate = useNavigate(); const [metrics, setMetrics] = useState([]); - const [rulesData, setRulesData] = useState(null); const [categories, setCategories] = useState([]); const [autoMetricOptions, setAutoMetricOptions] = useState({}); + const [expandedRowId, setExpandedRowId] = useState(null); const [modalMode, setModalMode] = useState(null); const isModalOpen = modalMode !== null; @@ -416,7 +454,6 @@ const MetricsPage: React.FC = () => { if (!(err instanceof Error) || !err.message) return fallback; const msg = err.message; - const apiPrefix = "API Error"; const apiIndex = msg.indexOf(": "); const raw = msg.startsWith(apiPrefix) && apiIndex !== -1 ? msg.slice(apiIndex + 2) : msg; @@ -426,30 +463,30 @@ const MetricsPage: React.FC = () => { if (typeof parsed === "string") return parsed; - if (parsed.metric_name) { - const metricNameError = Array.isArray(parsed.metric_name) - ? parsed.metric_name[0] - : parsed.metric_name; + if ((parsed as any).metric_name) { + const metricNameError = Array.isArray((parsed as any).metric_name) + ? (parsed as any).metric_name[0] + : (parsed as any).metric_name; if (String(metricNameError).toLowerCase().includes("already exists")) { return "A metric with this name already exists. Please choose a different name."; } return `Metric name: ${metricNameError}`; } - if (parsed.metric_key) { - const metricKeyError = Array.isArray(parsed.metric_key) - ? parsed.metric_key[0] - : parsed.metric_key; + if ((parsed as any).metric_key) { + const metricKeyError = Array.isArray((parsed as any).metric_key) + ? (parsed as any).metric_key[0] + : (parsed as any).metric_key; return `System metric: ${metricKeyError}`; } - if (parsed.non_field_errors) { - return Array.isArray(parsed.non_field_errors) - ? parsed.non_field_errors[0] - : parsed.non_field_errors; + if ((parsed as any).non_field_errors) { + return Array.isArray((parsed as any).non_field_errors) + ? (parsed as any).non_field_errors[0] + : (parsed as any).non_field_errors; } - const firstValue = Object.values(parsed)[0]; + const firstValue = Object.values(parsed as Record)[0]; if (Array.isArray(firstValue) && firstValue.length > 0) return String(firstValue[0]); if (typeof firstValue === "string") return firstValue; @@ -459,6 +496,16 @@ const MetricsPage: React.FC = () => { } }; + const modalType = modalMode === "create" ? newType : editType; + const modalAvailableCats = getAvailableCategoriesForType(modalType); + const modalOptionCategory = modalMode === "create" ? selectedOptionCategory : editOptionCategory; + const modalTemplate = modalMode === "create" ? selectedTemplate : editTemplate; + + const modalPreview = + modalOptionCategory && modalTemplate + ? modalAvailableCats?.[modalOptionCategory]?.templates?.[modalTemplate] ?? null + : null; + const addMetric = async (): Promise => { if (!newName.trim()) { setFormError("Metric name is required."); @@ -642,25 +689,15 @@ const MetricsPage: React.FC = () => { if (!rulesData) return "—"; if (m.value_type === "bool") { const obj = rulesData?.bool?.[m.option_category || "yes_no"]?.templates?.[m.rule || "standard"]; - return obj ? JSON.stringify(obj) : "—"; + return obj ? JSON.stringify(obj, null, 2) : "—"; } if (m.value_type === "range") { const obj = rulesData?.range?.[m.option_category || "file_ranges"]?.templates?.[m.rule || "standard"]; - return obj ? JSON.stringify(obj) : "—"; + return obj ? JSON.stringify(obj, null, 2) : "—"; } return "—"; }; - const modalType = modalMode === "create" ? newType : editType; - const modalAvailableCats = getAvailableCategoriesForType(modalType); - const modalOptionCategory = modalMode === "create" ? selectedOptionCategory : editOptionCategory; - const modalTemplate = modalMode === "create" ? selectedTemplate : editTemplate; - - const modalPreview = - modalOptionCategory && modalTemplate - ? modalAvailableCats?.[modalOptionCategory]?.templates?.[modalTemplate] ?? null - : null; - useEffect(() => { if (!isModalOpen) return; const onKey = (e: KeyboardEvent) => { @@ -714,7 +751,7 @@ const MetricsPage: React.FC = () => {
{ >
+