-
Notifications
You must be signed in to change notification settings - Fork 11
NO-JIRA: feat: display korrel8r markers in the graph #236
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -147,6 +147,7 @@ describe('Node', () => { | |
| count: 5, | ||
| }, | ||
| ], | ||
| markers: [], | ||
| }); | ||
| }); | ||
|
|
||
|
|
||
| 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, | ||
|
|
@@ -17,7 +17,9 @@ import { | |
| Model, | ||
| ModelKind, | ||
| Node, | ||
| NodeModel, | ||
| NodeShape, | ||
| NodeStatus, | ||
| SELECTION_EVENT, | ||
| TOP_TO_BOTTOM, | ||
| TopologyControlBar, | ||
|
|
@@ -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 { | ||
|
|
@@ -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} | ||
|
|
@@ -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} | ||
| </g> | ||
| ); | ||
| } | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid mutating At Lines 156-163, mutating 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 |
||
| 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], | ||
|
|
@@ -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> | ||
|
|
@@ -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>, | ||
| ), | ||
| ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Default At Line 273, Suggested fix- this.markers = node.markers;
+ this.markers = node.markers ?? [];📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
|
|
||||||||||
| get hasMarkers(): boolean { | ||||||||||
| return this.markers?.length > 0; | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
|
|
@@ -342,6 +350,6 @@ export class Graph { | |||||||||
| } | ||||||||||
|
|
||||||||||
| node(id: string): Node { | ||||||||||
| return this.nodeByClass[id]; | ||||||||||
| return this.nodeByClass?.[id]; | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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 -C2Repository: openshift/troubleshooting-panel-console-plugin Length of output: 1406 Use At line 353, bracket indexing 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 |
||||||||||
| } | ||||||||||
| } | ||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: openshift/troubleshooting-panel-console-plugin
Length of output: 148
🏁 Script executed:
Repository: openshift/troubleshooting-panel-console-plugin
Length of output: 765
🏁 Script executed:
Repository: openshift/troubleshooting-panel-console-plugin
Length of output: 72
🏁 Script executed:
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
📝 Committable suggestion
🤖 Prompt for AI Agents