diff --git a/korrel8r/korrel8r-openapi.yaml b/korrel8r/korrel8r-openapi.yaml
index 2559cd6..36997e0 100644
--- a/korrel8r/korrel8r-openapi.yaml
+++ b/korrel8r/korrel8r-openapi.yaml
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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]
diff --git a/web/src/__tests__/types.spec.ts b/web/src/__tests__/types.spec.ts
index 9bdf8e9..2b745b9 100644
--- a/web/src/__tests__/types.spec.ts
+++ b/web/src/__tests__/types.spec.ts
@@ -147,6 +147,7 @@ describe('Node', () => {
count: 5,
},
],
+ markers: [],
});
});
diff --git a/web/src/components/topology/Korrel8rTopology.tsx b/web/src/components/topology/Korrel8rTopology.tsx
index f36066a..9bbe280 100644
--- a/web/src/components/topology/Korrel8rTopology.tsx
+++ b/web/src/components/topology/Korrel8rTopology.tsx
@@ -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: (
+
+ {node.markers.map(({ marker, count }) => (
+
+ ))}
+
+ ),
+ };
+ }
+ return {};
+};
+
interface Korrel8rTopologyNodeProps {
- element: Node;
+ element: 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 = (
onSelect(e)}
+ {...nodeStatusProps(node)}
>
- {getIcon(node.class)}
+ {getIcon(node?.class)}
);
- if (node.error) {
- // Gray out, add error tool tip
+ if (node.disabled) {
+ // Wrapper to make node gray with error tool tip.
return (
- {node.error}){topologyNode}
+ {node.disabled}){topologyNode}
);
}
@@ -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([]);
- 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');
}
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,7 +193,7 @@ 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],
@@ -158,7 +201,7 @@ export const Korrel8rTopology: FC<{
const nodeMenu = useCallback(
(e: GraphElement): React.ReactElement[] => {
- const node = e.getData();
+ const node: korrel8r.Node = e.getData();
const menu = [
{node.class.toString()}
@@ -173,9 +216,8 @@ export const Korrel8rTopology: FC<{
setSelectedIds([node.id]);
navigator.clipboard.writeText(qc.query.toString());
}}
- icon={{`${qc.count} `}}
>
- {`${qc.query.selector} `}
+ {qc.query.selector} {`${qc.count}`}
,
),
);
diff --git a/web/src/components/topology/korrel8rtopology.css b/web/src/components/topology/korrel8rtopology.css
index e9297b3..b2da3d7 100644
--- a/web/src/components/topology/korrel8rtopology.css
+++ b/web/src/components/topology/korrel8rtopology.css
@@ -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);
@@ -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;
+}
diff --git a/web/src/korrel8r/client/index.ts b/web/src/korrel8r/client/index.ts
index af98ba1..d0923b0 100644
--- a/web/src/korrel8r/client/index.ts
+++ b/web/src/korrel8r/client/index.ts
@@ -57,6 +57,7 @@ export type {
ListGoalsErrors,
ListGoalsResponse,
ListGoalsResponses,
+ MarkerCount,
Neighbors,
Node,
Object,
diff --git a/web/src/korrel8r/client/types.gen.ts b/web/src/korrel8r/client/types.gen.ts
index c810752..32ac2e3 100644
--- a/web/src/korrel8r/client/types.gen.ts
+++ b/web/src/korrel8r/client/types.gen.ts
@@ -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;
/**
* Serialized result contents, may be large.
*/
@@ -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.
*/
@@ -541,6 +559,10 @@ export type ObjectsData = {
* Query string.
*/
query: Query;
+ /**
+ * Constrains the objects that will be included in results.
+ */
+ constraint?: Constraint;
};
url: '/objects';
};
diff --git a/web/src/korrel8r/types.ts b/web/src/korrel8r/types.ts
index a70067a..c81bfff 100644
--- a/web/src/korrel8r/types.ts
+++ b/web/src/korrel8r/types.ts
@@ -248,15 +248,18 @@ 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;
- error: Error;
+ markers: Array;
+ 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;
@@ -264,9 +267,14 @@ export class Node {
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;
+ }
+
+ 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];
}
}