Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions korrel8r/korrel8r-openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ info:
name: Apache 2.0
url: https://github.com/korrel8r/korrel8r/blob/main/LICENSE
version: v1alpha1
x-korrel8r-version: "0.10.1-dev"
externalDocs:
url: https://korrel8r.github.io/korrel8r/
description: Korrel8r User Guide
Expand Down Expand Up @@ -294,6 +295,13 @@ paths:
required: true
schema:
$ref: "#/components/schemas/Query"
- name: constraint
description: Constrains the objects that will be included in results.
in: query
style: form
explode: true
schema:
$ref: "#/components/schemas/Constraint"
responses:
"200":
description: OK
Expand Down Expand Up @@ -399,7 +407,6 @@ components:
Constraint:
description: Constrains the objects that will be included in search results.
type: object
x-go-type: korrel8r.Constraint
properties:
start:
type: string
Expand Down Expand Up @@ -553,18 +560,24 @@ components:
x-oapi-codegen-extra-tags:
jsonschema: "Full class name in DOMAIN:CLASS format."
queries:
type: array
x-go-type-skip-optional-pointer: true
description: Queries yielding results for this class.
type: array
items:
$ref: "#/components/schemas/QueryCount"
x-go-type-skip-optional-pointer: true
x-oapi-codegen-extra-tags:
jsonschema: "Queries yielding results for this class."
count:
type: integer
description: Number of results for this class, after de-duplication.
type: integer
x-oapi-codegen-extra-tags:
jsonschema: "Number of results for this class, after de-duplication."
markers:
description: Markers found on data objects for this node.
type: array
items:
$ref: "#/components/schemas/MarkerCount"
x-go-type-skip-optional-pointer: true
result:
description: Serialized result contents, may be large.
type: array
Expand All @@ -591,6 +604,18 @@ components:
x-oapi-codegen-extra-tags:
jsonschema: "Query for correlation data in DOMAIN:CLASS:SELECTOR format."

MarkerCount:
description: Marker with number of instances found.
type: object
required: [marker]
properties:
marker:
description: Marker for correlation data.
type: string
count:
description: Number of markers found, omitted if none.
type: integer

Rule:
type: object
required: [name]
Expand Down
1 change: 1 addition & 0 deletions web/src/__tests__/types.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ describe('Node', () => {
count: 5,
},
],
markers: [],
});
});

Expand Down
90 changes: 66 additions & 24 deletions web/src/components/topology/Korrel8rTopology.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Badge, Title } from '@patternfly/react-core';
import { Badge, Label, LabelGroup, Title } from '@patternfly/react-core';
import {
action,
ComponentFactory,
Expand All @@ -17,7 +17,9 @@ import {
Model,
ModelKind,
Node,
NodeModel,
NodeShape,
NodeStatus,
SELECTION_EVENT,
TOP_TO_BOTTOM,
TopologyControlBar,
Expand All @@ -33,12 +35,13 @@ import {
withSelection,
WithSelectionProps,
} from '@patternfly/react-topology';
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocationQuery } from '../../hooks/useLocationQuery';
import { useNavigateToQuery } from '../../hooks/useNavigateToQuery';
import * as korrel8r from '../../korrel8r/types';
import { getIcon } from '../icons';
import './korrel8rtopology.css';
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';

// DagreLayout with straight edges (no angular bendpoints).
class StraightEdgeDagreLayout extends DagreLayout {
Expand All @@ -56,14 +59,45 @@ const nodeLabel = (node: korrel8r.Node): string => {
return capitalize(c.name);
};

const nodeStatusProps = (
node: korrel8r.Node,
): {
nodeStatus?: NodeStatus;
showStatusDecorator?: boolean;
statusDecoratorTooltip?: React.ReactNode;
} => {
if (node.disabled) {
return {
showStatusDecorator: true,
statusDecoratorTooltip: node.disabled,
};
}
if (node.hasMarkers) {
return {
nodeStatus: NodeStatus.warning,
showStatusDecorator: true,
statusDecoratorTooltip: (
<LabelGroup>
{node.markers.map(({ marker, count }) => (
<Label key={marker}>
{marker} <Badge className="tp-plugin__topology_marker_badge">{count}</Badge>
</Label>
))}
</LabelGroup>
),
};
}
return {};
};

interface Korrel8rTopologyNodeProps {
element: Node;
element: Node<NodeModel, korrel8r.Node>;
}

const Korrel8rTopologyNode: FC<
Korrel8rTopologyNodeProps & WithContextMenuProps & WithSelectionProps & WithDragNodeProps
> = ({ element, onSelect, selected, onContextMenu, contextMenuOpen, dragNodeRef }) => {
const node = element.getData();
const node: korrel8r.Node = element.getData();
const topologyNode = (
<DefaultNode
element={element}
Expand All @@ -73,19 +107,22 @@ const Korrel8rTopologyNode: FC<
contextMenuOpen={contextMenuOpen}
dragNodeRef={dragNodeRef}
hover={false}
className={node.disabled ? 'tp-plugin__topology_node--disabled' : undefined}
label={nodeLabel(node)}
badge={node?.count?.toString() ?? '?'}
badgeClassName="tp-plugin__topology_node_badge"
hideContextMenuKebab={node?.queries?.length === 1}
onStatusDecoratorClick={(e) => onSelect(e)}
{...nodeStatusProps(node)}
>
<g transform={`translate(25, 25)`}>{getIcon(node.class)}</g>
<g transform={`translate(25, 25)`}>{getIcon(node?.class)}</g>
</DefaultNode>
);
if (node.error) {
// Gray out, add error tool tip
if (node.disabled) {
// Wrapper to make node gray with error tool tip.
return (
<g opacity="0.7" className="tp-plugin__topology_invalid_node">
<title>{node.error}</title>){topologyNode}
<title>{node.disabled}</title>){topologyNode}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Detect the exact malformed JSX token sequence.
rg -n "title>\\{node\\.disabled\\}</title>\\)\\{topologyNode\\}" web/src/components/topology/Korrel8rTopology.tsx

Repository: openshift/troubleshooting-panel-console-plugin

Length of output: 148


🏁 Script executed:

# View context around line 125 to understand the structure
cat -n web/src/components/topology/Korrel8rTopology.tsx | sed -n '115,135p'

Repository: openshift/troubleshooting-panel-console-plugin

Length of output: 765


🏁 Script executed:

# Search for graph.nodes mutations to verify state mutation concern
rg -n "graph\.nodes\s*=" web/src/components/topology/Korrel8rTopology.tsx

Repository: openshift/troubleshooting-panel-console-plugin

Length of output: 72


🏁 Script executed:

# Check node.class usage patterns
rg -n "node\.class" web/src/components/topology/Korrel8rTopology.tsx -A 2 -B 2

Repository: openshift/troubleshooting-panel-console-plugin

Length of output: 1027


Remove stray ) causing JSX parse failure.

At line 125, the closing </title> tag is followed by an extra ) that will cause a syntax error. The element should be followed directly by {topologyNode} on the next line.

Suggested fix
-        <title>{node.disabled}</title>){topologyNode}
+        <title>{node.disabled}</title>
+        {topologyNode}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<title>{node.disabled}</title>){topologyNode}
<title>{node.disabled}</title>
{topologyNode}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/topology/Korrel8rTopology.tsx` at line 125, In
Korrel8rTopology.tsx inside the JSX that renders the title and topologyNode (the
line containing <title>{node.disabled}</title>){topologyNode}), remove the stray
closing parenthesis so the fragment flows directly from the closing </title> to
{topologyNode}; locate the JSX in the Korrel8rTopology component where
node.disabled and topologyNode are used and change that sequence to end the
</title> tag and then immediately render {topologyNode}.

</g>
);
}
Expand All @@ -104,28 +141,34 @@ export const Korrel8rTopology: FC<{
}> = ({ graph, loggingAvailable, netobserveAvailable, constraint }) => {
const { t } = useTranslation('plugin__troubleshooting-panel-console-plugin');
const navigateToQuery = useNavigateToQuery();
const locationQuery = useLocationQuery();
const [selectedIds, setSelectedIds] = useState<string[]>([]);

const nodes = useMemo(
() =>
useEffect(() => {
if (!locationQuery) return;
const id = locationQuery.class.toString();
setSelectedIds(graph.node(id) ? [id] : []);
}, [graph, locationQuery]);

const nodes: NodeModel[] = useMemo(
(): NodeModel[] =>
graph.nodes.map((node: korrel8r.Node) => {
const newNode = { ...node };
if (newNode.error) {
if (node.disabled) {
// eslint-disable-next-line no-console
console.warn(`korrel8r node: ${newNode.error}`);
newNode.error = Error(t('Unable to find Console Link'));
} else if (newNode.class.domain === 'log' && !loggingAvailable) {
newNode.error = Error(t('Logging Plugin Disabled'));
} else if (newNode.class.domain === 'netflow' && !netobserveAvailable) {
newNode.error = Error(t('Netflow Plugin Disabled'));
console.warn(`korrel8r node: ${node.disabled}`);
node.disabled = t('Unable to find Console Link');
} else if (node.class.domain === 'log' && !loggingAvailable) {
node.disabled = t('Logging Plugin Disabled');
} else if (node.class.domain === 'netflow' && !netobserveAvailable) {
node.disabled = t('Netflow Plugin Disabled');
}
Comment on lines +153 to 164
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid mutating graph.nodes when deriving disabled UI state.

At Lines 156-163, mutating node.disabled in-place can leave nodes permanently disabled/wrongly labeled across later renders when availability flags change.

Suggested fix
-      graph.nodes.map((node: korrel8r.Node) => {
-        if (node.disabled) {
+      graph.nodes.map((node: korrel8r.Node) => {
+        const disabled =
+          node.disabled
+            ? t('Unable to find Console Link')
+            : node.class.domain === 'log' && !loggingAvailable
+              ? t('Logging Plugin Disabled')
+              : node.class.domain === 'netflow' && !netobserveAvailable
+                ? t('Netflow Plugin Disabled')
+                : undefined;
+        if (node.disabled) {
           // eslint-disable-next-line no-console
           console.warn(`korrel8r node: ${node.disabled}`);
-          node.disabled = t('Unable to find Console Link');
-        } else if (node.class.domain === 'log' && !loggingAvailable) {
-          node.disabled = t('Logging Plugin Disabled');
-        } else if (node.class.domain === 'netflow' && !netobserveAvailable) {
-          node.disabled = t('Netflow Plugin Disabled');
         }
         return {
           id: node.id,
@@
-          data: node,
+          data: { ...node, disabled },
         };
       }),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/topology/Korrel8rTopology.tsx` around lines 153 - 164, The
current mapping in nodes (useMemo) mutates the source graph.nodes by setting
node.disabled directly, which can persist incorrect state; instead, do not
modify graph.nodes in-place—create and return new NodeModel objects (or shallow
copies of each korrel8r.Node) and set the disabled display string on those
copies based on loggingAvailable, netobserveAvailable and the existing
node.disabled value (using korrel8r.Node, graph.nodes, node.disabled,
loggingAvailable, netobserveAvailable and t()) so the UI state is derived
immutably and availability changes won’t leak into the original graph data.

return {
id: newNode.id,
id: node.id,
type: 'node',
width: NODE_DIAMETER,
height: NODE_DIAMETER,
shape: NODE_SHAPE,
data: newNode,
data: node,
};
}),
[graph, loggingAvailable, netobserveAvailable, t],
Expand All @@ -150,15 +193,15 @@ export const Korrel8rTopology: FC<{
const id = selected?.[0]; // Select only one at a time.
setSelectedIds([id]);
const node = graph.node(id);
if (!node || node.error) return;
if (!node || node.disabled) return;
navigateToQuery(node.queries?.[0]?.query, constraint);
},
[graph, navigateToQuery, setSelectedIds, constraint],
);

const nodeMenu = useCallback(
(e: GraphElement<ElementModel, korrel8r.Node>): React.ReactElement[] => {
const node = e.getData();
const node: korrel8r.Node = e.getData();
const menu = [
<ContextMenuItem isDisabled={true} key={node.class.toString()}>
<Title headingLevel="h4">{node.class.toString()}</Title>
Expand All @@ -173,9 +216,8 @@ export const Korrel8rTopology: FC<{
setSelectedIds([node.id]);
navigator.clipboard.writeText(qc.query.toString());
}}
icon={<Badge>{`${qc.count} `}</Badge>}
>
{`${qc.query.selector} `}
{qc.query.selector} <Badge>{`${qc.count}`}</Badge>
</ContextMenuItem>,
),
);
Expand Down
15 changes: 11 additions & 4 deletions web/src/components/topology/korrel8rtopology.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
.tp-plugin__topology_invalid_node {
cursor: not-allowed;
}

.tp-plugin__topology_node_badge rect {
fill: var(--pf-t--global--color--brand--default);
stroke: var(--pf-t--global--color--brand--default);
Expand All @@ -10,3 +6,14 @@
.tp-plugin__topology_node_badge text {
fill: var(--pf-t--global--text--color--on-brand--default);
}

.tp-plugin__topology_marker_badge {
--pf-v6-c-badge--BackgroundColor: var(--pf-t--global--color--brand--default);
--pf-v6-c-badge--Color: var(--pf-t--global--text--color--on-brand--default);
}

.tp-plugin__topology_node--disabled {
opacity: 0.5;
filter: grayscale(100%);
pointer-events: auto;
}
1 change: 1 addition & 0 deletions web/src/korrel8r/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export type {
ListGoalsErrors,
ListGoalsResponse,
ListGoalsResponses,
MarkerCount,
Neighbors,
Node,
Object,
Expand Down
22 changes: 22 additions & 0 deletions web/src/korrel8r/client/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ export type Node = {
* Number of results for this class, after de-duplication.
*/
count?: number;
/**
* Markers found on data objects for this node.
*/
markers?: Array<MarkerCount>;
/**
* Serialized result contents, may be large.
*/
Expand All @@ -162,6 +166,20 @@ export type QueryCount = {
query: Query;
};

/**
* Marker with number of instances found.
*/
export type MarkerCount = {
/**
* Marker for correlation data.
*/
marker: string;
/**
* Number of markers found, omitted if none.
*/
count?: number;
};

/**
* Rule is a correlation rule with a list of queries and results counts found during navigation.
*/
Expand Down Expand Up @@ -541,6 +559,10 @@ export type ObjectsData = {
* Query string.
*/
query: Query;
/**
* Constrains the objects that will be included in results.
*/
constraint?: Constraint;
};
url: '/objects';
};
Expand Down
16 changes: 12 additions & 4 deletions web/src/korrel8r/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,25 +248,33 @@ export const unixSeconds = (d: Date | undefined): number | undefined => {
return Math.floor(unixMilliseconds(d) / 1000) || undefined;
};

export type MarkerCount = api.MarkerCount;

export class Node {
id: string;
count: number;
class: Class;
queries: Array<QueryCount>;
error: Error;
markers: Array<MarkerCount>;
disabled: string;

/** Construct a type-safe node from an API node.
* Does not throw, sets the `error` field on error.
* Does not throw, sets the `disabled` field if there is an error.
*/
constructor(node: api.Node) {
this.id = node.class;
this.count = node.count;
try {
this.class = Class.parse(node.class);
} catch (e) {
this.error = e;
this.disabled = e?.message ?? e.toString();
}
this.queries = QueryCount.array(node.queries ?? []);
this.markers = node.markers;
}
Comment on lines +273 to +274
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Default markers to an empty array in Node constructor.

At Line 273, node.markers is optional in the API contract; assigning it directly can leave this.markers undefined and break shape expectations.

Suggested fix
-    this.markers = node.markers;
+    this.markers = node.markers ?? [];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this.markers = node.markers;
}
this.markers = node.markers ?? [];
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/korrel8r/types.ts` around lines 273 - 274, Node constructor assigns
this.markers directly from node.markers which may be undefined; update the Node
constructor to default markers to an empty array (e.g., set this.markers =
node.markers ?? [] or use a fallback) so this.markers is always an array and
preserves the expected shape for consumers (refer to the Node constructor and
the this.markers / node.markers symbols).


get hasMarkers(): boolean {
return this.markers?.length > 0;
}
}

Expand Down Expand Up @@ -342,6 +350,6 @@ export class Graph {
}

node(id: string): Node {
return this.nodeByClass[id];
return this.nodeByClass?.[id];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Verify Map declaration and non-Map-style indexing usage.
rg -n "nodeByClass|new Map\\(|\\.set\\(|\\.get\\(|\\[id\\]|\\[n\\.class\\]" web/src/korrel8r/types.ts -C2

Repository: openshift/troubleshooting-panel-console-plugin

Length of output: 1406


Use Map#get() and Map#set() consistently for nodeByClass access instead of bracket notation.

At line 353, bracket indexing ?.[id] is used on a Map, which is not the Map API and bypasses type safety. The return type annotation claims Node but bracket notation actually returns Node | undefined. Use .get(id) instead, which properly returns Node | undefined.

Suggested fix
 export class Graph {
   nodes: Array<Node>;
   edges: Array<Edge>;

   private nodeByClass: Map<string, Node>;

   constructor(graph: api.Graph) {
     this.nodeByClass = new Map();
     this.nodes = (graph?.nodes ?? []).map((n) => {
       const node = new Node(n);
-      this.nodeByClass[n.class] = node;
+      this.nodeByClass.set(n.class, node);
       return node;
     });
@@
-  node(id: string): Node {
-    return this.nodeByClass?.[id];
+  node(id: string): Node | undefined {
+    return this.nodeByClass.get(id);
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/korrel8r/types.ts` at line 353, The getter is accessing a Map using
bracket notation (this.nodeByClass?.[id]) which bypasses the Map API and types;
replace that access with the Map#get method (this.nodeByClass?.get(id)) and
ensure the declared return type reflects Node | undefined if it currently
asserts Node; update any corresponding set usage to Map#set if needed so
nodeByClass is consistently accessed via .get/.set (refer to the nodeByClass
property and the function/method containing the return).

}
}