mirror of
https://github.com/thatmattlove/hyperglass.git
synced 2026-05-15 20:08:01 +00:00
fix(ui): improve BGP table field display formatting
- Fix MED field showing N/A when value is 0 (now correctly shows 0) - Hide Originator field when empty instead of showing N/A - Hide Age field when not available instead of showing N/A - Add HideableField component for fields that should be hidden when empty - Update string output formatting to skip hidden fields in text export Resolves display issues where valid zero values and unavailable fields were showing as N/A instead of proper formatting or being hidden.
This commit is contained in:
parent
716687db63
commit
4414d2ec03
4 changed files with 63 additions and 39 deletions
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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: <MonoField v={data.value} />,
|
||||
source_as: <MonoField v={data.value} />,
|
||||
active: <Active isActive={data.value} />,
|
||||
source_rid: <MonoField v={data.value} />,
|
||||
source_rid: <HideableField v={data.value} />,
|
||||
local_preference: <MonoField v={data.value} />,
|
||||
communities: <Communities communities={data.value} />,
|
||||
as_path: <ASPath path={data.value} active={data.row.values.active} />,
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Text as="span" fontSize="sm" fontFamily="mono" color="gray.500" {...rest}>
|
||||
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 (
|
||||
<Tooltip hasArrow label="Age information not available" placement="right">
|
||||
<Text fontSize="sm" color="gray.500" {...rest}>
|
||||
N/A
|
||||
</Text>
|
||||
</Tooltip>
|
||||
);
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const now = dayjs.utc();
|
||||
|
|
@ -265,3 +259,18 @@ const _RPKIState: React.ForwardRefRenderFunction<HTMLDivElement, RPKIStateProps>
|
|||
};
|
||||
|
||||
export const RPKIState = forwardRef<HTMLDivElement, RPKIStateProps>(_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 (
|
||||
<Text as="span" fontSize="sm" fontFamily="mono" {...rest}>
|
||||
{v}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue