diff --git a/jest.config.ts b/jest.config.ts index f9964c07..c670e879 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -24,7 +24,9 @@ const config: Config.InitialOptions = { transform: { '^.+\\.[t|j]sx?$': 'ts-jest', }, - transformIgnorePatterns: ['/node_modules/(?!(@patternfly|@openshift-console\\S*?)/.*)'], + transformIgnorePatterns: [ + '/node_modules/(?!(@patternfly|@openshift-console\\S*?|@kubevirt-ui)/.*)', + ], globals: { 'ts-jest': { isolatedModules: true, diff --git a/locales/en/plugin__nmstate-console-plugin.json b/locales/en/plugin__nmstate-console-plugin.json index c6b75800..6b1b07e4 100644 --- a/locales/en/plugin__nmstate-console-plugin.json +++ b/locales/en/plugin__nmstate-console-plugin.json @@ -29,6 +29,7 @@ "Add Label": "Add Label", "Add mapping": "Add mapping", "Add Option": "Add Option", + "Affects": "Affects", "Aggregation mode": "Aggregation mode", "All": "All", "An error occurred": "An error occurred", @@ -162,6 +163,7 @@ "Name": "Name", "Name already in use by another interface": "Name already in use by another interface", "Neighbors": "Neighbors", + "Network": "Network", "Network description": "Network description", "Network details": "Network details", "Network identity": "Network identity", @@ -171,19 +173,22 @@ "Network state": "Network state", "Network state controls whether the Linux bridge is active and participating in network traffic.": "Network state controls whether the Linux bridge is active and participating in network traffic.", "Network types": "Network types", + "Networking policies affecting this node": "Networking policies affecting this node", "No": "No", "No matching Nodes found for the labels": "No matching Nodes found for the labels", + "No networking policies apply to this node.": "No networking policies apply to this node.", "No NodeNetworkConfigurationPolicy found": "No NodeNetworkConfigurationPolicy found", "No NodeNetworkStates found": "No NodeNetworkStates found", "No owner": "No owner", "No physical networks defined yet": "No physical networks defined yet", + "Node label": "Node label", "Node network configuration": "Node network configuration", "Node network configuration graph legend": "Node network configuration graph legend", "Node network configuration name": "Node network configuration name", "Node network is configured and managed by NM state. Create a node network configuration policy to describe the requested network configuration on your nodes in the cluster. The node network configuration enactment reports the network policies enacted upon each node.": "Node network is configured and managed by NM state. Create a node network configuration policy to describe the requested network configuration on your nodes in the cluster. The node network configuration enactment reports the network policies enacted upon each node.", "Node Selector": "Node Selector", + "NodeNetworkConfigurationPolicies that have been applied or are pending for this node, defining how its interfaces, VLANs, bonds, and other network settings are managed.": "NodeNetworkConfigurationPolicies that have been applied or are pending for this node, defining how its interfaces, VLANs, bonds, and other network settings are managed.", "NodeNetworkConfigurationPolicy": "NodeNetworkConfigurationPolicy", - "NodeNetworkState": "NodeNetworkState", "nodes": "nodes", "Nodes": "Nodes", "Nodes configuration": "Nodes configuration", @@ -232,7 +237,10 @@ "Search by LLDP system name...": "Search by LLDP system name...", "Search by MAC address...": "Search by MAC address...", "Search by name": "Search by name", + "Search by node label (e.g. key=value)...": "Search by node label (e.g. key=value)...", "Search by VLAN name...": "Search by VLAN name...", + "See all NodeNetworkConfigurationPolicies": "See all NodeNetworkConfigurationPolicies", + "See all nodes in this cluster": "See all nodes in this cluster", "Select a bridge type below to determine how your network traffic will be handled.": "Select a bridge type below to determine how your network traffic will be handled.", "Select from the list": "Select from the list", "Select items": "Select items", diff --git a/locales/es/plugin__nmstate-console-plugin.json b/locales/es/plugin__nmstate-console-plugin.json index 967a1ecf..f9b8c993 100644 --- a/locales/es/plugin__nmstate-console-plugin.json +++ b/locales/es/plugin__nmstate-console-plugin.json @@ -34,6 +34,7 @@ "Add Label": "Agregar etiqueta", "Add mapping": "Agregar asociación", "Add Option": "Agregar opción", + "Affects": "", "Aggregation mode": "Modo de agregación", "All": "Todo", "An error occurred": "Ocurrió un error", @@ -167,6 +168,7 @@ "Name": "Nombre", "Name already in use by another interface": "Nombre ya en uso por otra interfaz", "Neighbors": "Vecinos", + "Network": "", "Network description": "Descripción de la red", "Network details": "Detalles de la red", "Network identity": "Identidad de red", @@ -176,19 +178,22 @@ "Network state": "Estado de la red", "Network state controls whether the Linux bridge is active and participating in network traffic.": "El estado de la red controla si el puente Linux está activo y participa en el tráfico de red.", "Network types": "Tipos de red", + "Networking policies affecting this node": "", "No": "No", "No matching Nodes found for the labels": "No se encontraron nodos coincidentes para las etiquetas", + "No networking policies apply to this node.": "", "No NodeNetworkConfigurationPolicy found": "No se encontró ninguna política de configuración de red de nodo", "No NodeNetworkStates found": "No se encontraron NodeNetworkStates", "No owner": "Sin propietario", "No physical networks defined yet": "Aún no se han definido redes físicas", + "Node label": "", "Node network configuration": "Configuración de red de nodos", "Node network configuration graph legend": "Leyenda del gráfico de configuración de la red de nodos", "Node network configuration name": "Nombre de configuración de red del nodo", "Node network is configured and managed by NM state. Create a node network configuration policy to describe the requested network configuration on your nodes in the cluster. The node network configuration enactment reports the network policies enacted upon each node.": "La red de nodos se configura y administra según el estado de NM. Cree una política de configuración de red de nodos para describir la configuración de red solicitada en los nodos del clúster. La promulgación de la configuración de red de nodos informa sobre las políticas de red implementadas en cada nodo.", "Node Selector": "Selector de nodos", + "NodeNetworkConfigurationPolicies that have been applied or are pending for this node, defining how its interfaces, VLANs, bonds, and other network settings are managed.": "", "NodeNetworkConfigurationPolicy": "NodeNetworkConfigurationPolicy", - "NodeNetworkState": "NodeNetworkState", "nodes": "nodos", "Nodes": "Nodos", "Nodes configuration": "Configuración de nodos", @@ -237,7 +242,10 @@ "Search by LLDP system name...": "Buscar por nombre del sistema LLDP…", "Search by MAC address...": "Buscar por dirección MAC…", "Search by name": "Buscar por nombre", + "Search by node label (e.g. key=value)...": "", "Search by VLAN name...": "Buscar por nombre de VLAN…", + "See all NodeNetworkConfigurationPolicies": "", + "See all nodes in this cluster": "", "Select a bridge type below to determine how your network traffic will be handled.": "Seleccione un tipo de puente a continuación para determinar cómo se manejará el tráfico de su red.", "Select from the list": "Seleccione de la lista", "Select items": "Seleccionar artículos", diff --git a/locales/fr/plugin__nmstate-console-plugin.json b/locales/fr/plugin__nmstate-console-plugin.json index 24a78919..aad948b9 100644 --- a/locales/fr/plugin__nmstate-console-plugin.json +++ b/locales/fr/plugin__nmstate-console-plugin.json @@ -34,6 +34,7 @@ "Add Label": "Ajouter une étiquette", "Add mapping": "Ajouter une carte", "Add Option": "Ajouter une option", + "Affects": "", "Aggregation mode": "Mode d'agrégation", "All": "Tous", "An error occurred": "Une erreur s’est produite", @@ -167,6 +168,7 @@ "Name": "Nom", "Name already in use by another interface": "Nom déjà utilisé par une autre interface", "Neighbors": "Voisins", + "Network": "", "Network description": "Description du réseau", "Network details": "Détails du réseau", "Network identity": "Identité du réseau", @@ -176,19 +178,22 @@ "Network state": "État du réseau", "Network state controls whether the Linux bridge is active and participating in network traffic.": "L'état du réseau détermine si le pont Linux est actif et participe au trafic réseau.", "Network types": "Types de réseaux", + "Networking policies affecting this node": "", "No": "Non", "No matching Nodes found for the labels": "Aucun nœud correspondant trouvé pour les étiquettes", + "No networking policies apply to this node.": "", "No NodeNetworkConfigurationPolicy found": "Aucune stratégie de configuration réseau de nœud trouvée", "No NodeNetworkStates found": "Aucun NodeNetworkStates trouvé", "No owner": "Aucun propriétaire", "No physical networks defined yet": "Aucun réseau physique n'est encore défini.", + "Node label": "", "Node network configuration": "Configuration du réseau de nœuds", "Node network configuration graph legend": "Légende du graphique de configuration du réseau de nœuds", "Node network configuration name": "Nom de la configuration réseau du nœud", "Node network is configured and managed by NM state. Create a node network configuration policy to describe the requested network configuration on your nodes in the cluster. The node network configuration enactment reports the network policies enacted upon each node.": "Le réseau des nœuds est configuré et géré par l'état NM. Créez une stratégie de configuration réseau pour décrire la configuration réseau souhaitée sur les nœuds du cluster. Le rapport d'exécution de la configuration réseau des nœuds indique les stratégies réseau appliquées à chaque nœud.", "Node Selector": "Sélecteur de nœud", + "NodeNetworkConfigurationPolicies that have been applied or are pending for this node, defining how its interfaces, VLANs, bonds, and other network settings are managed.": "", "NodeNetworkConfigurationPolicy": "NodeNetworkConfigurationPolicy", - "NodeNetworkState": "État du réseau du nœud", "nodes": "nœuds", "Nodes": "Nœuds", "Nodes configuration": "Configuration des nœuds", @@ -237,7 +242,10 @@ "Search by LLDP system name...": "Recherche par nom de système LLDP...", "Search by MAC address...": "Recherche par adresse MAC...", "Search by name": "Rechercher par nom", + "Search by node label (e.g. key=value)...": "", "Search by VLAN name...": "Recherche par nom de VLAN...", + "See all NodeNetworkConfigurationPolicies": "", + "See all nodes in this cluster": "", "Select a bridge type below to determine how your network traffic will be handled.": "Sélectionnez un type de pont ci-dessous pour déterminer comment votre trafic réseau sera géré.", "Select from the list": "Sélectionnez dans la liste", "Select items": "Sélectionner des éléments", diff --git a/locales/ja/plugin__nmstate-console-plugin.json b/locales/ja/plugin__nmstate-console-plugin.json index 832205d3..c806b46b 100644 --- a/locales/ja/plugin__nmstate-console-plugin.json +++ b/locales/ja/plugin__nmstate-console-plugin.json @@ -29,6 +29,7 @@ "Add Label": "ラベルの追加", "Add mapping": "マッピングの追加", "Add Option": "オプションの追加", + "Affects": "", "Aggregation mode": "アグリゲーションモード", "All": "すべて", "An error occurred": "エラーが発生しました", @@ -162,6 +163,7 @@ "Name": "名前", "Name already in use by another interface": "名前は別のインターフェイスですでに使用されています", "Neighbors": "ネイバー", + "Network": "", "Network description": "ネットワークの説明", "Network details": "ネットワークの詳細", "Network identity": "ネットワークアイデンティティー", @@ -171,19 +173,22 @@ "Network state": "ネットワーク状態", "Network state controls whether the Linux bridge is active and participating in network traffic.": "ネットワーク状態は、Linux ブリッジがアクティブで、ネットワークトラフィックに参加しているかどうかを制御します。", "Network types": "ネットワークタイプ", + "Networking policies affecting this node": "", "No": "No", "No matching Nodes found for the labels": "ラベルに一致するノードが見つかりません", + "No networking policies apply to this node.": "", "No NodeNetworkConfigurationPolicy found": "NodeNetworkConfigurationPolicy が見つかりませんでした", "No NodeNetworkStates found": "NodeNetworkStates が見つかりません", "No owner": "オーナーなし", "No physical networks defined yet": "物理ネットワークはまだ定義されていません", + "Node label": "", "Node network configuration": "ノードネットワーク設定", "Node network configuration graph legend": "ノードネットワーク設定グラフの凡例", "Node network configuration name": "ノードネットワーク設定名", "Node network is configured and managed by NM state. Create a node network configuration policy to describe the requested network configuration on your nodes in the cluster. The node network configuration enactment reports the network policies enacted upon each node.": "ノードネットワークは NM state によって設定および管理されます。クラスター内のノードに要求されたネットワーク設定を記述するノードネットワーク設定ポリシーを作成します。ノードネットワーク設定の施行は、各ノードに適用されたネットワークポリシーの状況を報告します。", "Node Selector": "ノードセレクター", + "NodeNetworkConfigurationPolicies that have been applied or are pending for this node, defining how its interfaces, VLANs, bonds, and other network settings are managed.": "", "NodeNetworkConfigurationPolicy": "NodeNetworkConfigurationPolicy", - "NodeNetworkState": "NodeNetworkState", "nodes": "ノード", "Nodes": "ノード", "Nodes configuration": "ノード設定", @@ -232,7 +237,10 @@ "Search by LLDP system name...": "LLDP システム名で検索...", "Search by MAC address...": "MAC アドレスで検索...", "Search by name": "名前で検索", + "Search by node label (e.g. key=value)...": "", "Search by VLAN name...": "VLAN 名で検索...", + "See all NodeNetworkConfigurationPolicies": "", + "See all nodes in this cluster": "", "Select a bridge type below to determine how your network traffic will be handled.": "以下のブリッジタイプを選択して、ネットワークトラフィックの処理方法を決定します。", "Select from the list": "リストから選択する", "Select items": "項目を選択する", diff --git a/locales/ko/plugin__nmstate-console-plugin.json b/locales/ko/plugin__nmstate-console-plugin.json index 1404c24f..8219ce3b 100644 --- a/locales/ko/plugin__nmstate-console-plugin.json +++ b/locales/ko/plugin__nmstate-console-plugin.json @@ -29,6 +29,7 @@ "Add Label": "레이블 추가", "Add mapping": "매핑 추가", "Add Option": "옵션 추가", + "Affects": "", "Aggregation mode": "집계 모드", "All": "전체", "An error occurred": "오류가 발생했습니다", @@ -162,6 +163,7 @@ "Name": "이름", "Name already in use by another interface": "다른 인터페이스에서 이미 사용 중인 이름", "Neighbors": "인접 노드", + "Network": "", "Network description": "네트워크 설명", "Network details": "네트워크 세부 정보", "Network identity": "네트워크 ID", @@ -171,19 +173,22 @@ "Network state": "네트워크 상태", "Network state controls whether the Linux bridge is active and participating in network traffic.": "네트워크 상태는 Linux 브리지가 활성화되고 네트워크 트래픽에 참여하는지 여부를 제어합니다.", "Network types": "네트워크 유형", + "Networking policies affecting this node": "", "No": "아니요", "No matching Nodes found for the labels": "라벨과 일치하는 노드가 없음", + "No networking policies apply to this node.": "", "No NodeNetworkConfigurationPolicy found": "NodeNetworkConfigurationPolicy를 찾을 수 없음", "No NodeNetworkStates found": "NodeNetworkStates를 찾을 수 없음", "No owner": "소유자 없음", "No physical networks defined yet": "아직 정의된 물리적 네트워크가 없음", + "Node label": "", "Node network configuration": "노드 네트워크 구성", "Node network configuration graph legend": "노드 네트워크 구성 그래프 범례", "Node network configuration name": "노드 네트워크 구성 이름", "Node network is configured and managed by NM state. Create a node network configuration policy to describe the requested network configuration on your nodes in the cluster. The node network configuration enactment reports the network policies enacted upon each node.": "노드 네트워크는 NM 상태로 구성 및 관리됩니다. 클러스터의 노드에 필요한 네트워크 구성을 설명하기 위해 노드 네트워크 구성 정책을 생성합니다. 노드 네트워크 구성 적용 내역은 각 노드에 적용된 네트워크 정책을 보고합니다.", "Node Selector": "노드 선택기", + "NodeNetworkConfigurationPolicies that have been applied or are pending for this node, defining how its interfaces, VLANs, bonds, and other network settings are managed.": "", "NodeNetworkConfigurationPolicy": "NodeNetworkConfigurationPolicy", - "NodeNetworkState": "NodeNetworkState", "nodes": "노드", "Nodes": "노드", "Nodes configuration": "노드 구성", @@ -232,7 +237,10 @@ "Search by LLDP system name...": "LLDP 시스템 이름으로 검색...", "Search by MAC address...": "MAC 주소로 검색...", "Search by name": "이름으로 검색", + "Search by node label (e.g. key=value)...": "", "Search by VLAN name...": "VLAN 이름으로 검색...", + "See all NodeNetworkConfigurationPolicies": "", + "See all nodes in this cluster": "", "Select a bridge type below to determine how your network traffic will be handled.": "아래 브리지 유형을 선택하여 네트워크 트래픽 처리 방법을 결정합니다.", "Select from the list": "목록에서 선택", "Select items": "항목 선택", diff --git a/locales/zh/plugin__nmstate-console-plugin.json b/locales/zh/plugin__nmstate-console-plugin.json index 1bed83f6..3aefaa5f 100644 --- a/locales/zh/plugin__nmstate-console-plugin.json +++ b/locales/zh/plugin__nmstate-console-plugin.json @@ -29,6 +29,7 @@ "Add Label": "添加标签", "Add mapping": "添加映射", "Add Option": "添加选项", + "Affects": "", "Aggregation mode": "聚合模式", "All": "所有", "An error occurred": "发生错误", @@ -162,6 +163,7 @@ "Name": "名称", "Name already in use by another interface": "名称已被另一个接口使用", "Neighbors": "邻居", + "Network": "", "Network description": "网络描述", "Network details": "网络详情", "Network identity": "网络身份", @@ -171,19 +173,22 @@ "Network state": "网络状态", "Network state controls whether the Linux bridge is active and participating in network traffic.": "网络状态控制 Linux 网桥是否活跃并参与网络流量。", "Network types": "网络类型", + "Networking policies affecting this node": "", "No": "否", "No matching Nodes found for the labels": "没有找到与标签匹配的节点", + "No networking policies apply to this node.": "", "No NodeNetworkConfigurationPolicy found": "没有找到 NodeNetworkConfigurationPolicy", "No NodeNetworkStates found": "没有找到 NodeNetworkStates", "No owner": "没有所有者", "No physical networks defined yet": "还没有定义物理网络", + "Node label": "", "Node network configuration": "节点网络配置", "Node network configuration graph legend": "节点网络配置图例", "Node network configuration name": "节点网络配置名称", "Node network is configured and managed by NM state. Create a node network configuration policy to describe the requested network configuration on your nodes in the cluster. The node network configuration enactment reports the network policies enacted upon each node.": "节点网络由 NM 状态配置和管理。创建一个节点网络配置策略来描述集群中节点上请求的网络配置。节点网络配置 enactment 会报告每个节点采用的网络策略。", "Node Selector": "节点选择器", + "NodeNetworkConfigurationPolicies that have been applied or are pending for this node, defining how its interfaces, VLANs, bonds, and other network settings are managed.": "", "NodeNetworkConfigurationPolicy": "NodeNetworkConfigurationPolicy", - "NodeNetworkState": "NodeNetworkState", "nodes": "节点", "Nodes": "节点", "Nodes configuration": "节点配置", @@ -232,7 +237,10 @@ "Search by LLDP system name...": "按 LLDP 系统名称搜索...", "Search by MAC address...": "按 MAC 地址搜索...", "Search by name": "按名称搜索", + "Search by node label (e.g. key=value)...": "", "Search by VLAN name...": "按 VLAN 名称搜索...", + "See all NodeNetworkConfigurationPolicies": "", + "See all nodes in this cluster": "", "Select a bridge type below to determine how your network traffic will be handled.": "在下面选择一个网桥类型,以确定您的网络流量将如何处理。", "Select from the list": "从列表中选择", "Select items": "选择项目", diff --git a/plugin-manifest.ts b/plugin-manifest.ts index f87a97b8..fb684496 100644 --- a/plugin-manifest.ts +++ b/plugin-manifest.ts @@ -1,9 +1,13 @@ +import { FeatureFlag } from '@openshift-console/dynamic-plugin-sdk'; import type { - EncodedExtension, ConsolePluginBuildMetadata, + EncodedExtension, } from '@openshift-console/dynamic-plugin-sdk-webpack'; -import { FeatureFlag } from '@openshift-console/dynamic-plugin-sdk'; +import { + NodeConfigurationNetworkPanelExtensions, + NodeConfigurationNetworkPanelModules, +} from './src/views/nodeConfigurationNetworkPanel/manifest'; import { NodeNetworkConfigurationExtensions, NodeNetworkConfigurationModules, @@ -25,6 +29,7 @@ export const pluginMetadata: ConsolePluginBuildMetadata = { ...PolicyExposedModules, ...StateExposedModules, ...NodeNetworkConfigurationModules, + ...NodeConfigurationNetworkPanelModules, ...PhysicalNetworksExposedModules, nmstateFlags: './utils/flags', }, @@ -43,5 +48,6 @@ export const extensions: EncodedExtension[] = [ ...PolicyExtensions, ...StateExtensions, ...NodeNetworkConfigurationExtensions, + ...NodeConfigurationNetworkPanelExtensions, ...PhysicalNetworksExtensions, ]; diff --git a/src/utils/components/MultiSelect/MultiSelect.tsx b/src/utils/components/MultiSelect/MultiSelect.tsx index f21060d6..82b9dc0b 100644 --- a/src/utils/components/MultiSelect/MultiSelect.tsx +++ b/src/utils/components/MultiSelect/MultiSelect.tsx @@ -8,7 +8,6 @@ import { SelectList, SelectOption, } from '@patternfly/react-core'; - import { useNMStateTranslation } from '@utils/hooks/useNMStateTranslation'; import './MultiSelect.scss'; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 6fba48fc..752b0848 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -8,3 +8,5 @@ export const NODE_HOSTNAME_LABEL = 'kubernetes.io/hostname'; export const NO_DATA_DASH = '-'; export const NMSTATE_I18N_NS = 'plugin__nmstate-console-plugin'; + +export const FORBIDDEN_STATUS = 403; diff --git a/src/views/nodenetworkconfiguration/components/BridgeIcon.tsx b/src/utils/topology/components/BridgeIcon.tsx similarity index 100% rename from src/views/nodenetworkconfiguration/components/BridgeIcon.tsx rename to src/utils/topology/components/BridgeIcon.tsx diff --git a/src/views/nodenetworkconfiguration/components/CustomGroup/CustomGroup.scss b/src/utils/topology/components/CustomGroup/CustomGroup.scss similarity index 100% rename from src/views/nodenetworkconfiguration/components/CustomGroup/CustomGroup.scss rename to src/utils/topology/components/CustomGroup/CustomGroup.scss diff --git a/src/views/nodenetworkconfiguration/components/CustomGroup/CustomGroup.tsx b/src/utils/topology/components/CustomGroup/CustomGroup.tsx similarity index 100% rename from src/views/nodenetworkconfiguration/components/CustomGroup/CustomGroup.tsx rename to src/utils/topology/components/CustomGroup/CustomGroup.tsx diff --git a/src/views/nodenetworkconfiguration/components/CustomNode/CustomNode.scss b/src/utils/topology/components/CustomNode/CustomNode.scss similarity index 100% rename from src/views/nodenetworkconfiguration/components/CustomNode/CustomNode.scss rename to src/utils/topology/components/CustomNode/CustomNode.scss diff --git a/src/views/nodenetworkconfiguration/components/CustomNode/CustomNode.tsx b/src/utils/topology/components/CustomNode/CustomNode.tsx similarity index 95% rename from src/views/nodenetworkconfiguration/components/CustomNode/CustomNode.tsx rename to src/utils/topology/components/CustomNode/CustomNode.tsx index 17a816d7..34fc28dc 100644 --- a/src/views/nodenetworkconfiguration/components/CustomNode/CustomNode.tsx +++ b/src/utils/topology/components/CustomNode/CustomNode.tsx @@ -8,7 +8,7 @@ import { WithSelectionProps, } from '@patternfly/react-topology'; -import { ICON_SIZE } from '../../utils/constants'; +import { ICON_SIZE } from '@utils/topology/utils/constants'; import './CustomNode.scss'; diff --git a/src/utils/topology/components/NodeNetworkingPoliciesPanel/NodeNetworkingPoliciesPanel.test.tsx b/src/utils/topology/components/NodeNetworkingPoliciesPanel/NodeNetworkingPoliciesPanel.test.tsx new file mode 100644 index 00000000..380332f1 --- /dev/null +++ b/src/utils/topology/components/NodeNetworkingPoliciesPanel/NodeNetworkingPoliciesPanel.test.tsx @@ -0,0 +1,107 @@ +import React from 'react'; + +import { IoK8sApiCoreV1Node } from '@kubevirt-ui/kubevirt-api/kubernetes/models'; +import { InterfaceType, V1NodeNetworkConfigurationPolicy } from '@kubevirt-ui/kubevirt-api/nmstate'; +import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; +import { cleanup, render, screen } from '@testing-library/react'; + +import NodeNetworkingPoliciesPanel from './NodeNetworkingPoliciesPanel'; + +jest.mock('react-router', () => ({ + Link: ({ to, children, ...props }: { to: string; children: React.ReactNode }) => ( + + {children} + + ), +})); + +jest.mock('@openshift-console/dynamic-plugin-sdk', () => ({ + ResourceLink: ({ name }: { name: string }) => {name}, + useK8sWatchResource: jest.fn(), +})); + +const mockUseK8sWatchResource = useK8sWatchResource as jest.Mock; + +const node = { + metadata: { + name: 'worker-0', + labels: { + 'node-role.kubernetes.io/worker': '', + }, + }, +} as IoK8sApiCoreV1Node; + +const createPolicy = ( + name: string, + nodeSelector?: Record, +): V1NodeNetworkConfigurationPolicy => + ({ + metadata: { name, uid: `${name}-uid` }, + spec: { + nodeSelector, + desiredState: { + interfaces: [{ name: 'eth0', type: InterfaceType.ETHERNET }], + }, + }, + } as V1NodeNetworkConfigurationPolicy); + +const renderPanel = ( + policies: V1NodeNetworkConfigurationPolicy[] | null, + loaded: boolean, + error: { message: string } | null = null, +) => { + mockUseK8sWatchResource.mockReturnValue([policies, loaded, error]); + return render(); +}; + +afterEach(() => { + cleanup(); + mockUseK8sWatchResource.mockReset(); +}); + +describe('NodeNetworkingPoliciesPanel', () => { + it('shows a loading spinner while policies are loading', () => { + renderPanel(null, false); + + expect(screen.getByRole('progressbar')).toBeTruthy(); + }); + + it('shows an error message when policy loading fails', () => { + renderPanel(null, true, { message: 'Failed to load policies' }); + + expect(screen.getByText('Failed to load policies')).toBeTruthy(); + }); + + it('shows empty state when no policies apply to the node', () => { + const policy = createPolicy('cluster-policy', { + 'node-role.kubernetes.io/control-plane': '', + }); + renderPanel([policy], true); + + expect(screen.getByText('No networking policies apply to this node.')).toBeTruthy(); + }); + + it('lists policies that apply to the node sorted by name', () => { + const policies = [ + createPolicy('policy-b', { 'node-role.kubernetes.io/worker': '' }), + createPolicy('policy-a'), + createPolicy('policy-c', { 'node-role.kubernetes.io/control-plane': '' }), + ]; + renderPanel(policies, true); + + expect(screen.getByText('policy-a')).toBeTruthy(); + expect(screen.getByText('policy-b')).toBeTruthy(); + expect(screen.queryByText('policy-c')).toBeNull(); + + const policyNames = screen.getAllByText(/^policy-[ab]$/).map((element) => element.textContent); + expect(policyNames).toEqual(['policy-a', 'policy-b']); + }); + + it('shows the link to all NodeNetworkConfigurationPolicies', () => { + renderPanel([], true); + + expect( + screen.getByRole('link', { name: 'See all NodeNetworkConfigurationPolicies' }), + ).toBeTruthy(); + }); +}); diff --git a/src/utils/topology/components/NodeNetworkingPoliciesPanel/NodeNetworkingPoliciesPanel.tsx b/src/utils/topology/components/NodeNetworkingPoliciesPanel/NodeNetworkingPoliciesPanel.tsx new file mode 100644 index 00000000..57c2c978 --- /dev/null +++ b/src/utils/topology/components/NodeNetworkingPoliciesPanel/NodeNetworkingPoliciesPanel.tsx @@ -0,0 +1,112 @@ +import React, { FC, useMemo } from 'react'; +import { Link } from 'react-router'; +import NodeNetworkConfigurationPolicyModel, { + NodeNetworkConfigurationPolicyModelGroupVersionKind, +} from '../../../../console-models/NodeNetworkConfigurationPolicyModel'; + +import { IoK8sApiCoreV1Node } from '@kubevirt-ui/kubevirt-api/kubernetes/models'; +import { V1NodeNetworkConfigurationPolicy } from '@kubevirt-ui/kubevirt-api/nmstate'; +import { ResourceLink, useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; +import { Spinner, Title } from '@patternfly/react-core'; +import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; +import { getResourceUrl } from '@utils/helpers'; +import { useNMStateTranslation } from '@utils/hooks/useNMStateTranslation'; +import { isPolicyAppliedInNode } from '@utils/resources/policies/utils'; + +import { formatPolicyAffectsDisplay } from './utils/formatPolicyAffectsDisplay'; + +type NodeNetworkingPoliciesPanelProps = { + node: IoK8sApiCoreV1Node; +}; + +const NodeNetworkingPoliciesPanel: FC = ({ node }) => { + const { t } = useNMStateTranslation(); + + const [policies, policiesLoaded, policiesLoadError] = useK8sWatchResource< + V1NodeNetworkConfigurationPolicy[] + >({ + groupVersionKind: NodeNetworkConfigurationPolicyModelGroupVersionKind, + isList: true, + namespaced: false, + }); + + const policiesForNode = useMemo( + () => + policies + ?.filter((policy) => isPolicyAppliedInNode(policy, node)) + .sort((a, b) => (a.metadata?.name || '').localeCompare(b.metadata?.name || '')) ?? [], + [policies, node], + ); + + const policiesListUrl = getResourceUrl({ model: NodeNetworkConfigurationPolicyModel }); + + return ( +
+ + {t('Networking policies affecting this node')} + +

+ {t( + 'NodeNetworkConfigurationPolicies that have been applied or are pending for this node, defining how its interfaces, VLANs, bonds, and other network settings are managed.', + )} +

+ + {!policiesLoaded && !policiesLoadError && ( +
+ +
+ )} + + {policiesLoadError && ( +

{policiesLoadError.message}

+ )} + + {policiesLoaded && !policiesLoadError && ( + <> + + + + + + + + + {policiesForNode.length === 0 ? ( + + + + ) : ( + policiesForNode.map((policy) => ( + + + + + )) + )} + +
{t('Name')}{t('Affects')}
+ + {t('No networking policies apply to this node.')} + +
+ + {formatPolicyAffectsDisplay(policy)}
+ + + {t('See all NodeNetworkConfigurationPolicies')} + + + )} +
+ ); +}; + +export default NodeNetworkingPoliciesPanel; diff --git a/src/utils/topology/components/NodeNetworkingPoliciesPanel/utils/formatPolicyAffectsDisplay.test.ts b/src/utils/topology/components/NodeNetworkingPoliciesPanel/utils/formatPolicyAffectsDisplay.test.ts new file mode 100644 index 00000000..ed1044d3 --- /dev/null +++ b/src/utils/topology/components/NodeNetworkingPoliciesPanel/utils/formatPolicyAffectsDisplay.test.ts @@ -0,0 +1,131 @@ +import { InterfaceType, V1NodeNetworkConfigurationPolicy } from '@kubevirt-ui/kubevirt-api/nmstate'; +import { LINK_AGGREGATION } from '@utils/components/PolicyForm/PolicyWizard/utils/constants'; +import { NO_DATA_DASH } from '@utils/constants'; + +import { formatPolicyAffectsDisplay } from './formatPolicyAffectsDisplay'; + +const createPolicy = ( + interfaces: V1NodeNetworkConfigurationPolicy['spec']['desiredState']['interfaces'], +): V1NodeNetworkConfigurationPolicy => + ({ + metadata: { name: 'test-policy' }, + spec: { + desiredState: { + interfaces, + }, + }, + } as V1NodeNetworkConfigurationPolicy); + +describe('formatPolicyAffectsDisplay', () => { + it('returns NO_DATA_DASH when policy has no interfaces', () => { + expect(formatPolicyAffectsDisplay(createPolicy(undefined))).toBe(NO_DATA_DASH); + expect(formatPolicyAffectsDisplay(createPolicy([]))).toBe(NO_DATA_DASH); + }); + + it('returns interface name for a simple ethernet interface', () => { + const policy = createPolicy([ + { + name: 'eth0', + type: InterfaceType.ETHERNET, + }, + ]); + + expect(formatPolicyAffectsDisplay(policy)).toBe('eth0'); + }); + + it('returns bond name with port names for bond interfaces', () => { + const policy = createPolicy([ + { + name: 'bond0', + type: InterfaceType.BOND, + [LINK_AGGREGATION]: { + port: ['eth0', 'eth1'], + }, + }, + ]); + + expect(formatPolicyAffectsDisplay(policy)).toBe('bond0 (eth0, eth1)'); + }); + + it('resolves bond port objects with a name property', () => { + const policy = createPolicy([ + { + name: 'bond0', + type: InterfaceType.BOND, + [LINK_AGGREGATION]: { + port: [{ name: 'eth0' }, { name: 'eth1' }], + }, + }, + ]); + + expect(formatPolicyAffectsDisplay(policy)).toBe('bond0 (eth0, eth1)'); + }); + + it('returns bridge name with port names for linux bridge interfaces', () => { + const policy = createPolicy([ + { + name: 'br0', + type: InterfaceType.LINUX_BRIDGE, + bridge: { + port: [{ name: 'eth0' }, { name: 'eth1' }], + }, + }, + ]); + + expect(formatPolicyAffectsDisplay(policy)).toBe('br0 (eth0, eth1)'); + }); + + it('returns bridge name with port names for ovs bridge interfaces', () => { + const policy = createPolicy([ + { + name: 'ovs-br0', + type: InterfaceType.OVS_BRIDGE, + bridge: { + port: [{ name: 'eth0' }], + }, + }, + ]); + + expect(formatPolicyAffectsDisplay(policy)).toBe('ovs-br0 (eth0)'); + }); + + it('returns bridge name with nested bond port names when bridge contains a bond port', () => { + const policy = createPolicy([ + { + name: 'br0', + type: InterfaceType.LINUX_BRIDGE, + bridge: { + port: [ + { name: 'eth0' }, + { + name: 'bond0', + [LINK_AGGREGATION]: { + port: ['eth1', 'eth2'], + }, + }, + ], + }, + }, + ]); + + expect(formatPolicyAffectsDisplay(policy)).toBe('br0 (eth0, eth1, eth2)'); + }); + + it('joins multiple interface affects with commas', () => { + const policy = createPolicy([ + { + name: 'eth0', + type: InterfaceType.ETHERNET, + }, + { + name: 'bond0', + type: InterfaceType.BOND, + [LINK_AGGREGATION]: { + port: ['eth1', 'eth2'], + }, + }, + ]); + + expect(formatPolicyAffectsDisplay(policy)).toBe('eth0, bond0 (eth1, eth2)'); + }); +}); diff --git a/src/utils/topology/components/NodeNetworkingPoliciesPanel/utils/formatPolicyAffectsDisplay.ts b/src/utils/topology/components/NodeNetworkingPoliciesPanel/utils/formatPolicyAffectsDisplay.ts new file mode 100644 index 00000000..a783337e --- /dev/null +++ b/src/utils/topology/components/NodeNetworkingPoliciesPanel/utils/formatPolicyAffectsDisplay.ts @@ -0,0 +1,60 @@ +import { + InterfaceType, + NodeNetworkConfigurationInterface, + V1NodeNetworkConfigurationPolicy, +} from '@kubevirt-ui/kubevirt-api/nmstate'; +import { LINK_AGGREGATION } from '@utils/components/PolicyForm/PolicyWizard/utils/constants'; +import { NO_DATA_DASH } from '@utils/constants'; + +const resolveBondPortName = (port: string | { name?: string } | undefined): string | undefined => { + if (!port) { + return undefined; + } + if (typeof port === 'string') { + return port; + } + + return port.name; +}; + +const formatBridgeInterfaceAffects = (iface: NodeNetworkConfigurationInterface): string | null => { + const ports = iface.bridge?.port || []; + const names = ports.flatMap((p) => { + const nestedPorts = p?.[LINK_AGGREGATION]?.port; + if (nestedPorts?.length) { + return nestedPorts + .map((np) => resolveBondPortName(np as string | { name?: string })) + .filter(Boolean); + } + return p?.name ? [p.name] : []; + }); + + return names.length ? `${iface.name} (${names.join(', ')})` : null; +}; + +const formatInterfaceAffects = (iface: NodeNetworkConfigurationInterface): string => { + const la = iface[LINK_AGGREGATION]; + const bondPortNames = la?.port?.map(resolveBondPortName).filter(Boolean); + + if (iface.type === InterfaceType.BOND && bondPortNames?.length) { + return `${iface.name} (${bondPortNames.join(', ')})`; + } + + if (iface.type === InterfaceType.LINUX_BRIDGE || iface.type === InterfaceType.OVS_BRIDGE) { + const bridgeText = formatBridgeInterfaceAffects(iface); + if (bridgeText) { + return bridgeText; + } + } + + return iface.name; +}; + +export const formatPolicyAffectsDisplay = (policy: V1NodeNetworkConfigurationPolicy): string => { + const interfaces = policy.spec?.desiredState?.interfaces; + if (!interfaces?.length) { + return NO_DATA_DASH; + } + + return interfaces.map(formatInterfaceAffects).join(', '); +}; diff --git a/src/utils/topology/components/TopologyControlBar/TopologyControlBar.tsx b/src/utils/topology/components/TopologyControlBar/TopologyControlBar.tsx new file mode 100644 index 00000000..9a5cf3d8 --- /dev/null +++ b/src/utils/topology/components/TopologyControlBar/TopologyControlBar.tsx @@ -0,0 +1,43 @@ +import React, { FC } from 'react'; + +import { + action, + Controller, + createTopologyControlButtons, + defaultControlButtonsOptions, + TopologyControlBar, +} from '@patternfly/react-topology'; + +type ControlBarProps = { + controller: Controller; +}; + +const ControlBar: FC = ({ controller }) => { + if (!controller) { + return null; + } + return ( + { + const scale = controller.getGraph().getScale(); + controller.getGraph().setScale(scale * 1.1); + }), + zoomOutCallback: action(() => { + const scale = controller.getGraph().getScale(); + controller.getGraph().setScale(scale * 0.9); + }), + fitToScreenCallback: action(() => { + controller.getGraph().fit(40); + }), + resetViewCallback: action(() => { + controller.getGraph().reset(); + controller.getGraph().layout(); + }), + })} + /> + ); +}; + +export default ControlBar; diff --git a/src/views/nodenetworkconfiguration/components/TopologyLegend/TopologyLegend.scss b/src/utils/topology/components/TopologyLegend/TopologyLegend.scss similarity index 100% rename from src/views/nodenetworkconfiguration/components/TopologyLegend/TopologyLegend.scss rename to src/utils/topology/components/TopologyLegend/TopologyLegend.scss diff --git a/src/views/nodenetworkconfiguration/components/TopologyLegend/TopologyLegend.tsx b/src/utils/topology/components/TopologyLegend/TopologyLegend.tsx similarity index 100% rename from src/views/nodenetworkconfiguration/components/TopologyLegend/TopologyLegend.tsx rename to src/utils/topology/components/TopologyLegend/TopologyLegend.tsx diff --git a/src/views/nodenetworkconfiguration/components/TopologyLegend/TopologyLegendItem.tsx b/src/utils/topology/components/TopologyLegend/TopologyLegendItem.tsx similarity index 100% rename from src/views/nodenetworkconfiguration/components/TopologyLegend/TopologyLegendItem.tsx rename to src/utils/topology/components/TopologyLegend/TopologyLegendItem.tsx diff --git a/src/views/nodenetworkconfiguration/components/TopologySidebar/CreatePolicyDrawer.tsx b/src/utils/topology/components/TopologySidebar/CreatePolicyDrawer.tsx similarity index 96% rename from src/views/nodenetworkconfiguration/components/TopologySidebar/CreatePolicyDrawer.tsx rename to src/utils/topology/components/TopologySidebar/CreatePolicyDrawer.tsx index 55cbcf6b..2bc3cec3 100644 --- a/src/views/nodenetworkconfiguration/components/TopologySidebar/CreatePolicyDrawer.tsx +++ b/src/utils/topology/components/TopologySidebar/CreatePolicyDrawer.tsx @@ -1,6 +1,5 @@ import React, { FC, useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router'; -import NodeNetworkConfigurationPolicyModel from 'src/console-models/NodeNetworkConfigurationPolicyModel'; import { useImmer } from 'use-immer'; import { k8sCreate } from '@openshift-console/dynamic-plugin-sdk'; @@ -17,6 +16,8 @@ import { } from '@utils/telemetry/constants'; import { logCreationFailed, logNMStateEvent, logNNCPCreated } from '@utils/telemetry/telemetry'; +import NodeNetworkConfigurationPolicyModel from '../../../../console-models/NodeNetworkConfigurationPolicyModel'; + type CreatePolicyDrawerProps = { onClose?: () => void; onSuccess?: (message: string) => void; diff --git a/src/views/nodenetworkconfiguration/components/TopologySidebar/CustomDrawer.tsx b/src/utils/topology/components/TopologySidebar/CustomDrawer.tsx similarity index 92% rename from src/views/nodenetworkconfiguration/components/TopologySidebar/CustomDrawer.tsx rename to src/utils/topology/components/TopologySidebar/CustomDrawer.tsx index 0c3f9ad8..d7fae709 100644 --- a/src/views/nodenetworkconfiguration/components/TopologySidebar/CustomDrawer.tsx +++ b/src/utils/topology/components/TopologySidebar/CustomDrawer.tsx @@ -1,10 +1,11 @@ import React, { FC, useState } from 'react'; -import StateDetailsPage from 'src/views/states/details/StateDetailsPage'; import { V1beta1NodeNetworkState } from '@kubevirt-ui/kubevirt-api/nmstate'; import { getRandomChars, isEmpty } from '@utils/helpers'; import useQueryParams from '@utils/hooks/useQueryParams'; +import StateDetailsPage from '../../../../views/states/details/StateDetailsPage'; + import useSelectedResources from './hooks/useSelectedResources'; import InterfaceDrawer from './InterfaceDrawer/InterfaceDrawer'; import { CREATE_POLICY_QUERY_PARAM, SELECTED_ID_QUERY_PARAM } from './constants'; @@ -13,8 +14,8 @@ import PolicyDrawer from './PolicyDrawer'; type DrawerProps = { states: V1beta1NodeNetworkState[]; - onClose: () => void; - onSuccess: (message: string) => void; + onClose?: () => void; + onSuccess?: (message: string) => void; }; const CustomDrawer: FC = ({ states, onClose, onSuccess }) => { diff --git a/src/views/nodenetworkconfiguration/components/TopologySidebar/InterfaceDrawer/InterfaceDrawer.tsx b/src/utils/topology/components/TopologySidebar/InterfaceDrawer/InterfaceDrawer.tsx similarity index 100% rename from src/views/nodenetworkconfiguration/components/TopologySidebar/InterfaceDrawer/InterfaceDrawer.tsx rename to src/utils/topology/components/TopologySidebar/InterfaceDrawer/InterfaceDrawer.tsx diff --git a/src/views/nodenetworkconfiguration/components/TopologySidebar/InterfaceDrawer/InterfaceDrawerDetailsTab.tsx b/src/utils/topology/components/TopologySidebar/InterfaceDrawer/InterfaceDrawerDetailsTab.tsx similarity index 100% rename from src/views/nodenetworkconfiguration/components/TopologySidebar/InterfaceDrawer/InterfaceDrawerDetailsTab.tsx rename to src/utils/topology/components/TopologySidebar/InterfaceDrawer/InterfaceDrawerDetailsTab.tsx diff --git a/src/views/nodenetworkconfiguration/components/TopologySidebar/InterfaceDrawer/InterfaceDrawerFooter.tsx b/src/utils/topology/components/TopologySidebar/InterfaceDrawer/InterfaceDrawerFooter.tsx similarity index 100% rename from src/views/nodenetworkconfiguration/components/TopologySidebar/InterfaceDrawer/InterfaceDrawerFooter.tsx rename to src/utils/topology/components/TopologySidebar/InterfaceDrawer/InterfaceDrawerFooter.tsx diff --git a/src/views/nodenetworkconfiguration/components/TopologySidebar/InterfaceDrawer/InterfaceDrawerYAMLTab.tsx b/src/utils/topology/components/TopologySidebar/InterfaceDrawer/InterfaceDrawerYAMLTab.tsx similarity index 100% rename from src/views/nodenetworkconfiguration/components/TopologySidebar/InterfaceDrawer/InterfaceDrawerYAMLTab.tsx rename to src/utils/topology/components/TopologySidebar/InterfaceDrawer/InterfaceDrawerYAMLTab.tsx diff --git a/src/views/nodenetworkconfiguration/components/TopologySidebar/InterfaceDrawer/NeighborInformation.tsx b/src/utils/topology/components/TopologySidebar/InterfaceDrawer/NeighborInformation.tsx similarity index 100% rename from src/views/nodenetworkconfiguration/components/TopologySidebar/InterfaceDrawer/NeighborInformation.tsx rename to src/utils/topology/components/TopologySidebar/InterfaceDrawer/NeighborInformation.tsx diff --git a/src/views/nodenetworkconfiguration/components/TopologySidebar/InterfaceDrawer/TopologyDrawer.tsx b/src/utils/topology/components/TopologySidebar/InterfaceDrawer/TopologyDrawer.tsx similarity index 92% rename from src/views/nodenetworkconfiguration/components/TopologySidebar/InterfaceDrawer/TopologyDrawer.tsx rename to src/utils/topology/components/TopologySidebar/InterfaceDrawer/TopologyDrawer.tsx index 23a625f5..3d277a2c 100644 --- a/src/views/nodenetworkconfiguration/components/TopologySidebar/InterfaceDrawer/TopologyDrawer.tsx +++ b/src/utils/topology/components/TopologySidebar/InterfaceDrawer/TopologyDrawer.tsx @@ -1,6 +1,5 @@ import React, { FC, ReactNode, useState } from 'react'; -import { useNavigate, useLocation } from 'react-router'; -import { useNMStateTranslation } from 'src/utils/hooks/useNMStateTranslation'; +import { useLocation, useNavigate } from 'react-router'; import { V1beta1NodeNetworkState } from '@kubevirt-ui/kubevirt-api/nmstate'; import { @@ -13,11 +12,12 @@ import { DrawerPanelBody, DrawerPanelContent, } from '@patternfly/react-core'; +import { useNMStateTranslation } from '@utils/hooks/useNMStateTranslation'; import { CREATE_POLICY_QUERY_PARAM } from '../constants'; import CustomDrawer from '../CustomDrawer'; -import '.././TopologySidebar.scss'; +import '../TopologySidebar.scss'; type Props = { states: V1beta1NodeNetworkState[]; diff --git a/src/views/nodenetworkconfiguration/components/TopologySidebar/InterfaceDrawer/constants.ts b/src/utils/topology/components/TopologySidebar/InterfaceDrawer/constants.ts similarity index 100% rename from src/views/nodenetworkconfiguration/components/TopologySidebar/InterfaceDrawer/constants.ts rename to src/utils/topology/components/TopologySidebar/InterfaceDrawer/constants.ts diff --git a/src/views/nodenetworkconfiguration/components/TopologySidebar/InterfaceDrawer/utils.ts b/src/utils/topology/components/TopologySidebar/InterfaceDrawer/utils.ts similarity index 100% rename from src/views/nodenetworkconfiguration/components/TopologySidebar/InterfaceDrawer/utils.ts rename to src/utils/topology/components/TopologySidebar/InterfaceDrawer/utils.ts diff --git a/src/views/nodenetworkconfiguration/components/TopologySidebar/PolicyDrawer.tsx b/src/utils/topology/components/TopologySidebar/PolicyDrawer.tsx similarity index 88% rename from src/views/nodenetworkconfiguration/components/TopologySidebar/PolicyDrawer.tsx rename to src/utils/topology/components/TopologySidebar/PolicyDrawer.tsx index c680c61f..307e6996 100644 --- a/src/views/nodenetworkconfiguration/components/TopologySidebar/PolicyDrawer.tsx +++ b/src/utils/topology/components/TopologySidebar/PolicyDrawer.tsx @@ -1,8 +1,5 @@ import React, { FC, useMemo } from 'react'; import { useLocation } from 'react-router'; -import PolicyDetailsPage from 'src/views/policies/details/PolicyDetailsPage'; -import PolicyPageTitle from 'src/views/policies/details/PolicyPageTitle'; -import PolicyYAMLPage from 'src/views/policies/details/PolicyYamlPage'; import { NodeNetworkConfigurationInterface, @@ -12,6 +9,10 @@ import { import { Tab, Tabs as TabsComponent, TabTitleText } from '@patternfly/react-core'; import { useNMStateTranslation } from '@utils/hooks/useNMStateTranslation'; +import PolicyDetailsPage from '../../../../views/policies/details/PolicyDetailsPage'; +import PolicyPageTitle from '../../../../views/policies/details/PolicyPageTitle'; +import PolicyYAMLPage from '../../../../views/policies/details/PolicyYamlPage'; + import './policy-drawer.scss'; type PolicyDrawerProps = { diff --git a/src/views/nodenetworkconfiguration/components/TopologySidebar/TopologySidebar.scss b/src/utils/topology/components/TopologySidebar/TopologySidebar.scss similarity index 75% rename from src/views/nodenetworkconfiguration/components/TopologySidebar/TopologySidebar.scss rename to src/utils/topology/components/TopologySidebar/TopologySidebar.scss index be27f7af..bebc517c 100644 --- a/src/views/nodenetworkconfiguration/components/TopologySidebar/TopologySidebar.scss +++ b/src/utils/topology/components/TopologySidebar/TopologySidebar.scss @@ -1,4 +1,15 @@ .nmstate-topology { + &__sidebar { + left: 0; + right: auto; + z-index: 500; + background-color: var(--pf-t--global--background--color--floating--default); + + &__content { + height: 100%; + } + + } .pf-v6-c-wizard__nav { width: 200px; } diff --git a/src/views/nodenetworkconfiguration/components/TopologySidebar/constants.ts b/src/utils/topology/components/TopologySidebar/constants.ts similarity index 100% rename from src/views/nodenetworkconfiguration/components/TopologySidebar/constants.ts rename to src/utils/topology/components/TopologySidebar/constants.ts diff --git a/src/views/nodenetworkconfiguration/components/TopologySidebar/hooks/useSelectedID.ts b/src/utils/topology/components/TopologySidebar/hooks/useSelectedID.ts similarity index 100% rename from src/views/nodenetworkconfiguration/components/TopologySidebar/hooks/useSelectedID.ts rename to src/utils/topology/components/TopologySidebar/hooks/useSelectedID.ts diff --git a/src/views/nodenetworkconfiguration/components/TopologySidebar/hooks/useSelectedResources.ts b/src/utils/topology/components/TopologySidebar/hooks/useSelectedResources.ts similarity index 96% rename from src/views/nodenetworkconfiguration/components/TopologySidebar/hooks/useSelectedResources.ts rename to src/utils/topology/components/TopologySidebar/hooks/useSelectedResources.ts index a80a6c5f..bdac510e 100644 --- a/src/views/nodenetworkconfiguration/components/TopologySidebar/hooks/useSelectedResources.ts +++ b/src/utils/topology/components/TopologySidebar/hooks/useSelectedResources.ts @@ -1,5 +1,4 @@ import { useMemo } from 'react'; -import { NodeModelGroupVersionKind } from 'src/console-models/NodeModel'; import { IoK8sApiCoreV1Node } from '@kubevirt-ui/kubevirt-api/kubernetes/models'; import { @@ -12,6 +11,8 @@ import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; import { isEmpty } from '@utils/helpers'; import { isPolicyAppliedInNode } from '@utils/resources/policies/utils'; +import { NodeModelGroupVersionKind } from '../../../../../console-models/NodeModel'; + const useSelectedResources = ( selectedId: string, states: V1beta1NodeNetworkState[], diff --git a/src/views/nodenetworkconfiguration/components/TopologySidebar/policy-drawer.scss b/src/utils/topology/components/TopologySidebar/policy-drawer.scss similarity index 100% rename from src/views/nodenetworkconfiguration/components/TopologySidebar/policy-drawer.scss rename to src/utils/topology/components/TopologySidebar/policy-drawer.scss diff --git a/src/views/nodenetworkconfiguration/utils/LevelsLayout.ts b/src/utils/topology/utils/LevelsLayout.ts similarity index 100% rename from src/views/nodenetworkconfiguration/utils/LevelsLayout.ts rename to src/utils/topology/utils/LevelsLayout.ts diff --git a/src/views/nodenetworkconfiguration/utils/constants.ts b/src/utils/topology/utils/constants.ts similarity index 100% rename from src/views/nodenetworkconfiguration/utils/constants.ts rename to src/utils/topology/utils/constants.ts diff --git a/src/views/nodenetworkconfiguration/utils/factory.ts b/src/utils/topology/utils/factory.ts similarity index 100% rename from src/views/nodenetworkconfiguration/utils/factory.ts rename to src/utils/topology/utils/factory.ts diff --git a/src/views/nodenetworkconfiguration/utils/position.ts b/src/utils/topology/utils/position.ts similarity index 81% rename from src/views/nodenetworkconfiguration/utils/position.ts rename to src/utils/topology/utils/position.ts index ba352797..8d473700 100644 --- a/src/views/nodenetworkconfiguration/utils/position.ts +++ b/src/utils/topology/utils/position.ts @@ -1,8 +1,7 @@ -import { Point, Visualization } from '@patternfly/react-topology'; +import { Controller, Point } from '@patternfly/react-topology'; +import { TOPOLOGY_LOCAL_STORAGE_KEY } from '@utils/topology/utils/constants'; -import { TOPOLOGY_LOCAL_STORAGE_KEY } from './constants'; - -export const saveNodePositions = (visualization: Visualization) => { +export const saveNodePositions = (visualization: Controller) => { const graph = visualization.getGraph(); const nodePositions = {}; @@ -19,7 +18,7 @@ export const saveNodePositions = (visualization: Visualization) => { localStorage.setItem(TOPOLOGY_LOCAL_STORAGE_KEY, JSON.stringify(nodePositions)); }; -export const restoreNodePositions = (visualization: Visualization) => { +export const restoreNodePositions = (visualization: Controller) => { const savedPositions = localStorage.getItem(TOPOLOGY_LOCAL_STORAGE_KEY); if (savedPositions) { const nodePositions = JSON.parse(savedPositions); diff --git a/src/utils/topology/utils/utils.test.ts b/src/utils/topology/utils/utils.test.ts new file mode 100644 index 00000000..3d8a6f29 --- /dev/null +++ b/src/utils/topology/utils/utils.test.ts @@ -0,0 +1,56 @@ +jest.mock('@patternfly/react-topology', () => ({ + EdgeModel: {}, + Model: {}, + ModelKind: { graph: 'graph' }, + NodeModel: {}, + NodeShape: {}, + NodeStatus: {}, +})); + +jest.mock('@utils/topology/components/BridgeIcon', () => ({ + __esModule: true, + default: () => null, +})); + +import { V1beta1NodeNetworkConfigurationEnactment } from '@kubevirt-ui/kubevirt-api/nmstate'; + +import { getCorrelatedEnactment } from './utils'; + +describe('getCorrelatedEnactment', () => { + const nodeName = 'worker-0'; + + it('returns enactment whose owner reference matches the node name', () => { + const matchingEnactment = { + metadata: { + name: 'worker-0-enactment', + ownerReferences: [{ name: nodeName }], + }, + } as V1beta1NodeNetworkConfigurationEnactment; + + const otherEnactment = { + metadata: { + name: 'worker-1-enactment', + ownerReferences: [{ name: 'worker-1' }], + }, + } as V1beta1NodeNetworkConfigurationEnactment; + + expect(getCorrelatedEnactment([otherEnactment, matchingEnactment], nodeName)).toBe( + matchingEnactment, + ); + }); + + it('returns undefined when no enactment matches the node name', () => { + const enactment = { + metadata: { + name: 'worker-1-enactment', + ownerReferences: [{ name: 'worker-1' }], + }, + } as V1beta1NodeNetworkConfigurationEnactment; + + expect(getCorrelatedEnactment([enactment], nodeName)).toBeUndefined(); + }); + + it('returns undefined for an empty enactments list', () => { + expect(getCorrelatedEnactment([], nodeName)).toBeUndefined(); + }); +}); diff --git a/src/views/nodenetworkconfiguration/utils/utils.ts b/src/utils/topology/utils/utils.ts similarity index 99% rename from src/views/nodenetworkconfiguration/utils/utils.ts rename to src/utils/topology/utils/utils.ts index f933d67d..ea322858 100644 --- a/src/views/nodenetworkconfiguration/utils/utils.ts +++ b/src/utils/topology/utils/utils.ts @@ -217,7 +217,7 @@ export const transformDataToTopologyModel = ( }; }; -const getCorrelatedEnactment = ( +export const getCorrelatedEnactment = ( availableEnhancments: V1beta1NodeNetworkConfigurationEnactment[], nnsName: string, ): V1beta1NodeNetworkConfigurationEnactment => { diff --git a/src/views/nodeConfigurationNetworkPanel/NodeTopologyPage.tsx b/src/views/nodeConfigurationNetworkPanel/NodeTopologyPage.tsx new file mode 100644 index 00000000..e8d9854b --- /dev/null +++ b/src/views/nodeConfigurationNetworkPanel/NodeTopologyPage.tsx @@ -0,0 +1,77 @@ +import React, { FC, useEffect, useMemo, useRef } from 'react'; +import { useLocation, useNavigate } from 'react-router'; + +import { IoK8sApiCoreV1Node } from '@kubevirt-ui/kubevirt-api/kubernetes/models'; +import { Flex, FlexItem } from '@patternfly/react-core'; +import { + ModelKind, + SELECTION_EVENT, + Visualization, + VisualizationProvider, +} from '@patternfly/react-topology'; + +import NodeNetworkingPoliciesPanel from '@utils/topology/components/NodeNetworkingPoliciesPanel/NodeNetworkingPoliciesPanel'; +import { SELECTED_ID_QUERY_PARAM } from '@utils/topology/components/TopologySidebar/constants'; +import { GRAPH_POSITIONING_EVENT, NODE_POSITIONING_EVENT } from '@utils/topology/utils/constants'; +import { componentFactory, layoutFactory } from '@utils/topology/utils/factory'; +import { saveNodePositions } from '@utils/topology/utils/position'; +import NodeTopology from './components/NodeTopology/NodeTopology'; + +import './components/NodeTopology/NodeTopology.scss'; + +type NodeTopologyProps = { + obj: IoK8sApiCoreV1Node; +}; + +const NodeTopologyPage: FC = ({ obj }) => { + const navigate = useNavigate(); + const location = useLocation(); + const pathnameRef = useRef(location.pathname); + + useEffect(() => { + pathnameRef.current = location.pathname; + }, [location.pathname]); + + const controller = useMemo(() => { + const visualization = new Visualization(); + visualization.registerLayoutFactory(layoutFactory); + visualization.registerComponentFactory(componentFactory); + + visualization.addEventListener(SELECTION_EVENT, (id) => { + const newParams = new URLSearchParams(id?.[0] ? { [SELECTED_ID_QUERY_PARAM]: id[0] } : {}); + navigate({ pathname: pathnameRef.current, search: newParams.toString() }); + }); + + visualization.addEventListener(NODE_POSITIONING_EVENT, () => saveNodePositions(visualization)); + visualization.addEventListener(GRAPH_POSITIONING_EVENT, () => saveNodePositions(visualization)); + visualization.setFitToScreenOnLayout(true); + visualization.fromModel({ + graph: { + id: 'nns-topology', + type: ModelKind.graph, + layout: 'Levels', + }, + }); + return visualization; + // No dependencies, we only want this to initialize once + }, []); + + return ( + + + + + + + + + + + ); +}; + +export default NodeTopologyPage; diff --git a/src/views/nodeConfigurationNetworkPanel/components/NodeTopology/NodeTopology.scss b/src/views/nodeConfigurationNetworkPanel/components/NodeTopology/NodeTopology.scss new file mode 100644 index 00000000..3767e149 --- /dev/null +++ b/src/views/nodeConfigurationNetworkPanel/components/NodeTopology/NodeTopology.scss @@ -0,0 +1,57 @@ +.nmstate-node-topology { + height: 100%; + &__topology { + height: 100%; + min-height: 600px; + } + + &__policies-panel-wrap { + flex-shrink: 0; + } + + &__policies-panel { + border-left: 1px solid var(--pf-t--global--border--color--default); + padding: var(--pf-t--global--spacer--lg); + min-width: 18rem; + max-width: 26rem; + height: 100%; + box-sizing: border-box; + background: var(--pf-t--global--background--color--primary--default); + } + + &__policies-panel-title { + font-size: var(--pf-t--global--font--size--heading--h4); + margin-bottom: var(--pf-t--global--spacer--sm); + } + + &__policies-panel-desc { + color: var(--pf-t--global--text--color--subtle); + font-size: var(--pf-t--global--font--size--body--default); + margin: 0 0 var(--pf-t--global--spacer--md); + } + + &__policies-panel-error { + color: var(--pf-t--global--text--color--status--danger--default); + font-size: var(--pf-t--global--font--size--body--sm); + margin: 0; + } + + &__policies-panel-muted { + color: var(--pf-t--global--text--color--subtle); + font-size: var(--pf-t--global--font--size--body--sm); + } + + &__policies-panel-loading { + display: flex; + justify-content: center; + padding: var(--pf-t--global--spacer--xl); + } + + &__policies-panel-table { + margin-bottom: var(--pf-t--global--spacer--md); + } + + &__policies-panel-footer-link { + font-size: var(--pf-t--global--font--size--body--default); + } +} diff --git a/src/views/nodeConfigurationNetworkPanel/components/NodeTopology/NodeTopology.tsx b/src/views/nodeConfigurationNetworkPanel/components/NodeTopology/NodeTopology.tsx new file mode 100644 index 00000000..6f03d5e8 --- /dev/null +++ b/src/views/nodeConfigurationNetworkPanel/components/NodeTopology/NodeTopology.tsx @@ -0,0 +1,129 @@ +import React, { FC, useEffect, useMemo, useRef } from 'react'; + +import { IoK8sApiCoreV1Node } from '@kubevirt-ui/kubevirt-api/kubernetes/models'; +import { + V1beta1NodeNetworkConfigurationEnactment, + V1beta1NodeNetworkState, +} from '@kubevirt-ui/kubevirt-api/nmstate'; +import { + NodeNetworkConfigurationEnactmentModelGroupVersionKind, + NodeNetworkStateModelGroupVersionKind, +} from '@models'; +import { + ListPageBody, + ListPageHeader, + useK8sWatchResource, +} from '@openshift-console/dynamic-plugin-sdk'; +import { Popover } from '@patternfly/react-core'; +import { + TopologyView, + useVisualizationController, + VisualizationSurface, +} from '@patternfly/react-topology'; +import AccessDenied from '@utils/components/AccessDenied/AccessDenied'; +import { getName } from '@utils/components/resources/selectors'; +import { FORBIDDEN_STATUS } from '@utils/constants'; +import { isEmpty } from '@utils/helpers'; +import { useNMStateTranslation } from '@utils/hooks/useNMStateTranslation'; +import useQueryParams from '@utils/hooks/useQueryParams'; +import { categorizeEnactments } from '@utils/resources/enactments/utils'; +import ControlBar from '@utils/topology/components/TopologyControlBar/TopologyControlBar'; +import TopologyLegend from '@utils/topology/components/TopologyLegend/TopologyLegend'; +import { restoreNodePositions } from '@utils/topology/utils/position'; +import { getCorrelatedEnactment, transformDataToTopologyModel } from '@utils/topology/utils/utils'; + +import NodeTopologySidebar from '../NodeTopologySidebar/NodeTopologySidebar'; +import NodeTopologyToolbar from '../NodeTopologyToolbar/NodeTopologyToolbar'; + +import './NodeTopology.scss'; + +type NodeTopologyProps = { + obj: IoK8sApiCoreV1Node; +}; + +const NodeTopology: FC = ({ obj }) => { + const { t } = useNMStateTranslation(); + const controller = useVisualizationController(); + const nodeNameRef = useRef(); + const nodeName = getName(obj); + + const queryParams = useQueryParams(); + + const [state, stateLoaded, stateError] = useK8sWatchResource({ + groupVersionKind: NodeNetworkStateModelGroupVersionKind, + isList: false, + name: nodeName, + namespaced: false, + }); + + const [enactments, enactmentsLoaded, enactmentsError] = useK8sWatchResource< + V1beta1NodeNetworkConfigurationEnactment[] + >({ + groupVersionKind: NodeNetworkConfigurationEnactmentModelGroupVersionKind, + isList: true, + namespaced: false, + }); + + const availableEnactments = useMemo(() => { + if (!enactmentsLoaded || enactmentsError || isEmpty(enactments)) { + return []; + } + const nodeEnactments = getCorrelatedEnactment(enactments, nodeName); + if (!nodeEnactments) { + return []; + } + const { available } = categorizeEnactments(nodeEnactments ? [nodeEnactments] : []); + return available; + }, [enactments, enactmentsLoaded, enactmentsError, nodeName]); + + useEffect(() => { + if (!controller || !stateLoaded || stateError || !state) { + return; + } + + const topologyModel = transformDataToTopologyModel( + [state], + [state], + availableEnactments, + null, + [nodeName], + ); + + controller.fromModel(topologyModel, true); + + if (nodeNameRef.current !== nodeName) { + nodeNameRef.current = nodeName; + restoreNodePositions(controller); + controller.getGraph().fit(40); + } + }, [controller, state, stateLoaded, stateError, availableEnactments, nodeName]); + + if (stateError?.response?.status === FORBIDDEN_STATUS) + return ( + <> + + + + + + ); + + return ( + } + viewToolbar={} + controlBar={} + > + + } + hasAutoWidth + triggerRef={() => document.getElementById('legend') as HTMLButtonElement} + /> + + ); +}; + +export default NodeTopology; diff --git a/src/views/nodeConfigurationNetworkPanel/components/NodeTopologySidebar/NodeTopologySidebar.tsx b/src/views/nodeConfigurationNetworkPanel/components/NodeTopologySidebar/NodeTopologySidebar.tsx new file mode 100644 index 00000000..46f92566 --- /dev/null +++ b/src/views/nodeConfigurationNetworkPanel/components/NodeTopologySidebar/NodeTopologySidebar.tsx @@ -0,0 +1,44 @@ +import React, { FC } from 'react'; +import { useLocation, useNavigate } from 'react-router'; + +import { V1beta1NodeNetworkState } from '@kubevirt-ui/kubevirt-api/nmstate'; +import { TopologySideBar } from '@patternfly/react-topology'; +import { isEmpty } from '@utils/helpers'; +import useQueryParams from '@utils/hooks/useQueryParams'; +import { SELECTED_ID_QUERY_PARAM } from '@utils/topology/components/TopologySidebar/constants'; +import CustomDrawer from '@utils/topology/components/TopologySidebar/CustomDrawer'; + +import '@utils/topology/components/TopologySidebar/TopologySidebar.scss'; + +type TopologySidebarProps = { + states: V1beta1NodeNetworkState[]; +}; + +const NodeTopologySidebar: FC = ({ states }) => { + const navigate = useNavigate(); + const location = useLocation(); + + const queryParams = useQueryParams(); + + const selectedIDExist = !isEmpty(queryParams?.[SELECTED_ID_QUERY_PARAM]); + + const showSidebar = selectedIDExist; + + const closeDrawer = () => { + navigate({ pathname: location.pathname, search: '' }); + }; + + return ( + +
+ +
+
+ ); +}; + +export default NodeTopologySidebar; diff --git a/src/views/nodeConfigurationNetworkPanel/components/NodeTopologyToolbar/NodeTopologyToolbar.scss b/src/views/nodeConfigurationNetworkPanel/components/NodeTopologyToolbar/NodeTopologyToolbar.scss new file mode 100644 index 00000000..9ac03221 --- /dev/null +++ b/src/views/nodeConfigurationNetworkPanel/components/NodeTopologyToolbar/NodeTopologyToolbar.scss @@ -0,0 +1,13 @@ +.nmstate-node-topology { + .pf-v6-c-toolbar__group.pf-topology-view__view-toolbar { + width: 100%; + + .pf-v6-c-toolbar { + width: 100%; + } + } + + div[id^='pf-topology-view-'] { + padding-bottom: 0; + } +} diff --git a/src/views/nodeConfigurationNetworkPanel/components/NodeTopologyToolbar/NodeTopologyToolbar.test.tsx b/src/views/nodeConfigurationNetworkPanel/components/NodeTopologyToolbar/NodeTopologyToolbar.test.tsx new file mode 100644 index 00000000..3a8e85a6 --- /dev/null +++ b/src/views/nodeConfigurationNetworkPanel/components/NodeTopologyToolbar/NodeTopologyToolbar.test.tsx @@ -0,0 +1,33 @@ +import React from 'react'; + +import { cleanup, fireEvent, render, screen } from '@testing-library/react'; + +import NodeTopologyToolbar from './NodeTopologyToolbar'; + +const mockNavigate = jest.fn(); + +jest.mock('react-router', () => ({ + useNavigate: () => mockNavigate, +})); + +afterEach(() => { + cleanup(); + mockNavigate.mockReset(); +}); + +describe('NodeTopologyToolbar', () => { + it('renders the network title and cluster navigation link', () => { + render(); + + expect(screen.getByRole('heading', { name: 'Network' })).toBeTruthy(); + expect(screen.getByRole('button', { name: 'See all nodes in this cluster' })).toBeTruthy(); + }); + + it('navigates to the cluster topology page when the link is clicked', () => { + render(); + + fireEvent.click(screen.getByRole('button', { name: 'See all nodes in this cluster' })); + + expect(mockNavigate).toHaveBeenCalledWith('/node-network-configuration'); + }); +}); diff --git a/src/views/nodeConfigurationNetworkPanel/components/NodeTopologyToolbar/NodeTopologyToolbar.tsx b/src/views/nodeConfigurationNetworkPanel/components/NodeTopologyToolbar/NodeTopologyToolbar.tsx new file mode 100644 index 00000000..ac2f02d5 --- /dev/null +++ b/src/views/nodeConfigurationNetworkPanel/components/NodeTopologyToolbar/NodeTopologyToolbar.tsx @@ -0,0 +1,33 @@ +import React, { FC } from 'react'; +import { useNavigate } from 'react-router'; + +import { Button, Flex, FlexItem, Title } from '@patternfly/react-core'; +import { useNMStateTranslation } from '@utils/hooks/useNMStateTranslation'; + +import './NodeTopologyToolbar.scss'; + +const NodeTopologyToolbar: FC = () => { + const { t } = useNMStateTranslation(); + const navigate = useNavigate(); + + return ( + + + {t('Network')} + + + + + + ); +}; + +export default NodeTopologyToolbar; diff --git a/src/views/nodeConfigurationNetworkPanel/manifest.ts b/src/views/nodeConfigurationNetworkPanel/manifest.ts new file mode 100644 index 00000000..d53bc6f7 --- /dev/null +++ b/src/views/nodeConfigurationNetworkPanel/manifest.ts @@ -0,0 +1,20 @@ +import type { EncodedExtension } from '@openshift-console/dynamic-plugin-sdk-webpack'; + +export const NodeConfigurationNetworkPanelModules = { + NodeTopologyPage: './views/nodeConfigurationNetworkPanel/NodeTopologyPage', +}; + +export const NodeConfigurationNetworkPanelExtensions: EncodedExtension[] = [ + { + type: 'console.node/sub-nav-tab', + properties: { + parentTab: 'configuration', + page: { + tabId: 'network', + name: '%plugin__nmstate-console-plugin~Network%', + priority: 100, + }, + component: { $codeRef: 'NodeTopologyPage' }, + }, + } as EncodedExtension, +]; diff --git a/src/views/nodenetworkconfiguration/Topology.tsx b/src/views/nodenetworkconfiguration/Topology.tsx index a372c642..65fa1cb7 100644 --- a/src/views/nodenetworkconfiguration/Topology.tsx +++ b/src/views/nodenetworkconfiguration/Topology.tsx @@ -1,7 +1,8 @@ import React, { FC, useEffect, useMemo, useState } from 'react'; import { useNavigate } from 'react-router'; import { NodeModelGroupVersionKind } from 'src/console-models/NodeModel'; -import './components/TopologySidebar/TopologySidebar.scss'; +import { useNMStateTranslation } from 'src/utils/hooks/useNMStateTranslation'; + import { IoK8sApiCoreV1Node } from '@kubevirt-ui/kubevirt-api/kubernetes/models'; import { V1beta1NodeNetworkConfigurationEnactment, @@ -18,11 +19,7 @@ import { } from '@openshift-console/dynamic-plugin-sdk'; import { Popover } from '@patternfly/react-core'; import { - action, - createTopologyControlButtons, - defaultControlButtonsOptions, SELECTION_EVENT, - TopologyControlBar, TopologyView, Visualization, VisualizationProvider, @@ -35,16 +32,19 @@ import useQueryParams from '@utils/hooks/useQueryParams'; import { categorizeEnactments } from '@utils/resources/enactments/utils'; import { filterPolicyAppliedNodes } from '@utils/resources/policies/utils'; -import TopologyLegend from './components/TopologyLegend/TopologyLegend'; -import { SELECTED_ID_QUERY_PARAM } from './components/TopologySidebar/constants'; -import { useNMStateTranslation } from 'src/utils/hooks/useNMStateTranslation'; -import TopologyDrawer from './components/TopologySidebar/InterfaceDrawer/TopologyDrawer'; -import { creatingPolicySignal } from './components/TopologySidebar/CreatePolicyDrawer'; +import ControlBar from '@utils/topology/components/TopologyControlBar/TopologyControlBar'; +import TopologyLegend from '@utils/topology/components/TopologyLegend/TopologyLegend'; +import { SELECTED_ID_QUERY_PARAM } from '@utils/topology/components/TopologySidebar/constants'; +import { creatingPolicySignal } from '@utils/topology/components/TopologySidebar/CreatePolicyDrawer'; +import TopologyDrawer from '@utils/topology/components/TopologySidebar/InterfaceDrawer/TopologyDrawer'; import TopologyToolbar from './components/TopologyToolbar/TopologyToolbar'; -import { GRAPH_POSITIONING_EVENT, NODE_POSITIONING_EVENT } from './utils/constants'; -import { componentFactory, layoutFactory } from './utils/factory'; -import { restoreNodePositions, saveNodePositions } from './utils/position'; -import { transformDataToTopologyModel } from './utils/utils'; +import { GRAPH_POSITIONING_EVENT, NODE_POSITIONING_EVENT } from '@utils/topology/utils/constants'; +import { componentFactory, layoutFactory } from '@utils/topology/utils/factory'; +import { restoreNodePositions, saveNodePositions } from '@utils/topology/utils/position'; +import { transformDataToTopologyModel } from '@utils/topology/utils/utils'; + +import '@utils/topology/components/TopologySidebar/TopologySidebar.scss'; +import { FORBIDDEN_STATUS } from '@utils/constants'; const Topology: FC = () => { useSignals(); @@ -149,7 +149,7 @@ const Topology: FC = () => { return () => cancelAnimationFrame(id); }, [visualization]); - if (statesError && statesError?.response?.status === 403) + if (statesError && statesError?.response?.status === FORBIDDEN_STATUS) return ( <> @@ -170,28 +170,7 @@ const Topology: FC = () => { setSelectedNodeFilters={setSelectedNodeFilters} /> } - controlBar={ - { - const scale = visualization.getGraph().getScale(); - visualization.getGraph().setScale(scale * 1.1); - }), - zoomOutCallback: action(() => { - const scale = visualization.getGraph().getScale(); - visualization.getGraph().setScale(scale * 0.9); - }), - fitToScreenCallback: action(() => { - visualization.getGraph().fit(40); - }), - resetViewCallback: action(() => { - visualization.getGraph().reset(); - visualization.getGraph().layout(); - }), - })} - /> - } + controlBar={} > diff --git a/src/views/nodenetworkconfiguration/components/TopologyToolbar/TopologyToolbar.tsx b/src/views/nodenetworkconfiguration/components/TopologyToolbar/TopologyToolbar.tsx index e3e858c9..2647923e 100644 --- a/src/views/nodenetworkconfiguration/components/TopologyToolbar/TopologyToolbar.tsx +++ b/src/views/nodenetworkconfiguration/components/TopologyToolbar/TopologyToolbar.tsx @@ -14,7 +14,7 @@ import { import { ListIcon } from '@patternfly/react-icons'; import { getResourceUrl } from '@utils/helpers'; import { useNMStateTranslation } from '@utils/hooks/useNMStateTranslation'; -import { CREATE_POLICY_QUERY_PARAM } from '../TopologySidebar/constants'; +import { CREATE_POLICY_QUERY_PARAM } from '@utils/topology/components/TopologySidebar/constants'; import TopologyToolbarFilter from './TopologyToolbarFilter'; import './TopologyToolbar.scss';