Skip to content
This repository has been archived by the owner on Nov 28, 2024. It is now read-only.

Commit

Permalink
[WIP] NNS topology
Browse files Browse the repository at this point in the history
Signed-off-by: Aviv Turgeman <[email protected]>
  • Loading branch information
avivtur committed Aug 13, 2024
1 parent 84abc5b commit b6a872f
Show file tree
Hide file tree
Showing 15 changed files with 1,028 additions and 29 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
"@openshift/dynamic-plugin-sdk-webpack": "~1.0.0",
"@patternfly/react-core": "5.1.1",
"@patternfly/react-table": "^5.1.1",
"@patternfly/react-icons": "5.2.1",
"@patternfly/react-topology": "5.2.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^12.0.0",
"@testing-library/react-hooks": "^8.0.1",
Expand Down
8 changes: 7 additions & 1 deletion plugin-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { ConsolePluginMetadata } from '@openshift-console/dynamic-plugin-sd

import { PolicyExposedModules, PolicyExtensions } from './src/views/policies/manifest';
import { StateExposedModules, StateExtensions } from './src/views/states/manifest';
import { TopologyExposedModules, TopologyExtensions } from './src/views/topology/manifest';

export const pluginMetadata = {
name: 'nmstate-console-plugin',
Expand All @@ -13,10 +14,15 @@ export const pluginMetadata = {
exposedModules: {
...PolicyExposedModules,
...StateExposedModules,
...TopologyExposedModules,
},
dependencies: {
'@console/pluginAPI': '*',
},
} as ConsolePluginMetadata;

export const extensions: EncodedExtension[] = [...PolicyExtensions, ...StateExtensions];
export const extensions: EncodedExtension[] = [
...PolicyExtensions,
...StateExtensions,
...TopologyExtensions,
];
4 changes: 1 addition & 3 deletions src/console-models/NodeNetworkStateModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { K8sModel } from '@openshift-console/dynamic-plugin-sdk/lib/api/common-t

import { modelToGroupVersionKind, modelToRef } from './modelUtils';

const NodeNetworkStateModel: K8sModel = {
export const NodeNetworkStateModel: K8sModel = {
label: 'NodeNetworkState',
labelPlural: 'NodeNetworkStates',
apiVersion: 'v1beta1',
Expand All @@ -17,5 +17,3 @@ const NodeNetworkStateModel: K8sModel = {

export const NodeNetworkStateModelGroupVersionKind = modelToGroupVersionKind(NodeNetworkStateModel);
export const NodeNetworkStateModelRef = modelToRef(NodeNetworkStateModel);

export default NodeNetworkStateModel;
8 changes: 4 additions & 4 deletions src/views/states/list/StatesList.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React, { FC, useState } from 'react';
import { useNMStateTranslation } from 'src/utils/hooks/useNMStateTranslation';

import {
NodeNetworkStateModel,
NodeNetworkStateModelGroupVersionKind,
NodeNetworkStateModelRef,
} from 'src/console-models';
import NodeNetworkStateModel from 'src/console-models/NodeNetworkStateModel';
import { useNMStateTranslation } from 'src/utils/hooks/useNMStateTranslation';

} from '@models';
import {
ListPageBody,
ListPageFilter,
Expand Down
3 changes: 1 addition & 2 deletions src/views/states/list/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import NodeNetworkStateModel from 'src/console-models/NodeNetworkStateModel';

import { NodeNetworkStateModel } from '@models';
import { getResourceUrl } from '@utils/helpers';

export const baseListUrl = getResourceUrl({ model: NodeNetworkStateModel });
Expand Down
13 changes: 3 additions & 10 deletions src/views/states/manifest.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
import type { EncodedExtension } from '@openshift/dynamic-plugin-sdk';
import type {
ExtensionK8sModel,
ResourceClusterNavItem,
ResourceListPage,
} from '@openshift-console/dynamic-plugin-sdk';

import NodeNetworkStateModel from '../../console-models/NodeNetworkStateModel';

const StateExtensionModel: ExtensionK8sModel = {
group: NodeNetworkStateModel.apiGroup as string,
kind: NodeNetworkStateModel.kind,
version: NodeNetworkStateModel.apiVersion,
};
import { NodeNetworkStateModelGroupVersionKind } from '../../console-models';

export const StateExposedModules = {
StatesList: './views/states/list/StatesList',
Expand All @@ -25,7 +18,7 @@ export const StateExtensions: EncodedExtension[] = [
perspective: 'admin',
name: '%plugin__nmstate-console-plugin~NodeNetworkState%',
section: 'networking',
model: StateExtensionModel,
model: NodeNetworkStateModelGroupVersionKind,
dataAttributes: {
'data-quickstart-id': 'qs-nav-state-list',
'data-test-id': 'state-nav-list',
Expand All @@ -36,7 +29,7 @@ export const StateExtensions: EncodedExtension[] = [
type: 'console.page/resource/list',
properties: {
perspective: 'admin',
model: StateExtensionModel,
model: NodeNetworkStateModelGroupVersionKind,
component: { $codeRef: 'StatesList' },
},
} as EncodedExtension<ResourceListPage>,
Expand Down
91 changes: 91 additions & 0 deletions src/views/topology/Topology.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React, { FC, useEffect, useState } from 'react';

import { NodeNetworkStateModelGroupVersionKind } from '@models';
import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk';
import {
action,
createTopologyControlButtons,
defaultControlButtonsOptions,
SELECTION_EVENT,
TopologyControlBar,
TopologySideBar,
TopologyView,
Visualization,
VisualizationProvider,
VisualizationSurface,
} from '@patternfly/react-topology';
import { V1beta1NodeNetworkState } from '@types';

import { componentFactory, layoutFactory } from './utils/factory';
import { transformDataToTopologyModel } from './utils/utils';

const Topology: FC = () => {
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [visualization, setVisualization] = useState<Visualization>(null);

const [data, loaded, error] = useK8sWatchResource<V1beta1NodeNetworkState[]>({
groupVersionKind: NodeNetworkStateModelGroupVersionKind,
isList: true,
namespaced: false,
});

useEffect(() => {
if (loaded && !error) {
const topologyModel = transformDataToTopologyModel(data);

if (!visualization) {
const newVisualization = new Visualization();
newVisualization.registerLayoutFactory(layoutFactory);
newVisualization.registerComponentFactory(componentFactory);
newVisualization.addEventListener(SELECTION_EVENT, setSelectedIds);
newVisualization.fromModel(topologyModel);
setVisualization(newVisualization);
} else {
visualization.fromModel(topologyModel);
}
}
}, [data, loaded, error]);

const topologySideBar = (
<TopologySideBar
className="topology-example-sidebar"
show={selectedIds.length > 0}
onClose={() => setSelectedIds([])}
>
<div style={{ marginTop: 27, marginLeft: 20, height: '800px' }}>{selectedIds[0]}</div>
</TopologySideBar>
);

return (
<TopologyView
sideBar={topologySideBar}
controlBar={
<TopologyControlBar
controlButtons={createTopologyControlButtons({
...defaultControlButtonsOptions,
zoomInCallback: action(() => {
visualization.getGraph().scaleBy(4 / 3);
}),
zoomOutCallback: action(() => {
visualization.getGraph().scaleBy(0.75);
}),
fitToScreenCallback: action(() => {
visualization.getGraph().fit(80);
}),
resetViewCallback: action(() => {
visualization.getGraph().reset();
visualization.getGraph().layout();
}),
legend: false,
})}
/>
}
>
<VisualizationProvider controller={visualization}>
<VisualizationSurface state={{ selectedIds }} />
</VisualizationProvider>
</TopologyView>
);
};

export default Topology;
17 changes: 17 additions & 0 deletions src/views/topology/components/CustomGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React, { FC } from 'react';

import { DefaultGroup, Node } from '@patternfly/react-topology';

type CustomGroupProps = {
element: Node;
} & any;

Check warning on line 7 in src/views/topology/components/CustomGroup.tsx

View workflow job for this annotation

GitHub Actions / Run linter and tests

Unexpected any. Specify a different type

const CustomGroup: FC<CustomGroupProps> = ({ element, ...rest }) => {
const data = element.getData();

return (
<DefaultGroup className="custom-group-node" badge={data?.badge} element={element} {...rest} />
);
};

export default CustomGroup;
42 changes: 42 additions & 0 deletions src/views/topology/components/CustomNode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React, { FC } from 'react';

import {
DefaultNode,
Node,
WithDndDropProps,
WithDragNodeProps,
WithSelectionProps,
} from '@patternfly/react-topology';

import { ICON_SIZE } from '../utils/constants';

type CustomNodeProps = {
element: Node;
} & WithSelectionProps &
WithDragNodeProps &
WithDndDropProps;

const CustomNode: FC<CustomNodeProps> = ({ element, onSelect, selected }) => {
const data = element.getData();
const Icon = data.icon;
const { width, height } = element.getBounds();

const xCenter = (width - ICON_SIZE) / 2;
const yCenter = (height - ICON_SIZE) / 2;

return (
<DefaultNode
badge={data.badge}
element={element}
onSelect={onSelect}
selected={selected}
truncateLength={8}
>
<g transform={`translate(${xCenter}, ${yCenter})`}>
<Icon style={{ color: '#393F44' }} width={ICON_SIZE} height={ICON_SIZE} />
</g>
</DefaultNode>
);
};

export default CustomNode;
33 changes: 33 additions & 0 deletions src/views/topology/manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { EncodedExtension } from '@openshift/dynamic-plugin-sdk';
import type { HrefNavItem, RoutePage } from '@openshift-console/dynamic-plugin-sdk';

export const TopologyExposedModules = {
Topology: './views/topology/Topology',
};

export const TopologyExtensions: EncodedExtension[] = [
{
type: 'console.navigation/href',
properties: {
id: 'topology',
perspective: 'admin',
href: 'nmstate-topology',
section: 'networking',
name: '%plugin__nmstate-console-plugin~Topology%',
dataAttributes: {
'data-quickstart-id': 'qs-nav-topology',
'data-test-id': 'state-nav-topology',
},
insertAfter: 'state',
},
} as EncodedExtension<HrefNavItem>,
{
type: 'console.page/route',
properties: {
path: ['nmstate-topology'],
component: {
$codeRef: 'Topology',
},
},
} as EncodedExtension<RoutePage>,
];
4 changes: 4 additions & 0 deletions src/views/topology/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const NODE_DIAMETER = 35;
export const CONNECTOR_TARGET_DROP = 'connector-target-drop';
export const GROUP = 'group';
export const ICON_SIZE = 15;
80 changes: 80 additions & 0 deletions src/views/topology/utils/factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {
ColaLayout,
ComponentFactory,
DefaultEdge,
DragObjectWithType,
Edge,
Graph,
GraphComponent,
graphDropTargetSpec,
GraphElement,
groupDropTargetSpec,
Layout,
LayoutFactory,
ModelKind,
Node,
nodeDragSourceSpec,
nodeDropTargetSpec,
withDndDrop,
withDragNode,
withPanZoom,
withSelection,
withTargetDrag,
} from '@patternfly/react-topology';

import CustomGroup from '../components/CustomGroup';
import CustomNode from '../components/CustomNode';

import { CONNECTOR_TARGET_DROP, GROUP } from './constants';

export const layoutFactory: LayoutFactory = (type: string, graph: Graph): Layout | undefined =>
new ColaLayout(graph);

export const componentFactory: ComponentFactory = (kind: ModelKind, type: string): any => {

Check warning on line 33 in src/views/topology/utils/factory.ts

View workflow job for this annotation

GitHub Actions / Run linter and tests

Unexpected any. Specify a different type
switch (type) {
case GROUP:
return withDndDrop(groupDropTargetSpec)(
withDragNode(nodeDragSourceSpec(GROUP))(withSelection()(CustomGroup)),
);
default:
switch (kind) {
case ModelKind.graph:
return withDndDrop(graphDropTargetSpec())(withPanZoom()(GraphComponent));
case ModelKind.node:
return withDndDrop(nodeDropTargetSpec([CONNECTOR_TARGET_DROP]))(
withDragNode(nodeDragSourceSpec(ModelKind.node, true, true))(
withSelection()(CustomNode),
),
);
case ModelKind.edge:
return withTargetDrag<
DragObjectWithType,
Node,
{ dragging?: boolean },
{
element: GraphElement;
}
>({
item: { type: CONNECTOR_TARGET_DROP },
begin: (monitor, props) => {
props.element.raise();
return props.element;
},
drag: (event, monitor, props) => {
(props.element as Edge).setEndPoint(event.x, event.y);
},
end: (dropResult, monitor, props) => {
if (monitor.didDrop() && dropResult && props) {
(props.element as Edge).setTarget(dropResult);
}
(props.element as Edge).setEndPoint();
},
collect: (monitor) => ({
dragging: monitor.isDragging(),
}),
})(DefaultEdge);
default:
return undefined;
}
}
};
Loading

0 comments on commit b6a872f

Please sign in to comment.