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') {