diff --git a/hyperglass/plugins/_builtin/mikrotik_garbage_output.py b/hyperglass/plugins/_builtin/mikrotik_garbage_output.py index c221aa4..8b48fb0 100644 --- a/hyperglass/plugins/_builtin/mikrotik_garbage_output.py +++ b/hyperglass/plugins/_builtin/mikrotik_garbage_output.py @@ -31,77 +31,81 @@ class MikrotikGarbageOutput(OutputPlugin): """Clean MikroTik traceroute output specifically.""" if not raw_output or not raw_output.strip(): return "" - + lines = raw_output.splitlines() cleaned_lines = [] found_header = False hop_data = {} # IP -> (line, sent_count) - + for line in lines: stripped = line.strip() - + # Skip empty lines if not stripped: continue - + # Skip interactive paging prompts if "-- [Q quit|C-z pause]" in stripped or "-- [Q quit|D dump|C-z pause]" in stripped: continue - + # Skip command echo lines if "tool traceroute" in stripped: continue - + # Look for the header line (ADDRESS LOSS SENT LAST AVG BEST WORST) if "ADDRESS" in stripped and "LOSS" in stripped and "SENT" in stripped: if not found_header: cleaned_lines.append(line) found_header = True continue - + # Only process data lines after we've found the header if found_header and stripped: # Try to extract IP address (IPv4 or IPv6) from the line - ipv4_match = re.match(r'^(\d+\.\d+\.\d+\.\d+)', stripped) - ipv6_match = re.match(r'^([0-9a-fA-F:]+)', stripped) if not ipv4_match else None - + ipv4_match = re.match(r"^(\d+\.\d+\.\d+\.\d+)", stripped) + ipv6_match = re.match(r"^([0-9a-fA-F:]+)", stripped) if not ipv4_match else None + if ipv4_match or ipv6_match: ip = ipv4_match.group(1) if ipv4_match else ipv6_match.group(1) - + # Extract the SENT count from the line (look for pattern like "0% 3" or "100% 2") - sent_match = re.search(r'\s+(\d+)%\s+(\d+)\s+', stripped) + sent_match = re.search(r"\s+(\d+)%\s+(\d+)\s+", stripped) sent_count = int(sent_match.group(2)) if sent_match else 0 - + # Keep the line with the highest SENT count (most complete data) if ip not in hop_data or sent_count > hop_data[ip][1]: hop_data[ip] = (line, sent_count) - elif sent_count == hop_data[ip][1] and "timeout" not in stripped and "timeout" in hop_data[ip][0]: + elif ( + sent_count == hop_data[ip][1] + and "timeout" not in stripped + and "timeout" in hop_data[ip][0] + ): # If SENT counts are equal, prefer non-timeout over timeout hop_data[ip] = (line, sent_count) elif "100%" in stripped and "timeout" in stripped: # Skip standalone timeout lines without IP continue - + # Reconstruct the output with only the best results if found_header and hop_data: result_lines = [cleaned_lines[0]] # Header - + # Sort by the order IPs first appeared, but use the best data for each seen_ips = [] for line in lines: stripped = line.strip() if found_header: - ipv4_match = re.match(r'^(\d+\.\d+\.\d+\.\d+)', stripped) - ipv6_match = re.match(r'^([0-9a-fA-F:]+)', stripped) if not ipv4_match else None - + ipv4_match = re.match(r"^(\d+\.\d+\.\d+\.\d+)", stripped) + ipv6_match = re.match(r"^([0-9a-fA-F:]+)", stripped) if not ipv4_match else None + if ipv4_match or ipv6_match: ip = ipv4_match.group(1) if ipv4_match else ipv6_match.group(1) if ip not in seen_ips and ip in hop_data: seen_ips.append(ip) result_lines.append(hop_data[ip][0]) - + return "\n".join(result_lines) - + return raw_output def process(self, *, output: OutputType, query: "Query") -> Series[str]: @@ -113,7 +117,7 @@ class MikrotikGarbageOutput(OutputPlugin): # If output is already processed/structured (not raw strings), pass it through unchanged if not isinstance(output, (tuple, list)): return output - + # Check if the tuple/list contains non-string objects (structured data) if output and not isinstance(output[0], str): return output @@ -125,16 +129,18 @@ class MikrotikGarbageOutput(OutputPlugin): if not isinstance(raw_output, str): cleaned_outputs.append(raw_output) continue - + # Se a saída já estiver vazia, não há nada a fazer. if not raw_output or not raw_output.strip(): cleaned_outputs.append("") continue # Check if this is traceroute output and handle it specially - if ("tool traceroute" in raw_output or - ("ADDRESS" in raw_output and "LOSS" in raw_output and "SENT" in raw_output) or - "-- [Q quit|C-z pause]" in raw_output): + if ( + "tool traceroute" in raw_output + or ("ADDRESS" in raw_output and "LOSS" in raw_output and "SENT" in raw_output) + or "-- [Q quit|C-z pause]" in raw_output + ): cleaned_output = self._clean_traceroute_output(raw_output) cleaned_outputs.append(cleaned_output) continue @@ -143,7 +149,7 @@ class MikrotikGarbageOutput(OutputPlugin): lines = raw_output.splitlines() filtered_lines = [] in_flags_section = False - + for line in lines: stripped_line = line.strip() diff --git a/hyperglass/ui/components/output/cell.tsx b/hyperglass/ui/components/output/cell.tsx index 6bf12b7..c4a7038 100644 --- a/hyperglass/ui/components/output/cell.tsx +++ b/hyperglass/ui/components/output/cell.tsx @@ -1,4 +1,4 @@ -import { MonoField, Active, Weight, Age, Communities, RPKIState, ASPath } from './fields'; +import { MonoField, Active, Weight, Age, Communities, RPKIState, ASPath, HideableField } from './fields'; import type { CellRenderProps } from '~/types'; @@ -18,7 +18,7 @@ export const Cell = (props: CellProps): JSX.Element => { peer_rid: , source_as: , active: , - source_rid: , + source_rid: , local_preference: , communities: , as_path: , diff --git a/hyperglass/ui/components/output/fields.tsx b/hyperglass/ui/components/output/fields.tsx index 416c123..9bf4768 100644 --- a/hyperglass/ui/components/output/fields.tsx +++ b/hyperglass/ui/components/output/fields.tsx @@ -51,8 +51,8 @@ dayjs.extend(utcPlugin); export const MonoField = (props: MonoFieldProps): JSX.Element => { const { v, ...rest } = props; - // Handle empty or undefined values - if (!v || (typeof v === 'string' && v.trim() === '')) { + // Handle empty or undefined values, but not zero values + if (v === null || v === undefined || (typeof v === 'string' && v.trim() === '')) { return ( N/A @@ -85,15 +85,9 @@ export const Active = (props: ActiveProps): JSX.Element => { export const Age = (props: AgeProps): JSX.Element => { const { inSeconds, ...rest } = props; - // Handle case where age is not available (e.g., MikroTik) + // Handle case where age is not available (e.g., MikroTik) - hide the field entirely if (inSeconds === -1) { - return ( - - - N/A - - - ); + return <>; } const now = dayjs.utc(); @@ -265,3 +259,18 @@ const _RPKIState: React.ForwardRefRenderFunction }; export const RPKIState = forwardRef(_RPKIState); + +export const HideableField = (props: MonoFieldProps): JSX.Element => { + const { v, ...rest } = props; + + // Hide the field entirely if value is empty or undefined + if (v === null || v === undefined || (typeof v === 'string' && v.trim() === '')) { + return <>; + } + + return ( + + {v} + + ); +}; diff --git a/hyperglass/ui/hooks/use-table-to-string.ts b/hyperglass/ui/hooks/use-table-to-string.ts index acd2449..4388a8e 100644 --- a/hyperglass/ui/hooks/use-table-to-string.ts +++ b/hyperglass/ui/hooks/use-table-to-string.ts @@ -102,6 +102,15 @@ export function useTableToString( const [header, accessor, align] = field; if (align !== null) { let value = route[accessor]; + + // Handle fields that should be hidden when empty/not available + if ((accessor === 'source_rid' || accessor === 'age') && + (value === null || value === undefined || + (typeof value === 'string' && value.trim() === '') || + (accessor === 'age' && value === -1))) { + continue; // Skip this field entirely + } + const fmtFunc = getFmtFunc(accessor) as (v: typeof value) => string; value = fmtFunc(value); if (accessor === 'prefix') {