diff --git a/locales/en/plugin__networking-console-plugin.json b/locales/en/plugin__networking-console-plugin.json index aef92cd4..85eb0c1b 100644 --- a/locales/en/plugin__networking-console-plugin.json +++ b/locales/en/plugin__networking-console-plugin.json @@ -22,6 +22,7 @@ "Accessible outside the cluster": "Accessible outside the cluster", "Accessible within the cluster only": "Accessible within the cluster only", "Actions": "Actions", + "Add": "Add", "Add allowed destination": "Add allowed destination", "Add allowed source": "Add allowed source", "Add alternate Service": "Add alternate Service", @@ -37,6 +38,7 @@ "Add port": "Add port", "Add ports to restrict traffic through them. If no ports are provided, your policy will make all ports accessible to traffic.": "Add ports to restrict traffic through them. If no ports are provided, your policy will make all ports accessible to traffic.", "Add subnet": "Add subnet", + "Add virtual machines": "Add virtual machines", "All incoming traffic is denied to Pods in {{namespace}}": "All incoming traffic is denied to Pods in {{namespace}}", "All outgoing traffic is allowed by default. Egress rules can be used to restrict outgoing traffic if the cluster network provider allows it. When using the OpenShift SDN cluster network provider, egress network policy is not supported.": "All outgoing traffic is allowed by default. Egress rules can be used to restrict outgoing traffic if the cluster network provider allows it. When using the OpenShift SDN cluster network provider, egress network policy is not supported.", "All outgoing traffic is denied from Pods in {{namespace}}": "All outgoing traffic is denied from Pods in {{namespace}}", @@ -57,11 +59,15 @@ "An error occurred.": "An error occurred.", "An error occurred. Please try again.": "An error occurred. Please try again.", "An OpenShift project is an alternative representation of a Kubernetes namespace.": "An OpenShift project is an alternative representation of a Kubernetes namespace.", + "annotation_one": "annotation", + "annotation_other": "annotation", "Annotations": "Annotations", "Any namespace": "Any namespace", "Any peer": "Any peer", "Any pod": "Any pod", "Any port": "Any port", + "Are you sure you want to remove <2> from <5>{nadName}?": "Are you sure you want to remove <2> from <5>{nadName}?", + "Are you sure you want to remove <2> from <5>{udnName}?": "Are you sure you want to remove <2> from <5>{udnName}?", "Are you sure?": "Are you sure?", "Assigns IP addresses from this subnet to Pods and VirtualMachines.": "Assigns IP addresses from this subnet to Pods and VirtualMachines.", "At creation time the namespace must be configured with <2>{{label}} label. Go to <5>Namespaces to create a new namespace.": "At creation time the namespace must be configured with <2>{{label}} label. Go to <5>Namespaces to create a new namespace.", @@ -126,12 +132,14 @@ "Delete NetworkAttachmentDefinition": "Delete NetworkAttachmentDefinition", "Delete Route": "Delete Route", "Delete Service": "Delete Service", + "Deletion protection": "Deletion protection", "Deny all egress traffic": "Deny all egress traffic", "Deny all ingress traffic": "Deny all ingress traffic", "Description": "Description", "Destination CA certificate": "Destination CA certificate", "Destinations added to this rule will allow traffic from the pods defined above. Destinations in this list are combined using a logical OR operation.": "Destinations added to this rule will allow traffic from the pods defined above. Destinations in this list are combined using a logical OR operation.", "Details": "Details", + "Disabled": "Disabled", "Display name": "Display name", "Do you need to set up custom DNS?": "Do you need to set up custom DNS?", "Dual-stack clusters may set 2 subnets (one for each IP family), otherwise only 1 subnet is allowed.": "Dual-stack clusters may set 2 subnets (one for each IP family), otherwise only 1 subnet is allowed.", @@ -155,8 +163,10 @@ "Egress rule": "Egress rule", "Egress rules": "Egress rules", "Enable {{kind}}": "Enable {{kind}}", + "Enabled": "Enabled", "Error": "Error", "Error details": "Error details", + "Error loading virtual machines": "Error loading virtual machines", "Example: 100": "Example: 100", "Exceptions": "Exceptions", "External DNS name this service resolves to (e.g. example.com).": "External DNS name this service resolves to (e.g. example.com).", @@ -167,6 +177,8 @@ "External name must be a valid hostname": "External name must be a valid hostname", "External service name": "External service name", "Failed": "Failed", + "Failed to add the following virtual machines: {{names}}": "Failed to add the following virtual machines: {{names}}", + "Failed to remove virtual machine from network": "Failed to remove virtual machine from network", "Failed to retrieve the list of projects": "Failed to retrieve the list of projects", "False": "False", "Form view": "Form view", @@ -195,6 +207,7 @@ "Ingresses": "Ingresses", "Input error: selectors must start and end by a letter or number, and can only contain -, _, / or . Offending value: {{offendingSelector}}": "Input error: selectors must start and end by a letter or number, and can only contain -, _, / or . Offending value: {{offendingSelector}}", "Insecure traffic": "Insecure traffic", + "Interface": "Interface", "Invalid IP address or subnet format": "Invalid IP address or subnet format", "Invalid subnet format": "Invalid subnet format", "Invalid YAML cannot be persisted": "Invalid YAML cannot be persisted", @@ -225,6 +238,7 @@ "Location": "Location", "Location of the resource that backs the service": "Location of the resource that backs the service", "MAC spoof check": "MAC spoof check", + "Manage columns": "Manage columns", "Managed by UserDefinedNetwork": "Managed by UserDefinedNetwork", "Match Expressions": "Match Expressions", "Match Labels": "Match Labels", @@ -278,6 +292,7 @@ "No results found for \"{{inputValue}}\"": "No results found for \"{{inputValue}}\"", "No route status": "No route status", "No selector": "No selector", + "No virtual machines found": "No virtual machines found", "Node": "Node", "Node port": "Node port", "NodeNetworkConfigurationPolicy": "NodeNetworkConfigurationPolicy", @@ -338,9 +353,12 @@ "Remove all": "Remove all", "Remove alternate Service": "Remove alternate Service", "Remove exception": "Remove exception", + "Remove from network": "Remove from network", "Remove peer": "Remove peer", "Remove port": "Remove port", "Remove subnets?": "Remove subnets?", + "Remove virtual machine from network?": "Remove virtual machine from network?", + "Reset to default": "Reset to default", "Resource name": "Resource name", "Restarts": "Restarts", "Restricted Access": "Restricted Access", @@ -357,11 +375,13 @@ "Rules": "Rules", "Running": "Running", "Save": "Save", + "Search by name...": "Search by name...", "Secondary network is only assigned to pods that use k8s.v1.cni.cncf.io/networks annotation to select given network.": "Secondary network is only assigned to pods that use k8s.v1.cni.cncf.io/networks annotation to select given network.", "Secure Route": "Secure Route", "Security": "Security", "Select a Project": "Select a Project", "Select a Service": "Select a Service", + "Select all": "Select all", "Select an operator": "Select an operator", "Select ClusterUserDefinedNetwork": "Select ClusterUserDefinedNetwork", "Select default ingress and egress deny rules": "Select default ingress and egress deny rules", @@ -371,6 +391,8 @@ "Select role": "Select role", "Select target port": "Select target port", "Select termination type": "Select termination type", + "Select virtual machines to connect to this network.": "Select virtual machines to connect to this network.", + "Selected categories will be displayed in the table.": "Selected categories will be displayed in the table.", "Selector": "Selector", "Selector is required": "Selector is required", "Selector keys must be unique": "Selector keys must be unique", @@ -453,6 +475,7 @@ "View events": "View events", "View logs": "View logs", "View naming information": "View naming information", + "Virtual Machines": "Virtual Machines", "VLAN": "VLAN", "VLAN tag number": "VLAN tag number", "Weight": "Weight", diff --git a/locales/es/plugin__networking-console-plugin.json b/locales/es/plugin__networking-console-plugin.json index 1c519d3b..fe9a7aa7 100644 --- a/locales/es/plugin__networking-console-plugin.json +++ b/locales/es/plugin__networking-console-plugin.json @@ -24,6 +24,7 @@ "Accessible outside the cluster": "Accesible fuera del clúster", "Accessible within the cluster only": "Accesible solo dentro del clúster", "Actions": "Acciones", + "Add": "Add", "Add allowed destination": "Agregar destino permitido", "Add allowed source": "Agregar fuente permitida", "Add alternate Service": "Agregar servicio alternativo", @@ -39,6 +40,7 @@ "Add port": "Agregar puerto", "Add ports to restrict traffic through them. If no ports are provided, your policy will make all ports accessible to traffic.": "Agregue puertos para restringir el tráfico a través de ellos. Si no se proporcionan puertos, su política hará que todos los puertos sean accesibles al tráfico.", "Add subnet": "Agregar subred", + "Add virtual machines": "Add virtual machines", "All incoming traffic is denied to Pods in {{namespace}}": "Se rechaza todo el tráfico entrante a los Pods en {{namespace}}", "All outgoing traffic is allowed by default. Egress rules can be used to restrict outgoing traffic if the cluster network provider allows it. When using the OpenShift SDN cluster network provider, egress network policy is not supported.": "Todo el tráfico saliente está permitido de forma predeterminada. Se pueden utilizar reglas de salida para restringir el tráfico saliente si el proveedor de red del clúster lo permite. Cuando se utiliza el proveedor de red del clúster OpenShift SDN, no se admite la política de red de salida.", "All outgoing traffic is denied from Pods in {{namespace}}": "Se deniega todo el tráfico saliente desde los Pods en {{namespace}}", @@ -59,11 +61,16 @@ "An error occurred.": "Ocurrió un error.", "An error occurred. Please try again.": "Ocurrió un error. Inténtelo de nuevo.", "An OpenShift project is an alternative representation of a Kubernetes namespace.": "Un proyecto OpenShift es una representación alternativa de un espacio de nombres de Kubernetes.", + "annotation_one": "annotation", + "annotation_many": "annotation", + "annotation_other": "annotation", "Annotations": "Anotaciones", "Any namespace": "Cualquier espacio de nombres", "Any peer": "Cualquier par", "Any pod": "Cualquier pod", "Any port": "Cualquier puerto", + "Are you sure you want to remove <2> from <5>{nadName}?": "Are you sure you want to remove <2> from <5>{nadName}?", + "Are you sure you want to remove <2> from <5>{udnName}?": "Are you sure you want to remove <2> from <5>{udnName}?", "Are you sure?": "¿Está seguro?", "Assigns IP addresses from this subnet to Pods and VirtualMachines.": "Asigna direcciones IP de esta subred a pods y máquinas virtuales.", "At creation time the namespace must be configured with <2>{{label}} label. Go to <5>Namespaces to create a new namespace.": "En el momento de la creación, el espacio de nombre debe configurarse con la etiqueta <2>{{label}}. Vaya a<5> Espacios de nombre para crear un nuevo espacio de nombre.", @@ -128,12 +135,14 @@ "Delete NetworkAttachmentDefinition": "Eliminar NetworkAttachmentDefinition", "Delete Route": "Eliminar ruta", "Delete Service": "Eliminar servicio", + "Deletion protection": "Deletion protection", "Deny all egress traffic": "Denegar todo el tráfico de salida", "Deny all ingress traffic": "Denegar todo el tráfico de entrada", "Description": "Descripción", "Destination CA certificate": "Certificado CA de destino", "Destinations added to this rule will allow traffic from the pods defined above. Destinations in this list are combined using a logical OR operation.": "Los destinos agregados a esta regla permitirán el tráfico desde los pods definidos anteriormente. Los destinos de esta lista se combinan mediante una operación lógica OR.", "Details": "Detalles", + "Disabled": "Disabled", "Display name": "Nombre para mostrar", "Do you need to set up custom DNS?": "¿Necesita configurar DNS personalizado?", "Dual-stack clusters may set 2 subnets (one for each IP family), otherwise only 1 subnet is allowed.": "Los clústeres de doble pila pueden configurar dos subredes (una para cada familia de IP). De lo contrario, solo se permite una subred.", @@ -157,8 +166,10 @@ "Egress rule": "Regla de salida", "Egress rules": "Reglas de salida", "Enable {{kind}}": "Permitir {{kind}}", + "Enabled": "Enabled", "Error": "Error", "Error details": "Error de detalles", + "Error loading virtual machines": "Error loading virtual machines", "Example: 100": "Ejemplo: 100", "Exceptions": "Excepciones", "External DNS name this service resolves to (e.g. example.com).": "External DNS name this service resolves to (e.g. example.com).", @@ -169,6 +180,8 @@ "External name must be a valid hostname": "External name must be a valid hostname", "External service name": "Nombre del servicio externo", "Failed": "Fallido", + "Failed to add the following virtual machines: {{names}}": "Failed to add the following virtual machines: {{names}}", + "Failed to remove virtual machine from network": "Failed to remove virtual machine from network", "Failed to retrieve the list of projects": "No se pudo recuperar la lista de proyectos", "False": "Falso", "Form view": "Vista de formulario", @@ -197,6 +210,7 @@ "Ingresses": "Ingresos", "Input error: selectors must start and end by a letter or number, and can only contain -, _, / or . Offending value: {{offendingSelector}}": "Error de entrada: los selectores deben comenzar y terminar con una letra o número, y solo pueden contener -, _, / o . Valor ofensivo: {{offendingSelector}}", "Insecure traffic": "Tráfico inseguro", + "Interface": "Interface", "Invalid IP address or subnet format": "Dirección IP o formato de subred no válidos", "Invalid subnet format": "Formato de subred no válido", "Invalid YAML cannot be persisted": "YAML no válido no se puede conservar", @@ -227,6 +241,7 @@ "Location": "Ubicación", "Location of the resource that backs the service": "Ubicación del recurso que respalda el servicio.", "MAC spoof check": "Comprobación de falsificación de MAC", + "Manage columns": "Manage columns", "Managed by UserDefinedNetwork": "Administrado por UserDefinedNetwork", "Match Expressions": "Expresiones de coincidencia", "Match Labels": "Etiquetas de coincidencia", @@ -280,6 +295,7 @@ "No results found for \"{{inputValue}}\"": "No se encontraron resultados para \"{{inputValue}}\"", "No route status": "Sin estado de ruta", "No selector": "Sin selector", + "No virtual machines found": "No virtual machines found", "Node": "Node", "Node port": "Puerto de nodo", "NodeNetworkConfigurationPolicy": "NodeNetworkConfigurationPolicy", @@ -340,9 +356,12 @@ "Remove all": "Quitar todo", "Remove alternate Service": "Quitar servicio alternativo", "Remove exception": "Quitar excepción", + "Remove from network": "Remove from network", "Remove peer": "Quitar par", "Remove port": "Quitar puerto", "Remove subnets?": "¿Desea eliminar subredes?", + "Remove virtual machine from network?": "Remove virtual machine from network?", + "Reset to default": "Reset to default", "Resource name": "Nombre del recurso", "Restarts": "Restarts", "Restricted Access": "Acceso restringido", @@ -359,11 +378,13 @@ "Rules": "Normas", "Running": "En ejecución", "Save": "Guardar", + "Search by name...": "Search by name...", "Secondary network is only assigned to pods that use k8s.v1.cni.cncf.io/networks annotation to select given network.": "La red secundaria solo se asigna a los pods que usan la anotación k8s.v1.cni.cncf.io/networks para seleccionar la red determinada.", "Secure Route": "Ruta segura", "Security": "Seguridad", "Select a Project": "Seleccionar un proyecto", "Select a Service": "Seleccionar un servicio", + "Select all": "Select all", "Select an operator": "Seleccionar un operador", "Select ClusterUserDefinedNetwork": "Seleccionar ClusterUserDefinedNetwork", "Select default ingress and egress deny rules": "Seleccione reglas predeterminadas de denegación de entrada y salida", @@ -373,6 +394,8 @@ "Select role": "Seleccionar rol", "Select target port": "Seleccionar puerto de destino", "Select termination type": "Seleccionar tipo de terminación", + "Select virtual machines to connect to this network.": "Select virtual machines to connect to this network.", + "Selected categories will be displayed in the table.": "Selected categories will be displayed in the table.", "Selector": "Selector", "Selector is required": "Selector is required", "Selector keys must be unique": "Selector keys must be unique", @@ -455,6 +478,7 @@ "View events": "Ver eventos", "View logs": "Ver registros", "View naming information": "Ver información de nombres", + "Virtual Machines": "Virtual Machines", "VLAN": "VLAN", "VLAN tag number": "Número de etiqueta VLAN", "Weight": "Peso", diff --git a/locales/fr/plugin__networking-console-plugin.json b/locales/fr/plugin__networking-console-plugin.json index b5e29230..e3a177ed 100644 --- a/locales/fr/plugin__networking-console-plugin.json +++ b/locales/fr/plugin__networking-console-plugin.json @@ -24,6 +24,7 @@ "Accessible outside the cluster": "Accessible en dehors du cluster", "Accessible within the cluster only": "Accessible au sein du cluster uniquement", "Actions": "Actions", + "Add": "Add", "Add allowed destination": "Ajouter une destination autorisée", "Add allowed source": "Ajouter une source autorisée", "Add alternate Service": "Ajouter un service alternatif", @@ -39,6 +40,7 @@ "Add port": "Ajouter un port", "Add ports to restrict traffic through them. If no ports are provided, your policy will make all ports accessible to traffic.": "Ajoutez les ports pour lesquels restreindre le trafic. Si aucun port n’est indiqué dans votre stratégie, tous les ports seront accessibles au trafic.", "Add subnet": "Ajouter un sous-réseau", + "Add virtual machines": "Add virtual machines", "All incoming traffic is denied to Pods in {{namespace}}": "Tout le trafic entrant est refusé aux pods dans {{namespace}}", "All outgoing traffic is allowed by default. Egress rules can be used to restrict outgoing traffic if the cluster network provider allows it. When using the OpenShift SDN cluster network provider, egress network policy is not supported.": "Tout le trafic sortant est autorisé par défaut. Les règles de sortie peuvent être utilisées pour restreindre le trafic sortant si le fournisseur de réseau du cluster le permet. Lors de l’utilisation du fournisseur de réseau de cluster OpenShift SDN, la stratégie réseau de sortie n’est pas prise en charge.", "All outgoing traffic is denied from Pods in {{namespace}}": "Tout le trafic sortant issu des pods dans {{namespace}} est refusé", @@ -59,11 +61,16 @@ "An error occurred.": "Une erreur s’est produite.", "An error occurred. Please try again.": "Une erreur s’est produite. Veuillez réessayer.", "An OpenShift project is an alternative representation of a Kubernetes namespace.": "Un projet OpenShift est une représentation alternative d’un espace de noms Kubernetes.", + "annotation_one": "annotation", + "annotation_many": "annotation", + "annotation_other": "annotation", "Annotations": "Annotations", "Any namespace": "N’importe quel espace de noms", "Any peer": "N’importe quel homologue", "Any pod": "N’importe quel pod", "Any port": "N’importe quel port", + "Are you sure you want to remove <2> from <5>{nadName}?": "Are you sure you want to remove <2> from <5>{nadName}?", + "Are you sure you want to remove <2> from <5>{udnName}?": "Are you sure you want to remove <2> from <5>{udnName}?", "Are you sure?": "Confirmez-vous cette action\u00a0?", "Assigns IP addresses from this subnet to Pods and VirtualMachines.": "Attribue des adresses IP de ce sous-réseau aux pods et aux machines virtuelles.", "At creation time the namespace must be configured with <2>{{label}} label. Go to <5>Namespaces to create a new namespace.": "Lors de sa création, l'espace de noms doit être configuré avec<2>{{label}} étiquette. Aller à<5> Espaces de noms pour créer un nouvel espace de noms.", @@ -128,12 +135,14 @@ "Delete NetworkAttachmentDefinition": "Supprimer NetworkAttachmentDefinition", "Delete Route": "Supprimer Route", "Delete Service": "Supprimer Service", + "Deletion protection": "Deletion protection", "Deny all egress traffic": "Refuser tout le trafic sortant", "Deny all ingress traffic": "Refuser tout le trafic entrant", "Description": "Description", "Destination CA certificate": "Certificat d’autorité de certification de destination", "Destinations added to this rule will allow traffic from the pods defined above. Destinations in this list are combined using a logical OR operation.": "Les destinations ajoutées à cette règle autoriseront le trafic provenant des pods définis ci-dessus. Les destinations de cette liste sont combinées à l’aide d’une opération logique OU.", "Details": "Détails", + "Disabled": "Disabled", "Display name": "Nom complet", "Do you need to set up custom DNS?": "Avez-vous besoin de configurer un DNS\u00a0personnalisé\u00a0?", "Dual-stack clusters may set 2 subnets (one for each IP family), otherwise only 1 subnet is allowed.": "Les clusters à double pile peuvent définir 2 sous-réseaux (un pour chaque famille IP), sinon un seul sous-réseau est autorisé.", @@ -157,8 +166,10 @@ "Egress rule": "Règle de sortie", "Egress rules": "Règles de sortie", "Enable {{kind}}": "Activer {{kind}}", + "Enabled": "Enabled", "Error": "Erreur", "Error details": "Détails de l’erreur", + "Error loading virtual machines": "Error loading virtual machines", "Example: 100": "Exemple: 100", "Exceptions": "Exceptions", "External DNS name this service resolves to (e.g. example.com).": "External DNS name this service resolves to (e.g. example.com).", @@ -169,6 +180,8 @@ "External name must be a valid hostname": "External name must be a valid hostname", "External service name": "Nom du service externe", "Failed": "Ayant échoué", + "Failed to add the following virtual machines: {{names}}": "Failed to add the following virtual machines: {{names}}", + "Failed to remove virtual machine from network": "Failed to remove virtual machine from network", "Failed to retrieve the list of projects": "Impossible de récupérer la liste des projets", "False": "Faux", "Form view": "Vue Formulaire", @@ -197,6 +210,7 @@ "Ingresses": "Entrées", "Input error: selectors must start and end by a letter or number, and can only contain -, _, / or . Offending value: {{offendingSelector}}": "Erreur de saisie\u00a0: les sélecteurs doivent commencer et se terminer par une lettre ou un chiffre, et ne peuvent contenir que les caractères -, _, / ou . Valeur erronée\u00a0: {{offendingSelector}}", "Insecure traffic": "Trafic non sécurisé", + "Interface": "Interface", "Invalid IP address or subnet format": "Adresse IP ou format de sous-réseau non valide", "Invalid subnet format": "Format de sous-réseau non valide", "Invalid YAML cannot be persisted": "Un code YAML non valide ne peut pas être rendu persistant", @@ -227,6 +241,7 @@ "Location": "Emplacement", "Location of the resource that backs the service": "Emplacement de la ressource qui soutient le service", "MAC spoof check": "Vérification d’usurpation d’identité MAC", + "Manage columns": "Manage columns", "Managed by UserDefinedNetwork": "Géré par UserDefinedNetwork", "Match Expressions": "Correspondance des expressions", "Match Labels": "Étiquettes correspondantes", @@ -280,6 +295,7 @@ "No results found for \"{{inputValue}}\"": "Aucun résultat trouvé pour \"{{inputValue}} \"", "No route status": "Aucun statut d’itinéraire", "No selector": "Aucun sélecteur", + "No virtual machines found": "No virtual machines found", "Node": "Node", "Node port": "Port du nœud", "NodeNetworkConfigurationPolicy": "NodeNetworkConfigurationPolicy", @@ -340,9 +356,12 @@ "Remove all": "Supprimer tout", "Remove alternate Service": "Supprimer un service alternatif", "Remove exception": "Supprimer l’exception", + "Remove from network": "Remove from network", "Remove peer": "Supprimer l’homologue", "Remove port": "Supprimer le port", "Remove subnets?": "Supprimer les sous-réseaux\u00a0?", + "Remove virtual machine from network?": "Remove virtual machine from network?", + "Reset to default": "Reset to default", "Resource name": "Nom de la ressource", "Restarts": "Restarts", "Restricted Access": "Accès restreint", @@ -359,11 +378,13 @@ "Rules": "Règles", "Running": "En cours d’exécution", "Save": "Enregistrer", + "Search by name...": "Search by name...", "Secondary network is only assigned to pods that use k8s.v1.cni.cncf.io/networks annotation to select given network.": "Le réseau secondaire n'est attribué qu'aux pods qui utilisent l'annotation k8s.v1.cni.cncf.io/networks pour sélectionner un réseau donné.", "Secure Route": "Sécuriser l’itinéraire", "Security": "Sécurité", "Select a Project": "Sélectionnez un projet", "Select a Service": "Sélectionner un service", + "Select all": "Select all", "Select an operator": "Sélectionnez un opérateur", "Select ClusterUserDefinedNetwork": "Sélectionnez ClusterUserDefinedNetwork", "Select default ingress and egress deny rules": "Sélectionner les règles de refus d’entrée et de sortie par défaut", @@ -373,6 +394,8 @@ "Select role": "Sélectionner un rôle", "Select target port": "Sélectionner le port cible", "Select termination type": "Sélectionner le type de terminaison", + "Select virtual machines to connect to this network.": "Select virtual machines to connect to this network.", + "Selected categories will be displayed in the table.": "Selected categories will be displayed in the table.", "Selector": "Selector", "Selector is required": "Selector is required", "Selector keys must be unique": "Selector keys must be unique", @@ -455,6 +478,7 @@ "View events": "Afficher les événements", "View logs": "Afficher les journaux", "View naming information": "Afficher les informations sur l’attribution de nom", + "Virtual Machines": "Virtual Machines", "VLAN": "VLAN", "VLAN tag number": "Numéro de balise VLAN", "Weight": "Poids", diff --git a/locales/ja/plugin__networking-console-plugin.json b/locales/ja/plugin__networking-console-plugin.json index 1a5ec408..8ecc82de 100644 --- a/locales/ja/plugin__networking-console-plugin.json +++ b/locales/ja/plugin__networking-console-plugin.json @@ -22,6 +22,7 @@ "Accessible outside the cluster": "クラスター外からアクセス可能", "Accessible within the cluster only": "クラスター内でのみアクセス可能", "Actions": "アクション", + "Add": "Add", "Add allowed destination": "許可される宛先の追加", "Add allowed source": "許可されるソースの追加", "Add alternate Service": "代替 Service の追加", @@ -37,6 +38,7 @@ "Add port": "ポートの追加", "Add ports to restrict traffic through them. If no ports are provided, your policy will make all ports accessible to traffic.": "トラフィックを制限するポートを追加します。ポートが指定されていない場合、ポリシーによりすべてのポートでトラフィックへのアクセスが可能になります。", "Add subnet": "サブネットの追加", + "Add virtual machines": "Add virtual machines", "All incoming traffic is denied to Pods in {{namespace}}": "すべての受信トラフィックが {{namespace}} の Pod に対して拒否されます", "All outgoing traffic is allowed by default. Egress rules can be used to restrict outgoing traffic if the cluster network provider allows it. When using the OpenShift SDN cluster network provider, egress network policy is not supported.": "すべての送信トラフィックはデフォルトで許可されます。Egress ルールは、クラスターネットワークプロバイダーが許可する場合に、送信トラフィックを制限するために使用できます。OpenShift SDN クラスターネットワークプロバイダーを使用する場合、Egress ネットワークポリシーはサポートされません。", "All outgoing traffic is denied from Pods in {{namespace}}": "すべての送信トラフィックは {{namespace}} の Pod で拒否されます", @@ -57,11 +59,14 @@ "An error occurred.": "エラーが発生しました。", "An error occurred. Please try again.": "エラーが発生しました。もう一度試してください。", "An OpenShift project is an alternative representation of a Kubernetes namespace.": "OpenShift プロジェクトは、Kubernetes namespace を別の形で表現したものです。", + "annotation_other": "annotation", "Annotations": "アノテーション", "Any namespace": "任意の namespace", "Any peer": "任意のピア", "Any pod": "任意の Pod", "Any port": "任意のポート", + "Are you sure you want to remove <2> from <5>{nadName}?": "Are you sure you want to remove <2> from <5>{nadName}?", + "Are you sure you want to remove <2> from <5>{udnName}?": "Are you sure you want to remove <2> from <5>{udnName}?", "Are you sure?": "操作を実行してもよろしいですか?", "Assigns IP addresses from this subnet to Pods and VirtualMachines.": "このサブネットから Pod と VirtualMachine に IP アドレスを割り当てます。", "At creation time the namespace must be configured with <2>{{label}} label. Go to <5>Namespaces to create a new namespace.": "作成時に、namespace に <2>{{label}} ラベルを設定する必要があります。<5>Namespaces に移動して、新しい namespace を作成してください。", @@ -126,12 +131,14 @@ "Delete NetworkAttachmentDefinition": "NetworkAttachmentDefinition の削除", "Delete Route": "Route の削除", "Delete Service": "Service の削除", + "Deletion protection": "Deletion protection", "Deny all egress traffic": "すべての Egress トラフィックを拒否", "Deny all ingress traffic": "すべての Ingress トラフィックを拒否", "Description": "説明", "Destination CA certificate": "宛先 CA 証明書", "Destinations added to this rule will allow traffic from the pods defined above. Destinations in this list are combined using a logical OR operation.": "このルールに追加された宛先は、上記で定義された Pod からのトラフィックを許可します。このリストの宛先は、論理 OR 演算を使用して結合されます。", "Details": "詳細", + "Disabled": "Disabled", "Display name": "表示名", "Do you need to set up custom DNS?": "カスタム DNS を設定する必要がありますか?", "Dual-stack clusters may set 2 subnets (one for each IP family), otherwise only 1 subnet is allowed.": "デュアルスタッククラスターでは、2 つのサブネット (IP ファミリーごとに 1 つ) を設定できます。それ以外の場合、サブネットは 1 つだけ許可されます。", @@ -155,8 +162,10 @@ "Egress rule": "Egress ルール", "Egress rules": "Egress ルール", "Enable {{kind}}": "{{kind}} の有効化", + "Enabled": "Enabled", "Error": "エラー", "Error details": "エラーの詳細", + "Error loading virtual machines": "Error loading virtual machines", "Example: 100": "例: 100", "Exceptions": "例外", "External DNS name this service resolves to (e.g. example.com).": "External DNS name this service resolves to (e.g. example.com).", @@ -167,6 +176,8 @@ "External name must be a valid hostname": "External name must be a valid hostname", "External service name": "外部サービス名", "Failed": "失敗", + "Failed to add the following virtual machines: {{names}}": "Failed to add the following virtual machines: {{names}}", + "Failed to remove virtual machine from network": "Failed to remove virtual machine from network", "Failed to retrieve the list of projects": "プロジェクトのリストを取得できませんでした", "False": "False", "Form view": "フォームビュー", @@ -195,6 +206,7 @@ "Ingresses": "Ingresses", "Input error: selectors must start and end by a letter or number, and can only contain -, _, / or . Offending value: {{offendingSelector}}": "入力エラー: セレクターは文字または数字で開始および終了する必要があり、-、_、/、または . しか含めることができません。無効な値: {{offendingSelector}}", "Insecure traffic": "非セキュアなトラフィック", + "Interface": "Interface", "Invalid IP address or subnet format": "無効な IP アドレスまたはサブネット形式", "Invalid subnet format": "無効なサブネット形式", "Invalid YAML cannot be persisted": "無効な YAML を永続化できません", @@ -225,6 +237,7 @@ "Location": "場所", "Location of the resource that backs the service": "サービスを支えるリソースの場所", "MAC spoof check": "MAC spoof チェック", + "Manage columns": "Manage columns", "Managed by UserDefinedNetwork": "UserDefinedNetwork によって管理されています", "Match Expressions": "一致式", "Match Labels": "一致ラベル", @@ -278,6 +291,7 @@ "No results found for \"{{inputValue}}\"": "\"{{inputValue}}\" の結果が見つかりません", "No route status": "ルートステータスがありません", "No selector": "セレクターがありません", + "No virtual machines found": "No virtual machines found", "Node": "Node", "Node port": "ノードポート", "NodeNetworkConfigurationPolicy": "NodeNetworkConfigurationPolicy", @@ -338,9 +352,12 @@ "Remove all": "すべてを削除", "Remove alternate Service": "代替 Service の削除", "Remove exception": "例外の削除", + "Remove from network": "Remove from network", "Remove peer": "ピアの削除", "Remove port": "ポートの削除", "Remove subnets?": "サブネットを削除しますか?", + "Remove virtual machine from network?": "Remove virtual machine from network?", + "Reset to default": "Reset to default", "Resource name": "リソース名", "Restarts": "Restarts", "Restricted Access": "制限されたアクセス", @@ -357,11 +374,13 @@ "Rules": "ルール", "Running": "実行中", "Save": "保存", + "Search by name...": "Search by name...", "Secondary network is only assigned to pods that use k8s.v1.cni.cncf.io/networks annotation to select given network.": "セカンダリーネットワークは、k8s.v1.cni.cncf.io/networks アノテーションを使用して特定のネットワークを選択する Pod にのみ割り当てられます。", "Secure Route": "セキュアなルート", "Security": "セキュリティー", "Select a Project": "プロジェクトの選択", "Select a Service": "Service の選択", + "Select all": "Select all", "Select an operator": "演算子の選択", "Select ClusterUserDefinedNetwork": "ClusterUserDefinedNetwork の選択", "Select default ingress and egress deny rules": "デフォルトの Ingress および Egress 拒否ルールの選択", @@ -371,6 +390,8 @@ "Select role": "ロールの選択", "Select target port": "ターゲットポートの選択", "Select termination type": "終端タイプの選択", + "Select virtual machines to connect to this network.": "Select virtual machines to connect to this network.", + "Selected categories will be displayed in the table.": "Selected categories will be displayed in the table.", "Selector": "Selector", "Selector is required": "Selector is required", "Selector keys must be unique": "Selector keys must be unique", @@ -453,6 +474,7 @@ "View events": "イベントの表示", "View logs": "ログの表示", "View naming information": "命名情報の表示", + "Virtual Machines": "Virtual Machines", "VLAN": "VLAN", "VLAN tag number": "VLAN タグ番号", "Weight": "重み", diff --git a/locales/ko/plugin__networking-console-plugin.json b/locales/ko/plugin__networking-console-plugin.json index 36e0142f..b0fe8c86 100644 --- a/locales/ko/plugin__networking-console-plugin.json +++ b/locales/ko/plugin__networking-console-plugin.json @@ -22,6 +22,7 @@ "Accessible outside the cluster": "클러스터 외부에서 액세스 가능", "Accessible within the cluster only": "클러스터 내에서만 액세스 가능", "Actions": "작업", + "Add": "Add", "Add allowed destination": "허용된 대상 추가", "Add allowed source": "허용된 소스 추가", "Add alternate Service": "대체 서비스 추가", @@ -37,6 +38,7 @@ "Add port": "포트 추가", "Add ports to restrict traffic through them. If no ports are provided, your policy will make all ports accessible to traffic.": "포트를 추가하여 포트를 통한 트래픽을 제한합니다. 포트가 제공되지 않으면 정책에 따라 모든 포트가 트래픽에 액세스할 수 있습니다.", "Add subnet": "서브넷 추가", + "Add virtual machines": "Add virtual machines", "All incoming traffic is denied to Pods in {{namespace}}": "수신되는 모든 트래픽은 {{namespace}}의 Pod에 거부됩니다.", "All outgoing traffic is allowed by default. Egress rules can be used to restrict outgoing traffic if the cluster network provider allows it. When using the OpenShift SDN cluster network provider, egress network policy is not supported.": "모든 나가는 트래픽은 기본적으로 허용됩니다. 클러스터 네트워크 공급자가 송신 트래픽을 허용하는 경우 송신 규칙을 사용하여 나가는 트래픽을 제한할 수 있습니다. OpenShift SDN 클러스터 네트워크 공급자를 사용하는 경우 송신 네트워크 정책이 지원되지 않습니다.", "All outgoing traffic is denied from Pods in {{namespace}}": "모든 나가는 트래픽은 {{namespace}}의 Pod에서 거부됩니다.", @@ -57,11 +59,14 @@ "An error occurred.": "오류가 발생하였습니다.", "An error occurred. Please try again.": "오류가 발생했습니다. 다시 시도하십시오.", "An OpenShift project is an alternative representation of a Kubernetes namespace.": "OpenShift 프로젝트는 Kubernetes 네임스페이스의 대체 표현입니다.", + "annotation_other": "annotation", "Annotations": "주석", "Any namespace": "모든 네임 스페이스", "Any peer": "모든 사용자", "Any pod": "모든 pod", "Any port": "모든 포트", + "Are you sure you want to remove <2> from <5>{nadName}?": "Are you sure you want to remove <2> from <5>{nadName}?", + "Are you sure you want to remove <2> from <5>{udnName}?": "Are you sure you want to remove <2> from <5>{udnName}?", "Are you sure?": "정말로 실행하시겠습니까?", "Assigns IP addresses from this subnet to Pods and VirtualMachines.": "이 서브넷의 IP 주소를 Pod 및 VirtualMachines에 할당합니다.", "At creation time the namespace must be configured with <2>{{label}} label. Go to <5>Namespaces to create a new namespace.": "생성 시 <2>{{label}} 라벨을 사용하여 네임스페이스를 구성해야 합니다. 새 네임스페이스를 생성하려면 <5>Namespaces 로 이동하십시오.", @@ -126,12 +131,14 @@ "Delete NetworkAttachmentDefinition": "NetworkAttachmentDefinition 삭제", "Delete Route": "경로 삭제", "Delete Service": "서비스 삭제", + "Deletion protection": "Deletion protection", "Deny all egress traffic": "모든 Egress 트래픽 거부", "Deny all ingress traffic": "모든 Ingress 트래픽 거부", "Description": "설명", "Destination CA certificate": "대상 CA 인증서", "Destinations added to this rule will allow traffic from the pods defined above. Destinations in this list are combined using a logical OR operation.": "이 규칙에 추가된 대상은 위에 정의된 pod의 트래픽을 허용합니다. 이 목록의 대상은 논리적 OR 연산을 사용하여 결합됩니다.", "Details": "세부 정보", + "Disabled": "Disabled", "Display name": "표시 이름", "Do you need to set up custom DNS?": "사용자 지정 DNS를 설정해야 합니까?", "Dual-stack clusters may set 2 subnets (one for each IP family), otherwise only 1 subnet is allowed.": "듀얼 스택 클러스터는 2개의 서브넷(IP 제품군마다 하나씩)을 설정할 수 있으며, 그렇지 않으면 서브넷은 1개만 허용됩니다.", @@ -155,8 +162,10 @@ "Egress rule": "Egress 규칙", "Egress rules": "송신 규칙", "Enable {{kind}}": "{{kind}} 활성화", + "Enabled": "Enabled", "Error": "오류", "Error details": "오류 정보", + "Error loading virtual machines": "Error loading virtual machines", "Example: 100": "예: 100", "Exceptions": "예외", "External DNS name this service resolves to (e.g. example.com).": "External DNS name this service resolves to (e.g. example.com).", @@ -167,6 +176,8 @@ "External name must be a valid hostname": "External name must be a valid hostname", "External service name": "외부 서비스 이름", "Failed": "실패", + "Failed to add the following virtual machines: {{names}}": "Failed to add the following virtual machines: {{names}}", + "Failed to remove virtual machine from network": "Failed to remove virtual machine from network", "Failed to retrieve the list of projects": "프로젝트 목록을 검색하지 못했습니다", "False": "False", "Form view": "양식보기", @@ -195,6 +206,7 @@ "Ingresses": "Ingress", "Input error: selectors must start and end by a letter or number, and can only contain -, _, / or . Offending value: {{offendingSelector}}": "입력 오류: 선택기는 문자나 숫자로 시작하고 끝나야 하며 -, _, / 또는 . 만 포함할 수 있습니다. 잘못된 값: {{offendingSelector}}", "Insecure traffic": "안전하지 않은 트래픽", + "Interface": "Interface", "Invalid IP address or subnet format": "잘못된 IP 주소 또는 서브넷 형식", "Invalid subnet format": "잘못된 서브넷 형식", "Invalid YAML cannot be persisted": "잘못된 YAML을 유지할 수 없습니다.", @@ -225,6 +237,7 @@ "Location": "위치", "Location of the resource that backs the service": "서비스를 백업할 리소스의 위치", "MAC spoof check": "MAC 스푸핑 검사", + "Manage columns": "Manage columns", "Managed by UserDefinedNetwork": "UserDefinedNetwork에 의해 관리", "Match Expressions": "표현식 일치", "Match Labels": "라벨 일치", @@ -278,6 +291,7 @@ "No results found for \"{{inputValue}}\"": "\"{{inputValue}}\"의 결과를 찾을 수 없음", "No route status": "라우팅 상태 없음", "No selector": "선택기 없음", + "No virtual machines found": "No virtual machines found", "Node": "Node", "Node port": "노드 포트", "NodeNetworkConfigurationPolicy": "NodeNetworkConfigurationPolicy", @@ -338,9 +352,12 @@ "Remove all": "모두 삭제", "Remove alternate Service": "대체 서비스 삭제", "Remove exception": "예외 제거", + "Remove from network": "Remove from network", "Remove peer": "피어 제거", "Remove port": "포트 제거", "Remove subnets?": "서브넷을 제거하시겠습니까?", + "Remove virtual machine from network?": "Remove virtual machine from network?", + "Reset to default": "Reset to default", "Resource name": "리소스 이름", "Restarts": "Restarts", "Restricted Access": "제한된 액세스", @@ -357,11 +374,13 @@ "Rules": "규칙", "Running": "실행 중", "Save": "저장", + "Search by name...": "Search by name...", "Secondary network is only assigned to pods that use k8s.v1.cni.cncf.io/networks annotation to select given network.": "보조 네트워크는 k8s.v1.cni.cncf.io/networks 주석을 사용하여 지정된 네트워크를 선택하는 Pod에만 할당됩니다.", "Secure Route": "안전한 경로", "Security": "보안", "Select a Project": "프로젝트 선택", "Select a Service": "서비스 선택", + "Select all": "Select all", "Select an operator": "Operator 선택", "Select ClusterUserDefinedNetwork": "ClusterUserDefinedNetwork 선택", "Select default ingress and egress deny rules": "기본 Ingress 및 Egress 거부 규칙 선택", @@ -371,6 +390,8 @@ "Select role": "역할 선택", "Select target port": "대상 포트 선택", "Select termination type": "종료 유형 선택", + "Select virtual machines to connect to this network.": "Select virtual machines to connect to this network.", + "Selected categories will be displayed in the table.": "Selected categories will be displayed in the table.", "Selector": "Selector", "Selector is required": "Selector is required", "Selector keys must be unique": "Selector keys must be unique", @@ -453,6 +474,7 @@ "View events": "이벤트보기", "View logs": "로그보기", "View naming information": "이름 지정 정보 보기", + "Virtual Machines": "Virtual Machines", "VLAN": "VLAN", "VLAN tag number": "VLAN 태그 번호", "Weight": "가중치", diff --git a/locales/zh/plugin__networking-console-plugin.json b/locales/zh/plugin__networking-console-plugin.json index 8a4fb61d..5da0269b 100644 --- a/locales/zh/plugin__networking-console-plugin.json +++ b/locales/zh/plugin__networking-console-plugin.json @@ -22,6 +22,7 @@ "Accessible outside the cluster": "在集群外可以访问", "Accessible within the cluster only": "仅在集群内可访问", "Actions": "行动", + "Add": "Add", "Add allowed destination": "添加允许的目的地", "Add allowed source": "添加允许的源", "Add alternate Service": "添加备用服务", @@ -37,6 +38,7 @@ "Add port": "添加端口", "Add ports to restrict traffic through them. If no ports are provided, your policy will make all ports accessible to traffic.": "添加端口以限制通过它们的流量。如果没有提供端口,您的策略将允许所有端口对流量进行访问。", "Add subnet": "添加子网", + "Add virtual machines": "Add virtual machines", "All incoming traffic is denied to Pods in {{namespace}}": "所有传入的流量都被 {{namespace}} 中的 Pod 拒绝", "All outgoing traffic is allowed by default. Egress rules can be used to restrict outgoing traffic if the cluster network provider allows it. When using the OpenShift SDN cluster network provider, egress network policy is not supported.": "所有外向流量都是默认允许的。如果集群网络供应商允许,可以使用 egress 规则来限制外向流量。在使用 OpenShift SDN 集群网络供应商时,不支持 egress 网络策略。", "All outgoing traffic is denied from Pods in {{namespace}}": "所有外向流量都会从 {{namespace}} 中的 Pod 拒绝", @@ -57,11 +59,14 @@ "An error occurred.": "发生错误。", "An error occurred. Please try again.": "发生错误。请再试一次。", "An OpenShift project is an alternative representation of a Kubernetes namespace.": "OpenShift 项目是 Kubernetes 命名空间的一个替代表示。", + "annotation_other": "annotation", "Annotations": "注解", "Any namespace": "任何命名空间", "Any peer": "任何对等者", "Any pod": "任何 pod", "Any port": "任何端口", + "Are you sure you want to remove <2> from <5>{nadName}?": "Are you sure you want to remove <2> from <5>{nadName}?", + "Are you sure you want to remove <2> from <5>{udnName}?": "Are you sure you want to remove <2> from <5>{udnName}?", "Are you sure?": "您确定吗?", "Assigns IP addresses from this subnet to Pods and VirtualMachines.": "将这个子网的 IP 地址分配给 Pod 和 VirtualMachines。", "At creation time the namespace must be configured with <2>{{label}} label. Go to <5>Namespaces to create a new namespace.": "在创建时,命名空间必须配置带有 <2>{{label}} 标签。进入 <5>Namespaces 以创建一个新命名空间。", @@ -126,12 +131,14 @@ "Delete NetworkAttachmentDefinition": "删除 NetworkAttachmentDefinition", "Delete Route": "创建路由", "Delete Service": "删除服务", + "Deletion protection": "Deletion protection", "Deny all egress traffic": "拒绝所有 egress 流量", "Deny all ingress traffic": "拒绝所有 ingress 流量", "Description": "描述", "Destination CA certificate": "目的 CA 证书", "Destinations added to this rule will allow traffic from the pods defined above. Destinations in this list are combined using a logical OR operation.": "添加到此规则中的目的地将允许来自上述定义的 pod 的流量。此列表是使用逻辑 OR 操作的目的地组合。", "Details": "详情", + "Disabled": "Disabled", "Display name": "显示名称", "Do you need to set up custom DNS?": "您需要设置自定义 DNS 吗?", "Dual-stack clusters may set 2 subnets (one for each IP family), otherwise only 1 subnet is allowed.": "双栈集群可以设置 2 个子网(每个 IP 系列一个),否则只允许 1 个子网。", @@ -155,8 +162,10 @@ "Egress rule": "Egress 规则", "Egress rules": "Egress 规则", "Enable {{kind}}": "启用 {{kind}}", + "Enabled": "Enabled", "Error": "错误", "Error details": "错误详情", + "Error loading virtual machines": "Error loading virtual machines", "Example: 100": "示例:100", "Exceptions": "例外", "External DNS name this service resolves to (e.g. example.com).": "External DNS name this service resolves to (e.g. example.com).", @@ -167,6 +176,8 @@ "External name must be a valid hostname": "External name must be a valid hostname", "External service name": "外部服务名称", "Failed": "失败", + "Failed to add the following virtual machines: {{names}}": "Failed to add the following virtual machines: {{names}}", + "Failed to remove virtual machine from network": "Failed to remove virtual machine from network", "Failed to retrieve the list of projects": "检索项目列表失败", "False": "假", "Form view": "表单视图", @@ -195,6 +206,7 @@ "Ingresses": "Ingresses", "Input error: selectors must start and end by a letter or number, and can only contain -, _, / or . Offending value: {{offendingSelector}}": "输入错误:选择器必须以字母或数字开头和结尾,并且只能包含 -、_、/ 或 . 输出值:{{offendingSelector}}", "Insecure traffic": "不安全的流量", + "Interface": "Interface", "Invalid IP address or subnet format": "无效的 IP 地址或子网格式", "Invalid subnet format": "无效的子网格式", "Invalid YAML cannot be persisted": "无效的 YAML 无法保留", @@ -225,6 +237,7 @@ "Location": "位置", "Location of the resource that backs the service": "支持服务的资源的位置", "MAC spoof check": "MAC spoof 检查", + "Manage columns": "Manage columns", "Managed by UserDefinedNetwork": "由 UserDefinedNetwork 管理", "Match Expressions": "匹配表达式", "Match Labels": "匹配标签", @@ -278,6 +291,7 @@ "No results found for \"{{inputValue}}\"": "没有为 \"{{inputValue}}\" 找到结果", "No route status": "没有路由状态", "No selector": "没有选择器", + "No virtual machines found": "No virtual machines found", "Node": "Node", "Node port": "节点端口", "NodeNetworkConfigurationPolicy": "NodeNetworkConfigurationPolicy", @@ -338,9 +352,12 @@ "Remove all": "删除所有", "Remove alternate Service": "删除备用服务", "Remove exception": "删除异常", + "Remove from network": "Remove from network", "Remove peer": "删除对等点", "Remove port": "删除端口", "Remove subnets?": "删除子网?", + "Remove virtual machine from network?": "Remove virtual machine from network?", + "Reset to default": "Reset to default", "Resource name": "资源名称", "Restarts": "Restarts", "Restricted Access": "限制的访问", @@ -357,11 +374,13 @@ "Rules": "规则", "Running": "运行", "Save": "保存", + "Search by name...": "Search by name...", "Secondary network is only assigned to pods that use k8s.v1.cni.cncf.io/networks annotation to select given network.": "二级网络只分配给使用 k8s.v1.cni.cncf.io/networks 注解来选择给定网络的 pod。", "Secure Route": "安全路由", "Security": "安全性", "Select a Project": "选择一个项目", "Select a Service": "选择一个服务", + "Select all": "Select all", "Select an operator": "选择 operator", "Select ClusterUserDefinedNetwork": "选择 ClusterUserDefinedNetwork", "Select default ingress and egress deny rules": "选择默认入口和出口拒绝规则", @@ -371,6 +390,8 @@ "Select role": "选择角色", "Select target port": "选择目标端口", "Select termination type": "选择终止类型", + "Select virtual machines to connect to this network.": "Select virtual machines to connect to this network.", + "Selected categories will be displayed in the table.": "Selected categories will be displayed in the table.", "Selector": "Selector", "Selector is required": "Selector is required", "Selector keys must be unique": "Selector keys must be unique", @@ -453,6 +474,7 @@ "View events": "查看事件", "View logs": "查看日志", "View naming information": "查看命名信息", + "Virtual Machines": "Virtual Machines", "VLAN": "VLAN", "VLAN tag number": "VLAN 标签号", "Weight": "权重", diff --git a/package-lock.json b/package-lock.json index 97f3c841..8a063d2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@patternfly/react-charts": "~8.5.1", "@patternfly/react-component-groups": "~6.5.0", "@patternfly/react-core": "~6.5.1", + "@patternfly/react-data-view": "~6.5.0", "@patternfly/react-drag-drop": "~6.5.1", "@patternfly/react-icons": "~6.5.1", "@patternfly/react-styles": "~6.5.1", @@ -3753,6 +3754,25 @@ "react-dom": "^17 || ^18 || ^19" } }, + "node_modules/@patternfly/react-data-view": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-data-view/-/react-data-view-6.5.0.tgz", + "integrity": "sha512-QTj8eg/pwchdxSpapqU3a6MZG7syiwzXy1La2hgAJtvQTR9ltkIg/GJJ4ota2O5wHzdSAVzTTdxiNuP33wmvbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@patternfly/react-component-groups": "^6.1.0", + "@patternfly/react-core": "^6.4.0", + "@patternfly/react-icons": "^6.4.0", + "@patternfly/react-table": "^6.4.0", + "clsx": "^2.1.1", + "react-jss": "^10.10.0" + }, + "peerDependencies": { + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + } + }, "node_modules/@patternfly/react-drag-drop": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/@patternfly/react-drag-drop/-/react-drag-drop-6.5.1.tgz", @@ -6394,6 +6414,16 @@ "node": ">=6" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", diff --git a/package.json b/package.json index e408010b..d3630408 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@patternfly/react-charts": "~8.5.1", "@patternfly/react-component-groups": "~6.5.0", "@patternfly/react-core": "~6.5.1", + "@patternfly/react-data-view": "6.5.0", "@patternfly/react-drag-drop": "~6.5.1", "@patternfly/react-icons": "~6.5.1", "@patternfly/react-styles": "~6.5.1", @@ -79,8 +80,8 @@ "pluralize": "^8.0.0", "prettier": "^3.2.5", "react": "18.3.1", - "react-dom": "18.3.1", "react-copy-to-clipboard": "^5.1.0", + "react-dom": "18.3.1", "react-i18next": "~16.5.8", "react-router": "~7.13.1", "resolve-url-loader": "^5.0.0", diff --git a/src/utils/components/ConfirmModal/ConfirmModal.tsx b/src/utils/components/ConfirmModal/ConfirmModal.tsx index 84fda239..fbe27283 100644 --- a/src/utils/components/ConfirmModal/ConfirmModal.tsx +++ b/src/utils/components/ConfirmModal/ConfirmModal.tsx @@ -6,6 +6,7 @@ import { useNetworkingTranslation } from '@utils/hooks/useNetworkingTranslation' export type ConfirmModalProps = { btnText: JSX.Element | string; + btnVariant?: ButtonVariant; closeModal?: () => void; executeFn: () => void; message: JSX.Element | string; @@ -14,6 +15,7 @@ export type ConfirmModalProps = { const ConfirmModal: FC = ({ btnText, + btnVariant = ButtonVariant.primary, closeModal, executeFn, message, @@ -27,17 +29,11 @@ const ConfirmModal: FC = ({ }; return ( - + {message} - + ) : ( + t('{{count}} annotation', { + count: annotationCount, + }) + )} + + + + + + + + + + + + + +
+ + + +
+
+ + + ); +}; + +export default NADDetails; diff --git a/src/views/nads/details/tabs/details/NADTypeDetails.tsx b/src/views/nads/details/tabs/details/NADTypeDetails.tsx index 2d424ae2..51ad6899 100644 --- a/src/views/nads/details/tabs/details/NADTypeDetails.tsx +++ b/src/views/nads/details/tabs/details/NADTypeDetails.tsx @@ -1,6 +1,5 @@ import React, { FC } from 'react'; -import { DetailsItemComponentProps } from '@openshift-console/dynamic-plugin-sdk'; import MutedText from '@utils/components/MutedText/MutedText'; import { useNetworkingTranslation } from '@utils/hooks/useNetworkingTranslation'; import { getConfigAsJSON, getType } from '@utils/resources/nads/selectors'; @@ -8,16 +7,18 @@ import { NetworkAttachmentDefinitionKind } from '@utils/resources/nads/types'; import { networkTypes } from '@views/nads/form/utils/types'; import { getLocalnetTopologyType } from '@views/nads/form/utils/utils'; -const NADTypeDetails: FC> = ({ - obj: nad, -}) => { +type NADTypeDetailsProps = { + nad: NetworkAttachmentDefinitionKind; +}; + +const NADTypeDetails: FC = ({ nad }) => { const { t } = useNetworkingTranslation(); const nadConfig = getConfigAsJSON(nad); const type = getLocalnetTopologyType(getType(nadConfig), nadConfig?.topology); if (!type) return ; - return networkTypes?.[type] || type; + return <>{networkTypes?.[type] || type}; }; export default NADTypeDetails; diff --git a/src/views/nads/details/tabs/virtualMachines/NADVirtualMachines.tsx b/src/views/nads/details/tabs/virtualMachines/NADVirtualMachines.tsx new file mode 100644 index 00000000..3d0f1991 --- /dev/null +++ b/src/views/nads/details/tabs/virtualMachines/NADVirtualMachines.tsx @@ -0,0 +1,201 @@ +import React, { FC, useCallback, useMemo, useState } from 'react'; +import { useSearchParams } from 'react-router'; +import * as _ from 'lodash'; + +import { + Button, + Card, + CardBody, + PageSection, + Pagination, + Stack, + StackItem, +} from '@patternfly/react-core'; +import { + DataView, + DataViewFilters, + DataViewTable, + DataViewTextFilter, + DataViewToolbar, + useDataViewFilters, + useDataViewPagination, + useDataViewSort, +} from '@patternfly/react-data-view'; +import useVMNetworkTableColumns from '@utils/components/VMNetworkTable/useVMNetworkTableColumns'; +import VMNetworkColumnManagement from '@utils/components/VMNetworkTable/VMNetworkColumnManagement'; +import { + getVMNetworkTableSortValue, + useVMNetworkDataViewContent, +} from '@utils/components/VMNetworkTable/vmNetworkDataViewContent'; +import { useNetworkingTranslation } from '@utils/hooks/useNetworkingTranslation'; +import useVMResourceLookups from '@utils/hooks/useVMResourceLookups'; +import { NetworkAttachmentDefinitionKind } from '@utils/resources/nads/types'; +import { getName } from '@utils/resources/shared'; + +import useNADVirtualMachines, { NADVirtualMachine } from '../../hooks/useNADVirtualMachines'; + +import AddNADVirtualMachinesModal from './components/AddNADVirtualMachinesModal'; +import NADVirtualMachineRowActions from './components/NADVirtualMachineRowActions'; +import { DEFAULT_PER_PAGE } from './constants'; + +type NADVirtualMachineFilters = { + name?: string; +}; + +const NADVirtualMachines: FC<{ obj?: NetworkAttachmentDefinitionKind }> = ({ obj: nad }) => { + const { t } = useNetworkingTranslation(); + const [searchParams, setSearchParams] = useSearchParams(); + const [isAddModalOpen, setIsAddModalOpen] = useState(false); + const { appliedColumns, applyColumns, resetColumns } = useVMNetworkTableColumns(); + + const { availableVirtualMachines, loaded, loadError, virtualMachines } = + useNADVirtualMachines(nad); + const { + loaded: resourcesLoaded, + loadError: resourcesLoadError, + lookups: vmResourceLookups, + } = useVMResourceLookups(); + + const { clearAllFilters, filters, onSetFilters } = useDataViewFilters({ + searchParams, + setSearchParams, + }); + + const { onPerPageSelect, onSetPage, page, perPage } = useDataViewPagination({ + page: 1, + perPage: DEFAULT_PER_PAGE, + searchParams, + setSearchParams, + }); + + const { direction, onSort, sortBy } = useDataViewSort({ + searchParams, + setSearchParams, + }); + + const filteredData = useMemo(() => { + const nameFilter = filters.name?.toLowerCase(); + + if (!nameFilter) { + return virtualMachines; + } + + return virtualMachines.filter(({ vm }) => getName(vm)?.toLowerCase().includes(nameFilter)); + }, [filters.name, virtualMachines]); + + const sortedData = useMemo(() => { + if (!sortBy) { + return filteredData; + } + + return _.orderBy( + filteredData, + [(item: NADVirtualMachine) => getVMNetworkTableSortValue(item, sortBy, vmResourceLookups)], + [direction === 'desc' ? 'desc' : 'asc'], + ); + }, [direction, filteredData, sortBy, vmResourceLookups]); + + const paginatedData = useMemo(() => { + const start = (page - 1) * perPage; + return sortedData.slice(start, start + perPage); + }, [page, perPage, sortedData]); + + const renderActions = useCallback( + ({ interfaceName, vm }: NADVirtualMachine) => + nad ? : null, + [nad], + ); + + const { bodyStates, columns, rows } = useVMNetworkDataViewContent({ + appliedColumns, + direction, + onSort, + paginatedData, + renderActions, + sortBy, + t, + vmResourceLookups, + }); + + const activeState = useMemo(() => { + if (loadError || resourcesLoadError) { + return 'error'; + } + + if (!loaded || !resourcesLoaded) { + return 'loading'; + } + + if (filteredData.length === 0) { + return 'empty'; + } + + return undefined; + }, [filteredData.length, loadError, loaded, resourcesLoadError, resourcesLoaded]); + + return ( + + + + + + + + + + onSetFilters(newValues)} + values={filters} + > + + + } + pagination={ +
+ + +
+ } + /> + +
+
+
+
+
+ {nad && ( + setIsAddModalOpen(false)} + /> + )} +
+ ); +}; + +export default NADVirtualMachines; diff --git a/src/views/nads/details/tabs/virtualMachines/components/AddNADVirtualMachinesModal.tsx b/src/views/nads/details/tabs/virtualMachines/components/AddNADVirtualMachinesModal.tsx new file mode 100644 index 00000000..ccd0e0b2 --- /dev/null +++ b/src/views/nads/details/tabs/virtualMachines/components/AddNADVirtualMachinesModal.tsx @@ -0,0 +1,187 @@ +import React, { FC, useCallback, useState } from 'react'; + +import { V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt'; +import { + Alert, + AlertVariant, + Button, + Modal, + ModalBody, + ModalFooter, + ModalHeader, + ModalVariant, + Pagination, +} from '@patternfly/react-core'; +import { + DataView, + DataViewFilters, + DataViewTable, + DataViewTextFilter, + DataViewToolbar, +} from '@patternfly/react-data-view'; +import { useNetworkingTranslation } from '@utils/hooks/useNetworkingTranslation'; +import { NetworkAttachmentDefinitionKind } from '@utils/resources/nads/types'; +import { getName, getNamespace } from '@utils/resources/shared'; +import { addVMToNAD } from '@utils/resources/vm/utils'; + +import useAddNADVMModalData from './hooks/useAddNADVMModalData'; +import useAddNADVMModalSelection from './hooks/useAddNADVMModalSelection'; +import useAddNADVMModalTable from './hooks/useAddNADVMModalTable'; + +type AddNADVirtualMachinesModalProps = { + availableVMs: V1VirtualMachine[]; + isOpen: boolean; + loaded: boolean; + nad: NetworkAttachmentDefinitionKind; + onClose: () => void; +}; + +const AddNADVirtualMachinesModal: FC = ({ + availableVMs, + isOpen, + loaded, + nad, + onClose, +}) => { + const { t } = useNetworkingTranslation(); + const [isSubmitting, setIsSubmitting] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); + + const onResetError = useCallback(() => setErrorMessage(''), []); + + const { + clearAllFilters, + direction, + filteredData, + filters, + onPerPageSelect, + onSetFilters, + onSetPage, + onSort, + page, + paginatedData, + perPage, + } = useAddNADVMModalData(availableVMs); + + const { areAllPaginatedSelected, areSomePaginatedSelected, onSelectAll, selected, selection } = + useAddNADVMModalSelection({ + isOpen, + isSubmitting, + onResetError, + paginatedData, + }); + + const { activeState, bodyStates, columns, headStates, rows } = useAddNADVMModalTable({ + areAllPaginatedSelected, + areSomePaginatedSelected, + direction, + filteredDataLength: filteredData.length, + isSubmitting, + loaded, + onSelectAll, + onSort, + paginatedData, + t, + }); + + const isAddDisabled = isSubmitting || availableVMs.length === 0 || selected.length === 0; + + const handleAdd = async () => { + if (selected.length === 0) { + return; + } + + setIsSubmitting(true); + setErrorMessage(''); + + try { + const results = await Promise.allSettled(selected.map((vm) => addVMToNAD(vm, nad))); + const failedNames = results.flatMap((result, index) => { + if (result.status === 'fulfilled') { + return []; + } + + const name = getName(selected[index]); + return name ? [`${getNamespace(selected[index])}/${name}`] : []; + }); + + if (failedNames.length > 0) { + setErrorMessage( + t('Failed to add the following virtual machines: {{names}}', { + names: failedNames.join(', '), + }), + ); + return; + } + + onClose(); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + + {errorMessage && ( + + {errorMessage} + + )} + + onSetFilters(newValues)} + values={filters} + > + + + } + pagination={ + + } + /> + + + + + + + + + ); +}; + +export default AddNADVirtualMachinesModal; diff --git a/src/views/nads/details/tabs/virtualMachines/components/NADVirtualMachineRowActions.tsx b/src/views/nads/details/tabs/virtualMachines/components/NADVirtualMachineRowActions.tsx new file mode 100644 index 00000000..8f9715ac --- /dev/null +++ b/src/views/nads/details/tabs/virtualMachines/components/NADVirtualMachineRowActions.tsx @@ -0,0 +1,75 @@ +import React, { FC, useCallback, useMemo } from 'react'; +import { Trans } from 'react-i18next'; + +import { modelToGroupVersionKind } from '@kubevirt-ui/kubevirt-api/console'; +import VirtualMachineModel from '@kubevirt-ui/kubevirt-api/console/models/VirtualMachineModel'; +import { V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt'; +import { Action, ResourceLink, useModal } from '@openshift-console/dynamic-plugin-sdk'; +import { ButtonVariant } from '@patternfly/react-core'; +import ActionsDropdown from '@utils/components/ActionsDropdown/ActionsDropdown'; +import ConfirmModal, { ConfirmModalProps } from '@utils/components/ConfirmModal/ConfirmModal'; +import { useNetworkingTranslation } from '@utils/hooks/useNetworkingTranslation'; +import { NetworkAttachmentDefinitionKind } from '@utils/resources/nads/types'; +import { asAccessReview, getName, getNamespace } from '@utils/resources/shared'; +import { removeVMFromNAD } from '@utils/resources/vm/utils'; +import { networkConsole } from '@utils/utils/helpers'; + +type NADVirtualMachineRowActionsProps = { + interfaceName: string; + nad: NetworkAttachmentDefinitionKind; + vm: V1VirtualMachine; +}; + +const NADVirtualMachineRowActions: FC = ({ + interfaceName, + nad, + vm, +}) => { + const { t } = useNetworkingTranslation(); + const createModal = useModal(); + const vmName = getName(vm); + const vmNamespace = getNamespace(vm); + const nadName = getName(nad); + + const onRemove = useCallback(() => { + createModal(ConfirmModal, { + btnText: t('Remove'), + btnVariant: ButtonVariant.danger, + executeFn: () => { + removeVMFromNAD(vm, interfaceName).catch((error) => { + networkConsole.warn('Failed to remove virtual machine from network', error); + }); + }, + message: ( + + Are you sure you want to remove{' '} + {' '} + from {nadName}? + + ), + title: t('Remove virtual machine from network?'), + }); + }, [createModal, interfaceName, nadName, t, vm, vmName, vmNamespace]); + + const actions = useMemo( + () => [ + { + accessReview: asAccessReview(VirtualMachineModel, vm, 'update'), + cta: onRemove, + id: 'remove-vm-from-nad-network', + label: t('Remove from network'), + }, + ], + [onRemove, t, vm], + ); + + return ; +}; + +export default NADVirtualMachineRowActions; diff --git a/src/views/nads/details/tabs/virtualMachines/components/hooks/useAddNADVMModalData.ts b/src/views/nads/details/tabs/virtualMachines/components/hooks/useAddNADVMModalData.ts new file mode 100644 index 00000000..f3e101c3 --- /dev/null +++ b/src/views/nads/details/tabs/virtualMachines/components/hooks/useAddNADVMModalData.ts @@ -0,0 +1,81 @@ +import { useMemo } from 'react'; +import { orderBy } from 'lodash'; + +import { V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt'; +import { + useDataViewFilters, + useDataViewPagination, + useDataViewSort, +} from '@patternfly/react-data-view'; +import { getName, getNamespace } from '@utils/resources/shared'; +import { getVMStatus } from '@utils/resources/vm/selectors'; + +import { DEFAULT_PER_PAGE } from '../../constants'; + +type AddVMFilters = { + name?: string; +}; + +const useAddNADVMModalData = (availableVMs: V1VirtualMachine[]) => { + const { clearAllFilters, filters, onSetFilters } = useDataViewFilters({}); + + const { onPerPageSelect, onSetPage, page, perPage } = useDataViewPagination({ + page: 1, + perPage: DEFAULT_PER_PAGE, + }); + + const { direction, onSort, sortBy } = useDataViewSort({}); + + const filteredData = useMemo(() => { + const nameFilter = filters.name?.toLowerCase(); + + if (!nameFilter) { + return availableVMs; + } + + return availableVMs.filter((vm) => getName(vm)?.toLowerCase().includes(nameFilter)); + }, [availableVMs, filters.name]); + + const sortedData = useMemo(() => { + if (!sortBy) { + return filteredData; + } + + const getSortValue = (vm: V1VirtualMachine) => { + switch (sortBy) { + case 'name': + return getName(vm) ?? ''; + case 'namespace': + return getNamespace(vm) ?? ''; + case 'status': + return getVMStatus(vm); + default: + return ''; + } + }; + + return orderBy(filteredData, [getSortValue], [direction === 'desc' ? 'desc' : 'asc']); + }, [direction, filteredData, sortBy]); + + const paginatedData = useMemo(() => { + const start = (page - 1) * perPage; + return sortedData.slice(start, start + perPage); + }, [page, perPage, sortedData]); + + return { + clearAllFilters, + direction, + filteredData, + filters, + onPerPageSelect, + onSetFilters, + onSetPage, + onSort, + page, + paginatedData, + perPage, + sortBy, + }; +}; + +export default useAddNADVMModalData; diff --git a/src/views/nads/details/tabs/virtualMachines/components/hooks/useAddNADVMModalSelection.ts b/src/views/nads/details/tabs/virtualMachines/components/hooks/useAddNADVMModalSelection.ts new file mode 100644 index 00000000..588e4434 --- /dev/null +++ b/src/views/nads/details/tabs/virtualMachines/components/hooks/useAddNADVMModalSelection.ts @@ -0,0 +1,116 @@ +import React, { useCallback, useEffect, useMemo } from 'react'; + +import { V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt'; +import { DataViewTr, isDataViewTrObject, useDataViewSelection } from '@patternfly/react-data-view'; +import { getName, getNamespace } from '@utils/resources/shared'; + +type UseAddNADVMModalSelectionParams = { + isOpen: boolean; + isSubmitting: boolean; + onResetError: () => void; + paginatedData: V1VirtualMachine[]; +}; + +const useAddNADVMModalSelection = ({ + isOpen, + isSubmitting, + onResetError, + paginatedData, +}: UseAddNADVMModalSelectionParams) => { + const { + isSelected: isVMSelected, + onSelect: onVMSelect, + selected, + setSelected, + } = useDataViewSelection({ + matchOption: (item, another) => + getName(item) === getName(another) && getNamespace(item) === getNamespace(another), + }); + + useEffect(() => { + if (isOpen) { + setSelected([]); + onResetError(); + } + // Only reset when the modal opens; setSelected is not stable across renders. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isOpen]); + + const vmByRowId = useMemo( + () => + new Map( + paginatedData.map((vm) => [`${getNamespace(vm)}/${getName(vm)}`, vm]), + ), + [paginatedData], + ); + + const areAllPaginatedSelected = useMemo( + () => paginatedData.length > 0 && paginatedData.every((vm) => isVMSelected(vm)), + [isVMSelected, paginatedData], + ); + + const areSomePaginatedSelected = useMemo( + () => paginatedData.some((vm) => isVMSelected(vm)) && !areAllPaginatedSelected, + [areAllPaginatedSelected, isVMSelected, paginatedData], + ); + + const vmMatches = useCallback( + (a: V1VirtualMachine, b: V1VirtualMachine) => + getName(a) === getName(b) && getNamespace(a) === getNamespace(b), + [], + ); + + const onSelectAll = useCallback( + (_event: React.FormEvent, isSelecting: boolean) => { + if (isSelecting) { + const newSelected = [...selected]; + + paginatedData.forEach((vm) => { + if (!newSelected.some((item) => vmMatches(item, vm))) { + newSelected.push(vm); + } + }); + setSelected(newSelected); + return; + } + + setSelected(selected.filter((item) => !paginatedData.some((vm) => vmMatches(item, vm)))); + }, + [paginatedData, selected, setSelected, vmMatches], + ); + + const selection = useMemo( + () => ({ + isSelectDisabled: () => isSubmitting, + isSelected: (row: DataViewTr) => { + const rowId = isDataViewTrObject(row) ? row.id : undefined; + const vm = rowId ? vmByRowId.get(rowId) : undefined; + + return vm ? isVMSelected(vm) : false; + }, + onSelect: (isSelecting: boolean, row: DataViewTr | DataViewTr[]) => { + const rowsToProcess = Array.isArray(row) ? row : [row]; + + rowsToProcess.forEach((item) => { + const rowId = isDataViewTrObject(item) ? item.id : undefined; + const vm = rowId ? vmByRowId.get(rowId) : undefined; + + if (vm) { + onVMSelect(isSelecting, vm); + } + }); + }, + }), + [isSubmitting, isVMSelected, onVMSelect, vmByRowId], + ); + + return { + areAllPaginatedSelected, + areSomePaginatedSelected, + onSelectAll, + selected, + selection, + }; +}; + +export default useAddNADVMModalSelection; diff --git a/src/views/nads/details/tabs/virtualMachines/components/hooks/useAddNADVMModalTable.tsx b/src/views/nads/details/tabs/virtualMachines/components/hooks/useAddNADVMModalTable.tsx new file mode 100644 index 00000000..35cca8ec --- /dev/null +++ b/src/views/nads/details/tabs/virtualMachines/components/hooks/useAddNADVMModalTable.tsx @@ -0,0 +1,216 @@ +import React, { useCallback, useMemo } from 'react'; +import { TFunction } from 'i18next'; + +import { modelToGroupVersionKind } from '@kubevirt-ui/kubevirt-api/console'; +import VirtualMachineModel from '@kubevirt-ui/kubevirt-api/console/models/VirtualMachineModel'; +import { V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt'; +import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; +import { Bullseye, EmptyState, Label, Spinner } from '@patternfly/react-core'; +import { DataViewTh } from '@patternfly/react-data-view'; +import { SearchIcon } from '@patternfly/react-icons'; +import { Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; +import { getName, getNamespace } from '@utils/resources/shared'; +import { getVMStatus } from '@utils/resources/vm/selectors'; + +import { ADD_VM_MODAL_COLUMN_COUNT, ADD_VM_MODAL_SORT_COLUMN_KEYS } from '../../constants'; +import { getVMStatusLabelColor } from '../../utils'; + +type UseAddNADVMModalTableParams = { + areAllPaginatedSelected: boolean; + areSomePaginatedSelected: boolean; + direction: 'asc' | 'desc' | undefined; + filteredDataLength: number; + isSubmitting: boolean; + loaded: boolean; + onSelectAll: (event: React.FormEvent, isSelecting: boolean) => void; + onSort: ( + event: MouseEvent | React.KeyboardEvent | React.MouseEvent, + key: string, + direction: 'asc' | 'desc', + ) => void; + paginatedData: V1VirtualMachine[]; + t: TFunction; +}; + +const useAddNADVMModalTable = ({ + areAllPaginatedSelected, + areSomePaginatedSelected, + direction, + filteredDataLength, + isSubmitting, + loaded, + onSelectAll, + onSort, + paginatedData, + t, +}: UseAddNADVMModalTableParams) => { + const getSortParams = useCallback( + (columnIndex: number) => ({ + columnIndex, + onSort: ( + event: MouseEvent | React.KeyboardEvent | React.MouseEvent, + _index: number | string, + newDirection: 'asc' | 'desc', + ) => onSort(event, ADD_VM_MODAL_SORT_COLUMN_KEYS[columnIndex], newDirection), + sortBy: { direction, index: columnIndex }, + }), + [direction, onSort], + ); + + const columns = useMemo( + () => [ + { + cell: ( + + ), + }, + { + cell: ( + + ), + }, + { + cell: ( + + ), + }, + ], + [getSortParams, t], + ); + + const rows = useMemo( + () => + paginatedData.map((vm) => { + const name = getName(vm); + const vmNamespace = getNamespace(vm); + const status = getVMStatus(vm); + const rowId = `${vmNamespace}/${name}`; + + return { + id: rowId, + row: [ + { + cell: ( + + ), + }, + { + cell: , + }, + { + cell: ( + + ), + }, + ], + }; + }), + [paginatedData], + ); + + const activeState = useMemo(() => { + if (!loaded) { + return 'loading'; + } + + if (filteredDataLength === 0) { + return 'empty'; + } + + return 'data'; + }, [filteredDataLength, loaded]); + + const headStates = useMemo( + () => ({ + data: ( + + + + {columns.map((column, index) => ( + {column.cell} + ))} + + + ), + empty: <>, + }), + [ + areAllPaginatedSelected, + areSomePaginatedSelected, + columns, + isSubmitting, + onSelectAll, + paginatedData.length, + t, + ], + ); + + const bodyStates = useMemo( + () => ({ + empty: ( + + + + + + + + ), + loading: ( + + + + + + + + + + ), + }), + [t], + ); + + return { + activeState, + bodyStates, + columns, + headStates, + rows, + }; +}; + +export default useAddNADVMModalTable; diff --git a/src/views/nads/details/tabs/virtualMachines/constants.ts b/src/views/nads/details/tabs/virtualMachines/constants.ts new file mode 100644 index 00000000..a29e7384 --- /dev/null +++ b/src/views/nads/details/tabs/virtualMachines/constants.ts @@ -0,0 +1,4 @@ +export const DEFAULT_PER_PAGE = 20; + +export const ADD_VM_MODAL_COLUMN_COUNT = 4; +export const ADD_VM_MODAL_SORT_COLUMN_KEYS = ['name', 'namespace', 'status'] as const; diff --git a/src/views/nads/details/tabs/virtualMachines/utils.ts b/src/views/nads/details/tabs/virtualMachines/utils.ts new file mode 100644 index 00000000..426b4001 --- /dev/null +++ b/src/views/nads/details/tabs/virtualMachines/utils.ts @@ -0,0 +1,15 @@ +export const getVMStatusLabelColor = (status: string) => { + if (!status) { + return 'grey'; + } + + if (status.includes('Error') || status === 'Stopped') { + return 'red'; + } + + if (status === 'Running') { + return 'green'; + } + + return 'orange'; +}; diff --git a/src/views/nads/details/tabs/yaml/NetworkAttachmentDefinitionYAML.tsx b/src/views/nads/details/tabs/yaml/NetworkAttachmentDefinitionYAML.tsx new file mode 100644 index 00000000..70f68546 --- /dev/null +++ b/src/views/nads/details/tabs/yaml/NetworkAttachmentDefinitionYAML.tsx @@ -0,0 +1,23 @@ +import React, { FC, Suspense } from 'react'; + +import { ResourceYAMLEditor } from '@openshift-console/dynamic-plugin-sdk'; +import Loading from '@utils/components/Loading/Loading'; +import { NetworkAttachmentDefinitionKind } from '@utils/resources/nads/types'; + +type NetworkAttachmentDefinitionYAMLProps = { + obj?: NetworkAttachmentDefinitionKind; +}; + +const NetworkAttachmentDefinitionYAML: FC = ({ + obj: nad, +}) => { + return !nad ? ( + + ) : ( + }> + + + ); +}; + +export default NetworkAttachmentDefinitionYAML; diff --git a/src/views/nads/list/components/NADsRow/NADsRow.tsx b/src/views/nads/list/components/NADsRow/NADsRow.tsx index 20e4b91a..92a65ee0 100644 --- a/src/views/nads/list/components/NADsRow/NADsRow.tsx +++ b/src/views/nads/list/components/NADsRow/NADsRow.tsx @@ -46,7 +46,7 @@ const NADsRow: FC = ({ activeColumnIDs, obj }) => { {type || } - + ); diff --git a/src/views/nads/manifest.ts b/src/views/nads/manifest.ts index 0cfede08..b3fec89b 100644 --- a/src/views/nads/manifest.ts +++ b/src/views/nads/manifest.ts @@ -1,6 +1,6 @@ import { - DetailsItem, ResourceActionProvider, + ResourceDetailsPage, ResourceListPage, ResourceNSNavItem, RoutePage, @@ -58,29 +58,6 @@ export const NADsExtensions: EncodedExtension[] = [ }, type: 'console.page/route', } as EncodedExtension, - { - properties: { - column: 'left', - component: { $codeRef: 'NetworkAttachmentDefintionDescriptionDetails' }, - id: 'nad-description-detail-item', - model: NetworkAttachmentDefinitionExtensionModel, - path: 'metadata.annotations.description', - title: 'Description', - }, - type: 'console.resource/details-item', - } as EncodedExtension, - { - properties: { - column: 'right', - component: { $codeRef: 'NetworkAttachmentDefintionTypeDetails' }, - id: 'nad-type-detail-item', - model: NetworkAttachmentDefinitionExtensionModel, - path: 'spec.config.type', - title: 'Type', - }, - type: 'console.resource/details-item', - } as EncodedExtension, - { properties: { model: NetworkAttachmentDefinitionExtensionModel, @@ -100,13 +77,21 @@ export const NADsExtensions: EncodedExtension[] = [ }, type: 'console.action/resource-provider', } as EncodedExtension, + { + flags: { + required: [FLAG_NET_ATTACH_DEF, FLAG_KUBEVIRT], + }, + properties: { + component: { $codeRef: 'NADDetailsPage' }, + model: NetworkAttachmentDefinitionExtensionModel, + }, + type: 'console.page/resource/details', + } as EncodedExtension, ]; export const NADsExposedModules: ConsolePluginBuildMetadata['exposedModules'] = { + NADDetailsPage: './views/nads/details/NADDetailsPage.tsx', NetworkAttachmentDefinitionFormPage: './views/nads/form/NetworkAttachmentDefinitionFormPage.tsx', NetworkAttachmentDefinitionList: './views/nads/list/NetworkAttachmentDefinitionList.tsx', - NetworkAttachmentDefintionDescriptionDetails: - './views/nads/details/tabs/details/NADDescriptionDetails.tsx', - NetworkAttachmentDefintionTypeDetails: './views/nads/details/tabs/details/NADTypeDetails.tsx', useNADsActions: './views/nads/actions/hooks/useNADsActions.tsx', }; diff --git a/src/views/udns/details/UDNDetailsPage.tsx b/src/views/udns/details/UDNDetailsPage.tsx new file mode 100644 index 00000000..8603f860 --- /dev/null +++ b/src/views/udns/details/UDNDetailsPage.tsx @@ -0,0 +1,44 @@ +import React, { FC } from 'react'; + +import { HorizontalNav, useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; +import StatusBox from '@utils/components/StatusBox/StatusBox'; +import { + ClusterUserDefinedNetworkModelGroupVersionKind, + UserDefinedNetworkModelGroupVersionKind, +} from '@utils/models'; +import { ClusterUserDefinedNetworkKind, UserDefinedNetworkKind } from '@utils/resources/udns/types'; + +import UDNDetailsPageTitle from './components/UDNDetailsPageTitle'; +import { useUDNTabs } from './hooks/useUDNTabs'; +import useUDNVirtualMachines from './hooks/useUDNVirtualMachines'; + +export type UDNDetailsPageProps = { + name: string; + namespace?: string; +}; + +const UDNDetailsPage: FC = ({ name, namespace }) => { + const isNamespaced = Boolean(namespace); + + const [udn, loaded, error] = useK8sWatchResource< + ClusterUserDefinedNetworkKind | UserDefinedNetworkKind + >({ + groupVersionKind: isNamespaced + ? UserDefinedNetworkModelGroupVersionKind + : ClusterUserDefinedNetworkModelGroupVersionKind, + name, + namespace, + }); + + const { virtualMachines } = useUDNVirtualMachines(udn); + const pages = useUDNTabs(virtualMachines.length); + + return ( + + + + + ); +}; + +export default UDNDetailsPage; diff --git a/src/views/udns/details/components/UDNDetailsPageTitle.tsx b/src/views/udns/details/components/UDNDetailsPageTitle.tsx new file mode 100644 index 00000000..73a20a76 --- /dev/null +++ b/src/views/udns/details/components/UDNDetailsPageTitle.tsx @@ -0,0 +1,42 @@ +import React, { FC } from 'react'; + +import { modelToRef } from '@kubevirt-ui/kubevirt-api/console'; +import { Title } from '@patternfly/react-core'; +import DetailsPageTitle from '@utils/components/DetailsPageTitle/DetailsPageTitle'; +import { useNetworkingTranslation } from '@utils/hooks/useNetworkingTranslation'; +import { getModel } from '@utils/resources/udns/selectors'; +import { ClusterUserDefinedNetworkKind, UserDefinedNetworkKind } from '@utils/resources/udns/types'; +import UDNActions from '@views/udns/actions/UDNActions'; + +type UDNDetailsPageTitleProps = { + udn: ClusterUserDefinedNetworkKind | UserDefinedNetworkKind; +}; + +const UDNDetailsPageTitle: FC = ({ udn }) => { + const { t } = useNetworkingTranslation(); + const model = getModel(udn); + + return ( + + + <span className="co-m-resource-icon co-m-resource-icon--lg" title={model.abbr}> + {model.abbr} + </span> + {udn?.metadata?.name} + + {udn && } + + ); +}; + +export default UDNDetailsPageTitle; diff --git a/src/views/udns/details/hooks/useUDNTabs.tsx b/src/views/udns/details/hooks/useUDNTabs.tsx new file mode 100644 index 00000000..1c12bf85 --- /dev/null +++ b/src/views/udns/details/hooks/useUDNTabs.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +import { Badge } from '@patternfly/react-core'; +import { useNetworkingTranslation } from '@utils/hooks/useNetworkingTranslation'; + +import UDNDetailsTab from '../tabs/details/UDNDetailsTab'; +import UDNVirtualMachines from '../tabs/virtualMachines/UDNVirtualMachines'; +import UserDefinedNetworkYAML from '../tabs/yaml/UserDefinedNetworkYAML'; + +export const useUDNTabs = (vmCount = 0) => { + const { t } = useNetworkingTranslation(); + + return [ + { + component: UDNDetailsTab, + href: '', + name: t('Details'), + }, + { + component: UserDefinedNetworkYAML, + href: 'yaml', + name: t('YAML'), + }, + { + component: UDNVirtualMachines, + href: 'virtual-machines', + name: ( + <> + {t('Virtual Machines')} + + {vmCount} + + + ), + }, + ]; +}; diff --git a/src/views/udns/details/hooks/useUDNVirtualMachines/index.ts b/src/views/udns/details/hooks/useUDNVirtualMachines/index.ts new file mode 100644 index 00000000..12be0109 --- /dev/null +++ b/src/views/udns/details/hooks/useUDNVirtualMachines/index.ts @@ -0,0 +1 @@ +export { default, type UDNVirtualMachine } from './useUDNVirtualMachines'; diff --git a/src/views/udns/details/hooks/useUDNVirtualMachines/useUDNVirtualMachines.ts b/src/views/udns/details/hooks/useUDNVirtualMachines/useUDNVirtualMachines.ts new file mode 100644 index 00000000..e055e3fc --- /dev/null +++ b/src/views/udns/details/hooks/useUDNVirtualMachines/useUDNVirtualMachines.ts @@ -0,0 +1,56 @@ +import { useCallback, useMemo } from 'react'; + +import { modelToGroupVersionKind } from '@kubevirt-ui/kubevirt-api/console'; +import NetworkAttachmentDefinitionModel from '@kubevirt-ui/kubevirt-api/console/models/NetworkAttachmentDefinitionModel'; +import VirtualMachineModel from '@kubevirt-ui/kubevirt-api/console/models/VirtualMachineModel'; +import { V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt'; +import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; +import { NetworkAttachmentDefinitionKind } from '@utils/resources/nads/types'; +import { UDNResource } from '@utils/resources/udns/helper'; +import { findNADForUDN } from '@utils/resources/vm/utils'; + +import { getUDNVirtualMachines, UDNVirtualMachine, vmUsesUDN } from './utils/utils'; + +export type { UDNVirtualMachine }; + +type UseUDNVirtualMachines = { + availableVirtualMachines: V1VirtualMachine[]; + getNADForVM: (vm: V1VirtualMachine) => NetworkAttachmentDefinitionKind | undefined; + loaded: boolean; + loadError: unknown; + virtualMachines: UDNVirtualMachine[]; +}; + +const useUDNVirtualMachines = (udn: UDNResource | undefined): UseUDNVirtualMachines => { + const [vms, vmsLoaded, vmsLoadError] = useK8sWatchResource({ + groupVersionKind: modelToGroupVersionKind(VirtualMachineModel), + isList: true, + }); + + const [nads, nadsLoaded, nadsLoadError] = useK8sWatchResource({ + groupVersionKind: modelToGroupVersionKind(NetworkAttachmentDefinitionModel), + isList: true, + }); + + const getNADForVM = useCallback( + (vm: V1VirtualMachine) => (udn ? findNADForUDN(nads, udn, vm) : undefined), + [nads, udn], + ); + + const virtualMachines = useMemo(() => getUDNVirtualMachines(vms, udn, nads), [nads, udn, vms]); + + const availableVirtualMachines = useMemo( + () => (vms ?? []).filter((vm) => !vmUsesUDN(vm, udn)), + [udn, vms], + ); + + return { + availableVirtualMachines, + getNADForVM, + loaded: vmsLoaded && nadsLoaded, + loadError: vmsLoadError || nadsLoadError, + virtualMachines, + }; +}; + +export default useUDNVirtualMachines; diff --git a/src/views/udns/details/hooks/useUDNVirtualMachines/utils/utils.ts b/src/views/udns/details/hooks/useUDNVirtualMachines/utils/utils.ts new file mode 100644 index 00000000..c3226a85 --- /dev/null +++ b/src/views/udns/details/hooks/useUDNVirtualMachines/utils/utils.ts @@ -0,0 +1,56 @@ +import { V1Network, V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt'; +import { NetworkAttachmentDefinitionKind } from '@utils/resources/nads/types'; +import { UDNResource } from '@utils/resources/udns/helper'; +import { INTERFACE_STATE_ABSENT } from '@utils/resources/vm/constants'; +import { getInterfaces, getNetworks } from '@utils/resources/vm/selectors'; +import { findNADForUDN, multusNetworkMatchesUDN } from '@utils/resources/vm/utils'; + +export type UDNVirtualMachine = { + interfaceName: string; + nad?: NetworkAttachmentDefinitionKind; + vm: V1VirtualMachine; +}; + +const networkUsesUDN = (network: V1Network, udn: UDNResource, vm: V1VirtualMachine): boolean => { + if (!multusNetworkMatchesUDN(network, udn, vm)) { + return false; + } + + const iface = getInterfaces(vm).find((item) => item.name === network.name); + + return Boolean(iface && iface.state !== INTERFACE_STATE_ABSENT); +}; + +export const vmUsesUDN = (vm: V1VirtualMachine, udn: UDNResource | undefined): boolean => { + if (!udn) { + return false; + } + + return getNetworks(vm).some((network) => networkUsesUDN(network, udn, vm)); +}; + +export const getUDNVirtualMachines = ( + vms: undefined | V1VirtualMachine[], + udn: UDNResource | undefined, + nads: NetworkAttachmentDefinitionKind[] | undefined, +): UDNVirtualMachine[] => { + if (!udn) { + return []; + } + + return (vms ?? []).flatMap((vm) => { + const matchingNetworks = getNetworks(vm).filter((network) => networkUsesUDN(network, udn, vm)); + + if (matchingNetworks.length === 0) { + return []; + } + + return [ + { + interfaceName: matchingNetworks[0].name, + nad: findNADForUDN(nads, udn, vm), + vm, + }, + ]; + }); +}; diff --git a/src/views/udns/details/tabs/details/UDNDetailsTab.tsx b/src/views/udns/details/tabs/details/UDNDetailsTab.tsx new file mode 100644 index 00000000..c2eabf57 --- /dev/null +++ b/src/views/udns/details/tabs/details/UDNDetailsTab.tsx @@ -0,0 +1,140 @@ +import React, { FC } from 'react'; +import { size } from 'lodash'; + +import { modelToGroupVersionKind } from '@kubevirt-ui/kubevirt-api/console'; +import { + ResourceLink, + Timestamp, + useAccessReview, + useAnnotationsModal, + useLabelsModal, +} from '@openshift-console/dynamic-plugin-sdk'; +import { + Button, + ButtonVariant, + DescriptionList as DL, + Grid, + GridItem, + PageSection, +} from '@patternfly/react-core'; +import { PencilAltIcon } from '@patternfly/react-icons'; +import { DetailsItem } from '@utils/components/DetailsItem/DetailsItem'; +import { LabelList } from '@utils/components/DetailsItem/LabelList'; +import DetailsSectionTitle from '@utils/components/DetailsSectionTitle/DetailsSectionTitle'; +import Loading from '@utils/components/Loading/Loading'; +import { OwnerReferences } from '@utils/components/OwnerReference/owner-references'; +import { useNetworkingTranslation } from '@utils/hooks/useNetworkingTranslation'; +import { getAnnotations, getLabels, getName, getNamespace, getUID } from '@utils/resources/shared'; +import { getModel, getUDNConditions } from '@utils/resources/udns/selectors'; +import { ClusterUserDefinedNetworkKind, UserDefinedNetworkKind } from '@utils/resources/udns/types'; +import UDNConditions from '@views/udns/list/components/UDNConditions'; + +import UDNLayerDetails from './UDNLayerDetails'; +import UDNTopologyDetails from './UDNTopologyDetails'; + +type UDNDetailsTabProps = { + obj?: ClusterUserDefinedNetworkKind | UserDefinedNetworkKind; +}; + +const UDNDetailsTab: FC = ({ obj: udn }) => { + const { t } = useNetworkingTranslation(); + const name = getName(udn); + const namespace = getNamespace(udn); + const model = udn ? getModel(udn) : undefined; + const annotationsModalLauncher = useAnnotationsModal(udn); + const labelsModalLauncher = useLabelsModal(udn); + + const [canUpdate] = useAccessReview({ + group: model?.apiGroup, + name, + namespace, + resource: model?.plural, + verb: 'patch', + }); + + if (!udn || !model) { + return ( + + + + ); + } + + const annotationCount = size(getAnnotations(udn, {})); + + return ( + + + + +
+ + {namespace && ( + + + + )} + + + + + {canUpdate ? ( + + ) : ( + t('annotation', { + count: annotationCount, + }) + )} + + + + + + + +
+
+ +
+ + + + + + + +
+
+
+
+ ); +}; + +export default UDNDetailsTab; diff --git a/src/views/udns/details/tabs/virtualMachines/UDNVirtualMachines.tsx b/src/views/udns/details/tabs/virtualMachines/UDNVirtualMachines.tsx new file mode 100644 index 00000000..12948d8b --- /dev/null +++ b/src/views/udns/details/tabs/virtualMachines/UDNVirtualMachines.tsx @@ -0,0 +1,203 @@ +import React, { FC, useCallback, useMemo, useState } from 'react'; +import { useSearchParams } from 'react-router'; +import { orderBy } from 'lodash'; + +import { + Button, + Card, + CardBody, + PageSection, + Pagination, + Stack, + StackItem, +} from '@patternfly/react-core'; +import { + DataView, + DataViewFilters, + DataViewTable, + DataViewTextFilter, + DataViewToolbar, + useDataViewFilters, + useDataViewPagination, + useDataViewSort, +} from '@patternfly/react-data-view'; +import useVMNetworkTableColumns from '@utils/components/VMNetworkTable/useVMNetworkTableColumns'; +import VMNetworkColumnManagement from '@utils/components/VMNetworkTable/VMNetworkColumnManagement'; +import { + getVMNetworkTableSortValue, + useVMNetworkDataViewContent, +} from '@utils/components/VMNetworkTable/vmNetworkDataViewContent'; +import { useNetworkingTranslation } from '@utils/hooks/useNetworkingTranslation'; +import useVMResourceLookups from '@utils/hooks/useVMResourceLookups'; +import { getName } from '@utils/resources/shared'; +import { ClusterUserDefinedNetworkKind, UserDefinedNetworkKind } from '@utils/resources/udns/types'; + +import useUDNVirtualMachines, { UDNVirtualMachine } from '../../hooks/useUDNVirtualMachines'; + +import AddUDNVirtualMachinesModal from './components/AddUDNVirtualMachinesModal'; +import UDNVirtualMachineRowActions from './components/UDNVirtualMachineRowActions'; +import { DEFAULT_PER_PAGE } from './constants'; + +type UDNVirtualMachineFilters = { + name?: string; +}; + +const UDNVirtualMachines: FC<{ + obj?: ClusterUserDefinedNetworkKind | UserDefinedNetworkKind; +}> = ({ obj: udn }) => { + const { t } = useNetworkingTranslation(); + const [searchParams, setSearchParams] = useSearchParams(); + const [isAddModalOpen, setIsAddModalOpen] = useState(false); + const { appliedColumns, applyColumns, resetColumns } = useVMNetworkTableColumns(); + + const { availableVirtualMachines, getNADForVM, loaded, loadError, virtualMachines } = + useUDNVirtualMachines(udn); + const { + loaded: resourcesLoaded, + loadError: resourcesLoadError, + lookups: vmResourceLookups, + } = useVMResourceLookups(); + + const { clearAllFilters, filters, onSetFilters } = useDataViewFilters({ + searchParams, + setSearchParams, + }); + + const { onPerPageSelect, onSetPage, page, perPage } = useDataViewPagination({ + page: 1, + perPage: DEFAULT_PER_PAGE, + searchParams, + setSearchParams, + }); + + const { direction, onSort, sortBy } = useDataViewSort({ + searchParams, + setSearchParams, + }); + + const filteredData = useMemo(() => { + const nameFilter = filters.name?.toLowerCase(); + + if (!nameFilter) { + return virtualMachines; + } + + return virtualMachines.filter(({ vm }) => getName(vm)?.toLowerCase().includes(nameFilter)); + }, [filters.name, virtualMachines]); + + const sortedData = useMemo(() => { + if (!sortBy) { + return filteredData; + } + + return orderBy( + filteredData, + [(item: UDNVirtualMachine) => getVMNetworkTableSortValue(item, sortBy, vmResourceLookups)], + [direction === 'desc' ? 'desc' : 'asc'], + ); + }, [direction, filteredData, sortBy, vmResourceLookups]); + + const paginatedData = useMemo(() => { + const start = (page - 1) * perPage; + return sortedData.slice(start, start + perPage); + }, [page, perPage, sortedData]); + + const renderActions = useCallback( + ({ interfaceName, vm }: UDNVirtualMachine) => + udn ? : null, + [udn], + ); + + const { bodyStates, columns, rows } = useVMNetworkDataViewContent({ + appliedColumns, + direction, + onSort, + paginatedData, + renderActions, + sortBy, + t, + vmResourceLookups, + }); + + const activeState = useMemo(() => { + if (loadError || resourcesLoadError) { + return 'error'; + } + + if (!loaded || !resourcesLoaded) { + return 'loading'; + } + + if (filteredData.length === 0) { + return 'empty'; + } + + return undefined; + }, [filteredData.length, loadError, loaded, resourcesLoadError, resourcesLoaded]); + + return ( + + + + + + + + + + onSetFilters(newValues)} + values={filters} + > + + + } + pagination={ +
+ + +
+ } + /> + +
+
+
+
+
+ {udn && ( + setIsAddModalOpen(false)} + /> + )} +
+ ); +}; + +export default UDNVirtualMachines; diff --git a/src/views/udns/details/tabs/virtualMachines/components/AddUDNVirtualMachinesModal.tsx b/src/views/udns/details/tabs/virtualMachines/components/AddUDNVirtualMachinesModal.tsx new file mode 100644 index 00000000..66e0ec47 --- /dev/null +++ b/src/views/udns/details/tabs/virtualMachines/components/AddUDNVirtualMachinesModal.tsx @@ -0,0 +1,216 @@ +import React, { FC, useCallback, useState } from 'react'; + +import { V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt'; +import { + Alert, + AlertVariant, + Button, + Modal, + ModalBody, + ModalFooter, + ModalHeader, + ModalVariant, + Pagination, +} from '@patternfly/react-core'; +import { + DataView, + DataViewFilters, + DataViewTable, + DataViewTextFilter, + DataViewToolbar, +} from '@patternfly/react-data-view'; +import { useNetworkingTranslation } from '@utils/hooks/useNetworkingTranslation'; +import { NetworkAttachmentDefinitionKind } from '@utils/resources/nads/types'; +import { getName } from '@utils/resources/shared'; +import { addVMToNAD } from '@utils/resources/vm/utils'; +import { networkConsole } from '@utils/utils/helpers'; + +import useAddUDNVMModalData from './hooks/useAddUDNVMModalData'; +import useAddUDNVMModalSelection from './hooks/useAddUDNVMModalSelection'; +import useAddUDNVMModalTable from './hooks/useAddUDNVMModalTable'; + +type AddUDNVirtualMachinesModalProps = { + availableVMs: V1VirtualMachine[]; + getNADForVM: (vm: V1VirtualMachine) => NetworkAttachmentDefinitionKind | undefined; + isOpen: boolean; + loaded: boolean; + onClose: () => void; +}; + +const AddUDNVirtualMachinesModal: FC = ({ + availableVMs, + getNADForVM, + isOpen, + loaded, + onClose, +}) => { + const { t } = useNetworkingTranslation(); + const [isSubmitting, setIsSubmitting] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); + + const onResetError = useCallback(() => setErrorMessage(''), []); + + const { + clearAllFilters, + direction, + filteredData, + filters, + onPerPageSelect, + onSetFilters, + onSetPage, + onSort, + page, + paginatedData, + perPage, + sortBy, + } = useAddUDNVMModalData(availableVMs); + + const { + areAllPaginatedSelected, + areSomePaginatedSelected, + onSelectAll, + selected, + selection, + setSelected, + } = useAddUDNVMModalSelection({ + isOpen, + isSubmitting, + onResetError, + paginatedData, + }); + + const { activeState, bodyStates, columns, headStates, rows } = useAddUDNVMModalTable({ + areAllPaginatedSelected, + areSomePaginatedSelected, + direction, + filteredDataLength: filteredData.length, + isSubmitting, + loaded, + onSelectAll, + onSort, + paginatedData, + sortBy, + t, + }); + + const isAddDisabled = isSubmitting || availableVMs.length === 0 || selected.length === 0; + + const handleAdd = async () => { + if (selected.length === 0) { + return; + } + + setIsSubmitting(true); + setErrorMessage(''); + + try { + const results = await Promise.allSettled( + selected.map((vm) => { + const nad = getNADForVM(vm); + + if (!nad) { + networkConsole.warn('Failed to add virtual machine to network', 'nad-not-found'); + return Promise.reject(new Error('Network attachment definition not found')); + } + + return addVMToNAD(vm, nad); + }), + ); + + const failedIndexes = results.flatMap((result, index) => { + if (result.status === 'fulfilled') { + return []; + } + + networkConsole.warn('Failed to add virtual machine to network', result.reason); + return [index]; + }); + + if (failedIndexes.length > 0) { + const failedVMs = failedIndexes.map((index) => selected[index]); + setSelected(failedVMs); + + const failedNames = failedVMs.flatMap((vm) => { + const name = getName(vm); + return name ? [name] : []; + }); + + setErrorMessage( + t('Failed to add the following virtual machines: {{names}}', { + names: failedNames.join(', '), + }), + ); + return; + } + + onClose(); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + + {errorMessage && ( + + {errorMessage} + + )} + + onSetFilters(newValues)} + values={filters} + > + + + } + pagination={ + + } + /> + + + + + + + + + ); +}; + +export default AddUDNVirtualMachinesModal; diff --git a/src/views/udns/details/tabs/virtualMachines/components/UDNVirtualMachineRowActions.tsx b/src/views/udns/details/tabs/virtualMachines/components/UDNVirtualMachineRowActions.tsx new file mode 100644 index 00000000..2913ed17 --- /dev/null +++ b/src/views/udns/details/tabs/virtualMachines/components/UDNVirtualMachineRowActions.tsx @@ -0,0 +1,83 @@ +import React, { FC, useCallback, useMemo } from 'react'; +import { Trans } from 'react-i18next'; + +import { modelToGroupVersionKind } from '@kubevirt-ui/kubevirt-api/console'; +import VirtualMachineModel from '@kubevirt-ui/kubevirt-api/console/models/VirtualMachineModel'; +import { V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt'; +import { Action, ResourceLink, useModal, useToast } from '@openshift-console/dynamic-plugin-sdk'; +import { AlertVariant, ButtonVariant } from '@patternfly/react-core'; +import ActionsDropdown from '@utils/components/ActionsDropdown/ActionsDropdown'; +import ConfirmModal, { ConfirmModalProps } from '@utils/components/ConfirmModal/ConfirmModal'; +import { useNetworkingTranslation } from '@utils/hooks/useNetworkingTranslation'; +import { asAccessReview, getName, getNamespace } from '@utils/resources/shared'; +import { ClusterUserDefinedNetworkKind, UserDefinedNetworkKind } from '@utils/resources/udns/types'; +import { removeVMFromNAD } from '@utils/resources/vm/utils'; +import { networkConsole } from '@utils/utils/helpers'; + +type UDNVirtualMachineRowActionsProps = { + interfaceName: string; + udn: ClusterUserDefinedNetworkKind | UserDefinedNetworkKind; + vm: V1VirtualMachine; +}; + +const UDNVirtualMachineRowActions: FC = ({ + interfaceName, + udn, + vm, +}) => { + const { t } = useNetworkingTranslation(); + const createModal = useModal(); + const { addToast } = useToast(); + const vmName = getName(vm); + const vmNamespace = getNamespace(vm); + const udnName = getName(udn); + + const onRemove = useCallback(() => { + createModal(ConfirmModal, { + btnText: t('Remove'), + btnVariant: ButtonVariant.danger, + executeFn: () => { + removeVMFromNAD(vm, interfaceName).catch((error) => { + networkConsole.warn('Failed to remove virtual machine from network', error); + addToast({ + content: error?.message || t('Failed to remove virtual machine from network'), + dismissible: true, + timeout: true, + title: t('An error occurred.'), + variant: AlertVariant.danger, + }); + }); + }, + message: ( + + Are you sure you want to remove{' '} + {' '} + from {udnName}? + + ), + title: t('Remove virtual machine from network?'), + }); + }, [addToast, createModal, interfaceName, t, udnName, vm, vmName, vmNamespace]); + + const actions = useMemo( + () => [ + { + accessReview: asAccessReview(VirtualMachineModel, vm, 'update'), + cta: onRemove, + id: 'remove-vm-from-udn-network', + label: t('Remove from network'), + }, + ], + [onRemove, t, vm], + ); + + return ; +}; + +export default UDNVirtualMachineRowActions; diff --git a/src/views/udns/details/tabs/virtualMachines/components/hooks/useAddUDNVMModalData.ts b/src/views/udns/details/tabs/virtualMachines/components/hooks/useAddUDNVMModalData.ts new file mode 100644 index 00000000..646122e7 --- /dev/null +++ b/src/views/udns/details/tabs/virtualMachines/components/hooks/useAddUDNVMModalData.ts @@ -0,0 +1,81 @@ +import { useMemo } from 'react'; +import { orderBy } from 'lodash'; + +import { V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt'; +import { + useDataViewFilters, + useDataViewPagination, + useDataViewSort, +} from '@patternfly/react-data-view'; +import { getName, getNamespace } from '@utils/resources/shared'; +import { getVMStatus } from '@utils/resources/vm/selectors'; + +import { DEFAULT_PER_PAGE } from '../../constants'; + +type AddVMFilters = { + name?: string; +}; + +const useAddUDNVMModalData = (availableVMs: V1VirtualMachine[]) => { + const { clearAllFilters, filters, onSetFilters } = useDataViewFilters({}); + + const { onPerPageSelect, onSetPage, page, perPage } = useDataViewPagination({ + page: 1, + perPage: DEFAULT_PER_PAGE, + }); + + const { direction, onSort, sortBy } = useDataViewSort({}); + + const filteredData = useMemo(() => { + const nameFilter = filters.name?.toLowerCase(); + + if (!nameFilter) { + return availableVMs; + } + + return availableVMs.filter((vm) => getName(vm)?.toLowerCase().includes(nameFilter)); + }, [availableVMs, filters.name]); + + const sortedData = useMemo(() => { + if (!sortBy) { + return filteredData; + } + + const getSortValue = (vm: V1VirtualMachine) => { + switch (sortBy) { + case 'name': + return getName(vm) ?? ''; + case 'namespace': + return getNamespace(vm) ?? ''; + case 'status': + return getVMStatus(vm); + default: + return ''; + } + }; + + return orderBy(filteredData, [getSortValue], [direction === 'desc' ? 'desc' : 'asc']); + }, [direction, filteredData, sortBy]); + + const paginatedData = useMemo(() => { + const start = (page - 1) * perPage; + return sortedData.slice(start, start + perPage); + }, [page, perPage, sortedData]); + + return { + clearAllFilters, + direction, + filteredData, + filters, + onPerPageSelect, + onSetFilters, + onSetPage, + onSort, + page, + paginatedData, + perPage, + sortBy, + }; +}; + +export default useAddUDNVMModalData; diff --git a/src/views/udns/details/tabs/virtualMachines/components/hooks/useAddUDNVMModalSelection.ts b/src/views/udns/details/tabs/virtualMachines/components/hooks/useAddUDNVMModalSelection.ts new file mode 100644 index 00000000..c00fdaa1 --- /dev/null +++ b/src/views/udns/details/tabs/virtualMachines/components/hooks/useAddUDNVMModalSelection.ts @@ -0,0 +1,117 @@ +import React, { useCallback, useEffect, useMemo } from 'react'; + +import { V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt'; +import { DataViewTr, isDataViewTrObject, useDataViewSelection } from '@patternfly/react-data-view'; +import { getName, getNamespace } from '@utils/resources/shared'; + +type UseAddUDNVMModalSelectionParams = { + isOpen: boolean; + isSubmitting: boolean; + onResetError: () => void; + paginatedData: V1VirtualMachine[]; +}; + +const useAddUDNVMModalSelection = ({ + isOpen, + isSubmitting, + onResetError, + paginatedData, +}: UseAddUDNVMModalSelectionParams) => { + const { + isSelected: isVMSelected, + onSelect: onVMSelect, + selected, + setSelected, + } = useDataViewSelection({ + matchOption: (item, another) => + getName(item) === getName(another) && getNamespace(item) === getNamespace(another), + }); + + useEffect(() => { + if (isOpen) { + setSelected([]); + onResetError(); + } + // Only reset when the modal opens; setSelected is not stable across renders. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isOpen]); + + const vmByRowId = useMemo( + () => + new Map( + paginatedData.map((vm) => [`${getNamespace(vm)}/${getName(vm)}`, vm]), + ), + [paginatedData], + ); + + const areAllPaginatedSelected = useMemo( + () => paginatedData.length > 0 && paginatedData.every((vm) => isVMSelected(vm)), + [isVMSelected, paginatedData], + ); + + const areSomePaginatedSelected = useMemo( + () => paginatedData.some((vm) => isVMSelected(vm)) && !areAllPaginatedSelected, + [areAllPaginatedSelected, isVMSelected, paginatedData], + ); + + const vmMatches = useCallback( + (a: V1VirtualMachine, b: V1VirtualMachine) => + getName(a) === getName(b) && getNamespace(a) === getNamespace(b), + [], + ); + + const onSelectAll = useCallback( + (_event: React.FormEvent, isSelecting: boolean) => { + if (isSelecting) { + const newSelected = [...selected]; + + paginatedData.forEach((vm) => { + if (!newSelected.some((item) => vmMatches(item, vm))) { + newSelected.push(vm); + } + }); + setSelected(newSelected); + return; + } + + setSelected(selected.filter((item) => !paginatedData.some((vm) => vmMatches(item, vm)))); + }, + [paginatedData, selected, setSelected, vmMatches], + ); + + const selection = useMemo( + () => ({ + isSelectDisabled: () => isSubmitting, + isSelected: (row: DataViewTr) => { + const rowId = isDataViewTrObject(row) ? row.id : undefined; + const vm = rowId ? vmByRowId.get(rowId) : undefined; + + return vm ? isVMSelected(vm) : false; + }, + onSelect: (isSelecting: boolean, row: DataViewTr | DataViewTr[]) => { + const rowsToProcess = Array.isArray(row) ? row : [row]; + + rowsToProcess.forEach((item) => { + const rowId = isDataViewTrObject(item) ? item.id : undefined; + const vm = rowId ? vmByRowId.get(rowId) : undefined; + + if (vm) { + onVMSelect(isSelecting, vm); + } + }); + }, + }), + [isSubmitting, isVMSelected, onVMSelect, vmByRowId], + ); + + return { + areAllPaginatedSelected, + areSomePaginatedSelected, + onSelectAll, + selected, + selection, + setSelected, + }; +}; + +export default useAddUDNVMModalSelection; diff --git a/src/views/udns/details/tabs/virtualMachines/components/hooks/useAddUDNVMModalTable.tsx b/src/views/udns/details/tabs/virtualMachines/components/hooks/useAddUDNVMModalTable.tsx new file mode 100644 index 00000000..52e45df1 --- /dev/null +++ b/src/views/udns/details/tabs/virtualMachines/components/hooks/useAddUDNVMModalTable.tsx @@ -0,0 +1,229 @@ +import React, { useCallback, useMemo } from 'react'; +import { TFunction } from 'i18next'; + +import { modelToGroupVersionKind } from '@kubevirt-ui/kubevirt-api/console'; +import VirtualMachineModel from '@kubevirt-ui/kubevirt-api/console/models/VirtualMachineModel'; +import { V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt'; +import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; +import { Bullseye, EmptyState, Label, Spinner } from '@patternfly/react-core'; +import { DataViewTh } from '@patternfly/react-data-view'; +import { SearchIcon } from '@patternfly/react-icons'; +import { Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; +import { getName, getNamespace } from '@utils/resources/shared'; +import { getVMStatus } from '@utils/resources/vm/selectors'; + +import { ADD_VM_MODAL_COLUMN_COUNT, ADD_VM_MODAL_SORT_COLUMN_KEYS } from '../../constants'; +import { getVMStatusLabelColor } from '../../utils'; + +type UseAddUDNVMModalTableParams = { + areAllPaginatedSelected: boolean; + areSomePaginatedSelected: boolean; + direction: 'asc' | 'desc' | undefined; + filteredDataLength: number; + isSubmitting: boolean; + loaded: boolean; + onSelectAll: (event: React.FormEvent, isSelecting: boolean) => void; + onSort: ( + event: MouseEvent | React.KeyboardEvent | React.MouseEvent, + key: string, + direction: 'asc' | 'desc', + ) => void; + paginatedData: V1VirtualMachine[]; + sortBy: string | undefined; + t: TFunction; +}; + +const useAddUDNVMModalTable = ({ + areAllPaginatedSelected, + areSomePaginatedSelected, + direction, + filteredDataLength, + isSubmitting, + loaded, + onSelectAll, + onSort, + paginatedData, + sortBy, + t, +}: UseAddUDNVMModalTableParams) => { + const getSortParams = useCallback( + (columnIndex: number) => { + const activeSortIndex = sortBy + ? ADD_VM_MODAL_SORT_COLUMN_KEYS.indexOf( + sortBy as (typeof ADD_VM_MODAL_SORT_COLUMN_KEYS)[number], + ) + : -1; + + return { + columnIndex, + onSort: ( + event: MouseEvent | React.KeyboardEvent | React.MouseEvent, + _index: number | string, + newDirection: 'asc' | 'desc', + ) => onSort(event, ADD_VM_MODAL_SORT_COLUMN_KEYS[columnIndex], newDirection), + sortBy: { + direction: activeSortIndex === columnIndex ? direction : undefined, + index: columnIndex, + }, + }; + }, + [direction, onSort, sortBy], + ); + + const columns = useMemo( + () => [ + { + cell: ( + + ), + }, + { + cell: ( + + ), + }, + { + cell: ( + + ), + }, + ], + [getSortParams, t], + ); + + const rows = useMemo( + () => + paginatedData.map((vm) => { + const name = getName(vm); + const vmNamespace = getNamespace(vm); + const status = getVMStatus(vm); + const rowId = `${vmNamespace}/${name}`; + + return { + id: rowId, + row: [ + { + cell: ( + + ), + }, + { + cell: , + }, + { + cell: ( + + ), + }, + ], + }; + }), + [paginatedData], + ); + + const activeState = useMemo(() => { + if (!loaded) { + return 'loading'; + } + + if (filteredDataLength === 0) { + return 'empty'; + } + + return 'data'; + }, [filteredDataLength, loaded]); + + const headStates = useMemo( + () => ({ + data: ( + + + + {columns.map((column, index) => ( + {column.cell} + ))} + + + ), + empty: <>, + }), + [ + areAllPaginatedSelected, + areSomePaginatedSelected, + columns, + isSubmitting, + onSelectAll, + paginatedData.length, + t, + ], + ); + + const bodyStates = useMemo( + () => ({ + empty: ( + + + + + + + + ), + loading: ( + + + + + + + + + + ), + }), + [t], + ); + + return { + activeState, + bodyStates, + columns, + headStates, + rows, + }; +}; + +export default useAddUDNVMModalTable; diff --git a/src/views/udns/details/tabs/virtualMachines/constants.ts b/src/views/udns/details/tabs/virtualMachines/constants.ts new file mode 100644 index 00000000..a29e7384 --- /dev/null +++ b/src/views/udns/details/tabs/virtualMachines/constants.ts @@ -0,0 +1,4 @@ +export const DEFAULT_PER_PAGE = 20; + +export const ADD_VM_MODAL_COLUMN_COUNT = 4; +export const ADD_VM_MODAL_SORT_COLUMN_KEYS = ['name', 'namespace', 'status'] as const; diff --git a/src/views/udns/details/tabs/virtualMachines/utils.ts b/src/views/udns/details/tabs/virtualMachines/utils.ts new file mode 100644 index 00000000..426b4001 --- /dev/null +++ b/src/views/udns/details/tabs/virtualMachines/utils.ts @@ -0,0 +1,15 @@ +export const getVMStatusLabelColor = (status: string) => { + if (!status) { + return 'grey'; + } + + if (status.includes('Error') || status === 'Stopped') { + return 'red'; + } + + if (status === 'Running') { + return 'green'; + } + + return 'orange'; +}; diff --git a/src/views/udns/details/tabs/yaml/UserDefinedNetworkYAML.tsx b/src/views/udns/details/tabs/yaml/UserDefinedNetworkYAML.tsx new file mode 100644 index 00000000..f64a4daa --- /dev/null +++ b/src/views/udns/details/tabs/yaml/UserDefinedNetworkYAML.tsx @@ -0,0 +1,21 @@ +import React, { FC, Suspense } from 'react'; + +import { ResourceYAMLEditor } from '@openshift-console/dynamic-plugin-sdk'; +import Loading from '@utils/components/Loading/Loading'; +import { ClusterUserDefinedNetworkKind, UserDefinedNetworkKind } from '@utils/resources/udns/types'; + +type UserDefinedNetworkYAMLProps = { + obj?: ClusterUserDefinedNetworkKind | UserDefinedNetworkKind; +}; + +const UserDefinedNetworkYAML: FC = ({ obj: udn }) => { + return !udn ? ( + + ) : ( + }> + + + ); +}; + +export default UserDefinedNetworkYAML; diff --git a/src/views/udns/manifest.ts b/src/views/udns/manifest.ts index 71b018f5..71a5c2a1 100644 --- a/src/views/udns/manifest.ts +++ b/src/views/udns/manifest.ts @@ -1,5 +1,5 @@ import { - DetailsItem, + ResourceDetailsPage, ResourceListPage, ResourceNSNavItem, RoutePage, @@ -77,46 +77,18 @@ export const UserDefinedNetworksExtensions: EncodedExtension[] = [ } as EncodedExtension, { properties: { - column: 'right', - component: { $codeRef: 'UserDefinedNetworkTopologyDetails' }, - id: 'cudn-topology-detail-item', + component: { $codeRef: 'UDNDetailsPage' }, model: ClusterUserDefinedNetworkExtensionModel, - path: 'spec.network.topology', - title: 'Topology', }, - type: 'console.resource/details-item', - } as EncodedExtension, + type: 'console.page/resource/details', + } as EncodedExtension, { properties: { - column: 'right', - component: { $codeRef: 'UserDefinedNetworkLayerDetails' }, - id: 'cudn-layer-detail-item', - model: ClusterUserDefinedNetworkExtensionModel, - title: '', - }, - type: 'console.resource/details-item', - } as EncodedExtension, - { - properties: { - column: 'right', - component: { $codeRef: 'UserDefinedNetworkTopologyDetails' }, - id: 'udn-topology-detail-item', - model: UserDefinedNetworkExtensionModel, - path: 'spec.topology', - title: 'Topology', - }, - type: 'console.resource/details-item', - } as EncodedExtension, - { - properties: { - column: 'right', - component: { $codeRef: 'UserDefinedNetworkLayerDetails' }, - id: 'udn-layer-detail-item', + component: { $codeRef: 'UDNDetailsPage' }, model: UserDefinedNetworkExtensionModel, - title: '', }, - type: 'console.resource/details-item', - } as EncodedExtension, + type: 'console.page/resource/details', + } as EncodedExtension, { properties: { component: { @@ -146,8 +118,7 @@ export const UserDefinedNetworksExtensions: EncodedExtension[] = [ ]; export const UserDefinedNetworksExposedModules: ConsolePluginBuildMetadata['exposedModules'] = { + UDNDetailsPage: './views/udns/details/UDNDetailsPage.tsx', UserDefinedNetworkFormPage: './views/udns/form/UserDefinedNetworkFormPage.tsx', - UserDefinedNetworkLayerDetails: './views/udns/details/tabs/details/UDNLayerDetails.tsx', UserDefinedNetworksList: './views/udns/list/UserDefinedNetworksList.tsx', - UserDefinedNetworkTopologyDetails: './views/udns/details/tabs/details/UDNTopologyDetails.tsx', };