diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_de_DE.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_de_DE.json index 2f6f6c5ddb1c..61389c95b72d 100644 --- a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_de_DE.json +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_de_DE.json @@ -34,5 +34,6 @@ "TopicsTab": "Topics", "TopicAclsTab": "ACL", "ConnectorsTab": "Connectors", - "ReplicationsTab": "Replikationsfluss" + "ReplicationsTab": "Replikationsfluss", + "deletionProtectionBadgeLabel": "Löschschutz" } diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_en_GB.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_en_GB.json index 5a3a5c9e19dd..d02e2bcdee82 100644 --- a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_en_GB.json +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_en_GB.json @@ -34,5 +34,6 @@ "TopicsTab": "Topics", "TopicAclsTab": "ACL", "ConnectorsTab": "Connectors", - "ReplicationsTab": "Replication flows" + "ReplicationsTab": "Replication flows", + "deletionProtectionBadgeLabel": "Protection against deletion" } diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_es_ES.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_es_ES.json index 422db07db938..43e488b6a35e 100644 --- a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_es_ES.json +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_es_ES.json @@ -34,5 +34,6 @@ "TopicsTab": "Topics", "TopicAclsTab": "ACL", "ConnectorsTab": "Conectores", - "ReplicationsTab": "Flujo de replicación" + "ReplicationsTab": "Flujo de replicación", + "deletionProtectionBadgeLabel": "Protección contra eliminación" } diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_fr_CA.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_fr_CA.json index 68e3d1a44dcf..d9d5c653ee52 100644 --- a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_fr_CA.json +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_fr_CA.json @@ -34,5 +34,6 @@ "deleteServiceButtonCancel": "Annuler", "deleteServiceButtonConfirm": "Supprimer", "deleteServiceIntegrationDescription": "Supprimer ce service entrainera la suppression de l'association d'intégration suivante :", - "deleteServiceIntegrationsDescription": "Supprimer ce service entrainera la suppression des associations d'intégration suivantes :" + "deleteServiceIntegrationsDescription": "Supprimer ce service entrainera la suppression des associations d'intégration suivantes :", + "deletionProtectionBadgeLabel": "Protection contre suppression" } diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_fr_FR.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_fr_FR.json index 68e3d1a44dcf..d9d5c653ee52 100644 --- a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_fr_FR.json +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_fr_FR.json @@ -34,5 +34,6 @@ "deleteServiceButtonCancel": "Annuler", "deleteServiceButtonConfirm": "Supprimer", "deleteServiceIntegrationDescription": "Supprimer ce service entrainera la suppression de l'association d'intégration suivante :", - "deleteServiceIntegrationsDescription": "Supprimer ce service entrainera la suppression des associations d'intégration suivantes :" + "deleteServiceIntegrationsDescription": "Supprimer ce service entrainera la suppression des associations d'intégration suivantes :", + "deletionProtectionBadgeLabel": "Protection contre suppression" } diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_it_IT.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_it_IT.json index b0d005821fcc..fbfa1d28f6e3 100644 --- a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_it_IT.json +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_it_IT.json @@ -34,5 +34,6 @@ "TopicsTab": "Topic", "TopicAclsTab": "ACL", "ConnectorsTab": "Connector", - "ReplicationsTab": "Fattore di replica" + "ReplicationsTab": "Fattore di replica", + "deletionProtectionBadgeLabel": "Protezione contro l'eliminazione" } diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_pl_PL.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_pl_PL.json index 832db2724263..5d66099932f7 100644 --- a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_pl_PL.json +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_pl_PL.json @@ -34,5 +34,6 @@ "TopicsTab": "Topiki", "TopicAclsTab": "ACL", "ConnectorsTab": "Konektory", - "ReplicationsTab": "Strumień replikacji" + "ReplicationsTab": "Strumień replikacji", + "deletionProtectionBadgeLabel": "Ochrona przed usunięciem" } diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_pt_PT.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_pt_PT.json index 860a17383a9a..2353e5879139 100644 --- a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_pt_PT.json +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/Messages_pt_PT.json @@ -34,5 +34,6 @@ "TopicsTab": "Topics", "TopicAclsTab": "ACL", "ConnectorsTab": "Connectors", - "ReplicationsTab": "Fluxo de replicação" + "ReplicationsTab": "Fluxo de replicação", + "deletionProtectionBadgeLabel": "Proteção contra eliminação" } diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_de_DE.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_de_DE.json index b83b3e0c3c96..61e30dd1baf0 100644 --- a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_de_DE.json +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_de_DE.json @@ -48,5 +48,8 @@ "kafkaSettingsRestApiTitle": "Kafka-REST-API", "kafkaSettingsRestApiDescription": "HTTP-REST-Interface für einen Kafka-Cluster (basierend auf dem Karapace-Projekt)", "kafkaSettingsSchemaRegistryTitle": "Schema Registry", - "kafkaSettingsSchemaRegistryDescription": "HTTP-REST-Interface zum Speichern von Apache-Kafka-Schemas (basierend auf dem Karapace-Projekt)" + "kafkaSettingsSchemaRegistryDescription": "HTTP-REST-Interface zum Speichern von Apache-Kafka-Schemas (basierend auf dem Karapace-Projekt)", + "serviceConfigurationServiceDeletionProtection": " Löschschutz", + "serviceDeletionProtectionActivated": "Aktiviert", + "serviceDeletionProtectionActivatedDeactivated": "Deaktiviert" } diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_en_GB.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_en_GB.json index 2d06fbee1a8b..ba66ea97970b 100644 --- a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_en_GB.json +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_en_GB.json @@ -48,5 +48,8 @@ "kafkaSettingsRestApiTitle": "Kafka REST API", "kafkaSettingsRestApiDescription": "HTTP REST interface for a Kafka cluster (based on the Karapace project)", "kafkaSettingsSchemaRegistryTitle": "Schema Registry", - "kafkaSettingsSchemaRegistryDescription": "HTTP REST service interface for storing Apache Kafka schemas (based on Karapace project)" + "kafkaSettingsSchemaRegistryDescription": "HTTP REST service interface for storing Apache Kafka schemas (based on Karapace project)", + "serviceConfigurationServiceDeletionProtection": " Delete protection", + "serviceDeletionProtectionActivated": "Enabled", + "serviceDeletionProtectionActivatedDeactivated": "Disabled" } diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_es_ES.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_es_ES.json index b9580b63f9b1..e41f9949882f 100644 --- a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_es_ES.json +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_es_ES.json @@ -48,5 +48,8 @@ "kafkaSettingsRestApiTitle": "Kafka REST API", "kafkaSettingsRestApiDescription": "Interfaz HTTP REST para un cluster Kafka (basada en el proyecto Karapace)", "kafkaSettingsSchemaRegistryTitle": "Schema Registry", - "kafkaSettingsSchemaRegistryDescription": "Interfaz de servicio HTTP REST para almacenar los esquemas Apache Kafka (basada en el proyecto Karapace)" + "kafkaSettingsSchemaRegistryDescription": "Interfaz de servicio HTTP REST para almacenar los esquemas Apache Kafka (basada en el proyecto Karapace)", + "serviceConfigurationServiceDeletionProtection": " Protección contra la eliminación", + "serviceDeletionProtectionActivated": "Activada", + "serviceDeletionProtectionActivatedDeactivated": "Desactivada" } diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_fr_CA.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_fr_CA.json index 6e2bfbb07405..ade18c735a7d 100644 --- a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_fr_CA.json +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_fr_CA.json @@ -24,6 +24,9 @@ "serviceConfigurationServiceName": "Nom", "serviceConfigurationServiceMaintenanceTime": "Heure de maintenance (UTC)", "serviceConfigurationServiceBackupTime": "Heure de backup (UTC)", + "serviceConfigurationServiceDeletionProtection": " Protection contre la suppression", + "serviceDeletionProtectionActivated": "Activée", + "serviceDeletionProtectionActivatedDeactivated": "Désactivée", "serviceConfigurationDeleteService": "Supprimer le service", "advancedConfigurationTitle": "Configuration avancée", "advancedConfigurationAddPropertyPlaceholder": "Selectionnez une propriété a ajouter", diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_fr_FR.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_fr_FR.json index 6e2bfbb07405..ade18c735a7d 100644 --- a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_fr_FR.json +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_fr_FR.json @@ -24,6 +24,9 @@ "serviceConfigurationServiceName": "Nom", "serviceConfigurationServiceMaintenanceTime": "Heure de maintenance (UTC)", "serviceConfigurationServiceBackupTime": "Heure de backup (UTC)", + "serviceConfigurationServiceDeletionProtection": " Protection contre la suppression", + "serviceDeletionProtectionActivated": "Activée", + "serviceDeletionProtectionActivatedDeactivated": "Désactivée", "serviceConfigurationDeleteService": "Supprimer le service", "advancedConfigurationTitle": "Configuration avancée", "advancedConfigurationAddPropertyPlaceholder": "Selectionnez une propriété a ajouter", diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_it_IT.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_it_IT.json index 3ff9ad1f02ff..b23a00f26e2d 100644 --- a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_it_IT.json +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_it_IT.json @@ -48,5 +48,8 @@ "kafkaSettingsRestApiTitle": "Kafka API REST", "kafkaSettingsRestApiDescription": "Interfaccia HTTP REST per un cluster Kafka (basata sul progetto Karapace)", "kafkaSettingsSchemaRegistryTitle": "Schema Registry", - "kafkaSettingsSchemaRegistryDescription": "Interfaccia di servizio HTTP REST per archiviare gli schemi Apache Kafka (basata sul progetto Karapace)" + "kafkaSettingsSchemaRegistryDescription": "Interfaccia di servizio HTTP REST per archiviare gli schemi Apache Kafka (basata sul progetto Karapace)", + "serviceConfigurationServiceDeletionProtection": " Protezione contro l'eliminazione", + "serviceDeletionProtectionActivated": "Attivata", + "serviceDeletionProtectionActivatedDeactivated": "Disattivata" } diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_pl_PL.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_pl_PL.json index 5da9fa51117e..c1bdc30214bc 100644 --- a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_pl_PL.json +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_pl_PL.json @@ -48,5 +48,8 @@ "kafkaSettingsRestApiTitle": "Kafka API REST", "kafkaSettingsRestApiDescription": "Interfejs HTTP REST dla klastra Kafka (oparty na projekcie Karapace)", "kafkaSettingsSchemaRegistryTitle": "Schema Registry", - "kafkaSettingsSchemaRegistryDescription": "Interfej usługi HTTP REST do przechowywania schematów Apache Kafka (oparty na projekcie Karapace)" + "kafkaSettingsSchemaRegistryDescription": "Interfej usługi HTTP REST do przechowywania schematów Apache Kafka (oparty na projekcie Karapace)", + "serviceConfigurationServiceDeletionProtection": " Ochrona przed usunięciem", + "serviceDeletionProtectionActivated": "Włączona", + "serviceDeletionProtectionActivatedDeactivated": "Wyłączona" } diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_pt_PT.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_pt_PT.json index bfba77b7edba..beb74cad9ed0 100644 --- a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_pt_PT.json +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/Messages_pt_PT.json @@ -48,5 +48,8 @@ "kafkaSettingsRestApiTitle": "Kafka API REST", "kafkaSettingsRestApiDescription": "Interface HTTP REST para um cluster Kafka (baseado no projeto Karapace)", "kafkaSettingsSchemaRegistryTitle": "Schema Registry", - "kafkaSettingsSchemaRegistryDescription": "Interface de serviço HTTP REST para armazenar os esquemas Apache Kafka (baseado no projeto Karapace)" + "kafkaSettingsSchemaRegistryDescription": "Interface de serviço HTTP REST para armazenar os esquemas Apache Kafka (baseado no projeto Karapace)", + "serviceConfigurationServiceDeletionProtection": " Proteção contra eliminação", + "serviceDeletionProtectionActivated": "Ativada", + "serviceDeletionProtectionActivatedDeactivated": "Desativado" } diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/deletionProtection/Messages_de_DE.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/deletionProtection/Messages_de_DE.json new file mode 100644 index 000000000000..3350a5435422 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/deletionProtection/Messages_de_DE.json @@ -0,0 +1,12 @@ +{ + "deletionProtectionTitle": "Löschschutz", + "deletionProtectionDescription": "Mit dieser Option können Sie Ihre Instanz vor versehentlichem Löschen schützen.", + "activateProtection": "Soll der Löschschutz aktiviert werden?", + "deactivateProtection": "Sind Sie sicher, dass Sie den Löschschutz für diesen Dienst deaktivieren möchten?", + "deletionProtectionButtonCancel": "Abbrechen", + "activateDeletionProtection": "Aktivieren", + "deactivateDeletionProtection": "Deaktivieren", + "updateServiceToastErrorTitle": "Fehler", + "updateServiceToastSuccessTitle": "Erfolg", + "updateServiceToastSuccessDescription": "Ihr Dienst wurde erfolgreich aktualisiert" +} diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/deletionProtection/Messages_en_GB.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/deletionProtection/Messages_en_GB.json new file mode 100644 index 000000000000..7d77bc77de16 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/deletionProtection/Messages_en_GB.json @@ -0,0 +1,12 @@ +{ + "deletionProtectionTitle": "Delete protection", + "deletionProtectionDescription": "This option allows you to protect your instance from accidental deletion.", + "activateProtection": "Do you want to enable delete protection?", + "deactivateProtection": "Are you sure you want to disable removal protection for this service?", + "deletionProtectionButtonCancel": "Cancel", + "activateDeletionProtection": "Activate", + "deactivateDeletionProtection": "Disable", + "updateServiceToastErrorTitle": "Error", + "updateServiceToastSuccessTitle": "Success", + "updateServiceToastSuccessDescription": "Your service has been successfully updated" +} diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/deletionProtection/Messages_es_ES.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/deletionProtection/Messages_es_ES.json new file mode 100644 index 000000000000..616cee8ec181 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/deletionProtection/Messages_es_ES.json @@ -0,0 +1,12 @@ +{ + "deletionProtectionTitle": "Protección contra la eliminación", + "deletionProtectionDescription": "Esta opción le permite proteger su instancia de una eliminación accidental.", + "activateProtection": "¿Desea activar la protección contra eliminación?", + "deactivateProtection": "¿Seguro que quiere desactivar la protección contra la eliminación para este servicio?", + "deletionProtectionButtonCancel": "Cancelar", + "activateDeletionProtection": "Activar", + "deactivateDeletionProtection": "Desactivar", + "updateServiceToastErrorTitle": "Error", + "updateServiceToastSuccessTitle": "Éxito", + "updateServiceToastSuccessDescription": "El servicio se ha actualizado correctamente" +} diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/deletionProtection/Messages_fr_CA.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/deletionProtection/Messages_fr_CA.json new file mode 100644 index 000000000000..b8b6d001e3dd --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/deletionProtection/Messages_fr_CA.json @@ -0,0 +1,12 @@ +{ + "deletionProtectionTitle": "Protection contre la suppression", + "deletionProtectionDescription": "Cette option vous permet de protéger votre instance d'une suppression accidentelle.", + "activateProtection": "Voulez-vous activer la protection contre la suppression?", + "deactivateProtection": "Etes-vous sûr de vouloir désactiver la protection contre la suppression pour ce service?", + "deletionProtectionButtonCancel": "Annuler", + "activateDeletionProtection": "Activer", + "deactivateDeletionProtection": "Désactiver", + "updateServiceToastErrorTitle": "Erreur", + "updateServiceToastSuccessTitle": "Succès", + "updateServiceToastSuccessDescription": "Votre service a été correctement mis à jour" +} diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/deletionProtection/Messages_fr_FR.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/deletionProtection/Messages_fr_FR.json new file mode 100644 index 000000000000..b8b6d001e3dd --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/deletionProtection/Messages_fr_FR.json @@ -0,0 +1,12 @@ +{ + "deletionProtectionTitle": "Protection contre la suppression", + "deletionProtectionDescription": "Cette option vous permet de protéger votre instance d'une suppression accidentelle.", + "activateProtection": "Voulez-vous activer la protection contre la suppression?", + "deactivateProtection": "Etes-vous sûr de vouloir désactiver la protection contre la suppression pour ce service?", + "deletionProtectionButtonCancel": "Annuler", + "activateDeletionProtection": "Activer", + "deactivateDeletionProtection": "Désactiver", + "updateServiceToastErrorTitle": "Erreur", + "updateServiceToastSuccessTitle": "Succès", + "updateServiceToastSuccessDescription": "Votre service a été correctement mis à jour" +} diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/deletionProtection/Messages_it_IT.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/deletionProtection/Messages_it_IT.json new file mode 100644 index 000000000000..728dcfc428c0 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/deletionProtection/Messages_it_IT.json @@ -0,0 +1,12 @@ +{ + "deletionProtectionTitle": "Protezione contro l'eliminazione", + "deletionProtectionDescription": "Questa opzione permette di proteggere l’istanza da eliminazioni accidentali.", + "activateProtection": "Attivare la protezione contro l'eliminazione?", + "deactivateProtection": "Vuoi davvero disattivare la protezione contro la cancellazione per questo servizio?", + "deletionProtectionButtonCancel": "Annulla", + "activateDeletionProtection": "Attiva", + "deactivateDeletionProtection": "Disattiva", + "updateServiceToastErrorTitle": "Errore", + "updateServiceToastSuccessTitle": "Risultato", + "updateServiceToastSuccessDescription": "Il servizio è stato aggiornato correttamente" +} diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/deletionProtection/Messages_pl_PL.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/deletionProtection/Messages_pl_PL.json new file mode 100644 index 000000000000..197947eec761 --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/deletionProtection/Messages_pl_PL.json @@ -0,0 +1,12 @@ +{ + "deletionProtectionTitle": "Ochrona przed usunięciem", + "deletionProtectionDescription": "Ta opcja pozwala chronić instancję przed przypadkowym usunięciem.", + "activateProtection": "Czy chcesz włączyć ochronę przed usunięciem?", + "deactivateProtection": "Czy na pewno chcesz wyłączyć ochronę przed usunięciem dla tej usługi?", + "deletionProtectionButtonCancel": "Anuluj", + "activateDeletionProtection": "Włącz", + "deactivateDeletionProtection": "Wyłącz", + "updateServiceToastErrorTitle": "Błąd", + "updateServiceToastSuccessTitle": "Sukces", + "updateServiceToastSuccessDescription": "Twoja usługa została poprawnie zaktualizowana" +} diff --git a/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/deletionProtection/Messages_pt_PT.json b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/deletionProtection/Messages_pt_PT.json new file mode 100644 index 000000000000..30127ee46e5c --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/public/translations/pci-databases-analytics/services/service/settings/deletionProtection/Messages_pt_PT.json @@ -0,0 +1,12 @@ +{ + "deletionProtectionTitle": "Proteção contra eliminação", + "deletionProtectionDescription": "Esta opção permite-lhe proteger a sua instância de uma eliminação acidental.", + "activateProtection": "Pretende ativar a proteção contra eliminação?", + "deactivateProtection": "Tem a certeza de que quer desativar a proteção contra eliminação para este serviço?", + "deletionProtectionButtonCancel": "Anular", + "activateDeletionProtection": "Ativar", + "deactivateDeletionProtection": "Desativar", + "updateServiceToastErrorTitle": "Erro", + "updateServiceToastSuccessTitle": "Sucesso", + "updateServiceToastSuccessDescription": "O seu serviço foi corretamente atualizado" +} diff --git a/packages/manager/apps/pci-databases-analytics/src/__tests__/helpers/mocks/services.ts b/packages/manager/apps/pci-databases-analytics/src/__tests__/helpers/mocks/services.ts index da3a519a626e..37abb9437fef 100644 --- a/packages/manager/apps/pci-databases-analytics/src/__tests__/helpers/mocks/services.ts +++ b/packages/manager/apps/pci-databases-analytics/src/__tests__/helpers/mocks/services.ts @@ -42,6 +42,7 @@ export const mockedService: database.Service = { }, category: database.engine.CategoryEnum.all, createdAt: '12/12/2022', + deletionProtection: false, description: 'serviceDescription', disk: { size: 1, diff --git a/packages/manager/apps/pci-databases-analytics/src/data/api/database/service.api.ts b/packages/manager/apps/pci-databases-analytics/src/data/api/database/service.api.ts index c39c61bfa993..937eea6e5aa2 100644 --- a/packages/manager/apps/pci-databases-analytics/src/data/api/database/service.api.ts +++ b/packages/manager/apps/pci-databases-analytics/src/data/api/database/service.api.ts @@ -65,6 +65,8 @@ export interface EditService extends ServiceData { restApi?: boolean; } & { schemaRegistry?: boolean; + } & { + deletionProtection?: boolean; } >; } diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/ServiceHeader.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/ServiceHeader.component.tsx index c0397e18a520..99c75d9ebbca 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/ServiceHeader.component.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/_components/ServiceHeader.component.tsx @@ -9,7 +9,8 @@ import { getRegionFlag } from '@/lib/flagHelper'; import Flag from '@/components/flag/Flag.component'; export const ServiceHeader = ({ service }: { service: database.Service }) => { - const { t } = useTranslation('regions'); + const { t } = useTranslation('pci-databases-analytics/services/service'); + const { t: tRegion } = useTranslation('regions'); return (
{ flagName={getRegionFlag(service.nodes[0].region)} className="w-3 h-2" /> - {t(`region_${service.nodes[0].region}`)} + {tRegion(`region_${service.nodes[0].region}`)}
+ {service?.deletionProtection && ( + + {t('deletionProtectionBadgeLabel')} + + )} diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/ServiceConfiguration.component.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/ServiceConfiguration.component.tsx index 05a047fae784..646e54bbb9a8 100644 --- a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/ServiceConfiguration.component.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/ServiceConfiguration.component.tsx @@ -2,6 +2,7 @@ import { Pen } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { + Badge, Button, Table, TableBody, @@ -89,7 +90,7 @@ const ServiceConfiguration = () => { {service.capabilities.service?.update && ( + + + )} {service.capabilities.service?.delete && ( + + + + + + ); +}; + +export default DeletionProtection; diff --git a/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/deletionProtection/DeletionProtection.spec.tsx b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/deletionProtection/DeletionProtection.spec.tsx new file mode 100644 index 000000000000..4d4f646888ae --- /dev/null +++ b/packages/manager/apps/pci-databases-analytics/src/pages/services/[serviceId]/settings/_components/deletionProtection/DeletionProtection.spec.tsx @@ -0,0 +1,95 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { + act, + fireEvent, + render, + screen, + waitFor, +} from '@testing-library/react'; +import { useToast } from '@datatr-ux/uxlib'; +import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; +import { mockedService } from '@/__tests__/helpers/mocks/services'; +import DeletionProtection from './DeletionProtection.modal'; +import * as serviceApi from '@/data/api/database/service.api'; +import { apiErrorMock } from '@/__tests__/helpers/mocks/cdbError'; + +describe('Settings delete modal', () => { + beforeEach(() => { + vi.mock('@/pages/services/[serviceId]/Service.context', () => ({ + useServiceData: vi.fn(() => ({ + projectId: 'projectId', + serviceId: 'serviceId', + service: mockedService, + })), + })); + vi.mock('@/data/api/database/service.api', () => ({ + getService: vi.fn(() => mockedService), + editService: vi.fn((service) => service), + })); + vi.mock('@datatr-ux/uxlib', async () => { + const mod = await vi.importActual('@datatr-ux/uxlib'); + const toastMock = vi.fn(); + return { + ...mod, + useToast: vi.fn(() => ({ + toast: toastMock, + })), + }; + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should render delete modal', async () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + + await waitFor(() => { + expect( + screen.getByTestId('deletion-protection-modal'), + ).toBeInTheDocument(); + expect( + screen.getByTestId('deletion-protection-submit-button'), + ).toBeInTheDocument(); + }); + }); + + it('should edit service on confimr', async () => { + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + act(() => { + fireEvent.click(screen.getByTestId('deletion-protection-submit-button')); + }); + await waitFor(() => { + expect(serviceApi.editService).toHaveBeenCalled(); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'updateServiceToastSuccessTitle', + description: 'updateServiceToastSuccessDescription', + }); + }); + }); + + it('should call onError when API fails', async () => { + vi.mocked(serviceApi.editService).mockImplementation(() => { + throw apiErrorMock; + }); + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + act(() => { + fireEvent.click(screen.getByTestId('deletion-protection-submit-button')); + }); + await waitFor(() => { + expect(serviceApi.editService).toHaveBeenCalled(); + expect(useToast().toast).toHaveBeenCalledWith({ + title: 'updateServiceToastErrorTitle', + description: apiErrorMock.response.data.message, + variant: 'destructive', + }); + }); + }); +}); diff --git a/packages/manager/apps/pci-databases-analytics/src/routes/routes.tsx b/packages/manager/apps/pci-databases-analytics/src/routes/routes.tsx index e52a3260a203..b199457653cb 100644 --- a/packages/manager/apps/pci-databases-analytics/src/routes/routes.tsx +++ b/packages/manager/apps/pci-databases-analytics/src/routes/routes.tsx @@ -579,6 +579,15 @@ export default [ ), ), }, + { + path: 'deletion-protection', + id: 'service.{service.engine}.settings.deletion-protection', + ...lazyRouteConfig(() => + import( + '@/pages/services/[serviceId]/settings/_components/deletionProtection/DeletionProtection.modal' + ), + ), + }, { path: 'update-version', id: 'service.{service.engine}.settings.update-version', diff --git a/packages/manager/apps/pci-databases-analytics/src/types/cloud/project/database/Service.ts b/packages/manager/apps/pci-databases-analytics/src/types/cloud/project/database/Service.ts index 3ad0655daa5d..d248b2131574 100644 --- a/packages/manager/apps/pci-databases-analytics/src/types/cloud/project/database/Service.ts +++ b/packages/manager/apps/pci-databases-analytics/src/types/cloud/project/database/Service.ts @@ -27,6 +27,8 @@ export interface Service { description: string; /** @deprecated Disk attributes of the cluster. DEPRECATED: use storage */ disk: Disk; + /** Deletion Protection */ + deletionProtection?: boolean; /** Enable Prometheus */ enablePrometheus?: boolean; /** List of all endpoints of the service */ diff --git a/packages/manager/apps/pci-databases-analytics/src/types/cloud/project/database/service/CapabilityEnum.ts b/packages/manager/apps/pci-databases-analytics/src/types/cloud/project/database/service/CapabilityEnum.ts index 2ac7edb6f218..dd3272e33dac 100644 --- a/packages/manager/apps/pci-databases-analytics/src/types/cloud/project/database/service/CapabilityEnum.ts +++ b/packages/manager/apps/pci-databases-analytics/src/types/cloud/project/database/service/CapabilityEnum.ts @@ -9,6 +9,7 @@ export enum CapabilityEnum { 'currentQueries' = 'currentQueries', 'currentQueriesCancel' = 'currentQueriesCancel', 'databases' = 'databases', + 'deletionProtection' = 'deletionProtection', 'enableWrites' = 'enableWrites', 'fork' = 'fork', 'indexes' = 'indexes', diff --git a/packages/manager/apps/pci-instances/package.json b/packages/manager/apps/pci-instances/package.json index 7a4dcb7460d1..5bbfb096c8eb 100644 --- a/packages/manager/apps/pci-instances/package.json +++ b/packages/manager/apps/pci-instances/package.json @@ -18,7 +18,7 @@ "@ovh-ux/manager-common-translations": "^0.18.2", "@ovh-ux/manager-config": "^8.6.1", "@ovh-ux/manager-core-api": "^0.18.3", - "@ovh-ux/manager-pci-common": "^0.17.0", + "@ovh-ux/manager-pci-common": "^0.18.3", "@ovh-ux/manager-react-components": "^1.48.0", "@ovh-ux/manager-react-core-application": "^0.12.3", "@ovh-ux/manager-react-shell-client": "^0.9.3", diff --git a/packages/manager/apps/pci-instances/public/translations/actions/Messages_de_DE.json b/packages/manager/apps/pci-instances/public/translations/actions/Messages_de_DE.json index d47f66fc5b3e..56417d068e74 100644 --- a/packages/manager/apps/pci-instances/public/translations/actions/Messages_de_DE.json +++ b/packages/manager/apps/pci-instances/public/translations/actions/Messages_de_DE.json @@ -58,5 +58,21 @@ "pci_instances_actions_shelve_instance_confirmation_message": "Sie sind im Begriff, Ihre Instanz {{ name }} auszusetzen. Die Ihrer Public Cloud Instanz vorbehaltenen Ressourcen werden freigegeben, ihre IP bleibt jedoch erhalten. Die Daten Ihrer lokalen Festplatte werden gespeichert. Nur der dafür verwendete Speicher wird Ihnen in Rechnung gestellt (1). Die Dauer des Vorgangs hängt von der Größe Ihrer lokalen Festplatte ab. Sie können Ihre Instanz jederzeit erneut aktivieren.", "pci_instances_actions_shelve_instance_nota_message": "(1) Bitte beachten Sie: Für monatlich berechnete Instanzen wird die übliche Abrechnung fortgesetzt, unabhängig vom Status der Dienstleistung.", "pci_instances_actions_instance_unavailable_action_error_message": "Diese Aktion ist für Ihre Instanz {{ name }} nicht verfügbar", - "pci_instances_actions_instance_success_message": "Der Status Ihrer Instanz {{ name }} wurde aktualisiert." + "pci_instances_actions_instance_success_message": "Der Status Ihrer Instanz {{ name }} wurde aktualisiert.", + "pci_instances_actions_instance_network_change_dns": "DNS Reverse ändern", + "pci_instances_actions_instance_network_activate_mitigation": "Schutz aktivieren/deaktivieren", + "pci_instances_actions_instance_network_firewall_settings": "Firewall konfigurieren", + "pci_instances_actions_instance_network_network_settings": "Private Netzwerke verwalten", + "pci_instances_actions_instance_network_network_attach": "Netzwerk hinzufügen", + "pci_instances_actions_instance_network_network_attach_title": "Ein oder mehrere private Netzwerke mit Ihrer Instanz verbinden", + "pci_instances_actions_instance_network_network_empty_message": "Sie verfügen über kein kompatibles privates Netzwerk, mit dem Sie diese Instanz verbinden können.", + "pci_instances_actions_instance_network_network_attach_success_message": "Das private Netzwerk {{ network }} wurde mit der Instanz {{ instance }} verbunden.", + "pci_instances_actions_instance_attach_error_message": "Bei der Aktualisierung der Instanz ist ein Fehler aufgetreten: {{ message }}", + "pci_instances_actions_instance_volume_attach_title": "Ein Volume mit Ihrer Instanz verbinden", + "pci_instances_actions_instance_volume_attach_empty_message": "In der Region, in der sich Ihre Instanz befindet, ist kein Volumen verfügbar.", + "pci_instances_actions_instance_volume_attach_selector_label": "Ressource anfügen", + "pci_instances_actions_instance_volume_attach_select": "Vorhandenes Volume auswählen", + "pci_instances_actions_instance_volume_attach_information": "Sie können ein vorhandenes Volume anhängen oder ein neues Volume erstellen, das an Ihre Instanz angehängt werden soll. ", + "pci_instances_actions_instance_volume_attach_create_link": "Volume erstellen", + "pci_instances_actions_instance_volume_attach_success_message": "Das Volume {{ volume }} wurde mit der Instanz {{ instance }} verbunden." } diff --git a/packages/manager/apps/pci-instances/public/translations/actions/Messages_en_GB.json b/packages/manager/apps/pci-instances/public/translations/actions/Messages_en_GB.json index 7a65980edb64..8ef2323cebb8 100644 --- a/packages/manager/apps/pci-instances/public/translations/actions/Messages_en_GB.json +++ b/packages/manager/apps/pci-instances/public/translations/actions/Messages_en_GB.json @@ -58,5 +58,21 @@ "pci_instances_actions_shelve_instance_confirmation_message": "You are about to suspend the {{ name }} instance. The resources dedicated to your Public Cloud instance will be removed, but the IP address will remain. The data on your local disk will be stored, and only the storage used for this will be billed (1). The duration of the operation depends on the size of your local disk. You can reactivate your instance at any time.", "pci_instances_actions_shelve_instance_nota_message": "(1) Warning: For instances billed monthly, standard billing will continue regardless of the service status.", "pci_instances_actions_instance_unavailable_action_error_message": "This action is not available for the {{ name }} instance.", - "pci_instances_actions_instance_success_message": "The status of your {{ name }} instance has been updated" + "pci_instances_actions_instance_success_message": "The status of your {{ name }} instance has been updated", + "pci_instances_actions_instance_network_change_dns": "Change reverse DNS", + "pci_instances_actions_instance_network_activate_mitigation": "Enable/Disable mitigation ", + "pci_instances_actions_instance_network_firewall_settings": "Configure the firewall ", + "pci_instances_actions_instance_network_network_settings": "Manage private networks", + "pci_instances_actions_instance_network_network_attach": "Attach a network ", + "pci_instances_actions_instance_network_network_attach_title": "Attach one or more private networks to your instance", + "pci_instances_actions_instance_network_network_empty_message": "You do not currently have any compatible private networks to attach to this instance.", + "pci_instances_actions_instance_network_network_attach_success_message": "The {{ network }} private network has been attached to the {{ instance }} instance.", + "pci_instances_actions_instance_attach_error_message": "An error has occurred updating the instance: {{ message }}", + "pci_instances_actions_instance_volume_attach_title": "Attach a volume to your instance", + "pci_instances_actions_instance_volume_attach_empty_message": "No volumes available in your instance’s region.", + "pci_instances_actions_instance_volume_attach_selector_label": "Link a resource", + "pci_instances_actions_instance_volume_attach_select": "Select an existing volume", + "pci_instances_actions_instance_volume_attach_information": "You can link an existing volume or create a new volume to link to your instance. ", + "pci_instances_actions_instance_volume_attach_create_link": "Create a volume ", + "pci_instances_actions_instance_volume_attach_success_message": "The {{ volume }} has been attached to the {{ instance }} instance." } diff --git a/packages/manager/apps/pci-instances/public/translations/actions/Messages_es_ES.json b/packages/manager/apps/pci-instances/public/translations/actions/Messages_es_ES.json index 31b28cf78163..470bc426f886 100644 --- a/packages/manager/apps/pci-instances/public/translations/actions/Messages_es_ES.json +++ b/packages/manager/apps/pci-instances/public/translations/actions/Messages_es_ES.json @@ -58,5 +58,21 @@ "pci_instances_actions_shelve_instance_confirmation_message": "Va a suspender su instancia {{ name }}. Los recursos dedicados a la instancia de Public Cloud se liberarán, pero la IP se conservará. Los datos de su disco local se almacenarán y solo se facturará el almacenamiento utilizado para este fin (1). La duración de la operación dependerá del tamaño del disco local. Podrá reactivar su instancia en cualquier momento.", "pci_instances_actions_shelve_instance_nota_message": "(1) Atención: En el caso de las instancias con facturación mensual, la facturación clásica seguirá aplicándose independientemente del estado del servicio.", "pci_instances_actions_instance_unavailable_action_error_message": "Esta acción no está disponible para su instancia {{ name }}", - "pci_instances_actions_instance_success_message": "El estado de su instancia {{ name }} se ha actualizado correctamente" + "pci_instances_actions_instance_success_message": "El estado de su instancia {{ name }} se ha actualizado correctamente", + "pci_instances_actions_instance_network_change_dns": "Cambiar el registro DNS inverso", + "pci_instances_actions_instance_network_activate_mitigation": "Activar/Desactivar la mitigación", + "pci_instances_actions_instance_network_firewall_settings": "Configurar el firewall", + "pci_instances_actions_instance_network_network_settings": "Gestionar las redes privadas", + "pci_instances_actions_instance_network_network_attach": "Asociar una red", + "pci_instances_actions_instance_network_network_attach_title": "Asociar una o más redes privadas a la instancia", + "pci_instances_actions_instance_network_network_empty_message": "Actualmente no tiene ninguna red privada compatible a la que asociar la instancia.", + "pci_instances_actions_instance_network_network_attach_success_message": "Se ha asociado la red privada {{ network }} a la instancia {{ instance }}.", + "pci_instances_actions_instance_attach_error_message": "Se ha producido un error al actualizar la instancia: {{ message }}.", + "pci_instances_actions_instance_volume_attach_title": "Asociar un volumen a su instancia", + "pci_instances_actions_instance_volume_attach_empty_message": "No hay ningún volumen disponible en la región de su instancia.", + "pci_instances_actions_instance_volume_attach_selector_label": "Asociar un recurso", + "pci_instances_actions_instance_volume_attach_select": "Seleccionar un volumen existente", + "pci_instances_actions_instance_volume_attach_information": "Puede asociar un volumen existente o crear un nuevo volumen para asociarlo a su instancia. ", + "pci_instances_actions_instance_volume_attach_create_link": "Crear un volumen", + "pci_instances_actions_instance_volume_attach_success_message": "Se ha asociado el volumen {{ volume }} a la instancia {{ instance }}." } diff --git a/packages/manager/apps/pci-instances/public/translations/actions/Messages_fr_CA.json b/packages/manager/apps/pci-instances/public/translations/actions/Messages_fr_CA.json index 57eaaa118918..26f9164b8dd8 100644 --- a/packages/manager/apps/pci-instances/public/translations/actions/Messages_fr_CA.json +++ b/packages/manager/apps/pci-instances/public/translations/actions/Messages_fr_CA.json @@ -58,5 +58,21 @@ "pci_instances_actions_shelve_instance_confirmation_message": "Vous allez suspendre votre instance {{ name }}. Les ressources dédiées à votre instance Public Cloud seront relâchées mais son IP sera conservée. Les données de votre disque local seront stockées, et seul le stockage utilisé pour cela sera facturé (1). La durée de l'opération est en fonction de la taille de votre disque local. Vous pourrez réactiver votre instance à tout moment.", "pci_instances_actions_shelve_instance_nota_message": "(1) Attention : Pour les instances passées au mois, la facturation classique continuera tout de même, quelque soit le statut du service.", "pci_instances_actions_instance_unavailable_action_error_message": "Cette action n'est pas disponible pour votre instance {{ name }}", - "pci_instances_actions_instance_success_message": "Le statut de votre instance {{ name }} a bien été mis à jour" + "pci_instances_actions_instance_success_message": "Le statut de votre instance {{ name }} a bien été mis à jour", + "pci_instances_actions_instance_network_change_dns": "Changer le reverse DNS", + "pci_instances_actions_instance_network_activate_mitigation": "Activer/Désactiver la mitigation", + "pci_instances_actions_instance_network_firewall_settings": "Configurer le pare-feu", + "pci_instances_actions_instance_network_network_settings": "Gérer les réseaux privés", + "pci_instances_actions_instance_network_network_attach": "Attacher un réseau", + "pci_instances_actions_instance_network_network_attach_title": "Attacher un ou des réseaux privés à votre instance", + "pci_instances_actions_instance_network_network_empty_message": "Vous ne possédez actuellement aucun réseau privé compatible auquel attacher cette instance.", + "pci_instances_actions_instance_network_network_attach_success_message": "Le réseau privé {{ network }} a été attaché à l'instance {{ instance }}.", + "pci_instances_actions_instance_attach_error_message": "Une erreur est survenue lors de la mise à jour de l'instance : {{ message }}", + "pci_instances_actions_instance_volume_attach_title": "Attacher un volume à votre instance", + "pci_instances_actions_instance_volume_attach_empty_message": "Aucun volume n’est disponible dans la région de votre instance.", + "pci_instances_actions_instance_volume_attach_selector_label": "Attacher une ressource", + "pci_instances_actions_instance_volume_attach_select": "Selectionner un volume existant", + "pci_instances_actions_instance_volume_attach_information": "Vous pouvez attacher un volume existant ou créer un nouveau volume à attacher à votre instance. ", + "pci_instances_actions_instance_volume_attach_create_link": "Créer un volume", + "pci_instances_actions_instance_volume_attach_success_message": "Le volume {{ volume }} a été attaché à l'instance {{ instance }}." } diff --git a/packages/manager/apps/pci-instances/public/translations/actions/Messages_fr_FR.json b/packages/manager/apps/pci-instances/public/translations/actions/Messages_fr_FR.json index 57eaaa118918..26f9164b8dd8 100644 --- a/packages/manager/apps/pci-instances/public/translations/actions/Messages_fr_FR.json +++ b/packages/manager/apps/pci-instances/public/translations/actions/Messages_fr_FR.json @@ -58,5 +58,21 @@ "pci_instances_actions_shelve_instance_confirmation_message": "Vous allez suspendre votre instance {{ name }}. Les ressources dédiées à votre instance Public Cloud seront relâchées mais son IP sera conservée. Les données de votre disque local seront stockées, et seul le stockage utilisé pour cela sera facturé (1). La durée de l'opération est en fonction de la taille de votre disque local. Vous pourrez réactiver votre instance à tout moment.", "pci_instances_actions_shelve_instance_nota_message": "(1) Attention : Pour les instances passées au mois, la facturation classique continuera tout de même, quelque soit le statut du service.", "pci_instances_actions_instance_unavailable_action_error_message": "Cette action n'est pas disponible pour votre instance {{ name }}", - "pci_instances_actions_instance_success_message": "Le statut de votre instance {{ name }} a bien été mis à jour" + "pci_instances_actions_instance_success_message": "Le statut de votre instance {{ name }} a bien été mis à jour", + "pci_instances_actions_instance_network_change_dns": "Changer le reverse DNS", + "pci_instances_actions_instance_network_activate_mitigation": "Activer/Désactiver la mitigation", + "pci_instances_actions_instance_network_firewall_settings": "Configurer le pare-feu", + "pci_instances_actions_instance_network_network_settings": "Gérer les réseaux privés", + "pci_instances_actions_instance_network_network_attach": "Attacher un réseau", + "pci_instances_actions_instance_network_network_attach_title": "Attacher un ou des réseaux privés à votre instance", + "pci_instances_actions_instance_network_network_empty_message": "Vous ne possédez actuellement aucun réseau privé compatible auquel attacher cette instance.", + "pci_instances_actions_instance_network_network_attach_success_message": "Le réseau privé {{ network }} a été attaché à l'instance {{ instance }}.", + "pci_instances_actions_instance_attach_error_message": "Une erreur est survenue lors de la mise à jour de l'instance : {{ message }}", + "pci_instances_actions_instance_volume_attach_title": "Attacher un volume à votre instance", + "pci_instances_actions_instance_volume_attach_empty_message": "Aucun volume n’est disponible dans la région de votre instance.", + "pci_instances_actions_instance_volume_attach_selector_label": "Attacher une ressource", + "pci_instances_actions_instance_volume_attach_select": "Selectionner un volume existant", + "pci_instances_actions_instance_volume_attach_information": "Vous pouvez attacher un volume existant ou créer un nouveau volume à attacher à votre instance. ", + "pci_instances_actions_instance_volume_attach_create_link": "Créer un volume", + "pci_instances_actions_instance_volume_attach_success_message": "Le volume {{ volume }} a été attaché à l'instance {{ instance }}." } diff --git a/packages/manager/apps/pci-instances/public/translations/actions/Messages_it_IT.json b/packages/manager/apps/pci-instances/public/translations/actions/Messages_it_IT.json index c6ce4acc747d..c41ada036a27 100644 --- a/packages/manager/apps/pci-instances/public/translations/actions/Messages_it_IT.json +++ b/packages/manager/apps/pci-instances/public/translations/actions/Messages_it_IT.json @@ -58,5 +58,21 @@ "pci_instances_actions_shelve_instance_confirmation_message": "Stai per sospendere l'istanza {{ name }}. Le risorse dedicate all'istanza Public Cloud verranno liberate ma l’IP verrà mantenuto. I dati del disco locale saranno salvati e la fatturazione verrà applicata solo allo storage utilizzato per questo fine. La durata dell'operazione varia in base alla dimensione del disco locale. È possibile riattivare l'istanza in qualsiasi momento.", "pci_instances_actions_shelve_instance_nota_message": "(1) Attenzione: per le istanze con fatturazione mensile, la fatturazione classica continuerà a essere applicata indipendentemente dallo stato del servizio.", "pci_instances_actions_instance_unavailable_action_error_message": "Questa azione non è disponibile per l’istanza {{ name }}", - "pci_instances_actions_instance_success_message": "Lo stato della tua istanza {{ name }} è stato aggiornato correttamente." + "pci_instances_actions_instance_success_message": "Lo stato della tua istanza {{ name }} è stato aggiornato correttamente.", + "pci_instances_actions_instance_network_change_dns": "Modifica il reverse DNS ", + "pci_instances_actions_instance_network_activate_mitigation": "Attiva/Disattiva la mitigazione", + "pci_instances_actions_instance_network_firewall_settings": "Configura il firewall", + "pci_instances_actions_instance_network_network_settings": "Gestisci le reti private", + "pci_instances_actions_instance_network_network_attach": "Associa una rete", + "pci_instances_actions_instance_network_network_attach_title": "Associa una o più reti private alla tua istanza", + "pci_instances_actions_instance_network_network_empty_message": "Al momento non sono presenti reti private compatibili a cui associare questa istanza.", + "pci_instances_actions_instance_network_network_attach_success_message": "La rete privata {{ network }} è stata associata all’istanza {{ instance }}.", + "pci_instances_actions_instance_attach_error_message": "Si è verificato un errore durante l’aggiornamento dell’istanza: {{ message }} ", + "pci_instances_actions_instance_volume_attach_title": "Associa un volume alla tua istanza", + "pci_instances_actions_instance_volume_attach_empty_message": "Nessun volume disponibile nella Region della tua istanza.", + "pci_instances_actions_instance_volume_attach_selector_label": "Associare una risorsa", + "pci_instances_actions_instance_volume_attach_select": "Selezionare un volume esistente", + "pci_instances_actions_instance_volume_attach_information": "Puoi associare un volume esistente o creare un nuovo volume da associare alla tua istanza. ", + "pci_instances_actions_instance_volume_attach_create_link": "Creare un volume", + "pci_instances_actions_instance_volume_attach_success_message": "Il volume {{ volume }} è stato associato all’istanza {{ instance }}." } diff --git a/packages/manager/apps/pci-instances/public/translations/actions/Messages_pl_PL.json b/packages/manager/apps/pci-instances/public/translations/actions/Messages_pl_PL.json index 061fb584cfac..8f8e9e85bc58 100644 --- a/packages/manager/apps/pci-instances/public/translations/actions/Messages_pl_PL.json +++ b/packages/manager/apps/pci-instances/public/translations/actions/Messages_pl_PL.json @@ -58,5 +58,21 @@ "pci_instances_actions_shelve_instance_confirmation_message": "Chcesz zawiesić instancję {{name}}. Zasoby dedykowane Twojej instancji Public Cloud zostaną zwolnione, ale jej IP zostanie zachowane. Dane z Twojego lokalnego dysku będą przechowywane. Fakturowana będzie tylko przestrzeń dyskowa używana do tego celu. Czas operacji zależy od rozmiaru dysku lokalnego. W każdej chwili będziesz mógł ponownie włączyć instancję.", "pci_instances_actions_shelve_instance_nota_message": "(1) Uwaga: w przypadku instancji rozliczanych w trybie miesięcznym, będą one nadal fakturowane zgodnie z tym trybem, niezależnie od statusu usługi.", "pci_instances_actions_instance_unavailable_action_error_message": "Ta operacja nie jest dostępna dla instancji {{name}}", - "pci_instances_actions_instance_success_message": "Status instancji {{name}} został zaktualizowany" + "pci_instances_actions_instance_success_message": "Status instancji {{name}} został zaktualizowany", + "pci_instances_actions_instance_network_change_dns": "Zmień rewers DNS", + "pci_instances_actions_instance_network_activate_mitigation": "Włącz/Wyłącz mitygację", + "pci_instances_actions_instance_network_firewall_settings": "Konfiguracja zapory", + "pci_instances_actions_instance_network_network_settings": "Zarządzaj prywatnymi sieciami", + "pci_instances_actions_instance_network_network_attach": "Przypisz sieć", + "pci_instances_actions_instance_network_network_attach_title": "Przypisz prywatną sieć lub sieci do Twojej instancji", + "pci_instances_actions_instance_network_network_empty_message": "Nie posiadasz aktualnie żadnej kompatybilnej sieci prywatnej, którą mógłbyś przypisać do tej instancji.", + "pci_instances_actions_instance_network_network_attach_success_message": "Sieć prywatna {{ network }} została przypisana do instancji {{ instance }}.", + "pci_instances_actions_instance_attach_error_message": "Wystąpił błąd podczas aktualizacji instancji: {{ message }}", + "pci_instances_actions_instance_volume_attach_title": "Przypisz wolumen do Twojej instancji", + "pci_instances_actions_instance_volume_attach_empty_message": "Brak wolumenów dostępnych w regionie, w którym hostowana jest Twoja instancja", + "pci_instances_actions_instance_volume_attach_selector_label": "Przypisz zasób", + "pci_instances_actions_instance_volume_attach_select": "Wybierz istniejący wolumen", + "pci_instances_actions_instance_volume_attach_information": "Możesz przypisać istniejący wolumen lub utworzyć nowy, który będzie przypisany do Twojej instancji. ", + "pci_instances_actions_instance_volume_attach_create_link": "Utwórz wolumen", + "pci_instances_actions_instance_volume_attach_success_message": "Wolumen {{ volume }} został przypisany do instancji {{ instance }}." } diff --git a/packages/manager/apps/pci-instances/public/translations/actions/Messages_pt_PT.json b/packages/manager/apps/pci-instances/public/translations/actions/Messages_pt_PT.json index 4ef7be1e92f7..8812c7ab1aec 100644 --- a/packages/manager/apps/pci-instances/public/translations/actions/Messages_pt_PT.json +++ b/packages/manager/apps/pci-instances/public/translations/actions/Messages_pt_PT.json @@ -58,5 +58,21 @@ "pci_instances_actions_shelve_instance_confirmation_message": "A sua instância {{ name }} vai ser suspensa. Os recursos dedicados à sua instância Public Cloud serão libertados, mas o seu IP será conservado. Os dados do seu disco local serão armazenados, e apenas será faturado o armazenamento utilizado (1). A duração da operação depende do tamanho do seu disco local. Poderá reativar a sua instância a qualquer momento.", "pci_instances_actions_shelve_instance_nota_message": "(1) Atenção: Para as instâncias com faturação mensal, a faturação clássica continuará a ser aplicada independentemente do estado do serviço.", "pci_instances_actions_instance_unavailable_action_error_message": "Esta ação não está disponível para a sua instância {{ name }}", - "pci_instances_actions_instance_success_message": "O estado da sua instância {{ name }} foi atualizado" + "pci_instances_actions_instance_success_message": "O estado da sua instância {{ name }} foi atualizado", + "pci_instances_actions_instance_network_change_dns": "Alterar a reverse DNS", + "pci_instances_actions_instance_network_activate_mitigation": "Ativar/Desativar a mitigação", + "pci_instances_actions_instance_network_firewall_settings": "Configurar a firewall", + "pci_instances_actions_instance_network_network_settings": "Gerir as redes privadas", + "pci_instances_actions_instance_network_network_attach": "Associar uma rede", + "pci_instances_actions_instance_network_network_attach_title": "Associar uma ou várias redes privadas à sua instância", + "pci_instances_actions_instance_network_network_empty_message": "Atualmente não tem nenhuma rede privada compatível para associar a esta instância.", + "pci_instances_actions_instance_network_network_attach_success_message": "A rede privada {{ network }} foi associada à instância {{ instance }}.", + "pci_instances_actions_instance_attach_error_message": "Ocorreu um erro ao atualizar a instância: {{ message }}.", + "pci_instances_actions_instance_volume_attach_title": "Associar um volume à sua instância", + "pci_instances_actions_instance_volume_attach_empty_message": "Nenhum volume disponível na região da sua instância.", + "pci_instances_actions_instance_volume_attach_selector_label": "Associar um recurso", + "pci_instances_actions_instance_volume_attach_select": "Selecionar um volume existente", + "pci_instances_actions_instance_volume_attach_information": "Pode associar um volume existente ou criar um novo volume a associar à sua instância. ", + "pci_instances_actions_instance_volume_attach_create_link": "Criar um volume", + "pci_instances_actions_instance_volume_attach_success_message": "O volume {{ volume }} foi associado à instância {{ instance }}." } diff --git a/packages/manager/apps/pci-instances/public/translations/common/Messages_de_DE.json b/packages/manager/apps/pci-instances/public/translations/common/Messages_de_DE.json index bbf1a9b04769..cb93ca73a9d6 100644 --- a/packages/manager/apps/pci-instances/public/translations/common/Messages_de_DE.json +++ b/packages/manager/apps/pci-instances/public/translations/common/Messages_de_DE.json @@ -7,5 +7,6 @@ "pci_instances_common_cancel": "Abbrechen", "pci_instances_common_confirm": "Bestätigen", "pci_instances_common_404_error_message": "Ein Fehler 404 („File Not Found“) ist aufgetreten.", - "pci_instances_common_pending": "Wird ausgeführt..." + "pci_instances_common_pending": "Wird ausgeführt...", + "pci_instances_common_search": "Suchen" } diff --git a/packages/manager/apps/pci-instances/public/translations/common/Messages_en_GB.json b/packages/manager/apps/pci-instances/public/translations/common/Messages_en_GB.json index 659dcafecc0f..f3eb4e8e09d5 100644 --- a/packages/manager/apps/pci-instances/public/translations/common/Messages_en_GB.json +++ b/packages/manager/apps/pci-instances/public/translations/common/Messages_en_GB.json @@ -7,5 +7,6 @@ "pci_instances_common_cancel": "Cancel", "pci_instances_common_confirm": "Confirm", "pci_instances_common_404_error_message": "A 404 “File Not Found” error was encountered.", - "pci_instances_common_pending": "In progress" + "pci_instances_common_pending": "In progress", + "pci_instances_common_search": "Search" } diff --git a/packages/manager/apps/pci-instances/public/translations/common/Messages_es_ES.json b/packages/manager/apps/pci-instances/public/translations/common/Messages_es_ES.json index dac8e84825aa..5c8177e9db2c 100644 --- a/packages/manager/apps/pci-instances/public/translations/common/Messages_es_ES.json +++ b/packages/manager/apps/pci-instances/public/translations/common/Messages_es_ES.json @@ -7,5 +7,6 @@ "pci_instances_common_cancel": "Cancelar", "pci_instances_common_confirm": "Confirmar", "pci_instances_common_404_error_message": "Se ha producido un error 404 «File Not Found».", - "pci_instances_common_pending": "En curso..." + "pci_instances_common_pending": "En curso...", + "pci_instances_common_search": "Buscar" } diff --git a/packages/manager/apps/pci-instances/public/translations/common/Messages_fr_CA.json b/packages/manager/apps/pci-instances/public/translations/common/Messages_fr_CA.json index 626c3be6d6a0..b021491e0ebe 100644 --- a/packages/manager/apps/pci-instances/public/translations/common/Messages_fr_CA.json +++ b/packages/manager/apps/pci-instances/public/translations/common/Messages_fr_CA.json @@ -7,5 +7,6 @@ "pci_instances_common_cancel": "Annuler", "pci_instances_common_confirm": "Confirmer", "pci_instances_common_404_error_message": "Une erreur 404 « File Not Found » a été rencontrée.", - "pci_instances_common_pending": "En cours..." + "pci_instances_common_pending": "En cours...", + "pci_instances_common_search": "Rechercher" } diff --git a/packages/manager/apps/pci-instances/public/translations/common/Messages_fr_FR.json b/packages/manager/apps/pci-instances/public/translations/common/Messages_fr_FR.json index 626c3be6d6a0..b021491e0ebe 100644 --- a/packages/manager/apps/pci-instances/public/translations/common/Messages_fr_FR.json +++ b/packages/manager/apps/pci-instances/public/translations/common/Messages_fr_FR.json @@ -7,5 +7,6 @@ "pci_instances_common_cancel": "Annuler", "pci_instances_common_confirm": "Confirmer", "pci_instances_common_404_error_message": "Une erreur 404 « File Not Found » a été rencontrée.", - "pci_instances_common_pending": "En cours..." + "pci_instances_common_pending": "En cours...", + "pci_instances_common_search": "Rechercher" } diff --git a/packages/manager/apps/pci-instances/public/translations/common/Messages_it_IT.json b/packages/manager/apps/pci-instances/public/translations/common/Messages_it_IT.json index 16d5dfd89c1b..51f2d9013b66 100644 --- a/packages/manager/apps/pci-instances/public/translations/common/Messages_it_IT.json +++ b/packages/manager/apps/pci-instances/public/translations/common/Messages_it_IT.json @@ -7,5 +7,6 @@ "pci_instances_common_cancel": "Annullare", "pci_instances_common_confirm": "Confermare", "pci_instances_common_404_error_message": "Si è verificato un errore 404 \"File Not Found\".", - "pci_instances_common_pending": "In corso..." + "pci_instances_common_pending": "In corso...", + "pci_instances_common_search": "Cercare" } diff --git a/packages/manager/apps/pci-instances/public/translations/common/Messages_pl_PL.json b/packages/manager/apps/pci-instances/public/translations/common/Messages_pl_PL.json index 10dfb3ba4d03..c9fae3b65de1 100644 --- a/packages/manager/apps/pci-instances/public/translations/common/Messages_pl_PL.json +++ b/packages/manager/apps/pci-instances/public/translations/common/Messages_pl_PL.json @@ -7,5 +7,6 @@ "pci_instances_common_cancel": "Anuluj", "pci_instances_common_confirm": "Zatwierdź", "pci_instances_common_404_error_message": "Wystąpił błąd 404 „File Not Found”.", - "pci_instances_common_pending": "W trakcie..." + "pci_instances_common_pending": "W trakcie...", + "pci_instances_common_search": "Szukaj" } diff --git a/packages/manager/apps/pci-instances/public/translations/common/Messages_pt_PT.json b/packages/manager/apps/pci-instances/public/translations/common/Messages_pt_PT.json index 3a4d4adb0174..e26f582b130d 100644 --- a/packages/manager/apps/pci-instances/public/translations/common/Messages_pt_PT.json +++ b/packages/manager/apps/pci-instances/public/translations/common/Messages_pt_PT.json @@ -7,5 +7,6 @@ "pci_instances_common_cancel": "Anular", "pci_instances_common_confirm": "Confirmar", "pci_instances_common_404_error_message": "Ocorreu um erro 404 «File Not Found».", - "pci_instances_common_pending": "Em curso..." + "pci_instances_common_pending": "Em curso...", + "pci_instances_common_search": "Procurar" } diff --git a/packages/manager/apps/pci-instances/public/translations/dashboard/Messages_de_DE.json b/packages/manager/apps/pci-instances/public/translations/dashboard/Messages_de_DE.json new file mode 100644 index 000000000000..a0a99e86c59a --- /dev/null +++ b/packages/manager/apps/pci-instances/public/translations/dashboard/Messages_de_DE.json @@ -0,0 +1,29 @@ +{ + "pci_instances_dashboard_edit_name_error_message": "Umbenennen der Instanz fehlgeschlagen, bitte versuchen Sie es in einigen Augenblicken noch einmal", + "pci_instances_dashboard_tab_info_title": "Allgemeine Informationen", + "pci_instances_dashboard_tab_vnc_title": "VNC-Konsole", + "pci_instances_dashboard_info_title": "Informationen", + "pci_instances_dashboard_upgrade_model": "Modell ändern", + "pci_instances_dashboard_memory_title": "Arbeitsspeicher", + "pci_instances_dashboard_processor_title": "Prozessor", + "pci_instances_dashboard_price_title": "Preis", + "pci_instances_dashboard_property_title": "Instanzeigenschaften", + "pci_instances_dashboard_storage_title": "Speicher", + "pci_instances_dashboard_public_bandwidth_title": "Öffentliche Bandbreite", + "pci_instances_dashboard_attach_volumes": "Volume hinzufügen", + "pci_instances_dashboard_edit_image": "Image ändern", + "pci_instances_dashboard_id_openstack_title": "ID", + "pci_instances_dashboard_ssh_key": "SSH-Schlüssel", + "pci_instances_dashboard_network_title": "Netzwerke", + "pci_instances_dashboard_network_ipv4": "IPv4", + "pci_instances_dashboard_network_gateway": "Gateway", + "pci_instances_dashboard_network_dns": "Reverse DNS", + "pci_instances_dashboard_network_ipv6": "IPv6", + "pci_instances_dashboard_network_private_title": "Private Netzwerke", + "pci_instances_dashboard_network_connexion": "Verbindungsinformationen", + "pci_instances_dashboard_all_actions": "Zusätzliche Aktionen", + "pci_instances_dashboard_instance_price_label": "Preis der Instanz: ", + "pci_instances_dashboard_licence_price_label": "Preis der Lizenz: ", + "pci_instances_dashboard_public_network_title": "Öffentliche Netzwerke", + "pci_instances_dashboard_network_floating_title": "Floating IP" +} diff --git a/packages/manager/apps/pci-instances/public/translations/dashboard/Messages_en_GB.json b/packages/manager/apps/pci-instances/public/translations/dashboard/Messages_en_GB.json new file mode 100644 index 000000000000..b637c8961292 --- /dev/null +++ b/packages/manager/apps/pci-instances/public/translations/dashboard/Messages_en_GB.json @@ -0,0 +1,29 @@ +{ + "pci_instances_dashboard_edit_name_error_message": "Unable to rename the instance. Please try again in a few moments", + "pci_instances_dashboard_tab_info_title": "General information", + "pci_instances_dashboard_tab_vnc_title": "VNC console", + "pci_instances_dashboard_info_title": "Information", + "pci_instances_dashboard_upgrade_model": "Modify template", + "pci_instances_dashboard_memory_title": "RAM", + "pci_instances_dashboard_processor_title": "Processor", + "pci_instances_dashboard_price_title": "Price", + "pci_instances_dashboard_property_title": "Instance details", + "pci_instances_dashboard_storage_title": "Storage", + "pci_instances_dashboard_public_bandwidth_title": "Public bandwidth", + "pci_instances_dashboard_attach_volumes": "Attach a volume ", + "pci_instances_dashboard_edit_image": "Change image", + "pci_instances_dashboard_id_openstack_title": "ID", + "pci_instances_dashboard_ssh_key": "SSH key", + "pci_instances_dashboard_network_title": "Networks", + "pci_instances_dashboard_network_ipv4": "IPv4", + "pci_instances_dashboard_network_gateway": "Gateway", + "pci_instances_dashboard_network_dns": "Reverse DNS", + "pci_instances_dashboard_network_ipv6": "IPv6", + "pci_instances_dashboard_network_private_title": "Private networks", + "pci_instances_dashboard_network_connexion": "Login information", + "pci_instances_dashboard_all_actions": "Additional actions", + "pci_instances_dashboard_instance_price_label": "Instance price: ", + "pci_instances_dashboard_licence_price_label": "Licence price: ", + "pci_instances_dashboard_public_network_title": "Public networks", + "pci_instances_dashboard_network_floating_title": "Floating IP" +} diff --git a/packages/manager/apps/pci-instances/public/translations/dashboard/Messages_es_ES.json b/packages/manager/apps/pci-instances/public/translations/dashboard/Messages_es_ES.json new file mode 100644 index 000000000000..935193c52818 --- /dev/null +++ b/packages/manager/apps/pci-instances/public/translations/dashboard/Messages_es_ES.json @@ -0,0 +1,29 @@ +{ + "pci_instances_dashboard_edit_name_error_message": "No se ha podido renombrar la instancia, por favor, vuelva a intentarlo más tarde", + "pci_instances_dashboard_tab_info_title": "Información general", + "pci_instances_dashboard_tab_vnc_title": "Consola VNC", + "pci_instances_dashboard_info_title": "Información", + "pci_instances_dashboard_upgrade_model": "Editar la plantilla", + "pci_instances_dashboard_memory_title": "RAM", + "pci_instances_dashboard_processor_title": "Procesador", + "pci_instances_dashboard_price_title": "Precio", + "pci_instances_dashboard_property_title": "Propiedades de la instancia", + "pci_instances_dashboard_storage_title": "Almacenamiento", + "pci_instances_dashboard_public_bandwidth_title": "Ancho de banda público", + "pci_instances_dashboard_attach_volumes": "Asociar un volumen", + "pci_instances_dashboard_edit_image": "Cambiar imagen", + "pci_instances_dashboard_id_openstack_title": "ID", + "pci_instances_dashboard_ssh_key": "Llave SSH", + "pci_instances_dashboard_network_title": "Redes", + "pci_instances_dashboard_network_ipv4": "IPv4", + "pci_instances_dashboard_network_gateway": "Gateway", + "pci_instances_dashboard_network_dns": "Registro DNS inverso", + "pci_instances_dashboard_network_ipv6": "IPv6", + "pci_instances_dashboard_network_private_title": "Redes privadas", + "pci_instances_dashboard_network_connexion": "Datos de conexión", + "pci_instances_dashboard_all_actions": "Acciones adicionales", + "pci_instances_dashboard_instance_price_label": "Precio de la instancia: ", + "pci_instances_dashboard_licence_price_label": "Precio de la licencia: ", + "pci_instances_dashboard_public_network_title": "Redes públicas", + "pci_instances_dashboard_network_floating_title": "Floating IP" +} diff --git a/packages/manager/apps/pci-instances/public/translations/dashboard/Messages_fr_CA.json b/packages/manager/apps/pci-instances/public/translations/dashboard/Messages_fr_CA.json new file mode 100644 index 000000000000..3af6eef8ac49 --- /dev/null +++ b/packages/manager/apps/pci-instances/public/translations/dashboard/Messages_fr_CA.json @@ -0,0 +1,29 @@ +{ + "pci_instances_dashboard_edit_name_error_message": "Le renommage de l'instance a échoué, merci de recommencer dans quelques instants", + "pci_instances_dashboard_tab_info_title": "Informations générales", + "pci_instances_dashboard_tab_vnc_title": "Console VNC", + "pci_instances_dashboard_info_title": "Informations", + "pci_instances_dashboard_upgrade_model": "Modifier le modèle", + "pci_instances_dashboard_memory_title": "Mémoire vive", + "pci_instances_dashboard_processor_title": "Processeur", + "pci_instances_dashboard_price_title": "Prix", + "pci_instances_dashboard_property_title": "Propriétés de l'instance", + "pci_instances_dashboard_storage_title": "Stockage", + "pci_instances_dashboard_public_bandwidth_title": "Bande passante publique", + "pci_instances_dashboard_attach_volumes": "Attacher un volume", + "pci_instances_dashboard_edit_image": "Changer l'image", + "pci_instances_dashboard_id_openstack_title": "ID", + "pci_instances_dashboard_ssh_key": "Clé SSH", + "pci_instances_dashboard_network_title": "Réseaux", + "pci_instances_dashboard_network_ipv4": "IPv4", + "pci_instances_dashboard_network_gateway": "Gateway", + "pci_instances_dashboard_network_dns": "Reverse DNS", + "pci_instances_dashboard_network_ipv6": "IPv6", + "pci_instances_dashboard_network_private_title": "Réseaux privés", + "pci_instances_dashboard_network_connexion": "Informations de connexion", + "pci_instances_dashboard_all_actions": "Actions supplémentaires", + "pci_instances_dashboard_instance_price_label": "Prix de l'instance : ", + "pci_instances_dashboard_licence_price_label": "Prix de la licence : ", + "pci_instances_dashboard_public_network_title": "Réseaux publics", + "pci_instances_dashboard_network_floating_title": "Floating IP" +} diff --git a/packages/manager/apps/pci-instances/public/translations/dashboard/Messages_fr_FR.json b/packages/manager/apps/pci-instances/public/translations/dashboard/Messages_fr_FR.json new file mode 100644 index 000000000000..3af6eef8ac49 --- /dev/null +++ b/packages/manager/apps/pci-instances/public/translations/dashboard/Messages_fr_FR.json @@ -0,0 +1,29 @@ +{ + "pci_instances_dashboard_edit_name_error_message": "Le renommage de l'instance a échoué, merci de recommencer dans quelques instants", + "pci_instances_dashboard_tab_info_title": "Informations générales", + "pci_instances_dashboard_tab_vnc_title": "Console VNC", + "pci_instances_dashboard_info_title": "Informations", + "pci_instances_dashboard_upgrade_model": "Modifier le modèle", + "pci_instances_dashboard_memory_title": "Mémoire vive", + "pci_instances_dashboard_processor_title": "Processeur", + "pci_instances_dashboard_price_title": "Prix", + "pci_instances_dashboard_property_title": "Propriétés de l'instance", + "pci_instances_dashboard_storage_title": "Stockage", + "pci_instances_dashboard_public_bandwidth_title": "Bande passante publique", + "pci_instances_dashboard_attach_volumes": "Attacher un volume", + "pci_instances_dashboard_edit_image": "Changer l'image", + "pci_instances_dashboard_id_openstack_title": "ID", + "pci_instances_dashboard_ssh_key": "Clé SSH", + "pci_instances_dashboard_network_title": "Réseaux", + "pci_instances_dashboard_network_ipv4": "IPv4", + "pci_instances_dashboard_network_gateway": "Gateway", + "pci_instances_dashboard_network_dns": "Reverse DNS", + "pci_instances_dashboard_network_ipv6": "IPv6", + "pci_instances_dashboard_network_private_title": "Réseaux privés", + "pci_instances_dashboard_network_connexion": "Informations de connexion", + "pci_instances_dashboard_all_actions": "Actions supplémentaires", + "pci_instances_dashboard_instance_price_label": "Prix de l'instance : ", + "pci_instances_dashboard_licence_price_label": "Prix de la licence : ", + "pci_instances_dashboard_public_network_title": "Réseaux publics", + "pci_instances_dashboard_network_floating_title": "Floating IP" +} diff --git a/packages/manager/apps/pci-instances/public/translations/dashboard/Messages_it_IT.json b/packages/manager/apps/pci-instances/public/translations/dashboard/Messages_it_IT.json new file mode 100644 index 000000000000..a57d3caf7598 --- /dev/null +++ b/packages/manager/apps/pci-instances/public/translations/dashboard/Messages_it_IT.json @@ -0,0 +1,29 @@ +{ + "pci_instances_dashboard_edit_name_error_message": "La ridenominazione dell’istanza non è andata a buon fine, ti invitiamo a riprovare tra qualche istante", + "pci_instances_dashboard_tab_info_title": "Informazioni generali", + "pci_instances_dashboard_tab_vnc_title": "Console VNC", + "pci_instances_dashboard_info_title": "Informazioni", + "pci_instances_dashboard_upgrade_model": "Modifica il modello", + "pci_instances_dashboard_memory_title": "RAM", + "pci_instances_dashboard_processor_title": "Processore", + "pci_instances_dashboard_price_title": "Prezzo", + "pci_instances_dashboard_property_title": "Proprietà dell’istanza", + "pci_instances_dashboard_storage_title": "Storage", + "pci_instances_dashboard_public_bandwidth_title": "Banda passante pubblica", + "pci_instances_dashboard_attach_volumes": "Associa un volume", + "pci_instances_dashboard_edit_image": "Cambiare immagine", + "pci_instances_dashboard_id_openstack_title": "ID", + "pci_instances_dashboard_ssh_key": "Chiave SSH", + "pci_instances_dashboard_network_title": "Reti", + "pci_instances_dashboard_network_ipv4": "IPv4", + "pci_instances_dashboard_network_gateway": "Gateway", + "pci_instances_dashboard_network_dns": "Reverse DNS", + "pci_instances_dashboard_network_ipv6": "IPv6", + "pci_instances_dashboard_network_private_title": "Reti private", + "pci_instances_dashboard_network_connexion": "Informazioni di connessione", + "pci_instances_dashboard_all_actions": "Azioni aggiuntive", + "pci_instances_dashboard_instance_price_label": "Prezzo dell'istanza: ", + "pci_instances_dashboard_licence_price_label": "Prezzo della licenza: ", + "pci_instances_dashboard_public_network_title": "Reti pubbliche", + "pci_instances_dashboard_network_floating_title": "Floating IP" +} diff --git a/packages/manager/apps/pci-instances/public/translations/dashboard/Messages_pl_PL.json b/packages/manager/apps/pci-instances/public/translations/dashboard/Messages_pl_PL.json new file mode 100644 index 000000000000..9d27ab392fe3 --- /dev/null +++ b/packages/manager/apps/pci-instances/public/translations/dashboard/Messages_pl_PL.json @@ -0,0 +1,29 @@ +{ + "pci_instances_dashboard_edit_name_error_message": "Zmiana nazwy instancji nie powiodła się. Spróbuj ponownie za kilka minut", + "pci_instances_dashboard_tab_info_title": "Informacje ogólne", + "pci_instances_dashboard_tab_vnc_title": "Konsola VNC", + "pci_instances_dashboard_info_title": "Informacje", + "pci_instances_dashboard_upgrade_model": "Zmień szablon", + "pci_instances_dashboard_memory_title": "Pamięć operacyjna", + "pci_instances_dashboard_processor_title": "Procesor", + "pci_instances_dashboard_price_title": "Cena", + "pci_instances_dashboard_property_title": "Właściwości instancji", + "pci_instances_dashboard_storage_title": "Przestrzeń dyskowa", + "pci_instances_dashboard_public_bandwidth_title": "Przepustowość do sieci publicznej", + "pci_instances_dashboard_attach_volumes": "Przypisz wolumen", + "pci_instances_dashboard_edit_image": "Zmień obraz", + "pci_instances_dashboard_id_openstack_title": "ID", + "pci_instances_dashboard_ssh_key": "Klucz SSH", + "pci_instances_dashboard_network_title": "Sieć", + "pci_instances_dashboard_network_ipv4": "IPv4", + "pci_instances_dashboard_network_gateway": "Gateway", + "pci_instances_dashboard_network_dns": "Rewers DNS", + "pci_instances_dashboard_network_ipv6": "IPv6", + "pci_instances_dashboard_network_private_title": "Sieci prywatne", + "pci_instances_dashboard_network_connexion": "Informacje na temat połączenia", + "pci_instances_dashboard_all_actions": "Dodatkowe działania", + "pci_instances_dashboard_instance_price_label": "Cena za instancję: ", + "pci_instances_dashboard_licence_price_label": "Cena licencji: ", + "pci_instances_dashboard_public_network_title": "Sieci publiczne", + "pci_instances_dashboard_network_floating_title": "Floating IP" +} diff --git a/packages/manager/apps/pci-instances/public/translations/dashboard/Messages_pt_PT.json b/packages/manager/apps/pci-instances/public/translations/dashboard/Messages_pt_PT.json new file mode 100644 index 000000000000..7ca434edfe78 --- /dev/null +++ b/packages/manager/apps/pci-instances/public/translations/dashboard/Messages_pt_PT.json @@ -0,0 +1,29 @@ +{ + "pci_instances_dashboard_edit_name_error_message": "A alteração do nome da instância falhou, agradecemos que comece de novo dentro de alguns instantes", + "pci_instances_dashboard_tab_info_title": "Informações gerais", + "pci_instances_dashboard_tab_vnc_title": "Consola VNC", + "pci_instances_dashboard_info_title": "Informações", + "pci_instances_dashboard_upgrade_model": "Alterar o modelo", + "pci_instances_dashboard_memory_title": "RAM", + "pci_instances_dashboard_processor_title": "Processador", + "pci_instances_dashboard_price_title": "Preço", + "pci_instances_dashboard_property_title": "Propriedades da instância", + "pci_instances_dashboard_storage_title": "Armazenamento", + "pci_instances_dashboard_public_bandwidth_title": "Largura de banda pública", + "pci_instances_dashboard_attach_volumes": "Associar um volume", + "pci_instances_dashboard_edit_image": "Alterar a imagem", + "pci_instances_dashboard_id_openstack_title": "ID", + "pci_instances_dashboard_ssh_key": "Chave SSH", + "pci_instances_dashboard_network_title": "Redes", + "pci_instances_dashboard_network_ipv4": "IPv4", + "pci_instances_dashboard_network_gateway": "Gateway", + "pci_instances_dashboard_network_dns": "Reverse DNS", + "pci_instances_dashboard_network_ipv6": "IPv6", + "pci_instances_dashboard_network_private_title": "Redes privadas", + "pci_instances_dashboard_network_connexion": "Informações de ligação", + "pci_instances_dashboard_all_actions": "Ações suplementares", + "pci_instances_dashboard_instance_price_label": "Preço da instância ", + "pci_instances_dashboard_licence_price_label": "Preço da licença: ", + "pci_instances_dashboard_public_network_title": "Redes públicas", + "pci_instances_dashboard_network_floating_title": "IP flutuante" +} diff --git a/packages/manager/apps/pci-instances/src/adapters/tanstack-query/store/instances/queryKeys.ts b/packages/manager/apps/pci-instances/src/adapters/tanstack-query/store/instances/queryKeys.ts new file mode 100644 index 000000000000..011b1eed224d --- /dev/null +++ b/packages/manager/apps/pci-instances/src/adapters/tanstack-query/store/instances/queryKeys.ts @@ -0,0 +1,33 @@ +import { Query } from '@tanstack/react-query'; +import { instancesQueryKey } from '@/utils'; + +export const listQueryKeyPredicate = (projectId: string) => (query: Query) => + instancesQueryKey(projectId, ['list']).every((elt) => + query.queryKey.includes(elt), + ); + +export const instanceQueryKey = ( + projectId: string, + instance: string, + region: string | null, +): string[] => + instancesQueryKey(projectId, [ + 'region', + String(region), + 'instance', + instance, + 'withBackups', + 'withImage', + 'withNetworks', + 'withVolumes', + ]); + +export const instanceQueryKeyPredicate = ( + projectId: string, + instanceId: string, +) => (query: Query) => + instancesQueryKey(projectId, [ + 'region', + 'instance', + instanceId, + ]).every((elt) => query.queryKey.includes(elt)); diff --git a/packages/manager/apps/pci-instances/src/adapters/tanstack-query/store/instances/selectors.ts b/packages/manager/apps/pci-instances/src/adapters/tanstack-query/store/instances/selectors.ts new file mode 100644 index 000000000000..8e0ad4cf9cbf --- /dev/null +++ b/packages/manager/apps/pci-instances/src/adapters/tanstack-query/store/instances/selectors.ts @@ -0,0 +1,41 @@ +import { QueryClient, InfiniteData } from '@tanstack/react-query'; +import { TAggregatedInstanceDto } from '@/types/instance/api.type'; +import { instanceQueryKeyPredicate, listQueryKeyPredicate } from './queryKeys'; +import { TInstance } from '@/types/instance/entity.type'; + +export const selectInstanceById = ( + projectId: string, + instanceId: string, + queryClient: QueryClient, +): TInstance | undefined => { + const queries = queryClient.getQueriesData({ + predicate: instanceQueryKeyPredicate(projectId, instanceId), + }); + + return queries + .flatMap(([, queryData]) => queryData) + .find((instance) => instance?.id === instanceId); +}; + +export const selectAggregatedInstanceById = ( + projectId: string, + id: string | undefined, + queryClient: QueryClient, +): TAggregatedInstanceDto | undefined => { + if (!id) return undefined; + + const data = queryClient.getQueriesData< + InfiniteData + >({ + predicate: listQueryKeyPredicate(projectId), + }); + + return data.reduce((acc: TAggregatedInstanceDto | undefined, [, result]) => { + if (acc) return acc; + if (result) { + const foundInstance = result.pages.flat().find((elt) => elt.id === id); + return foundInstance ?? acc; + } + return acc; + }, undefined); +}; diff --git a/packages/manager/apps/pci-instances/src/adapters/tanstack-query/store/instances/updaters.ts b/packages/manager/apps/pci-instances/src/adapters/tanstack-query/store/instances/updaters.ts new file mode 100644 index 000000000000..5a382bde0c14 --- /dev/null +++ b/packages/manager/apps/pci-instances/src/adapters/tanstack-query/store/instances/updaters.ts @@ -0,0 +1,121 @@ +import { InfiniteData, QueryClient } from '@tanstack/react-query'; +import { isEqual } from 'lodash'; +import fp from 'lodash/fp'; +import { buildPartialAggregatedInstanceDto } from '@/data/hooks/instance/builder/instanceDto.builder'; +import { TAggregatedInstanceDto } from '@/types/instance/api.type'; +import { TInstance, TPartialInstance } from '@/types/instance/entity.type'; +import { listQueryKeyPredicate, instanceQueryKey } from './queryKeys'; + +type TUpdateAggregatedInstancesFromCache = ( + queryClient: QueryClient, + payload: { + projectId: string; + instance: Pick & + Partial; + }, +) => void; + +export const updateAggregatedInstancesFromCache: TUpdateAggregatedInstancesFromCache = ( + queryClient: QueryClient, + { projectId, instance }, +) => { + const queries = queryClient.getQueriesData< + InfiniteData + >({ + predicate: listQueryKeyPredicate(projectId), + }); + + queries.forEach(([queryKey, queryData]) => { + if (!queryData) return; + + const updatedPages: TAggregatedInstanceDto[][] = queryData.pages.map( + (page): TAggregatedInstanceDto[] => { + const foundIndex = fp.findIndex(fp.propEq('id', instance.id), page); + if (foundIndex === -1) return page; + + const previousInstance = page[foundIndex]; + const mergedInstance = { ...previousInstance, ...instance }; + + if (isEqual(previousInstance, mergedInstance)) return page; + + return fp.update( + foundIndex, + () => mergedInstance, + page, + ) as TAggregatedInstanceDto[]; + }, + ); + + const isPageModified = updatedPages.some( + (page, i) => page !== queryData.pages[i], + ); + + if (!isPageModified) return; + + queryClient.setQueryData>( + queryKey, + (prevData) => { + if (!prevData) return undefined; + return { ...prevData, pages: updatedPages }; + }, + ); + }); +}; + +type TUpdateInstanceFromCacheArgs = { + queryClient: QueryClient; + projectId: string; + instance: TPartialInstance; + region?: string | null; +}; + +export const updateInstanceFromCache = ({ + queryClient, + projectId, + instance, + region = null, +}: TUpdateInstanceFromCacheArgs) => + queryClient.setQueryData( + instanceQueryKey(projectId, instance.id, region), + (prevData) => { + if (!prevData) return undefined; + return { ...prevData, ...instance }; + }, + ); + +type TUpdateAllInstancesFromCacheArgs = { + projectId: string; + instance: TPartialInstance; + region?: string; +}; + +export const updateAllInstancesFromCache = ( + queryClient: QueryClient, + { projectId, instance, region }: TUpdateAllInstancesFromCacheArgs, +) => { + updateAggregatedInstancesFromCache(queryClient, { + projectId, + instance: buildPartialAggregatedInstanceDto(instance), + }); + + updateInstanceFromCache({ + queryClient, + projectId, + instance, + region, + }); +}; + +type TResetInstanceCacheArgs = { + projectId: string; + instanceId: string; + region: string; +}; + +export const resetInstanceCache = ( + queryClient: QueryClient, + { projectId, region, instanceId }: TResetInstanceCacheArgs, +) => + queryClient.invalidateQueries({ + queryKey: instanceQueryKey(projectId, instanceId, region), + }); diff --git a/packages/manager/apps/pci-instances/src/components/breadcrumb/Breadcrumb.component.tsx b/packages/manager/apps/pci-instances/src/components/breadcrumb/Breadcrumb.component.tsx index 12322d970c2d..a0d20ec344f2 100644 --- a/packages/manager/apps/pci-instances/src/components/breadcrumb/Breadcrumb.component.tsx +++ b/packages/manager/apps/pci-instances/src/components/breadcrumb/Breadcrumb.component.tsx @@ -1,5 +1,9 @@ import { useProjectUrl } from '@ovh-ux/manager-react-components'; -import { OsdsBreadcrumb } from '@ovhcloud/ods-components/react'; +import { + Breadcrumb as OdsBreadcrumb, + BreadcrumbItem, + BreadcrumbLink, +} from '@ovhcloud/ods-react'; import { FC } from 'react'; import { useTranslation } from 'react-i18next'; import { useHref } from 'react-router-dom'; @@ -22,19 +26,25 @@ export const Breadcrumb: FC = ({ const projectUrl = useProjectUrl('public-cloud'); const { t } = useTranslation('common'); + const breadcrumbItems: TBreadcrumbItem[] = [ + { + href: projectUrl, + label: projectLabel, + }, + { + href: backHref, + label: t('pci_instances_common_instances_title'), + }, + ...items, + ]; + return ( - + + {breadcrumbItems.map((item) => ( + + {item.label} + + ))} + ); }; diff --git a/packages/manager/apps/pci-instances/src/components/clipboard/Clipboard.component.tsx b/packages/manager/apps/pci-instances/src/components/clipboard/Clipboard.component.tsx new file mode 100644 index 000000000000..bbc9e532600c --- /dev/null +++ b/packages/manager/apps/pci-instances/src/components/clipboard/Clipboard.component.tsx @@ -0,0 +1,32 @@ +import { + Button, + BUTTON_SIZE, + BUTTON_VARIANT, + Clipboard as OdsClipboard, + ClipboardControl, + ClipboardTrigger, + Icon, + ICON_NAME, +} from '@ovhcloud/ods-react'; +import { FC } from 'react'; + +type TClipboardProps = { + value: string; + buttonSize?: BUTTON_SIZE; + buttonVariant?: BUTTON_VARIANT; +}; + +export const Clipboard: FC = ({ + value, + buttonSize = BUTTON_SIZE.xs, + buttonVariant = BUTTON_VARIANT.ghost, +}) => ( + + + + + + +); diff --git a/packages/manager/apps/pci-instances/src/components/clipboard/Clipboard.spec.tsx b/packages/manager/apps/pci-instances/src/components/clipboard/Clipboard.spec.tsx new file mode 100644 index 000000000000..deafb74069e2 --- /dev/null +++ b/packages/manager/apps/pci-instances/src/components/clipboard/Clipboard.spec.tsx @@ -0,0 +1,34 @@ +import { describe, expect } from 'vitest'; +import { act, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { Clipboard } from './Clipboard.component'; + +const mockWriteText = vi.fn(); + +Object.defineProperty(navigator, 'clipboard', { + value: { + writeText: mockWriteText, + }, + configurable: true, +}); + +describe('Considering the GoBack component', () => { + it('should display the value', () => { + const value = 'value to copy'; + + const { getByDisplayValue } = render(); + + expect(getByDisplayValue(value)).toBeVisible(); + }); + + it('should copy the value when clicking the button', async () => { + const value = 'value to copy'; + + const { getByRole } = render(); + + await act(async () => { + await userEvent.click(getByRole('button')); + }); + expect(mockWriteText).toHaveBeenCalledWith(value); + }); +}); diff --git a/packages/manager/apps/pci-instances/src/components/input/InputCancellable.component.tsx b/packages/manager/apps/pci-instances/src/components/input/InputCancellable.component.tsx new file mode 100644 index 000000000000..ef3780a043eb --- /dev/null +++ b/packages/manager/apps/pci-instances/src/components/input/InputCancellable.component.tsx @@ -0,0 +1,48 @@ +import { FC, FormEvent } from 'react'; +import { Button, Icon, Input, InputProp } from '@ovhcloud/ods-react'; +import { useTranslation } from 'react-i18next'; +import { NAMESPACES } from '@ovh-ux/manager-common-translations'; +import { clsx } from 'clsx'; + +type TInputCancellableProps = { + onCancel?: () => void; + onSubmit?: () => void; +} & InputProp; + +export const InputCancellable: FC = ({ + onCancel: onClear, + onSubmit, + className, + ...props +}) => { + const { t } = useTranslation(NAMESPACES.ACTIONS); + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + onSubmit?.(); + }; + + return ( +
+ + + +
+ ); +}; + +export default InputCancellable; diff --git a/packages/manager/apps/pci-instances/src/components/input/InputCancellable.spec.tsx b/packages/manager/apps/pci-instances/src/components/input/InputCancellable.spec.tsx new file mode 100644 index 000000000000..69b1573e0cde --- /dev/null +++ b/packages/manager/apps/pci-instances/src/components/input/InputCancellable.spec.tsx @@ -0,0 +1,49 @@ +import { describe, it, vi, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import InputCancellable from './InputCancellable.component'; + +const handleClear = vi.fn(); +const handleValidate = vi.fn(); +const handleChange = vi.fn(); + +const InputComponent = () => ( + +); + +describe('Input Component', () => { + it('should render input with its default value', () => { + render(); + + expect(screen.getByDisplayValue('NAME')).toBeInTheDocument(); + }); + + it('should call onChange props', async () => { + render(); + + await userEvent.type(screen.getByRole('textbox'), 'new_text'); + + expect(handleChange).toHaveBeenCalled(); + }); + + it('should call handleClear when cancel', async () => { + render(); + + await userEvent.click(screen.getByLabelText('cancel')); + + expect(handleClear).toHaveBeenCalled(); + }); + + it('should call handleValid', async () => { + render(); + + await userEvent.click(screen.getByLabelText('validate')); + + expect(handleValidate).toHaveBeenCalled(); + }); +}); diff --git a/packages/manager/apps/pci-instances/src/components/menu/ActionMenuItem.component.tsx b/packages/manager/apps/pci-instances/src/components/menu/ActionMenuItem.component.tsx new file mode 100644 index 000000000000..9905b378455e --- /dev/null +++ b/packages/manager/apps/pci-instances/src/components/menu/ActionMenuItem.component.tsx @@ -0,0 +1,29 @@ +import { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useHref } from 'react-router-dom'; +import { Icon, Link } from '@ovhcloud/ods-react'; +import { TAction } from '@/pages/instances/instance/dashboard/view-models/selectInstanceDashboard'; + +const linkClassname = + 'w-full box-border p-5 bg-none hover:bg-none hover:bg-[--ods-color-primary-100] focus-visible:bg-[--ods-color-primary-100] focus-visible:rounded-sm focus-visible:outline-none text-blue-700 hover:text-blue-500 focus-visible:text-blue-500'; + +export const ActionMenuItem: FC = ({ label, link }) => { + const { t } = useTranslation('list'); + const internalHref = useHref(link.path); + + const href = link.isExternal ? link.path : internalHref; + + return ( +
+ + {t(label)} + {link.isTargetBlank && } + +
+ ); +}; diff --git a/packages/manager/apps/pci-instances/src/components/menu/ActionMenuItem.spec.tsx b/packages/manager/apps/pci-instances/src/components/menu/ActionMenuItem.spec.tsx new file mode 100644 index 000000000000..282ceb21f4cc --- /dev/null +++ b/packages/manager/apps/pci-instances/src/components/menu/ActionMenuItem.spec.tsx @@ -0,0 +1,72 @@ +import { describe, expect, vi, beforeEach } from 'vitest'; +import { render } from '@testing-library/react'; +import { useHref } from 'react-router-dom'; +import { ActionMenuItem } from './ActionMenuItem.component'; + +vi.mock('react-router-dom', () => ({ + useHref: vi.fn(), +})); + +describe('Considering the ActionsMenu components', () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + describe('target', () => { + it('should render a menu item without a target', () => { + const label = 'Item 1'; + const link = { path: '/item1', isExternal: false }; + + const { getByText } = render( + , + ); + + const linkElement = getByText(label); + expect(linkElement).toBeVisible(); + expect(linkElement).not.toHaveAttribute('target'); + }); + + it('should render a menu item with a target blank link', () => { + const label = 'Item 2'; + const link = { path: '/item2', isExternal: true, isTargetBlank: true }; + + const { getByText } = render( + , + ); + + const linkElement = getByText(label); + expect(linkElement).toBeVisible(); + expect(linkElement).toHaveAttribute('target', '_blank'); + }); + }); + + describe('external link', () => { + it('should be relative path if link is not external', () => { + vi.resetAllMocks(); + vi.mocked(useHref).mockReturnValue('relativePath'); + const label = 'Item 2'; + const link = { path: '/item2', isExternal: false }; + + const { getByText } = render( + , + ); + + const linkElement = getByText(label); + expect(linkElement).toHaveAttribute('href', 'relativePath'); + expect(useHref).toHaveBeenCalledWith('/item2'); + }); + + it('should full path if link is external', () => { + vi.resetAllMocks(); + const label = 'Item 2'; + const link = { path: 'https://site', isExternal: true }; + + const { getByText } = render( + , + ); + + const linkElement = getByText(label); + expect(linkElement).toHaveAttribute('href', 'https://site'); + }); + }); +}); diff --git a/packages/manager/apps/pci-instances/src/components/menu/ActionsMenu.component.tsx b/packages/manager/apps/pci-instances/src/components/menu/ActionsMenu.component.tsx index f52bcb246a06..d3480cc8c972 100644 --- a/packages/manager/apps/pci-instances/src/components/menu/ActionsMenu.component.tsx +++ b/packages/manager/apps/pci-instances/src/components/menu/ActionsMenu.component.tsx @@ -1,82 +1,54 @@ -/* eslint-disable react/no-multi-comp */ -import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; -import { ODS_ICON_NAME, ODS_ICON_SIZE } from '@ovhcloud/ods-components'; -import { OsdsIcon } from '@ovhcloud/ods-components/react'; -import { FC } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useHref } from 'react-router-dom'; +import { FC, useState } from 'react'; import { Button, + BUTTON_COLOR, + BUTTON_VARIANT, + ButtonProp, Divider, + Icon, + ICON_NAME, Popover, PopoverContent, PopoverTrigger, - Link, } from '@ovhcloud/ods-react'; import { DeepReadonly } from '@/types/utils.type'; - -export type TActionsMenuItem = DeepReadonly<{ - label: string; - link: { - path: string; - isExternal: boolean; - }; -}>; +import { ActionMenuItem } from './ActionMenuItem.component'; +import { TAction } from '@/pages/instances/instance/dashboard/view-models/selectInstanceDashboard'; export type TActionsMenuProps = DeepReadonly<{ - items: Map; + items: Map; + actionButton?: Pick; }>; -export type TActionsMenuLinkProps = DeepReadonly<{ - item: TActionsMenuItem; -}>; - -const linkClassname = - 'w-full box-border p-4 bg-none hover:bg-none hover:bg-[--ods-color-primary-100] hover:rounded-sm focus-visible:bg-[--ods-color-primary-100] focus-visible:rounded-sm focus-visible:outline-none text-blue-700 hover:text-blue-500 focus-visible:text-blue-500'; - -export const ActionMenuItem: FC = ({ item }) => { - const { t } = useTranslation('list'); - const internalHref = useHref(item.link.path); - - const href = item.link.isExternal ? item.link.path : internalHref; - - return ( -
- - {t(item.label)} - -
- ); -}; +export const ActionsMenu: FC = ({ items, actionButton }) => { + const [isOpen, setOpen] = useState(false); + const { size } = items; -export const ActionsMenu = ({ items }: TActionsMenuProps) => { return ( - - e.preventDefault()} - asChild - disabled={!items.size} - > - - - {Array.from(items.entries()).map(([group, item], index, arr) => ( + setOpen(false)} className="p-0"> + {Array.from(items.entries()).map(([group, item], index) => (
- {item.map((elt) => ( - + {index !== 0 && } + {item.map((action: TAction) => ( + ))} - {arr.length - 1 !== index && }
))}
diff --git a/packages/manager/apps/pci-instances/src/components/menu/ActionsMenu.spec.tsx b/packages/manager/apps/pci-instances/src/components/menu/ActionsMenu.spec.tsx index 689bc2da8630..2f2799f5d098 100644 --- a/packages/manager/apps/pci-instances/src/components/menu/ActionsMenu.spec.tsx +++ b/packages/manager/apps/pci-instances/src/components/menu/ActionsMenu.spec.tsx @@ -1,32 +1,14 @@ -import { describe, expect, test, vi } from 'vitest'; +import { describe, expect, vi } from 'vitest'; import { act, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { Popover, PopoverContent } from '@ovhcloud/ods-react'; -import { - ActionsMenu, - ActionMenuItem, - TActionsMenuItem, -} from './ActionsMenu.component'; +import { ActionsMenu } from './ActionsMenu.component'; vi.mock('react-router-dom', () => ({ useHref: () => '/item1', })); -const prepareTest = (item: TActionsMenuItem) => { - render( - - - - - , - ); - return screen.getByTestId('actions-menu-item'); -}; - describe('Considering the ActionsMenu components', () => { - test('Should render a menu with grouped items', async () => { - const user = userEvent.setup(); - + it('should render a menu with grouped items', async () => { const items = new Map([ [ 'details', @@ -44,44 +26,14 @@ describe('Considering the ActionsMenu components', () => { ], ], ]); + render(); + const button = screen.getByTestId('actions-menu-button'); - expect(screen.getByTestId('actions-menu-button')).toBeInTheDocument(); + expect(screen.getByTestId('actions-menu-button')).toBeVisible(); await act(async () => { - await user.click(button); + await userEvent.click(button); }); expect(screen.getAllByTestId('actions-menu-item')).toHaveLength(2); }); - - test('Should render a menu item with an internal link', () => { - const item = { - label: 'Item 1', - isDisabled: false, - link: { path: '/item1', isExternal: false }, - }; - const elt = prepareTest(item); - expect(elt).toBeInTheDocument(); - expect(elt).toHaveAttribute('href', '/item1'); - }); - - test('Should render a menu item with an external link', () => { - const item = { - label: 'Item 2', - isDisabled: false, - link: { path: '/item2', isExternal: true }, - }; - const elt = prepareTest(item); - expect(elt).toBeInTheDocument(); - expect(elt).toHaveAttribute('href', '/item2'); - }); - - test('Should render a menu item with a custom label', () => { - const item = { - label: 'Custom Label', - isDisabled: false, - link: { path: '/item1', isExternal: false }, - }; - const elt = prepareTest(item); - expect(elt).toHaveTextContent('Custom Label'); - }); }); diff --git a/packages/manager/apps/pci-instances/src/components/modal/Modal.component.tsx b/packages/manager/apps/pci-instances/src/components/modal/Modal.component.tsx index e2f42da4bc8e..a2624af9d31c 100644 --- a/packages/manager/apps/pci-instances/src/components/modal/Modal.component.tsx +++ b/packages/manager/apps/pci-instances/src/components/modal/Modal.component.tsx @@ -1,15 +1,15 @@ import { NAMESPACES } from '@ovh-ux/manager-common-translations'; import { useTranslation } from 'react-i18next'; import { useId } from 'react'; -import { OsdsText } from '@ovhcloud/ods-components/react'; import { - ODS_BUTTON_VARIANT, - ODS_TEXT_COLOR_HUE, - ODS_TEXT_LEVEL, - ODS_TEXT_SIZE, -} from '@ovhcloud/ods-components'; -import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; -import { Modal as ODSModal, ModalContent, Button } from '@ovhcloud/ods-react'; + Button, + BUTTON_COLOR, + BUTTON_VARIANT, + Modal as ODSModal, + ModalContent, + Text, + TEXT_PRESET, +} from '@ovhcloud/ods-react'; export type TModalVariant = 'primary' | 'warning' | 'critical'; @@ -21,6 +21,7 @@ const Modal = ({ handleInstanceAction, variant = 'primary', dismissible = false, + disabled, }: { title: string; onModalClose: () => void; @@ -29,6 +30,7 @@ const Modal = ({ handleInstanceAction: () => void; variant?: TModalVariant; dismissible?: boolean; + disabled?: boolean; }) => { const { t } = useTranslation(NAMESPACES.ACTIONS); const id = useId(); @@ -46,29 +48,22 @@ const Modal = ({ dismissible={dismissible} >
- - {title} - + {title} {children}
diff --git a/packages/manager/apps/pci-instances/src/components/navigation/GoBack.component.tsx b/packages/manager/apps/pci-instances/src/components/navigation/GoBack.component.tsx index 5659a1a78a45..c95a5bd89ae1 100644 --- a/packages/manager/apps/pci-instances/src/components/navigation/GoBack.component.tsx +++ b/packages/manager/apps/pci-instances/src/components/navigation/GoBack.component.tsx @@ -10,7 +10,7 @@ export const GoBack: FC = () => { const backHref = useHref('..'); return ( diff --git a/packages/manager/apps/pci-instances/src/components/navigation/GoBack.spec.tsx b/packages/manager/apps/pci-instances/src/components/navigation/GoBack.spec.tsx index bfa2b39a0684..c719844a22d3 100644 --- a/packages/manager/apps/pci-instances/src/components/navigation/GoBack.spec.tsx +++ b/packages/manager/apps/pci-instances/src/components/navigation/GoBack.spec.tsx @@ -18,7 +18,7 @@ describe('Considering the GoBack component', () => { test('Should be rendered with correct class and href attribute', () => { const goBackElement = getGoBackElement(); expect(goBackElement).toBeInTheDocument(); - expect(goBackElement).toHaveClass('mt-12 mb-3'); + expect(goBackElement).toHaveClass('my-3'); expect(goBackElement).toHaveAttribute('href', backHref); }); diff --git a/packages/manager/apps/pci-instances/src/components/priceLabel/PriceLabel.component.tsx b/packages/manager/apps/pci-instances/src/components/priceLabel/PriceLabel.component.tsx new file mode 100644 index 000000000000..2cb024f2dbb9 --- /dev/null +++ b/packages/manager/apps/pci-instances/src/components/priceLabel/PriceLabel.component.tsx @@ -0,0 +1,32 @@ +import { FC } from 'react'; +import { OsdsText } from '@ovhcloud/ods-components/react'; +import { ODS_TEXT_SIZE, ODS_TEXT_LEVEL } from '@ovhcloud/ods-components'; +import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; +import { useCatalogPrice } from '@ovh-ux/manager-react-components'; + +type TPriceLabelProps = { + type: 'hour' | 'month' | 'licence' | string; + value: number; +}; + +const PriceLabel: FC = ({ value, type }) => { + const { + getFormattedHourlyCatalogPrice, + getFormattedMonthlyCatalogPrice, + } = useCatalogPrice(3); + + return ( + + {type === 'month' + ? getFormattedMonthlyCatalogPrice(value) + : getFormattedHourlyCatalogPrice(value)} + + ); +}; + +export default PriceLabel; diff --git a/packages/manager/apps/pci-instances/src/components/tab/TabsPanel.component.tsx b/packages/manager/apps/pci-instances/src/components/tab/TabsPanel.component.tsx new file mode 100644 index 000000000000..4b9e76a14652 --- /dev/null +++ b/packages/manager/apps/pci-instances/src/components/tab/TabsPanel.component.tsx @@ -0,0 +1,42 @@ +import { FC, useEffect, useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { Tabs, TabList, Tab, TabsValueChangeEvent } from '@ovhcloud/ods-react'; + +type TTabItem = { + label: string; + to: string; + badge?: string; +}; + +type TTabsPanelProps = { + tabs: TTabItem[]; +}; + +const TabsPanel: FC = ({ tabs }) => { + const location = useLocation(); + const [activeTab, setActiveTab] = useState(''); + const navigate = useNavigate(); + + const handleTabChange = (event: TabsValueChangeEvent) => + navigate(event.value); + + useEffect(() => { + const defaultTab = tabs.find((tab) => location.pathname === tab.to); + + if (defaultTab) setActiveTab(defaultTab.to); + }, [location.pathname, tabs]); + + return ( + + + {tabs.map((tab) => ( + + {tab.label} + + ))} + + + ); +}; + +export default TabsPanel; diff --git a/packages/manager/apps/pci-instances/src/data/api/instance.ts b/packages/manager/apps/pci-instances/src/data/api/instance.ts index 1350e5295ae3..ae8ea661acc5 100644 --- a/packages/manager/apps/pci-instances/src/data/api/instance.ts +++ b/packages/manager/apps/pci-instances/src/data/api/instance.ts @@ -21,12 +21,15 @@ type TInstanceAction = | 'snapshot' | 'activeMonthlyBilling'; +const instanceUrl = (projectId: string, instanceId: string) => + `/cloud/project/${projectId}/instance/${instanceId}`; + const instanceActionUrl = ( projectId: string, instanceId: string, action: TInstanceAction, ): string => { - const basePathname = `/cloud/project/${projectId}/instance/${instanceId}`; + const basePathname = instanceUrl(projectId, instanceId); return action === 'delete' ? basePathname : `${basePathname}/${action}`; }; @@ -172,3 +175,48 @@ export const getInstance = async ({ .then((response: AxiosResponse) => mapDtoToInstance(response.data), ); + +type TUpdateInstanceName = { + projectId: string; + instanceId: string; + instanceName: string; +}; + +export const updateInstanceName = ({ + projectId, + instanceId, + instanceName, +}: TUpdateInstanceName): Promise => + v6.put(instanceUrl(projectId, instanceId), { + instanceName, + }); + +type TAttachNetworkArgs = { + projectId: string; + instanceId: string; + networkId: string; +}; + +export const attachNetwork = ({ + projectId, + instanceId, + networkId, +}: TAttachNetworkArgs): Promise => + v6.post(`/cloud/project/${projectId}/instance/${instanceId}/interface`, { + networkId, + }); + +type TAttachVolumeArgs = { + projectId: string; + instanceId: string; + volumeId: string; +}; + +export const attachVolume = ({ + projectId, + volumeId, + instanceId, +}: TAttachVolumeArgs): Promise => + v6.post(`/cloud/project/${projectId}/volume/${volumeId}/attach`, { + instanceId, + }); diff --git a/packages/manager/apps/pci-instances/src/data/api/mapper/instance.mapper.ts b/packages/manager/apps/pci-instances/src/data/api/mapper/instance.mapper.ts index a9905e118a93..7d4f8746d648 100644 --- a/packages/manager/apps/pci-instances/src/data/api/mapper/instance.mapper.ts +++ b/packages/manager/apps/pci-instances/src/data/api/mapper/instance.mapper.ts @@ -36,7 +36,7 @@ const mapVolume = (volume: TInstanceVolumeDto): TInstanceVolume => ({ const mapPricing = (pricingDto: TInstancePriceDto): TInstancePrice => ({ ...pricingDto, currencyCode: pricingDto.price.currencyCode, - priceInUCents: pricingDto.price.priceInUCents, + priceInUcents: pricingDto.price.priceInUcents, text: pricingDto.price.text, value: pricingDto.price.value, }); diff --git a/packages/manager/apps/pci-instances/src/data/api/mapper/network.mapper.ts b/packages/manager/apps/pci-instances/src/data/api/mapper/network.mapper.ts new file mode 100644 index 000000000000..1d2b84c390ab --- /dev/null +++ b/packages/manager/apps/pci-instances/src/data/api/mapper/network.mapper.ts @@ -0,0 +1,8 @@ +import { TNetworkDto } from '@/types/network/api.type'; +import { TNetwork } from '@/types/network/entity.type'; + +export const mapNetworkDtoToNetwork = (dto: TNetworkDto): TNetwork => { + return { + ...dto, + }; +}; diff --git a/packages/manager/apps/pci-instances/src/data/api/network.ts b/packages/manager/apps/pci-instances/src/data/api/network.ts new file mode 100644 index 000000000000..4dbd2703a4cc --- /dev/null +++ b/packages/manager/apps/pci-instances/src/data/api/network.ts @@ -0,0 +1,18 @@ +import { v6 } from '@ovh-ux/manager-core-api'; +import { TNetwork } from '@/types/network/entity.type'; +import { TNetworkDto } from '@/types/network/api.type'; +import { mapNetworkDtoToNetwork } from './mapper/network.mapper'; + +export const getNetworks = ({ + projectId, + region, +}: { + projectId: string; + region: string; +}): Promise => + v6 + .get(`/cloud/project/${projectId}/region/${region}/network`) + .then((response) => response.data.map(mapNetworkDtoToNetwork)); + +export const getReverseDns = (ip: string): Promise => + v6.get(`/ip/${ip}/reverse`).then((response) => response.data); diff --git a/packages/manager/apps/pci-instances/src/data/api/volume.ts b/packages/manager/apps/pci-instances/src/data/api/volume.ts new file mode 100644 index 000000000000..334c0d4813dc --- /dev/null +++ b/packages/manager/apps/pci-instances/src/data/api/volume.ts @@ -0,0 +1,14 @@ +import { v6 } from '@ovh-ux/manager-core-api'; +import { TVolume } from '@/types/volume/common.type'; +import { TVolumeDto } from '@/types/volume/api.type'; + +export const getVolumes = ({ + projectId, + region, +}: { + projectId: string; + region: string; +}): Promise => + v6 + .get(`/cloud/project/${projectId}/region/${region}/volume`) + .then((response) => response.data); diff --git a/packages/manager/apps/pci-instances/src/data/hooks/instance/action/useInstanceAction.spec.tsx b/packages/manager/apps/pci-instances/src/data/hooks/instance/action/useInstanceAction.spec.tsx index 6d5c1ad5e3d5..84fb76e9f347 100644 --- a/packages/manager/apps/pci-instances/src/data/hooks/instance/action/useInstanceAction.spec.tsx +++ b/packages/manager/apps/pci-instances/src/data/hooks/instance/action/useInstanceAction.spec.tsx @@ -4,12 +4,13 @@ import { FC, PropsWithChildren } from 'react'; import { afterEach, describe, expect, test, vi } from 'vitest'; import { act, renderHook, waitFor } from '@testing-library/react'; import { isAxiosError } from 'axios'; -import { updateInstanceFromCache, useInstances } from '../useInstances'; +import { useInstances } from '../useInstances'; import { setupInstancesServer } from '@/__mocks__/instance/node'; import { TAggregatedInstanceDto } from '@/types/instance/api.type'; import { TInstancesServerResponse } from '@/__mocks__/instance/handlers'; -import { TMutationFnType, useBaseInstanceAction } from './useInstanceAction'; +import { useBaseInstanceAction } from './useInstanceAction'; import { TAggregatedInstance } from '@/types/instance/entity.type'; +import { updateAggregatedInstancesFromCache } from '@/adapters/tanstack-query/store/instances/updaters'; // initializers const initQueryClient = () => { @@ -31,7 +32,7 @@ const initQueryClient = () => { type Data = { projectId: string; instance: TAggregatedInstanceDto | null; - type: TMutationFnType | null; + type: string | null; queryPayload?: TAggregatedInstanceDto[]; mutationPayload?: null; }; @@ -84,7 +85,7 @@ let server: SetupServer; const handleError = vi.fn(); const handleSuccess = vi.fn( (instance: TAggregatedInstanceDto, queryClient: QueryClient) => () => - updateInstanceFromCache(queryClient, { + updateAggregatedInstancesFromCache(queryClient, { projectId: fakeProjectId, instance: { ...instance, pendingTask: true }, }), @@ -137,7 +138,7 @@ describe('Considering the useInstanceAction hook', () => { const { result: useInstanceActionResult } = renderHook( () => - useBaseInstanceAction(type, projectId, { + useBaseInstanceAction(type, projectId, 'fake-region', { onSuccess: handleSuccess( instance as TAggregatedInstanceDto, queryClient, @@ -154,9 +155,9 @@ describe('Considering the useInstanceAction hook', () => { ); expect(useInstanceActionResult.current.isIdle).toBeTruthy(); act(() => - useInstanceActionResult.current.mutationHandler( - instance as TAggregatedInstanceDto, - ), + useInstanceActionResult.current.mutationHandler({ + instance: instance as TAggregatedInstanceDto, + }), ); if (mutationPayload === undefined) { diff --git a/packages/manager/apps/pci-instances/src/data/hooks/instance/action/useInstanceAction.ts b/packages/manager/apps/pci-instances/src/data/hooks/instance/action/useInstanceAction.ts index 8c648a104089..bd97387c177a 100644 --- a/packages/manager/apps/pci-instances/src/data/hooks/instance/action/useInstanceAction.ts +++ b/packages/manager/apps/pci-instances/src/data/hooks/instance/action/useInstanceAction.ts @@ -14,20 +14,14 @@ import { } from '@/data/api/instance'; import { DeepReadonly } from '@/types/utils.type'; import { instancesQueryKey } from '@/utils'; +import queryClient from '@/queryClient'; +import { updateAllInstancesFromCache } from '@/adapters/tanstack-query/store/instances/updaters'; -export type TMutationFnType = - | 'delete' - | 'start' - | 'stop' - | 'shelve' - | 'unshelve' - | 'soft-reboot' - | 'hard-reboot' - | 'reinstall' - | 'billing/monthly/activate'; - -export type TUseInstanceActionCallbacks = DeepReadonly<{ - onSuccess?: (data?: null) => void; +export type TUseInstanceActionCallbacks< + TData = unknown, + TVariables = unknown +> = DeepReadonly<{ + onSuccess?: (data: TData, variables: TVariables) => void; onError?: (error: unknown) => void; }>; @@ -36,13 +30,18 @@ const unknownError = new Error('Unknwon Error'); type TUseInstanceActionParams = { projectId: string; mutationKeySuffix: string | null; + region: string; mutationFn: (variables: V) => Promise; - callbacks?: TUseInstanceActionCallbacks; + callbacks?: TUseInstanceActionCallbacks; }; -const useInstanceAction = ({ +const useInstanceAction = < + V extends { instance: { id: string } }, + R extends null +>({ projectId, mutationKeySuffix, + region, mutationFn, callbacks = {}, }: TUseInstanceActionParams) => { @@ -56,7 +55,22 @@ const useInstanceAction = ({ mutationKey, mutationFn, onError, - onSuccess, + onSuccess: (data, variables) => { + const newInstance = { + id: variables.instance.id, + task: { isPending: true, status: null }, + actions: [], + }; + + // TODO: refactor or move it to separate concern + updateAllInstancesFromCache(queryClient, { + projectId, + instance: newInstance, + region, + }); + + return onSuccess?.(data, variables); + }, }); return { @@ -72,10 +86,15 @@ type TBackupMutationFnVariables = { export const useInstanceBackupAction = ( projectId: string, - callbacks: TUseInstanceActionCallbacks = {}, + region: string, + callbacks: TUseInstanceActionCallbacks< + unknown, + TBackupMutationFnVariables + > = {}, ) => useInstanceAction({ projectId, + region, mutationKeySuffix: 'backup', mutationFn: useCallback( ({ instance, snapshotName }) => { @@ -99,10 +118,12 @@ type TRescueMutationFnVariables = { export const useInstanceRescueAction = ( projectId: string, + region: string, callbacks: TUseInstanceActionCallbacks = {}, ) => useInstanceAction({ projectId, + region, mutationKeySuffix: 'rescue', mutationFn: useCallback( ({ instance, imageId, isRescue }) => { @@ -121,10 +142,12 @@ type TReinstallMutationFnVariables = { export const useInstanceReinstallAction = ( projectId: string, + region: string, callbacks: TUseInstanceActionCallbacks = {}, ) => useInstanceAction({ projectId, + region, mutationKeySuffix: 'reinstall', mutationFn: useCallback( ({ instance, imageId }) => @@ -135,17 +158,17 @@ export const useInstanceReinstallAction = ( }); export const useBaseInstanceAction = ( - type: TMutationFnType | null, + type: string | null, projectId: string, + region: string, callbacks: TUseInstanceActionCallbacks = {}, ) => - useInstanceAction<{ id: string; imageId: string }, null>({ + useInstanceAction<{ instance: { id: string; imageId: string } }, null>({ projectId, + region, mutationKeySuffix: type, mutationFn: useCallback( - (instance?: { id: string; imageId: string }) => { - if (!instance) return Promise.reject(unknownError); - const { id, imageId } = instance; + ({ instance: { id, imageId } }) => { switch (type) { case 'delete': return deleteInstance(projectId, id); diff --git a/packages/manager/apps/pci-instances/src/data/hooks/instance/builder/instanceDto.builder.spec.tsx b/packages/manager/apps/pci-instances/src/data/hooks/instance/builder/instanceDto.builder.spec.tsx index 0292fbef0c4e..46a873ae2e7b 100644 --- a/packages/manager/apps/pci-instances/src/data/hooks/instance/builder/instanceDto.builder.spec.tsx +++ b/packages/manager/apps/pci-instances/src/data/hooks/instance/builder/instanceDto.builder.spec.tsx @@ -1,18 +1,28 @@ import { describe, expect, test } from 'vitest'; -import { buildPartialInstanceDto } from './instanceDto.builder'; +import { + buildPartialInstanceDto, + buildPartialAggregatedInstanceDto, +} from './instanceDto.builder'; import { TAggregatedInstanceDto, - TPartialInstanceDto, + TPartialAggregatedInstanceDto, } from '@/types/instance/api.type'; +import { TPartialInstance } from '@/types/instance/entity.type'; type Data = { desc: string; - initial: TPartialInstanceDto; + initial: TPartialAggregatedInstanceDto; steps: [ keyof TAggregatedInstanceDto, TAggregatedInstanceDto[keyof TAggregatedInstanceDto], ][]; - expected: TPartialInstanceDto; + expected: TPartialAggregatedInstanceDto; +}; + +type TInstanceData = { + desc: string; + instance: TPartialInstance; + expected: TPartialAggregatedInstanceDto; }; describe("Considering the 'buildPartialInstanceDto' function", () => { @@ -64,3 +74,85 @@ describe("Considering the 'buildPartialInstanceDto' function", () => { expect(result).toEqual(expected); }); }); + +describe("Considering the 'buildPartialAggregatedInstanceDto' function", () => { + test.each([ + { + desc: 'Should return TPartialAggregatedInstanceDto with only id and name', + instance: { + id: 'instance-1', + name: 'instance 1', + flavor: { + id: 'fake-flavor', + name: 'fake-flavor-name', + specs: null, + }, + }, + expected: { id: 'instance-1', name: 'instance 1' }, + }, + { + desc: 'Should return TPartialAggregatedInstanceDto with id and region', + instance: { + id: 'instance-1', + region: { name: 'fake-region', type: '', availabilityZone: null }, + }, + expected: { + id: 'instance-1', + region: 'fake-region', + }, + }, + { + desc: 'Should return TPartialAggregatedInstanceDto with id and status', + instance: { + id: 'instance-1', + status: 'BUILDING', + }, + expected: { + id: 'instance-1', + status: 'BUILDING', + }, + }, + { + desc: + 'Should return TPartialAggregatedInstanceDto with id and pedingTask', + instance: { + id: 'instance-1', + task: { + isPending: true, + status: 'waiting', + }, + }, + expected: { + id: 'instance-1', + taskState: 'waiting', + pendingTask: true, + }, + }, + { + desc: 'Should return TPartialAggregatedInstanceDto with actions', + instance: { + id: 'instance-1', + actions: [{ name: 'activate_monthly_billing', group: 'details' }], + }, + expected: { + id: 'instance-1', + actions: [{ name: 'activate_monthly_billing', group: 'details' }], + }, + }, + { + desc: 'Should return TPartialAggregatedInstanceDto with volumes', + instance: { + id: 'instance-1', + volumes: [{ id: 'fake-volume-1', name: 'fake-volume-1', size: 3 }], + }, + expected: { + id: 'instance-1', + volumes: [{ id: 'fake-volume-1', name: 'fake-volume-1' }], + }, + }, + ])('$desc', ({ instance, expected }: TInstanceData) => { + const result = buildPartialAggregatedInstanceDto(instance); + + expect(result).toEqual(expected); + }); +}); diff --git a/packages/manager/apps/pci-instances/src/data/hooks/instance/builder/instanceDto.builder.ts b/packages/manager/apps/pci-instances/src/data/hooks/instance/builder/instanceDto.builder.ts index 8449a7fa00b7..93d61966ff8f 100644 --- a/packages/manager/apps/pci-instances/src/data/hooks/instance/builder/instanceDto.builder.ts +++ b/packages/manager/apps/pci-instances/src/data/hooks/instance/builder/instanceDto.builder.ts @@ -1,9 +1,15 @@ import { TAggregatedInstanceDto, - TPartialInstanceDto, + TPartialAggregatedInstanceDto, } from '@/types/instance/api.type'; +import { + TInstanceAddresses, + TPartialInstance, +} from '@/types/instance/entity.type'; -export const buildPartialInstanceDto = (initial: TPartialInstanceDto) => ({ +export const buildPartialInstanceDto = ( + initial: TPartialAggregatedInstanceDto, +) => ({ with( key: K, value: TAggregatedInstanceDto[K], @@ -14,3 +20,68 @@ export const buildPartialInstanceDto = (initial: TPartialInstanceDto) => ({ return initial; }, }); + +const mapAddresses = (addresses: TInstanceAddresses) => + Array.from(addresses).flatMap(([type, items]) => + items.map((address) => ({ + ip: address.ip, + version: address.version, + type, + gatewayIp: address.subnet?.gatewayIP ?? '', + })), + ); + +export const buildPartialAggregatedInstanceDto = ( + instance: TPartialInstance, +): TPartialAggregatedInstanceDto => { + const baseAggregatedInstance: TPartialAggregatedInstanceDto = { + id: instance.id, + }; + + const withName = (dto: TPartialAggregatedInstanceDto) => + instance.name ? { ...dto, name: instance.name } : dto; + + const withRegion = (dto: TPartialAggregatedInstanceDto) => + instance.region ? { ...dto, region: instance.region.name } : dto; + + const withAddresses = (dto: TPartialAggregatedInstanceDto) => + instance.addresses + ? { ...dto, addresses: mapAddresses(instance.addresses) } + : dto; + + const withActions = (dto: TPartialAggregatedInstanceDto) => + instance.actions ? { ...dto, actions: instance.actions } : dto; + + const withVolumes = (dto: TPartialAggregatedInstanceDto) => + instance.volumes + ? { + ...dto, + volumes: instance.volumes.map(({ id, name }) => ({ + id, + name: name ?? '', + })), + } + : dto; + + const withStatus = (dto: TPartialAggregatedInstanceDto) => + instance.status ? { ...dto, status: instance.status } : dto; + + const withPendingTask = (dto: TPartialAggregatedInstanceDto) => + instance.task + ? { + ...dto, + pendingTask: instance.task.isPending, + taskState: instance.task.status ?? '', + } + : dto; + + return [ + withName, + withRegion, + withAddresses, + withActions, + withVolumes, + withStatus, + withPendingTask, + ].reduce((acc, fn) => fn(acc), baseAggregatedInstance); +}; diff --git a/packages/manager/apps/pci-instances/src/data/hooks/instance/polling/useInstancesPolling.ts b/packages/manager/apps/pci-instances/src/data/hooks/instance/polling/useInstancesPolling.ts index 4bdda40759eb..b2380985cc54 100644 --- a/packages/manager/apps/pci-instances/src/data/hooks/instance/polling/useInstancesPolling.ts +++ b/packages/manager/apps/pci-instances/src/data/hooks/instance/polling/useInstancesPolling.ts @@ -5,7 +5,7 @@ import { UseQueryOptions, UseQueryResult, } from '@tanstack/react-query'; -import { useCallback } from 'react'; +import { useCallback, useEffect } from 'react'; import { ApiError } from '@ovh-ux/manager-core-api'; import { getInstance } from '@/data/api/instance'; import { useProjectId } from '@/hooks/project/useProjectId'; @@ -81,13 +81,15 @@ export const useInstancesPolling = ( ), }); - polledInstances.forEach(({ id, data, error, isLoading }): void => { - if (isLoading) return undefined; - if (error && isApiErrorResponse(error)) { - return onError?.(error, id); - } - return onSuccess?.(data); - }); + useEffect(() => { + polledInstances.forEach(({ id, data, error, isLoading }): void => { + if (isLoading) return undefined; + if (error && isApiErrorResponse(error)) { + return onError?.(error, id); + } + return onSuccess?.(data); + }); + }, [onError, onSuccess, polledInstances]); return polledInstances; }; diff --git a/packages/manager/apps/pci-instances/src/data/hooks/instance/selectors/instances.selector.ts b/packages/manager/apps/pci-instances/src/data/hooks/instance/selectors/instances.selector.ts index 81ca4385317a..59ed51e92e1c 100644 --- a/packages/manager/apps/pci-instances/src/data/hooks/instance/selectors/instances.selector.ts +++ b/packages/manager/apps/pci-instances/src/data/hooks/instance/selectors/instances.selector.ts @@ -1,78 +1,27 @@ import { InfiniteData } from '@tanstack/react-query'; -import { - TAggregatedInstanceDto, - TInstanceStatusDto, -} from '@/types/instance/api.type'; +import { TAggregatedInstanceDto } from '@/types/instance/api.type'; import { TAggregatedInstanceAddress, TAggregatedInstance, TAggregatedInstanceAction, TAggregatedInstanceActions, TInstanceAddressType, - TAggregatedInstanceStatus, - TInstanceStatusSeverity, } from '@/types/instance/entity.type'; import { TActionName } from '@/types/instance/common.type'; - -const getInstanceStatusSeverity = ( - status: TInstanceStatusDto, -): TInstanceStatusSeverity => { - switch (status) { - case 'BUILDING': - case 'REBOOT': - case 'REBUILD': - case 'REVERT_RESIZE': - case 'SOFT_DELETED': - case 'VERIFY_RESIZE': - case 'MIGRATING': - case 'RESIZE': - case 'BUILD': - case 'SHUTOFF': - case 'RESCUE': - case 'SHELVED': - case 'SHELVED_OFFLOADED': - case 'RESCUING': - case 'UNRESCUING': - case 'SNAPSHOTTING': - case 'RESUMING': - case 'HARD_REBOOT': - case 'PASSWORD': - case 'PAUSED': - return 'warning'; - case 'DELETED': - case 'ERROR': - case 'STOPPED': - case 'SUSPENDED': - case 'UNKNOWN': - return 'error'; - case 'ACTIVE': - case 'RESCUED': - case 'RESIZED': - return 'success'; - default: - return 'info'; - } -}; +import { getInstanceStatus } from '@/pages/instances/mapper/status.mapper'; const getInstanceTaskState = ( taskState: string, ): TAggregatedInstance['taskState'] => taskState.length > 0 ? taskState : null; -const getInstanceStatus = ( - status: TInstanceStatusDto, -): TAggregatedInstanceStatus => ({ - label: status, - severity: getInstanceStatusSeverity(status), -}); - const getActionHrefByName = ( projectUrl: string, name: TActionName, { region, id }: Pick, ): TAggregatedInstanceAction['link'] => { if (name === 'details') { - return { path: id, isExternal: false }; + return { path: `${id}?region=${region}`, isExternal: false }; } if (name === 'edit') { diff --git a/packages/manager/apps/pci-instances/src/data/hooks/instance/useInstance.spec.tsx b/packages/manager/apps/pci-instances/src/data/hooks/instance/useInstance.spec.tsx new file mode 100644 index 000000000000..78df69d61f5a --- /dev/null +++ b/packages/manager/apps/pci-instances/src/data/hooks/instance/useInstance.spec.tsx @@ -0,0 +1,74 @@ +import { FC, PropsWithChildren } from 'react'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { act, renderHook, waitFor } from '@testing-library/react'; +import { describe, it, vi } from 'vitest'; +import { useUpdateInstanceName, useInstance } from './useInstance'; +import { updateInstanceName, getInstance } from '@/data/api/instance'; +import queryClient from '@/queryClient'; +import { TInstance } from '@/types/instance/entity.type'; + +const projectId = '8c8c4fd6d4414aa29fc777752b00005198664'; +const instanceId = 'fake-id'; +const region = 'fake-region'; +const fakeInstance = { + id: instanceId, + region: { + name: region, + }, + task: { isPending: false, status: null }, +} as TInstance; +const editInstanceNameMock = vi.fn(); + +vi.mock('@/data/api/instance'); +vi.mocked(getInstance).mockResolvedValue(fakeInstance); +vi.mocked(updateInstanceName).mockImplementation(editInstanceNameMock); + +const wrapper: FC = ({ children }) => ( + {children} +); + +describe('useInstance', () => { + it('should return correctly Instance detail', async () => { + const { result } = renderHook(() => useInstance({ region, instanceId }), { + wrapper, + }); + + expect(getInstance).toHaveBeenCalledWith({ + projectId, + instanceId, + region, + }); + expect(result.current.isPending).toBe(true); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + expect(result.current.data).toStrictEqual(fakeInstance); + }); +}); + +describe('useUpdateInstanceName', () => { + it('should updates instance name successfully when mutate', async () => { + const mockSuccess = vi.fn(); + const mockError = vi.fn(); + editInstanceNameMock.mockResolvedValue(null); + + const { result } = renderHook( + () => + useUpdateInstanceName({ + projectId, + instanceId, + callbacks: { + onSuccess: mockSuccess, + onError: mockError, + }, + }), + { wrapper }, + ); + + act(() => result.current.mutate({ instanceName: 'new_instanceName' })); + + await waitFor(() => { + expect(updateInstanceName).toHaveBeenCalled(); + expect(mockSuccess).toHaveBeenCalled(); + expect(mockError).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/manager/apps/pci-instances/src/data/hooks/instance/useInstance.ts b/packages/manager/apps/pci-instances/src/data/hooks/instance/useInstance.ts index 354c32d55b14..2f96c2f05054 100644 --- a/packages/manager/apps/pci-instances/src/data/hooks/instance/useInstance.ts +++ b/packages/manager/apps/pci-instances/src/data/hooks/instance/useInstance.ts @@ -1,9 +1,21 @@ -import { useQuery, UseQueryOptions } from '@tanstack/react-query'; -import { getInstance, TGetInstanceQueryParams } from '@/data/api/instance'; +import { + useQuery, + UseQueryOptions, + useMutation, + useQueryClient, +} from '@tanstack/react-query'; +import { + attachNetwork, + attachVolume, + getInstance, + TGetInstanceQueryParams, + updateInstanceName, +} from '@/data/api/instance'; import { useProjectId } from '@/hooks/project/useProjectId'; import { instancesQueryKey } from '@/utils'; import { TInstance } from '@/types/instance/entity.type'; import { DeepReadonly } from '@/types/utils.type'; +import { resetInstanceCache } from '@/adapters/tanstack-query/store/instances/updaters'; export type TUseInstanceQueryOptions = Pick< UseQueryOptions, @@ -17,6 +29,11 @@ type TUseInstanceArgs = DeepReadonly<{ queryOptions?: TUseInstanceQueryOptions; }>; +const getPendingTasks = (data?: TInstance) => + data?.task.isPending + ? [{ instanceId: data.id, region: data.region.name }] + : []; + export const useInstance = ({ region, instanceId, @@ -25,7 +42,7 @@ export const useInstance = ({ }: TUseInstanceArgs) => { const projectId = useProjectId(); - return useQuery({ + const { data, ...query } = useQuery({ ...queryOptions, queryKey: instancesQueryKey(projectId, [ 'region', @@ -36,4 +53,110 @@ export const useInstance = ({ ]), queryFn: () => getInstance({ projectId, region, instanceId, params }), }); + + return { + data, + pendingTasks: getPendingTasks(data), + ...query, + }; +}; + +type TInstanceNameMutationFnVariables = { instanceName: string }; + +type TUpdateInstanceCallbacks = DeepReadonly<{ + onSuccess?: ( + data: null, + variables: TInstanceNameMutationFnVariables, + context: unknown, + ) => void; + onError?: (error: unknown) => void; +}>; + +type TUpdateInstanceNameArgs = { + projectId: string; + instanceId: string; + callbacks?: TUpdateInstanceCallbacks; +}; + +export const useUpdateInstanceName = ({ + projectId, + instanceId, + callbacks = {}, +}: TUpdateInstanceNameArgs) => { + const { onError, onSuccess } = callbacks; + + return useMutation({ + mutationFn: ({ instanceName }: TInstanceNameMutationFnVariables) => + updateInstanceName({ projectId, instanceId, instanceName }), + onSuccess, + onError, + }); +}; + +type TAttachNetworkMutationFnVariables = { networkId: string }; + +type TUseAttachNetworkCallbacks = DeepReadonly<{ + onSuccess?: ( + data: unknown, + variables: TAttachNetworkMutationFnVariables, + ) => void; + onError?: (error: unknown) => void; +}>; + +type TInstanceArgs = { + projectId: string; + instanceId: string; + region: string; +}; + +export const useAttachNetwork = ({ + projectId, + instanceId, + region, + callbacks = {}, +}: TInstanceArgs & { callbacks: TUseAttachNetworkCallbacks }) => { + const queryClient = useQueryClient(); + const { onSuccess, onError } = callbacks; + + return useMutation({ + mutationFn: ({ networkId }: TAttachNetworkMutationFnVariables) => + attachNetwork({ projectId, instanceId, networkId }), + onSuccess: (data, variables) => { + void resetInstanceCache(queryClient, { projectId, region, instanceId }); + + onSuccess?.(data, variables); + }, + onError, + }); +}; + +type TAttachVolumeMutationFnVariables = { volumeId: string }; + +type TUseAttachVolumeCallbacks = DeepReadonly<{ + onSuccess?: ( + data: unknown, + variables: TAttachVolumeMutationFnVariables, + ) => void; + onError?: (error: unknown) => void; +}>; + +export const useAttachVolume = ({ + projectId, + instanceId, + region, + callbacks = {}, +}: TInstanceArgs & { callbacks: TUseAttachVolumeCallbacks }) => { + const queryClient = useQueryClient(); + const { onSuccess, onError } = callbacks; + + return useMutation({ + mutationFn: ({ volumeId }: TAttachVolumeMutationFnVariables) => + attachVolume({ projectId, instanceId, volumeId }), + onSuccess: (data, variables) => { + void resetInstanceCache(queryClient, { projectId, region, instanceId }); + + onSuccess?.(data, variables); + }, + onError, + }); }; diff --git a/packages/manager/apps/pci-instances/src/data/hooks/instance/useInstances.ts b/packages/manager/apps/pci-instances/src/data/hooks/instance/useInstances.ts index 80f4245ec21b..3dd8ecdd976a 100644 --- a/packages/manager/apps/pci-instances/src/data/hooks/instance/useInstances.ts +++ b/packages/manager/apps/pci-instances/src/data/hooks/instance/useInstances.ts @@ -1,8 +1,6 @@ import { InfiniteData, keepPreviousData, - Query, - QueryClient, QueryKey, useInfiniteQuery, useQueryClient, @@ -10,8 +8,6 @@ import { import { useCallback, useEffect, useMemo } from 'react'; import { useProjectUrl } from '@ovh-ux/manager-react-components'; import { Filter } from '@ovh-ux/manager-core-api'; -import { isEqual } from 'lodash'; -import fp from 'lodash/fp'; import { getInstances } from '@/data/api/instance'; import { instancesQueryKey } from '@/utils'; import { TAggregatedInstanceDto } from '@/types/instance/api.type'; @@ -19,6 +15,7 @@ import { TAggregatedInstance } from '@/types/instance/entity.type'; import { instancesSelector } from './selectors/instances.selector'; import { useProjectId } from '@/hooks/project/useProjectId'; import { DeepReadonly } from '@/types/utils.type'; +import { listQueryKeyPredicate } from '@/adapters/tanstack-query/store/instances/queryKeys'; type FilterWithLabel = Filter & { label: string }; @@ -29,65 +26,10 @@ export type TUseInstancesQueryParams = DeepReadonly<{ filters: FilterWithLabel[]; }>; -export type TUpdateInstanceFromCache = ( - queryClient: QueryClient, - payload: { - projectId: string; - instance: Pick & - Partial; - }, -) => void; - -const listQueryKeyPredicate = (projectId: string) => (query: Query) => - instancesQueryKey(projectId, ['list']).every((elt) => - query.queryKey.includes(elt), - ); - -export const updateInstanceFromCache: TUpdateInstanceFromCache = ( - queryClient: QueryClient, - { projectId, instance }, -) => { - const queries = queryClient.getQueriesData< - InfiniteData - >({ - predicate: listQueryKeyPredicate(projectId), - }); - - queries.forEach(([queryKey, queryData]) => { - if (!queryData) return; - - const updatedPages: TAggregatedInstanceDto[][] = queryData.pages.map( - (page): TAggregatedInstanceDto[] => { - const foundIndex = fp.findIndex(fp.propEq('id', instance.id), page); - if (foundIndex === -1) return page; - - const previousInstance = page[foundIndex]; - const mergedInstance = { ...previousInstance, ...instance }; - - if (isEqual(previousInstance, mergedInstance)) return page; - - return fp.update( - foundIndex, - () => mergedInstance, - page, - ) as TAggregatedInstanceDto[]; - }, - ); - - const isPageModified = updatedPages.some( - (page, i) => page !== queryData.pages[i], - ); - - if (!isPageModified) return; - - queryClient.setQueryData>( - queryKey, - (prevData) => { - if (!prevData) return undefined; - return { ...prevData, pages: updatedPages }; - }, - ); - }); +export type TInstanceArgs = { + projectId: string; + instanceId: string; + region: string; }; const getPendingTasks = (data?: TAggregatedInstance[]) => @@ -95,29 +37,6 @@ const getPendingTasks = (data?: TAggregatedInstance[]) => ?.filter(({ pendingTask }) => pendingTask) .map(({ id, region }) => ({ instanceId: id, region })) ?? []; -export const getInstanceById = ( - projectId: string, - id: string | undefined, - queryClient: QueryClient, -): TAggregatedInstanceDto | undefined => { - if (!id) return undefined; - - const data = queryClient.getQueriesData< - InfiniteData - >({ - predicate: listQueryKeyPredicate(projectId), - }); - - return data.reduce((acc: TAggregatedInstanceDto | undefined, [, result]) => { - if (acc) return acc; - if (result) { - const foundInstance = result.pages.flat().find((elt) => elt.id === id); - return foundInstance ?? acc; - } - return acc; - }, undefined); -}; - const getInconsistency = (data: TAggregatedInstance[] | undefined): boolean => !!data?.some((elt) => elt.status.label === 'UNKNOWN'); diff --git a/packages/manager/apps/pci-instances/src/data/hooks/network/useNetwork.spec.tsx b/packages/manager/apps/pci-instances/src/data/hooks/network/useNetwork.spec.tsx new file mode 100644 index 000000000000..065a43e60d56 --- /dev/null +++ b/packages/manager/apps/pci-instances/src/data/hooks/network/useNetwork.spec.tsx @@ -0,0 +1,46 @@ +import { FC, PropsWithChildren } from 'react'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { renderHook, waitFor } from '@testing-library/react'; +import { describe, it, vi } from 'vitest'; +import { useNetworks, useReverseDns } from './useNetwork'; +import queryClient from '@/queryClient'; +import { getNetworks, getReverseDns } from '@/data/api/network'; + +const projectId = 'projectId-test'; +const region = 'fake-region'; +const fakeIp = 'fake-ip'; + +vi.mock('@/data/api/network'); +vi.mocked(getNetworks).mockResolvedValue([]); +vi.mocked(getReverseDns).mockResolvedValue([]); + +const wrapper: FC = ({ children }) => ( + {children} +); + +describe('useNetwork', () => { + it('should return correctly network detail', async () => { + const { result } = renderHook(() => useNetworks(projectId, region), { + wrapper, + }); + + expect(getNetworks).toHaveBeenCalledWith({ + projectId, + region, + }); + expect(result.current.isPending).toBe(true); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + }); +}); + +describe('useReverseDns', () => { + it('should call correctly dns api', async () => { + const { result } = renderHook(() => useReverseDns(fakeIp), { + wrapper, + }); + + expect(getReverseDns).toHaveBeenCalledWith(fakeIp); + expect(result.current.isPending).toBe(true); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + }); +}); diff --git a/packages/manager/apps/pci-instances/src/data/hooks/network/useNetwork.ts b/packages/manager/apps/pci-instances/src/data/hooks/network/useNetwork.ts new file mode 100644 index 000000000000..2c4474547993 --- /dev/null +++ b/packages/manager/apps/pci-instances/src/data/hooks/network/useNetwork.ts @@ -0,0 +1,20 @@ +import { useQuery, UseQueryOptions } from '@tanstack/react-query'; +import { getNetworks, getReverseDns } from '@/data/api/network'; +import { TNetwork } from '@/types/network/entity.type'; + +export const useNetworks = ( + projectId: string, + region: string, + options?: Omit, 'queryKey' | 'queryFn'>, +) => + useQuery({ + queryKey: ['project', projectId, 'region', region, 'network'], + queryFn: () => getNetworks({ projectId, region }), + ...options, + }); + +export const useReverseDns = (ip: string) => + useQuery({ + queryKey: ['ip', ip, 'reverse'], + queryFn: () => getReverseDns(ip), + }); diff --git a/packages/manager/apps/pci-instances/src/data/hooks/volume/useVolume.spec.tsx b/packages/manager/apps/pci-instances/src/data/hooks/volume/useVolume.spec.tsx new file mode 100644 index 000000000000..befa0f8b070f --- /dev/null +++ b/packages/manager/apps/pci-instances/src/data/hooks/volume/useVolume.spec.tsx @@ -0,0 +1,32 @@ +import { FC, PropsWithChildren } from 'react'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { renderHook, waitFor } from '@testing-library/react'; +import { describe, it, vi } from 'vitest'; +import { useVolumes } from './useVolume'; +import queryClient from '@/queryClient'; +import { getVolumes } from '@/data/api/volume'; + +const projectId = 'projectId-test'; +const region = 'fake-region'; + +vi.mock('@/data/api/volume'); +vi.mocked(getVolumes).mockResolvedValue([]); + +const wrapper: FC = ({ children }) => ( + {children} +); + +describe('useVolumes', () => { + it('should return correctly volumes details', async () => { + const { result } = renderHook(() => useVolumes(projectId, region), { + wrapper, + }); + + expect(getVolumes).toHaveBeenCalledWith({ + projectId, + region, + }); + expect(result.current.isPending).toBe(true); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + }); +}); diff --git a/packages/manager/apps/pci-instances/src/data/hooks/volume/useVolume.ts b/packages/manager/apps/pci-instances/src/data/hooks/volume/useVolume.ts new file mode 100644 index 000000000000..235592bea350 --- /dev/null +++ b/packages/manager/apps/pci-instances/src/data/hooks/volume/useVolume.ts @@ -0,0 +1,14 @@ +import { useQuery, UseQueryOptions } from '@tanstack/react-query'; +import { TVolume } from '@/types/volume/common.type'; +import { getVolumes } from '@/data/api/volume'; + +export const useVolumes = ( + projectId: string, + region: string, + options?: Omit, 'queryKey' | 'queryFn'>, +) => + useQuery({ + queryKey: ['project', projectId, 'region', region, 'volume'], + queryFn: () => getVolumes({ projectId, region }), + ...options, + }); diff --git a/packages/manager/apps/pci-instances/src/hooks/instance/action/useActionSection.ts b/packages/manager/apps/pci-instances/src/hooks/instance/action/useActionSection.ts deleted file mode 100644 index 3d02dae1d276..000000000000 --- a/packages/manager/apps/pci-instances/src/hooks/instance/action/useActionSection.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { actionSectionRegex } from '@/constants'; -import { usePathMatch } from '@/hooks/url/usePathMatch'; -import { TSectionType } from '@/types/instance/action/action.type'; - -export const useActionSection = (): TSectionType | null => { - const section = usePathMatch(actionSectionRegex); - return section; -}; diff --git a/packages/manager/apps/pci-instances/src/hooks/migration/useSuspendNonMigratedRoutes.tsx b/packages/manager/apps/pci-instances/src/hooks/migration/useSuspendNonMigratedRoutes.tsx index 1d3f916bdfbc..55fca5883e47 100644 --- a/packages/manager/apps/pci-instances/src/hooks/migration/useSuspendNonMigratedRoutes.tsx +++ b/packages/manager/apps/pci-instances/src/hooks/migration/useSuspendNonMigratedRoutes.tsx @@ -1,49 +1,69 @@ -import { ComponentType, useCallback, useContext } from 'react'; -import { ShellContext } from '@ovh-ux/manager-react-shell-client'; -import { matchPath, useLocation } from 'react-router-dom'; -import { useSuspenseQuery } from '@tanstack/react-query'; - -const getPromise = () => { - // We need to have 42 because tanstack query needs data to be returned - // 1000 is an arbitrary time, we just need to wait for the container to accept the hash change and do its thing - return new Promise((resolve) => setTimeout(resolve, 1000, 42)); -}; +import { ComponentType, ReactNode } from 'react'; +import { ShellContextType } from '@ovh-ux/manager-react-shell-client'; +import { LoaderFunction, matchPath, useLoaderData } from 'react-router-dom'; + +const DASHBOARD_HASH = '/pci/projects/:projectId/instances/:instanceId'; + +export const suspendNonMigratedRoutesLoader = ( + shell: ShellContextType, + loaderCallback?: LoaderFunction, +): LoaderFunction => { + const containerHashes = + shell.environment.applications['pci-instances']?.container.hashes; + + const routes = new Set(containerHashes); -export function useSuspendNonMigrateRoutes() { - const { environment } = useContext(ShellContext); - const location = useLocation(); + const hasAccessToDashboard = Array.from(routes).some((hash) => + hash.includes(':instanceId'), + ); + const dashboardHashAlreadyExists = Array.from(routes).find( + (elt) => elt === DASHBOARD_HASH, + ); - const routes = environment.applications['pci-instances']?.container.hashes; + return (loaderParams, ...restArgs) => { + const { pathname } = new URL(loaderParams.request.url); + + if (!pathname.endsWith('/new') && !dashboardHashAlreadyExists) { + if (hasAccessToDashboard) routes.add(DASHBOARD_HASH); + } - const queryFn = useCallback(() => { const isRouteAvailable = - !routes || routes.some((r) => matchPath(r, location.pathname) !== null); + !routes.size || + Array.from(routes).some((r) => matchPath(r, pathname) !== null); - // We need to have 42 because tanstack query needs data to be returned - return !isRouteAvailable ? getPromise() : Promise.resolve(42); - }, [routes, location.pathname]); + // We send an object to trigger the redirection and handle it inside the `WithSuspendedMigratedRoutes` HOC + if (!isRouteAvailable) return { routeMigrated: false }; - return useSuspenseQuery({ - // Here we use the window one to have the latest hash value - queryKey: ['suspense-promise-to-allow-rerender', window.location.hash], - queryFn, - }); -} + return loaderCallback?.(loaderParams, ...restArgs) ?? null; + }; +}; export const withSuspendedMigrateRoutes = ( - Component: ComponentType>, + Component?: ComponentType>, ): { - (): JSX.Element; + (): ReactNode; displayName: string; } => { function WithSuspendedMigratedRoutes() { - useSuspendNonMigrateRoutes(); + const loaderData = useLoaderData(); + + const needsSuspense = + typeof loaderData === 'object' && + loaderData !== null && + 'routeMigrated' in loaderData && + loaderData.routeMigrated === false; - return ; + // If we need to move to angular app we suspend with a 1s promise to let the container receive the event and display a loader + if (needsSuspense) + // eslint-disable-next-line @typescript-eslint/only-throw-error + throw new Promise((resolve) => setTimeout(resolve, 1000, 42)); + return !!Component && ; } - WithSuspendedMigratedRoutes.displayName = `withSuspendedMigratedRoutesHoC(${Component.displayName ?? - Component.name})`; + WithSuspendedMigratedRoutes.displayName = Component + ? `withSuspendedMigratedRoutesHoC(${Component.displayName ?? + Component.name})` + : 'withSuspendedMigratedRoutesHoC'; return WithSuspendedMigratedRoutes; }; diff --git a/packages/manager/apps/pci-instances/src/hooks/url/useDedicatedUrl.ts b/packages/manager/apps/pci-instances/src/hooks/url/useDedicatedUrl.ts new file mode 100644 index 000000000000..1027931d5c9c --- /dev/null +++ b/packages/manager/apps/pci-instances/src/hooks/url/useDedicatedUrl.ts @@ -0,0 +1,17 @@ +import { useContext, useState, useEffect } from 'react'; +import { ShellContext } from '@ovh-ux/manager-react-shell-client'; + +export const useDedicatedUrl = () => { + const { + shell: { navigation }, + } = useContext(ShellContext); + const [dedicatedUrl, setDedicatedUrl] = useState(''); + + useEffect(() => { + navigation + .getURL('dedicated', '#/configuration/ip', {}) + .then((data) => setDedicatedUrl(data as string)); + }, [navigation]); + + return dedicatedUrl; +}; diff --git a/packages/manager/apps/pci-instances/src/hooks/url/usePathMatch.spec.tsx b/packages/manager/apps/pci-instances/src/hooks/url/usePathMatch.spec.tsx deleted file mode 100644 index c92f87225cad..000000000000 --- a/packages/manager/apps/pci-instances/src/hooks/url/usePathMatch.spec.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { renderHook } from '@testing-library/react'; -import * as module from 'react-router-dom'; -import { describe, expect, test, vi } from 'vitest'; -import { usePathMatch } from './usePathMatch'; - -const mockedUseLocation = vi.spyOn(module, 'useLocation'); - -describe('Considering the usePathMatch hook', () => { - type Data = { - pathname: string; - regex: RegExp; - expectedResult: string | null; - }; - test.each` - pathname | regex | expectedResult - ${'/foo/bar'} | ${/foo/} | ${'foo'} - ${'/foo/bar'} | ${/bar/} | ${'bar'} - ${'/foo/bar'} | ${/baz/} | ${null} - ${'/foo/bar/baz'} | ${/foo\/bar/} | ${'foo/bar'} - ${'/foo/bar/baz'} | ${/bar\/baz/} | ${'bar/baz'} - `( - `When pathname is '$pathname' and regex is '$regex', then expect result to be '$expectedResult'`, - ({ pathname, regex, expectedResult }: Data) => { - mockedUseLocation.mockReturnValue({ - pathname, - } as module.Location); - - const { result } = renderHook(() => usePathMatch(regex)); - - expect(result.current).toBe(expectedResult); - }, - ); -}); diff --git a/packages/manager/apps/pci-instances/src/hooks/url/usePathMatch.ts b/packages/manager/apps/pci-instances/src/hooks/url/usePathMatch.ts deleted file mode 100644 index 9a905adc2dc1..000000000000 --- a/packages/manager/apps/pci-instances/src/hooks/url/usePathMatch.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { useLocation } from 'react-router-dom'; -import { getPathMatch } from '@/utils'; - -export const usePathMatch = (regex: RegExp): T | null => { - const location = useLocation(); - return getPathMatch(location.pathname, regex); -}; diff --git a/packages/manager/apps/pci-instances/src/main.tsx b/packages/manager/apps/pci-instances/src/main.tsx index fb0fdf7dc471..713bdd304e7b 100644 --- a/packages/manager/apps/pci-instances/src/main.tsx +++ b/packages/manager/apps/pci-instances/src/main.tsx @@ -8,6 +8,7 @@ import { import '@ovhcloud/ods-themes/default'; import '@/vite-hmr'; import App from './App'; +import '@ovh-ux/manager-pci-common/dist/style.css'; import './index.scss'; const init = async ( diff --git a/packages/manager/apps/pci-instances/src/pages/instances/Instances.page.tsx b/packages/manager/apps/pci-instances/src/pages/instances/Instances.page.tsx index d59beb7b639d..e85a501fc990 100644 --- a/packages/manager/apps/pci-instances/src/pages/instances/Instances.page.tsx +++ b/packages/manager/apps/pci-instances/src/pages/instances/Instances.page.tsx @@ -31,6 +31,7 @@ import { Search } from 'lucide-react'; import { FC, FormEvent, useCallback, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { NAMESPACES } from '@ovh-ux/manager-common-translations'; import { Navigate, Outlet, @@ -38,13 +39,11 @@ import { useRouteLoaderData, } from 'react-router-dom'; import clsx from 'clsx'; -import NotFoundPage from '../404/NotFound.page'; import { useInstances } from '@/data/hooks/instance/useInstances'; import { Breadcrumb } from '@/components/breadcrumb/Breadcrumb.component'; import { Spinner } from '@/components/spinner/Spinner.component'; import { SECTIONS } from '@/routes/routes'; import { SearchNotifications } from '@/components/SearchNotifications/SearchNotifications'; -import { useActionSection } from '@/hooks/instance/action/useActionSection'; import { CHANGELOG_LINKS } from '@/constants'; import DatagridComponent from './datagrid/components/Datagrid.component'; @@ -87,7 +86,7 @@ const SearchBar = ({ ); }; const Instances: FC = () => { - const { t } = useTranslation(['list', 'common']); + const { t } = useTranslation(['list', 'common', NAMESPACES.ACTIONS]); const project = useRouteLoaderData('root') as TProject | undefined; const createInstanceHref = useHref('./new'); @@ -95,18 +94,13 @@ const Instances: FC = () => { const { filters, addFilter, removeFilter } = useColumnFilters(); const filterPopoverRef = useRef(null); - const section = useActionSection(); - const routeLoaderData = useRouteLoaderData(section ?? '') as - | { - notFoundAction?: boolean; - } - | undefined; const { data, isFetchingNextPage, refresh, isFetching, + isPending, isRefetching, } = useInstances({ limit: 20, @@ -162,10 +156,6 @@ const Instances: FC = () => { if (data && !data.length && !filters.length && !isFetching) return ; - if (routeLoaderData?.notFoundAction) { - return ; - } - return ( <> @@ -245,7 +235,7 @@ const Instances: FC = () => { size={ODS_ICON_SIZE.sm} color={ODS_THEME_COLOR_INTENT.primary} /> - {t('common:pci_instances_common_filter')} + {t(`${NAMESPACES.ACTIONS}:filter`)} @@ -279,7 +269,7 @@ const Instances: FC = () => { )}
- {!isFetching && } + {!isPending && } ); }; diff --git a/packages/manager/apps/pci-instances/src/pages/instances/action/BackupAction.page.tsx b/packages/manager/apps/pci-instances/src/pages/instances/action/BackupAction.page.tsx new file mode 100644 index 000000000000..7ca79d7f3b25 --- /dev/null +++ b/packages/manager/apps/pci-instances/src/pages/instances/action/BackupAction.page.tsx @@ -0,0 +1,127 @@ +import { useTranslation } from 'react-i18next'; +import { + useCatalogPrice, + useNotifications, +} from '@ovh-ux/manager-react-components'; +import { useMemo, useState } from 'react'; +import { Input, Text, TEXT_PRESET } from '@ovhcloud/ods-react'; +import { useNavigate } from 'react-router-dom'; +import { DefaultError } from '@tanstack/react-query'; +import ActionModal from '@/components/actionModal/ActionModal.component'; +import { useInstanceBackupAction } from '@/data/hooks/instance/action/useInstanceAction'; +import { useInstanceBackupPrice } from '@/data/hooks/instance/action/useInstanceBackupPrice'; +import { + useInstanceActionModal, + useInstanceParams, +} from '@/pages/instances/action/hooks/useInstanceActionModal'; +import { useProjectId } from '@/hooks/project/useProjectId'; +import { isApiErrorResponse } from '@/utils'; + +const BackupActionPage = () => { + const projectId = useProjectId(); + const { instanceId, region } = useInstanceParams(); + + const { t, i18n } = useTranslation('actions'); + const { addError, addInfo } = useNotifications(); + const navigate = useNavigate(); + + const { instance, isLoading } = useInstanceActionModal( + region, + instanceId, + 'backup', + ); + + const locale = i18n.language.replace('_', '-'); + const defaultSnapshotName = useMemo( + () => + `${instance?.name} ${new Date().toLocaleDateString(locale, { + year: 'numeric', + month: 'long', + day: 'numeric', + })}`, + [instance?.name, locale], + ); + + const [snapshotName, setSnapshotName] = useState(defaultSnapshotName); + + const { + price: backupPrice, + isLoading: isBackupLoading, + } = useInstanceBackupPrice(projectId, region); + + const closeModal = () => navigate('..'); + + const onError = (rawError: unknown) => { + const errorMessage = isApiErrorResponse(rawError) + ? rawError.response?.data.message + : (rawError as DefaultError).message; + addError( + t(`pci_instances_actions_backup_instance_error_message`, { + name: instance?.name, + error: errorMessage, + }), + true, + ); + }; + + const { mutationHandler, isPending } = useInstanceBackupAction( + projectId, + region, + { + onError, + onSuccess: (_data, variables) => { + addInfo( + t(`pci_instances_actions_backup_instance_success_message`, { + name: variables.snapshotName, + }), + true, + ); + + closeModal(); + }, + }, + ); + + const handleInstanceAction = () => { + if (instance) mutationHandler({ instance, snapshotName }); + }; + + const { getFormattedCatalogPrice } = useCatalogPrice(3); + + const price = backupPrice ? getFormattedCatalogPrice(backupPrice) : null; + return ( + +
+ + {t('pci_instances_actions_backup_instance_name_label')} + + ) => + setSnapshotName(e.target.value) + } + className="min-h-[40px]" + /> + {!!price && !isBackupLoading && ( + + {t('pci_instances_actions_backup_instance_price', { + price, + })} + + )} +
+
+ ); +}; + +export default BackupActionPage; diff --git a/packages/manager/apps/pci-instances/src/pages/instances/action/BackupActionPage.tsx b/packages/manager/apps/pci-instances/src/pages/instances/action/BackupActionPage.tsx deleted file mode 100644 index 5dd40c4d7725..000000000000 --- a/packages/manager/apps/pci-instances/src/pages/instances/action/BackupActionPage.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { useTranslation } from 'react-i18next'; -import { useCatalogPrice } from '@ovh-ux/manager-react-components'; -import { useMemo, useState } from 'react'; -import { Input, Text } from '@ovhcloud/ods-react'; -import ActionModal from '@/components/actionModal/ActionModal.component'; -import { TRescueActionPageProps } from './RescueAction.page'; -import { useInstanceBackupAction } from '@/data/hooks/instance/action/useInstanceAction'; -import { useInstanceBackupPrice } from '@/data/hooks/instance/action/useInstanceBackupPrice'; - -type TBackupActionPageProps = Omit & { - section: 'backup'; -}; - -const BackupActionPage = ({ - title, - onModalClose, - section, - instance, - projectId, - onError, - onSuccess, - isLoading, -}: TBackupActionPageProps) => { - const { t, i18n } = useTranslation('actions'); - const locale = i18n.language.replace('_', '-'); - const defaultSnapshotName = useMemo( - () => - `${instance?.name} ${new Date().toLocaleDateString(locale, { - year: 'numeric', - month: 'long', - day: 'numeric', - })}`, - [instance?.name, locale], - ); - - const [snapshotName, setSnapshotName] = useState(defaultSnapshotName); - - const { - price: backupPrice, - isLoading: isBackupLoading, - } = useInstanceBackupPrice(projectId, instance?.region ?? ''); - - const { mutationHandler, isPending } = useInstanceBackupAction(projectId, { - onError, - onSuccess, - }); - - const handleInstanceAction = () => { - if (instance) mutationHandler({ instance, snapshotName }); - }; - - const { getFormattedCatalogPrice } = useCatalogPrice(3); - - const price = backupPrice ? getFormattedCatalogPrice(backupPrice) : null; - return ( - -
- {t('pci_instances_actions_backup_instance_name_label')} - ) => - setSnapshotName(e.target.value) - } - className="min-h-[40px]" - /> - {!!price && !isBackupLoading && ( - - {t('pci_instances_actions_backup_instance_price', { - price, - })} - - )} -
-
- ); -}; - -export default BackupActionPage; diff --git a/packages/manager/apps/pci-instances/src/pages/instances/action/BaseAction.page.tsx b/packages/manager/apps/pci-instances/src/pages/instances/action/BaseAction.page.tsx index 117f79f3982f..45a1cdbf2d19 100644 --- a/packages/manager/apps/pci-instances/src/pages/instances/action/BaseAction.page.tsx +++ b/packages/manager/apps/pci-instances/src/pages/instances/action/BaseAction.page.tsx @@ -1,43 +1,89 @@ -import { FC } from 'react'; -import { useBaseInstanceAction } from '@/data/hooks/instance/action/useInstanceAction'; +import { FC, useMemo } from 'react'; +import { DefaultError } from '@tanstack/react-query'; +import { useNotifications } from '@ovh-ux/manager-react-components'; +import { Trans, useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import { OsdsLink } from '@ovhcloud/ods-components/react'; +import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; +import { OdsHTMLAnchorElementTarget } from '@ovhcloud/ods-common-core'; +import { + useInstanceActionModal, + useInstanceParams, +} from '@/pages/instances/action/hooks/useInstanceActionModal'; +import { isApiErrorResponse, replaceToSnakeCase } from '@/utils'; +import { useProjectId } from '@/hooks/project/useProjectId'; import ActionModal from '@/components/actionModal/ActionModal.component'; -import { TInstanceActionModalViewModel } from './view-models/selectInstanceForActionModal'; - -type BaseInstanceActionSection = - | 'delete' - | 'start' - | 'stop' - | 'shelve' - | 'unshelve' - | 'soft-reboot' - | 'hard-reboot' - | 'reinstall' - | 'billing/monthly/activate'; +import { useBaseInstanceAction } from '@/data/hooks/instance/action/useInstanceAction'; +import { TSectionType } from '@/types/instance/action/action.type'; export type TBaseInstanceActionPageProps = { - title: string; - section: BaseInstanceActionSection; - projectId: string; - onError: (error: unknown) => void; - onSuccess: () => void; - onModalClose: () => void; - instance: TInstanceActionModalViewModel; - isLoading: boolean; + section: TSectionType; }; const BaseInstanceActionPage: FC = ({ - title, section, - projectId, - onError, - onSuccess, - onModalClose, - instance, - isLoading, }) => { + const projectId = useProjectId(); + const { instanceId, region } = useInstanceParams(); + + const { addError, addInfo } = useNotifications(); + const navigate = useNavigate(); + const { t } = useTranslation(['actions']); + + const { instance, isLoading } = useInstanceActionModal( + region, + instanceId, + section, + ); + + const snakeCaseSection = useMemo(() => replaceToSnakeCase(section), [ + section, + ]); + + const closeModal = () => navigate('..'); + + const onSuccess = () => { + addInfo( + , + ]} + />, + true, + ); + + closeModal(); + }; + + const onError = (rawError: unknown) => { + const errorMessage = isApiErrorResponse(rawError) + ? rawError.response?.data.message + : (rawError as DefaultError).message; + addError( + t(`pci_instances_actions_${snakeCaseSection}_instance_error_message`, { + name: instance?.name, + error: errorMessage, + }), + true, + ); + }; + const { mutationHandler, isPending } = useBaseInstanceAction( section, projectId, + region, { onError, onSuccess, @@ -45,15 +91,15 @@ const BaseInstanceActionPage: FC = ({ ); const handleInstanceAction = () => { - if (instance) mutationHandler(instance); + if (instance) mutationHandler({ instance }); }; return ( & { - section: 'billing/monthly/activate'; -}; +const section = 'billing/monthly/activate'; + +const BillingMonthlyActionPage = () => { + const projectId = useProjectId(); + const { instanceId, region } = useInstanceParams(); -const BillingMonthlyActionPage: FC = ({ - title, - onModalClose, - section, - instance, - projectId, - onError, - onSuccess, - isLoading, -}) => { const { ovhSubsidiary } = useContext(ShellContext).environment.getUser(); + const { addError, addInfo } = useNotifications(); + const navigate = useNavigate(); + const { t } = useTranslation(['actions']); + + const { instance, isLoading } = useInstanceActionModal( + region, + instanceId, + section, + ); + + const closeModal = () => navigate('..'); + + const onSuccess = () => { + addInfo( + t( + `pci_instances_actions_billing_monthly_activate_instance_success_message`, + { + name: instance?.name, + }, + ), + true, + ); + + closeModal(); + }; + + const onError = (rawError: unknown) => { + const errorMessage = isApiErrorResponse(rawError) + ? rawError.response?.data.message + : (rawError as DefaultError).message; + addError( + t( + `pci_instances_actions_billing_monthly_activate_instance_error_message`, + { + name: instance?.name, + error: errorMessage, + }, + ), + true, + ); + }; const { mutationHandler, isPending } = useBaseInstanceAction( section, projectId, + region, { onError, onSuccess, @@ -40,7 +79,7 @@ const BillingMonthlyActionPage: FC = ({ ); const handleInstanceAction = () => { if (!instance) return; - mutationHandler(instance); + mutationHandler({ instance }); }; const pricingHref = useMemo(() => { @@ -52,12 +91,13 @@ const BillingMonthlyActionPage: FC = ({ return (
{ - const { t } = useTranslation(['actions', 'common']); - const navigate = useNavigate(); - const projectId = useProjectId(); - const { instanceId, regionId } = useParams(); - const { addError, addInfo } = useNotifications(); - const section = useActionSection(); - - const snakeCaseSection = useMemo( - () => (section ? replaceToSnakeCase(section) : ''), - [section], - ); - - const { instance, isLoading } = useInstanceActionModal( - regionId, - instanceId, - section, - ); - - const executeSuccessCallback = useCallback((): void => { - if (!instance) return; - const newInstance = { id: instance.id, pendingTask: true, actions: [] }; - updateInstanceFromCache(queryClient, { - projectId, - instance: newInstance, - }); - }, [instance, projectId]); - - const onModalClose = () => navigate('..'); - - const onSuccess = () => { - executeSuccessCallback(); - - const isRescue = section === 'rescue/start'; - - if (isRescue) { - addInfo( - , - ]} - />, - true, - ); - } - - if (section === 'shelve' || section === 'stop') { - addInfo( - , - ]} - />, - true, - ); - } else { - addInfo( - t( - `pci_instances_actions_${snakeCaseSection}_instance_success_message`, - { - name: instance?.name, - }, - ), - true, - ); - } - - onModalClose(); - }; - - const onError = (rawError: unknown) => { - const errorMessage = isApiErrorResponse(rawError) - ? rawError.response?.data.message - : (rawError as DefaultError).message; - addError( - t(`pci_instances_actions_${snakeCaseSection}_instance_error_message`, { - name: instance?.name, - error: errorMessage, - }), - true, - ); - onModalClose(); - }; - - if (!instanceId || !section) return ; - - const title = t(`pci_instances_actions_${snakeCaseSection}_instance_title`); - - const modalProps = { - title, - projectId, - onError, - onSuccess, - onModalClose, - instance, - isLoading, - }; - - if (section === 'backup') { - return ; - } - - if (section === 'rescue/start' || section === 'rescue/end') { - return ; - } - - if (section === 'billing/monthly/activate') { - return ( - - ); - } - - if (section === 'reinstall' && instance?.isImageDeprecated) { - return ; - } - - return ; -}; - -export default InstanceAction; diff --git a/packages/manager/apps/pci-instances/src/pages/instances/action/ReinstallAction.page.tsx b/packages/manager/apps/pci-instances/src/pages/instances/action/ReinstallAction.page.tsx new file mode 100644 index 000000000000..1da2ba1ad7f4 --- /dev/null +++ b/packages/manager/apps/pci-instances/src/pages/instances/action/ReinstallAction.page.tsx @@ -0,0 +1,182 @@ +import { FC, useCallback, useEffect, useState } from 'react'; +import { TFunction } from 'i18next'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import { useNotifications } from '@ovh-ux/manager-react-components'; +import { DefaultError } from '@tanstack/react-query'; +import { + useBaseInstanceAction, + useInstanceReinstallAction, +} from '@/data/hooks/instance/action/useInstanceAction'; +import ActionModal, { + TActionModalProps, +} from '@/components/actionModal/ActionModal.component'; +import ImageSelector from '@/components/imageSelector/ImageSelector.component'; +import { imagesRescueSelector } from '@/data/hooks/image/selector/image.selector'; +import { useImages } from '@/data/hooks/image/useImages'; +import { useProjectId } from '@/hooks/project/useProjectId'; +import { + useInstanceActionModal, + useInstanceParams, +} from '@/pages/instances/action/hooks/useInstanceActionModal'; +import { isApiErrorResponse } from '@/utils'; + +export type TReinstallActionPageProps = Omit< + TActionModalProps, + 'children' | 'section' | 'handleInstanceAction' | 'isPending' +> & { + projectId: string; + onError: (error: unknown) => void; + onSuccess: () => void; +}; + +const ReinstallActionImages: FC = ({ + instance, + projectId, + onSuccess, + onError, + ...actionProps +}) => { + const { t } = useTranslation('actions'); + const [imageId, setImageId] = useState(''); + const { data: images, isLoading: isImageLoading } = useImages({ + projectId, + region: instance?.region ?? '', + params: { + visibility: 'public', + }, + selectFn: (data) => imagesRescueSelector(data, t as TFunction), + }); + + const { mutationHandler, isPending } = useInstanceReinstallAction( + projectId, + instance?.region ?? '', + { + onError, + onSuccess, + }, + ); + + useEffect(() => { + if (images?.[0]) { + setImageId(images[0].value); + } + }, [images]); + + const handleInstanceAction = () => { + if (instance) mutationHandler({ instance, imageId }); + }; + + return ( + + + + ); +}; + +const ReinstallActionBase: FC = ({ + instance, + projectId, + onSuccess, + onError, + ...actionModalProps +}) => { + const { mutationHandler, isPending } = useBaseInstanceAction( + 'reinstall', + projectId, + instance?.region ?? '', + { + onError, + onSuccess, + }, + ); + + const handleInstanceAction = () => { + if (instance) mutationHandler({ instance }); + }; + + return ( + + ); +}; + +const ReinstallActionPage: FC = () => { + const projectId = useProjectId(); + const { instanceId, region } = useInstanceParams(); + + const { t } = useTranslation('actions'); + const navigate = useNavigate(); + const { addError, addInfo } = useNotifications(); + + const { instance, isLoading } = useInstanceActionModal( + region, + instanceId, + 'reinstall', + ); + + const closeModal = useCallback(() => navigate('..'), [navigate]); + + const onSuccess = useCallback(() => { + addInfo( + t(`pci_instances_actions_reinstall_instance_success_message`, { + name: instance?.name, + }), + true, + ); + + closeModal(); + }, [closeModal, addInfo, t, instance]); + + const onError = useCallback( + (rawError: unknown) => { + const errorMessage = isApiErrorResponse(rawError) + ? rawError.response?.data.message + : (rawError as DefaultError).message; + + addError( + t(`pci_instances_actions_reinstall_instance_error_message`, { + name: instance?.name, + error: errorMessage, + }), + true, + ); + }, + [addError, t, instance], + ); + + const modalProps = { + title: t(`pci_instances_actions_reinstall_instance_title`), + projectId, + onModalClose: closeModal, + instance, + isLoading, + onSuccess, + onError, + variant: 'warning', + } as TReinstallActionPageProps; + + return instance?.isImageDeprecated ? ( + + ) : ( + + ); +}; + +export default ReinstallActionPage; diff --git a/packages/manager/apps/pci-instances/src/pages/instances/action/ReinstallActionPage.tsx b/packages/manager/apps/pci-instances/src/pages/instances/action/ReinstallActionPage.tsx deleted file mode 100644 index e71f9f7fe65b..000000000000 --- a/packages/manager/apps/pci-instances/src/pages/instances/action/ReinstallActionPage.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { FC, useEffect, useState } from 'react'; -import { TFunction } from 'i18next'; -import { useTranslation } from 'react-i18next'; -import { useInstanceReinstallAction } from '@/data/hooks/instance/action/useInstanceAction'; -import ActionModal from '@/components/actionModal/ActionModal.component'; -import ImageSelector from '@/components/imageSelector/ImageSelector.component'; -import { imagesRescueSelector } from '@/data/hooks/image/selector/image.selector'; -import { useImages } from '@/data/hooks/image/useImages'; -import { TInstanceActionModalViewModel } from './view-models/selectInstanceForActionModal'; - -export type TReinstallActionPageProps = { - title: string; - section: 'reinstall'; - projectId: string; - onError: (error: unknown) => void; - onSuccess: () => void; - onModalClose: () => void; - instance: TInstanceActionModalViewModel; - isLoading: boolean; -}; - -const ReinstallActionPage: FC = ({ - title, - section, - projectId, - onError, - onSuccess, - onModalClose, - instance, - isLoading, -}) => { - const { t } = useTranslation('actions'); - const [imageId, setImageId] = useState(''); - const { data: images, isLoading: isImageLoading } = useImages({ - projectId, - region: instance?.region ?? '', - params: { - visibility: 'public', - }, - selectFn: (data) => imagesRescueSelector(data, t as TFunction), - }); - - const { mutationHandler, isPending } = useInstanceReinstallAction(projectId, { - onError, - onSuccess, - }); - - useEffect(() => { - if (images && images.length > 0) { - setImageId(images[0]?.value ?? ''); - } - }, [images]); - - const handleInstanceAction = () => { - if (instance) mutationHandler({ instance, imageId }); - }; - - return ( - - - - ); -}; - -export default ReinstallActionPage; diff --git a/packages/manager/apps/pci-instances/src/pages/instances/action/RescueAction.page.tsx b/packages/manager/apps/pci-instances/src/pages/instances/action/RescueAction.page.tsx index 5789ee889377..b9b78b321821 100644 --- a/packages/manager/apps/pci-instances/src/pages/instances/action/RescueAction.page.tsx +++ b/packages/manager/apps/pci-instances/src/pages/instances/action/RescueAction.page.tsx @@ -1,40 +1,43 @@ import { FC, useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; +import { Trans, useTranslation } from 'react-i18next'; import { TFunction } from 'i18next'; +import { useNavigate } from 'react-router-dom'; +import { DefaultError } from '@tanstack/react-query'; +import { useNotifications } from '@ovh-ux/manager-react-components'; import ActionModal from '@/components/actionModal/ActionModal.component'; import { useImages } from '@/data/hooks/image/useImages'; import { useInstanceRescueAction } from '@/data/hooks/instance/action/useInstanceAction'; import { imagesRescueSelector } from '@/data/hooks/image/selector/image.selector'; import ImageSelector from '@/components/imageSelector/ImageSelector.component'; -import { TInstanceActionModalViewModel } from './view-models/selectInstanceForActionModal'; - -type TRescueActionSection = 'rescue/start' | 'rescue/end'; +import { useProjectId } from '@/hooks/project/useProjectId'; +import { isApiErrorResponse } from '@/utils'; +import { + useInstanceActionModal, + useInstanceParams, +} from '@/pages/instances/action/hooks/useInstanceActionModal'; +import { TSectionType } from '@/types/instance/action/action.type'; export type TRescueActionPageProps = { - title: string; - section: TRescueActionSection; - projectId: string; - onError: (error: unknown) => void; - onSuccess: () => void; - onModalClose: () => void; - instance: TInstanceActionModalViewModel; - isLoading: boolean; + section: TSectionType; }; -export const RescueActionPage: FC = ({ - title, - section, - projectId, - onError, - onSuccess, - onModalClose, - instance, - isLoading, -}) => { +const RescueActionPage: FC = ({ section }) => { + const projectId = useProjectId(); + const { instanceId, region } = useInstanceParams(); + const { t } = useTranslation('actions'); + const navigate = useNavigate(); + const { addError, addInfo } = useNotifications(); + + const { instance, isLoading } = useInstanceActionModal( + region, + instanceId, + section, + ); + const { data: images, isLoading: isImageLoading } = useImages({ projectId, - region: instance?.region ?? '', + region, params: { visibility: 'public', }, @@ -50,11 +53,65 @@ export const RescueActionPage: FC = ({ }, [images]); const isRescueMode = section === 'rescue/start'; + const snakeCaseSection = isRescueMode ? 'rescue_start' : 'rescue_end'; - const { mutationHandler, isPending } = useInstanceRescueAction(projectId, { - onError, - onSuccess, - }); + const closeModal = () => navigate('..'); + + const onSuccess = () => { + if (isRescueMode) { + addInfo( + , + ]} + />, + true, + ); + } else { + addInfo( + t( + `pci_instances_actions_${snakeCaseSection}_instance_success_message`, + { + name: instance?.name, + }, + ), + true, + ); + } + + closeModal(); + }; + + const onError = (rawError: unknown) => { + const errorMessage = isApiErrorResponse(rawError) + ? rawError.response?.data.message + : (rawError as DefaultError).message; + addError( + t(`pci_instances_actions_${snakeCaseSection}_instance_error_message`, { + name: instance?.name, + error: errorMessage, + }), + true, + ); + }; + + const { mutationHandler, isPending } = useInstanceRescueAction( + projectId, + region, + { + onError, + onSuccess, + }, + ); const handleInstanceAction = () => { if (instance) @@ -63,10 +120,10 @@ export const RescueActionPage: FC = ({ return ( = ({ ); }; + +export default RescueActionPage; diff --git a/packages/manager/apps/pci-instances/src/pages/instances/action/components/modal/ActionModalContent.component.tsx b/packages/manager/apps/pci-instances/src/pages/instances/action/components/modal/ActionModalContent.component.tsx index c0265c30fd01..eb514bd7fd60 100644 --- a/packages/manager/apps/pci-instances/src/pages/instances/action/components/modal/ActionModalContent.component.tsx +++ b/packages/manager/apps/pci-instances/src/pages/instances/action/components/modal/ActionModalContent.component.tsx @@ -1,14 +1,13 @@ import { FC, useMemo } from 'react'; import { - OsdsMessage, - OsdsSkeleton, - OsdsText, -} from '@ovhcloud/ods-components/react'; -import { ODS_MESSAGE_TYPE, ODS_TEXT_SIZE } from '@ovhcloud/ods-components'; -import { - ODS_THEME_COLOR_INTENT, - ODS_THEME_TYPOGRAPHY_LEVEL, -} from '@ovhcloud/ods-common-theming'; + ICON_NAME, + Message, + MESSAGE_COLOR, + MessageBody, + MessageIcon, + Skeleton, + Text, +} from '@ovhcloud/ods-react'; import { useTranslation } from 'react-i18next'; import { isCustomUrlSection, replaceToSnakeCase } from '@/utils'; import { TSectionType } from '@/types/instance/action/action.type'; @@ -90,36 +89,26 @@ export const ActionModalContent: FC = ({
{/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment */} {[...new Array(3)].map((_elt, index) => ( - + ))}
) : ( <> {labels.map((label) => ( - + {label} - + ))} {children} {warningMessage && ( - - - {warningMessage} - - + + {warningMessage} + )} ); diff --git a/packages/manager/apps/pci-instances/src/pages/instances/action/hooks/useInstanceActionModal.tsx b/packages/manager/apps/pci-instances/src/pages/instances/action/hooks/useInstanceActionModal.tsx index c78b5e852733..1a4cfd46423f 100644 --- a/packages/manager/apps/pci-instances/src/pages/instances/action/hooks/useInstanceActionModal.tsx +++ b/packages/manager/apps/pci-instances/src/pages/instances/action/hooks/useInstanceActionModal.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { useNotifications } from '@ovh-ux/manager-react-components'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useProjectId } from '@/hooks/project/useProjectId'; import { TInstanceActionDto } from '@/types/instance/api.type'; @@ -11,8 +11,11 @@ import { selectInstanceForActionModal, TInstanceActionModalViewModel, } from '../view-models/selectInstanceForActionModal'; -import { getInstanceById } from '@/data/hooks/instance/useInstances'; import { useInstance } from '@/data/hooks/instance/useInstance'; +import { + selectAggregatedInstanceById, + selectInstanceById, +} from '@/adapters/tanstack-query/store/instances/selectors'; const formatSection = (section: TSectionType | null): string | null => { if (!section) return null; @@ -51,7 +54,9 @@ export const useInstanceActionModal = ( const { t } = useTranslation('actions'); const aggregatedInstance = useMemo( - () => getInstanceById(projectId, instanceId, queryClient), + () => + selectAggregatedInstanceById(projectId, instanceId, queryClient) ?? + selectInstanceById(projectId, instanceId || '', queryClient), [instanceId, projectId, queryClient], ); @@ -115,3 +120,21 @@ export const useInstanceActionModal = ( isLoading, }; }; + +/** + * This allows to retrieve the correct parameters depending on the route we are + */ +export const useInstanceParams = () => { + const { instanceId: paramInstanceId, region: paramRegion } = useParams(); + const [searchParams] = useSearchParams(); + + const instanceId = paramInstanceId ?? searchParams.get('instanceId'); + const region = paramRegion ?? searchParams.get('region') ?? 'null'; + + if (!instanceId) throw new Error('instanceId is required'); + + return { + instanceId, + region, + }; +}; diff --git a/packages/manager/apps/pci-instances/src/pages/instances/action/view-models/selectInstanceForActionModal.ts b/packages/manager/apps/pci-instances/src/pages/instances/action/view-models/selectInstanceForActionModal.ts index ff65f1635f4d..19eacf779f06 100644 --- a/packages/manager/apps/pci-instances/src/pages/instances/action/view-models/selectInstanceForActionModal.ts +++ b/packages/manager/apps/pci-instances/src/pages/instances/action/view-models/selectInstanceForActionModal.ts @@ -1,5 +1,5 @@ import { TAggregatedInstanceDto } from '@/types/instance/api.type'; -import { TInstance } from '@/types/instance/entity.type'; +import { TInstance, TInstanceAddress } from '@/types/instance/entity.type'; export type TInstanceActionModalViewModel = { id: string; @@ -12,14 +12,17 @@ export type TInstanceActionModalViewModel = { const mapAggregatedInstanceDto = (instance: TAggregatedInstanceDto) => ({ isImageDeprecated: instance.isImageDeprecated, - ip: instance.addresses[0]?.ip, + ip: instance.addresses[0]?.ip ?? '', region: instance.region, imageId: instance.imageId, }); const mapInstance = (instance: TInstance) => ({ isImageDeprecated: instance.image?.deprecated ?? false, - ip: instance.addresses.values().next().value?.[0]?.ip ?? '', + ip: + (instance.addresses.values().next().value as + | TInstanceAddress[] + | undefined)?.[0]?.ip ?? '', region: instance.region.name, imageId: instance.image?.id ?? '', }); diff --git a/packages/manager/apps/pci-instances/src/pages/instances/datagrid/components/Datagrid.component.tsx b/packages/manager/apps/pci-instances/src/pages/instances/datagrid/components/Datagrid.component.tsx index 92834a84ca8c..7b3c2635e454 100644 --- a/packages/manager/apps/pci-instances/src/pages/instances/datagrid/components/Datagrid.component.tsx +++ b/packages/manager/apps/pci-instances/src/pages/instances/datagrid/components/Datagrid.component.tsx @@ -6,6 +6,7 @@ import { } from '@ovh-ux/manager-react-components'; import { Dispatch, SetStateAction, useEffect, useMemo } from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { NAMESPACES } from '@ovh-ux/manager-common-translations'; import { OsdsLink, OsdsText } from '@ovhcloud/ods-components/react'; import { Filter } from '@ovh-ux/manager-core-api'; import { usePciUrl } from '@ovh-ux/manager-pci-common'; @@ -16,14 +17,13 @@ import { import { NameIdCell } from '@/pages/instances/datagrid/components/cell/NameIdCell.component'; import { useInstances } from '@/data/hooks/instance/useInstances'; import { ActionsCell } from '@/pages/instances/datagrid/components/cell/ActionsCell.component'; - -import { StatusCell } from '@/pages/instances/datagrid/components/cell/StatusCell.component'; import { ListCell } from '@/pages/instances/datagrid/components/cell/ListCell.component'; import { mapAddressesToListItems } from '@/pages/instances/mapper'; import { DeepReadonly } from '@/types/utils.type'; import { TAggregatedInstance } from '@/types/instance/entity.type'; import { useDatagridPolling } from '../hooks/useDatagridPolling'; import { TextCell } from '@/pages/instances/datagrid/components/cell/TextCell.component'; +import { TaskStatus } from '../../task/TaskStatus.component'; type TFilterWithLabel = Filter & { label: string }; type TSorting = { @@ -68,7 +68,13 @@ const DatagridComponent = ({ onSortChange, }: TDatagridComponentProps) => { const pciUrl = usePciUrl(); - const { t } = useTranslation(['list', 'common', 'actions']); + const { t } = useTranslation([ + 'list', + 'common', + 'actions', + NAMESPACES.STATUS, + NAMESPACES.REGION, + ]); const { translateMicroRegion } = useTranslatedMicroRegions(); const { addWarning, @@ -122,7 +128,7 @@ const DatagridComponent = ({ /> ); }, - label: t('pci_instances_list_column_region'), + label: t(`${NAMESPACES.REGION}:localisation`), isSortable: false, }, { @@ -192,14 +198,15 @@ const DatagridComponent = ({ const isPolling = !!pollingInstance && pollingInstance.isPolling; return ( - ); }, - label: t('pci_instances_list_column_status'), + label: t(`${NAMESPACES.STATUS}:status`), isSortable: false, }, { diff --git a/packages/manager/apps/pci-instances/src/pages/instances/datagrid/components/cell/LoadingCell.component.tsx b/packages/manager/apps/pci-instances/src/pages/instances/datagrid/components/cell/LoadingCell.component.tsx index d46e1356a0d3..b0ff0f87fbb5 100644 --- a/packages/manager/apps/pci-instances/src/pages/instances/datagrid/components/cell/LoadingCell.component.tsx +++ b/packages/manager/apps/pci-instances/src/pages/instances/datagrid/components/cell/LoadingCell.component.tsx @@ -1,5 +1,5 @@ -import { OsdsSkeleton } from '@ovhcloud/ods-components/react'; import { FC, PropsWithChildren } from 'react'; +import { Skeleton } from '@ovhcloud/ods-react'; export type TLoadingCellProps = PropsWithChildren<{ isLoading: boolean; @@ -7,6 +7,6 @@ export type TLoadingCellProps = PropsWithChildren<{ export const LoadingCell: FC = ({ isLoading, children }) => (
- {isLoading ? : children} + {isLoading ? : children}
); diff --git a/packages/manager/apps/pci-instances/src/pages/instances/datagrid/components/cell/NameIdCell.component.tsx b/packages/manager/apps/pci-instances/src/pages/instances/datagrid/components/cell/NameIdCell.component.tsx index bca723862b91..331f3834544a 100644 --- a/packages/manager/apps/pci-instances/src/pages/instances/datagrid/components/cell/NameIdCell.component.tsx +++ b/packages/manager/apps/pci-instances/src/pages/instances/datagrid/components/cell/NameIdCell.component.tsx @@ -11,15 +11,19 @@ type TNameIdCellProps = { isLoading: boolean; }; -export const NameIdCell: FC = ({ isLoading, instance }) => ( - - - {instance.name} - - {instance.id} - -); +export const NameIdCell: FC = ({ isLoading, instance }) => { + const detailHref = useHref(`${instance.id}?region=${instance.region}`); + + return ( + + + {instance.name} + + {instance.id} + + ); +}; diff --git a/packages/manager/apps/pci-instances/src/pages/instances/datagrid/hooks/useDatagridPolling.ts b/packages/manager/apps/pci-instances/src/pages/instances/datagrid/hooks/useDatagridPolling.ts index a0c5b3147f40..9ddcee21a565 100644 --- a/packages/manager/apps/pci-instances/src/pages/instances/datagrid/hooks/useDatagridPolling.ts +++ b/packages/manager/apps/pci-instances/src/pages/instances/datagrid/hooks/useDatagridPolling.ts @@ -3,7 +3,6 @@ import { useCallback, useMemo } from 'react'; import { useNotifications } from '@ovh-ux/manager-react-components'; import { ApiError } from '@ovh-ux/manager-core-api'; import { useTranslation } from 'react-i18next'; -import { updateInstanceFromCache } from '@/data/hooks/instance/useInstances'; import { TInstance } from '@/types/instance/entity.type'; import { useProjectId } from '@/hooks/project/useProjectId'; import { buildPartialInstanceDto } from '@/data/hooks/instance/builder/instanceDto.builder'; @@ -13,6 +12,7 @@ import { useInstancesPolling, } from '@/data/hooks/instance/polling/useInstancesPolling'; import { selectPollingDataForDatagrid } from '../view-models/selectPollingDataForDatagrid'; +import { updateAggregatedInstancesFromCache } from '@/adapters/tanstack-query/store/instances/updaters'; const getPartialDeletedInstanceDto = (id: string) => buildPartialInstanceDto({ id }) @@ -43,7 +43,7 @@ export const useDatagridPolling = (pendingTasks: TPendingTask[]) => { const { status, task } = instance; const isDeleted = !task.isPending && status === 'DELETED'; const deletedInstance = getPartialDeletedInstanceDto(instance.id); - updateInstanceFromCache(queryClient, { + updateAggregatedInstancesFromCache(queryClient, { projectId, instance: isDeleted ? deletedInstance : getPartialInstanceDto(instance), }); @@ -65,7 +65,7 @@ export const useDatagridPolling = (pendingTasks: TPendingTask[]) => { (error: ApiError, instanceId: string) => { if (error.response?.status === 404) { const deletedInstance = getPartialDeletedInstanceDto(instanceId); - updateInstanceFromCache(queryClient, { + updateAggregatedInstancesFromCache(queryClient, { projectId, instance: deletedInstance, }); diff --git a/packages/manager/apps/pci-instances/src/pages/instances/instance/Instance.page.tsx b/packages/manager/apps/pci-instances/src/pages/instances/instance/Instance.page.tsx index a16adf6c5bf9..943e20b344e5 100644 --- a/packages/manager/apps/pci-instances/src/pages/instances/instance/Instance.page.tsx +++ b/packages/manager/apps/pci-instances/src/pages/instances/instance/Instance.page.tsx @@ -1,13 +1,89 @@ -import { PageLayout } from '@ovh-ux/manager-react-components'; -import { FC } from 'react'; +import { + ChangelogButton, + PageLayout, + PciGuidesHeader, + Notifications, +} from '@ovh-ux/manager-react-components'; +import { FC, useMemo } from 'react'; +import { Outlet, useResolvedPath, useRouteLoaderData } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { NAMESPACES } from '@ovh-ux/manager-common-translations'; +import { TProject, useParam } from '@ovh-ux/manager-pci-common'; +import { Breadcrumb } from '@/components/breadcrumb/Breadcrumb.component'; import { GoBack } from '@/components/navigation/GoBack.component'; +import TabsPanel from '@/components/tab/TabsPanel.component'; import InstanceWrapper from './InstanceWrapper.page'; +import { CHANGELOG_LINKS } from '@/constants'; +import InstanceName from './dashboard/components/InstanceName.component'; +import { useDashboard } from './dashboard/hooks/useDashboard'; +import { LoadingCell } from '../datagrid/components/cell/LoadingCell.component'; +import InstanceErrorGuard from './InstanceErrorGuard.page'; -const Instance: FC = () => ( - - - - - -); +const Instance: FC = () => { + const { t } = useTranslation([NAMESPACES.DASHBOARD, 'dashboard']); + const project = useRouteLoaderData('root') as TProject; + const { instanceId, region } = useParam('region', 'instanceId'); + const dashboardPath = useResolvedPath(''); + const vncPath = useResolvedPath(`../${instanceId}/vnc`); // vnc will be redirect to the legacy page until migration done + + const { instance, isPending: isInstanceLoading, error } = useDashboard({ + region, + instanceId, + }); + + const tabs = useMemo(() => { + const defaultTab = [ + { + label: t('general_information'), + to: dashboardPath.pathname, + }, + ]; + + return instance?.isEditEnabled + ? [ + ...defaultTab, + { + label: t('dashboard:pci_instances_dashboard_tab_vnc_title'), + to: vncPath.pathname, + }, + ] + : defaultTab; + }, [dashboardPath.pathname, vncPath.pathname, t, instance?.isEditEnabled]); + + return ( + + + + +
+
+
+ + {instance && } + +
+
+ + +
+
+
+
+ +
+ +
+ +
+
+ +
+
+
+
+ ); +}; export default Instance; diff --git a/packages/manager/apps/pci-instances/src/pages/instances/instance/InstanceErrorGuard.page.tsx b/packages/manager/apps/pci-instances/src/pages/instances/instance/InstanceErrorGuard.page.tsx new file mode 100644 index 000000000000..86c06404eb08 --- /dev/null +++ b/packages/manager/apps/pci-instances/src/pages/instances/instance/InstanceErrorGuard.page.tsx @@ -0,0 +1,35 @@ +import { mapUnknownErrorToBannerError } from '@/utils'; +import { ErrorBanner } from '@ovh-ux/manager-react-components'; +import { ShellContext } from '@ovh-ux/manager-react-shell-client'; +import { FC, PropsWithChildren, useContext } from 'react'; +import { useNavigate } from 'react-router-dom'; + +const InstanceErrorGuard: FC> = ({ + children, + error, +}) => { + const nav = useContext(ShellContext).shell.navigation; + const errorMessage = mapUnknownErrorToBannerError(error); + const navigate = useNavigate(); + + const navigateToListingPage = () => { + navigate('..'); + }; + + const reloadPage = () => { + nav.reload(); + }; + + if (error) + return ( + + ); + + return children; +}; + +export default InstanceErrorGuard; diff --git a/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/Dashboard.page.tsx b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/Dashboard.page.tsx new file mode 100644 index 000000000000..b0e181c643b4 --- /dev/null +++ b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/Dashboard.page.tsx @@ -0,0 +1,16 @@ +import { FC } from 'react'; +import { Outlet } from 'react-router-dom'; +import InstanceGeneralInfoBlock from './components/InstanceGeneralInfoBlock.component'; +import InstancePropertyBlock from './components/InstancePropertyBlock.component'; +import InstanceNetworkingBlock from './components/InstanceNetworkingBlock.component'; + +const Dashboard: FC = () => ( +
+ + + + +
+); + +export default Dashboard; diff --git a/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/action/AttachNetwork.page.tsx b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/action/AttachNetwork.page.tsx new file mode 100644 index 000000000000..6765f30d9dd0 --- /dev/null +++ b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/action/AttachNetwork.page.tsx @@ -0,0 +1,90 @@ +import { FC, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { useNotifications } from '@ovh-ux/manager-react-components'; +import { DefaultError } from '@tanstack/react-query'; +import Modal from '@/components/modal/Modal.component'; +import { useAttachNetwork } from '@/data/hooks/instance/useInstance'; +import { Spinner } from '@/components/spinner/Spinner.component'; +import { isApiErrorResponse } from '@/utils'; +import NetworkSelector from '../components/NetworkSelector.component'; +import { useProjectId } from '@/hooks/project/useProjectId'; +import { useUnattachedPrivateNetworks } from '../hooks/useDashboardAction'; +import { useInstanceParams } from '@/pages/instances/action/hooks/useInstanceActionModal'; + +const AttachNetworkModal: FC = () => { + const { t } = useTranslation('actions'); + const projectId = useProjectId(); + const { instanceId, region } = useInstanceParams(); + const navigate = useNavigate(); + const handleModalClose = () => navigate('..'); + const [networkId, setNetworkId] = useState(''); + const { addError, addSuccess } = useNotifications(); + + const { + networks, + instance, + isPending: isNetworkPending, + } = useUnattachedPrivateNetworks({ projectId, region, instanceId }); + + const { isPending: isAttaching, mutate: attachNetwork } = useAttachNetwork({ + projectId, + instanceId, + region, + callbacks: { + onSuccess: (_data, variables) => { + const network = networks.find( + ({ value }) => value === variables.networkId, + ); + + addSuccess( + t( + 'pci_instances_actions_instance_network_network_attach_success_message', + { + network: network!.label, + instance, + }, + ), + true, + ); + + handleModalClose(); + }, + onError: (error) => { + const errorMessage = isApiErrorResponse(error) + ? error.response?.data.message + : (error as DefaultError).message; + addError( + t('pci_instances_actions_instance_attach_error_message', { + message: errorMessage, + }), + true, + ); + handleModalClose(); + }, + }, + }); + + const isPending = isNetworkPending || isAttaching; + + return ( + attachNetwork({ networkId })} + onModalClose={handleModalClose} + variant="primary" + > + {isPending ? ( +
+ +
+ ) : ( + + )} +
+ ); +}; + +export default AttachNetworkModal; diff --git a/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/action/AttachVolume.page.tsx b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/action/AttachVolume.page.tsx new file mode 100644 index 000000000000..40d66661050c --- /dev/null +++ b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/action/AttachVolume.page.tsx @@ -0,0 +1,90 @@ +import { FC, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { DefaultError } from '@tanstack/react-query'; +import { useNotifications } from '@ovh-ux/manager-react-components'; +import Modal from '@/components/modal/Modal.component'; +import { Spinner } from '@/components/spinner/Spinner.component'; +import { useUnattachedVolumes } from '../hooks/useDashboardAction'; +import { useProjectId } from '@/hooks/project/useProjectId'; +import VolumeSelector from '../components/VolumeSelector.component'; +import { useAttachVolume } from '@/data/hooks/instance/useInstance'; +import { isApiErrorResponse } from '@/utils'; +import { useInstanceParams } from '@/pages/instances/action/hooks/useInstanceActionModal'; + +const AttachVolume: FC = () => { + const { t } = useTranslation('actions'); + const projectId = useProjectId(); + const { instanceId, region } = useInstanceParams(); + const navigate = useNavigate(); + const handleModalClose = () => navigate('..'); + const [volumeId, setVolumeId] = useState(''); + const { addError, addSuccess } = useNotifications(); + + const { + volumes, + instance, + isPending: isVolumePending, + } = useUnattachedVolumes({ + projectId, + region, + instanceId, + }); + + const { isPending: isAttaching, mutate: attachVolume } = useAttachVolume({ + projectId, + instanceId, + region, + callbacks: { + onSuccess: (_data, variables) => { + const volume = volumes.find( + ({ value }) => value === variables.volumeId, + ); + addSuccess( + t('pci_instances_actions_instance_volume_attach_success_message', { + volume: volume!.label, + instance, + }), + true, + ); + + handleModalClose(); + }, + onError: (error) => { + const errorMessage = isApiErrorResponse(error) + ? error.response?.data.message + : (error as DefaultError).message; + addError( + t('pci_instances_actions_instance_attach_error_message', { + message: errorMessage, + }), + true, + ); + handleModalClose(); + }, + }, + }); + + const isPending = isVolumePending || isAttaching; + + return ( + attachVolume({ volumeId })} + onModalClose={handleModalClose} + variant="primary" + > + {isPending ? ( +
+ +
+ ) : ( + + )} +
+ ); +}; + +export default AttachVolume; diff --git a/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/DashboardCardLayout.component.tsx b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/DashboardCardLayout.component.tsx new file mode 100644 index 000000000000..4d368c9c003f --- /dev/null +++ b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/DashboardCardLayout.component.tsx @@ -0,0 +1,19 @@ +import { FC, PropsWithChildren } from 'react'; +import { Card, Divider, Text } from '@ovhcloud/ods-react'; + +type TDashboardCardLayoutProps = { title: string }; + +const DashboardCardLayout: FC> = ({ + children, + title, +}) => ( + +
+ {title} + +
+
{children}
+
+); + +export default DashboardCardLayout; diff --git a/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/DashboardTile.component.tsx b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/DashboardTile.component.tsx new file mode 100644 index 000000000000..57d49600cb77 --- /dev/null +++ b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/DashboardTile.component.tsx @@ -0,0 +1,39 @@ +import { FC, PropsWithChildren } from 'react'; +import { Divider, Text, TEXT_PRESET } from '@ovhcloud/ods-react'; +import { LoadingCell } from '@/pages/instances/datagrid/components/cell/LoadingCell.component'; + +type TDashboardTileProps = PropsWithChildren<{ + isLoading?: boolean; + label?: string; + withoutDivider?: boolean; +}>; + +export const DashboardTileBlock: FC = ({ + label, + isLoading = false, + withoutDivider = false, + children, +}) => ( +
+
+ {label && ( +
+ {label} +
+ )} +
+
+ {children} +
+ {!withoutDivider && } +
+); + +export const DashboardTileText: FC = ({ + children, + ...props +}) => ( + + {children} + +); diff --git a/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/InstanceGeneralInfoBlock.component.tsx b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/InstanceGeneralInfoBlock.component.tsx new file mode 100644 index 000000000000..cb6600a58788 --- /dev/null +++ b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/InstanceGeneralInfoBlock.component.tsx @@ -0,0 +1,182 @@ +import { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { NAMESPACES } from '@ovh-ux/manager-common-translations'; +import { useHref } from 'react-router-dom'; +import { OsdsLink } from '@ovhcloud/ods-components/react'; +import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming'; +import { + Links, + LinkType, + useTranslatedMicroRegions, +} from '@ovh-ux/manager-react-components'; +import { RegionChipByType } from '@ovh-ux/manager-pci-common'; +import { BUTTON_VARIANT, Text, TEXT_PRESET } from '@ovhcloud/ods-react'; +import DashboardCardLayout from './DashboardCardLayout.component'; +import PriceLabel from '@/components/priceLabel/PriceLabel.component'; +import { useDashboard } from '../hooks/useDashboard'; +import { + DashboardTileBlock, + DashboardTileText, +} from './DashboardTile.component'; +import { useDashboardPolling } from '../hooks/useDashboardPolling'; +import { TaskStatus } from '@/pages/instances/task/TaskStatus.component'; +import { ActionsMenu } from '@/components/menu/ActionsMenu.component'; +import { useInstanceParams } from '@/pages/instances/action/hooks/useInstanceActionModal'; +import { Clipboard } from '@/components/clipboard/Clipboard.component'; + +const InstanceGeneralInfoBlock: FC = () => { + const { t } = useTranslation([ + 'dashboard', + 'list', + 'actions', + NAMESPACES.FORM, + NAMESPACES.STATUS, + NAMESPACES.REGION, + NAMESPACES.ACTIONS, + ]); + const { translateMicroRegion } = useTranslatedMicroRegions(); + const { instanceId, region } = useInstanceParams(); + const hrefEditInstance = useHref(`../${instanceId}/edit`); + const hrefBillingMonthlyActivate = useHref( + `../${instanceId}/billing/monthly/activate?region=${region}`, + ); + const hrefDeleteInstance = useHref( + `../${instanceId}/delete?region=${region}`, + ); + + const { instance, pendingTasks, isPending: isInstanceLoading } = useDashboard( + { + region, + instanceId, + }, + ); + + const polling = useDashboardPolling({ + instanceId, + region, + pendingTasks, + }); + + return ( + + +
+ +
+
+ +
+ {instance?.flavor?.name} + {instance?.isEditEnabled && ( + + )} +
+
+ {instance && ( + + 0} + taskState={instance.task.status} + status={instance.status} + /> + + )} + +
+ + {instance?.region.availabilityZone ?? + translateMicroRegion(instance?.region.name || '')} + + {instance && ( + + )} +
+
+ + {instance?.flavor?.ram} + + + {instance?.flavor?.cpu} + + +
+
+ {instance?.pricings.map(({ value, type, label }) => ( +
+ + {t(`pci_instances_dashboard_${label}_price_label`)} + + +
+ ))} +
+ {instance?.canActivateMonthlyBilling && ( + + )} +
+
+ {instance && ( + +
+ + {t('pci_instances_dashboard_all_actions')} + + +
+
+ )} + {instance?.isDeleteEnabled && ( + + + {t(`${NAMESPACES.ACTIONS}:delete`)} + + + )} +
+ ); +}; + +export default InstanceGeneralInfoBlock; diff --git a/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/InstanceName.component.tsx b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/InstanceName.component.tsx new file mode 100644 index 000000000000..abf1104c538c --- /dev/null +++ b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/InstanceName.component.tsx @@ -0,0 +1,105 @@ +import { FC, useCallback, useState } from 'react'; +import { useNotifications } from '@ovh-ux/manager-react-components'; +import { useTranslation } from 'react-i18next'; +import { clsx } from 'clsx'; +import { useQueryClient } from '@tanstack/react-query'; +import { Button, Icon, Text } from '@ovhcloud/ods-react'; +import { useUpdateInstanceName } from '@/data/hooks/instance/useInstance'; +import { useProjectId } from '@/hooks/project/useProjectId'; +import { TInstanceDashboardViewModel } from '../view-models/selectInstanceDashboard'; +import InputCancellable from '@/components/input/InputCancellable.component'; +import { TPartialInstance } from '@/types/instance/entity.type'; +import { updateAllInstancesFromCache } from '@/adapters/tanstack-query/store/instances/updaters'; + +type TInstanceNameProps = { + instance: NonNullable; +}; + +const InstanceName: FC = ({ instance }) => { + const { t } = useTranslation('dashboard'); + const queryClient = useQueryClient(); + const { addError } = useNotifications(); + const projectId = useProjectId(); + const [isEditing, setIsEditing] = useState(false); + const [instanceName, setInstanceName] = useState(instance.name); + + const closeEdit = () => setIsEditing(!isEditing); + + const handleSuccessUpdate = useCallback( + (newInstanceName: string) => { + const newInstance: TPartialInstance = { + id: instance.id, + name: newInstanceName, + }; + + updateAllInstancesFromCache(queryClient, { + projectId, + instance: newInstance, + region: instance.region.name, + }); + }, + [instance.id, projectId, queryClient, instance.region.name], + ); + + const { + isPending, + variables, + mutate: editInstanceName, + } = useUpdateInstanceName({ + projectId, + instanceId: instance.id, + callbacks: { + onSuccess: (_data, { instanceName }) => handleSuccessUpdate(instanceName), + onError: () => + addError(t('pci_instances_dashboard_edit_name_error_message'), true), + }, + }); + + const handleCancel = () => { + setInstanceName(instance.name); + closeEdit(); + }; + + const handleSubmit = () => { + editInstanceName({ instanceName }); + closeEdit(); + }; + + return ( +
+ {isEditing ? ( + setInstanceName(target.value)} + onCancel={handleCancel} + onSubmit={handleSubmit} + className="h-[2.5em]" + /> + ) : ( + + {isPending ? variables.instanceName : instance.name} + + )} + {instance.isEditEnabled && !isEditing && ( + + )} +
+ ); +}; + +export default InstanceName; diff --git a/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/InstanceNetworkingBlock.component.tsx b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/InstanceNetworkingBlock.component.tsx new file mode 100644 index 000000000000..6a261fffae43 --- /dev/null +++ b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/InstanceNetworkingBlock.component.tsx @@ -0,0 +1,140 @@ +import { FC, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useProjectUrl } from '@ovh-ux/manager-react-components'; +import { Text, TEXT_PRESET } from '@ovhcloud/ods-react'; +import DashboardCardLayout from './DashboardCardLayout.component'; +import { useProjectId } from '@/hooks/project/useProjectId'; +import { useDashboard } from '../hooks/useDashboard'; +import { ActionsMenu } from '@/components/menu/ActionsMenu.component'; +import NetworkItem from './NetworkItem.component'; +import { DashboardTileBlock } from './DashboardTile.component'; +import { useDedicatedUrl } from '@/hooks/url/useDedicatedUrl'; +import { useInstanceParams } from '@/pages/instances/action/hooks/useInstanceActionModal'; +import ReverseDNS from './ReverseDNS.component'; + +const InstanceNetworkingBlock: FC = () => { + const { t } = useTranslation(['dashboard', 'list', 'actions']); + const { region, instanceId } = useInstanceParams(); + const dedicatedUrl = useDedicatedUrl(); + const projectId = useProjectId(); + const projectUrl = useProjectUrl('public-cloud'); + + const { instance, isPending: isInstanceLoading } = useDashboard({ + region, + instanceId, + }); + + const publicIPs = useMemo( + () => + instance?.addresses.get('floating') ?? instance?.addresses.get('public'), + [instance?.addresses], + ); + + const publicIpActionsLinks = useMemo(() => { + const ipV4 = publicIPs?.find((publicIp) => publicIp.version === 4)?.ip; + const ipParams = `ip=${ipV4}&ipBlock=${ipV4}`; + + return [ + { + label: t('actions:pci_instances_actions_instance_network_change_dns'), + link: { + path: dedicatedUrl, + isExternal: true, + isTargetBlank: true, + }, + }, + { + label: t( + 'actions:pci_instances_actions_instance_network_activate_mitigation', + ), + link: { + path: `${dedicatedUrl}?action=mitigation&${ipParams}&serviceName=${projectId}`, + isExternal: true, + isTargetBlank: true, + }, + }, + { + label: t( + 'actions:pci_instances_actions_instance_network_firewall_settings', + ), + link: { + path: `${dedicatedUrl}?action=toggleFirewall&${ipParams}`, + isExternal: true, + isTargetBlank: true, + }, + }, + ]; + }, [publicIPs, dedicatedUrl, projectId, t]); + + const privateIps = useMemo(() => instance?.addresses.get('private'), [ + instance?.addresses, + ]); + + const privateIpActionsLinks = useMemo( + () => + instance?.isEditEnabled + ? new Map([ + [ + 'private_ip', + [ + { + label: t( + 'actions:pci_instances_actions_instance_network_network_settings', + ), + link: { + path: `${projectUrl}/private-networks`, + isExternal: true, + }, + }, + { + label: t( + 'actions:pci_instances_actions_instance_network_network_attach', + ), + link: { + path: 'network/private/attach', + isExternal: false, + }, + }, + ], + ], + ]) + : new Map(), + [projectUrl, t, instance?.isEditEnabled], + ); + + return ( + + + {publicIPs?.map((publicIp) => ( + + + + ))} + + +
+ + {t('pci_instances_dashboard_network_private_title')} + + +
+ {privateIps?.map((privateIp) => ( + + ))} +
+
+ ); +}; + +export default InstanceNetworkingBlock; diff --git a/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/InstancePropertyBlock.component.tsx b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/InstancePropertyBlock.component.tsx new file mode 100644 index 000000000000..e2924d01b9ee --- /dev/null +++ b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/InstancePropertyBlock.component.tsx @@ -0,0 +1,96 @@ +import { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useProjectUrl } from '@ovh-ux/manager-react-components'; +import { Link as RouterLink } from 'react-router-dom'; +import { Icon, Link, Text } from '@ovhcloud/ods-react'; +import DashboardCardLayout from './DashboardCardLayout.component'; +import { useDashboard } from '../hooks/useDashboard'; +import { DashboardTileBlock } from './DashboardTile.component'; +import { useInstanceParams } from '@/pages/instances/action/hooks/useInstanceActionModal'; +import { Clipboard } from '@/components/clipboard/Clipboard.component'; + +const InstancePropertyBlock: FC = () => { + const { t } = useTranslation(['dashboard', 'list']); + const projectUrl = useProjectUrl('public-cloud'); + const { region, instanceId } = useInstanceParams(); + + const { instance, isPending: isInstanceLoading } = useDashboard({ + region, + instanceId, + }); + + return ( + + + {instance?.flavor?.storage} + + + {instance?.flavor?.publicBandwidth} + + +
+ {instance?.volumes.length ? ( + instance.volumes.map(({ id, name }) => ( + + {name} + + )) + ) : ( + - + )} + {instance?.isEditEnabled && ( + + {t('pci_instances_dashboard_attach_volumes')} + + + )} +
+
+ + {instance?.image} + {instance?.isEditEnabled && ( + + {t('pci_instances_dashboard_edit_image')} + + + )} + + {instance?.sshKey && ( + + {instance.sshKey} + + )} + {instance?.login && ( + +
+ +
+
+ )} +
+ ); +}; + +export default InstancePropertyBlock; diff --git a/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/NetworkItem.component.tsx b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/NetworkItem.component.tsx new file mode 100644 index 000000000000..803fe5b436ae --- /dev/null +++ b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/NetworkItem.component.tsx @@ -0,0 +1,87 @@ +import { FC, PropsWithChildren } from 'react'; +import { useTranslation } from 'react-i18next'; +import { OsdsText } from '@ovhcloud/ods-components/react'; +import { ODS_TEXT_LEVEL, ODS_TEXT_SIZE } from '@ovhcloud/ods-components'; +import { + ODS_THEME_COLOR_HUE, + ODS_THEME_COLOR_INTENT, +} from '@ovhcloud/ods-common-theming'; +import { FormField, FormFieldLabel } from '@ovhcloud/ods-react'; +import StatusChip from '@/components/statusChip/StatusChip.component'; +import { ActionsMenu } from '@/components/menu/ActionsMenu.component'; +import { TInstanceAddress } from '@/types/instance/entity.type'; +import { Clipboard } from '@/components/clipboard/Clipboard.component'; +import { TAction } from '@/pages/instances/instance/dashboard/view-models/selectInstanceDashboard'; + +type TNetworkItemProps = { + address: TInstanceAddress; + isFloatingIp?: boolean; + actions?: TAction[]; +}; + +const IPAddressItem: FC<{ + label: string; + value: string; + actions?: TAction[]; +}> = ({ label, value, actions }) => ( +
+ + {label} + + +
+ {actions && ( + + )} +
+
+); + +const NetworkItem: FC> = ({ + address, + actions, + isFloatingIp, + children, +}) => { + const { t } = useTranslation('dashboard'); + + return ( +
+
+ + {address.subnet?.network.name} + + {isFloatingIp && ( + + )} +
+ + {address.subnet?.gatewayIP && ( + + )} + {children} +
+ ); +}; + +export default NetworkItem; diff --git a/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/NetworkSelector.component.tsx b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/NetworkSelector.component.tsx new file mode 100644 index 000000000000..92a305bb4515 --- /dev/null +++ b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/NetworkSelector.component.tsx @@ -0,0 +1,53 @@ +import { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + ICON_NAME, + Message, + MESSAGE_COLOR, + MessageBody, + MessageIcon, + Select, + SelectContent, + SelectControl, +} from '@ovhcloud/ods-react'; +import { NAMESPACES } from '@ovh-ux/manager-common-translations'; +import { TUnattachedResource } from '../view-models/selectUnattachedResource'; + +type TNetworkSelectorProps = { + networks: TUnattachedResource[]; + onValueChange: (id: string) => void; +}; + +const NetworkSelector: FC = ({ + networks, + onValueChange, +}) => { + const { t } = useTranslation([NAMESPACES.ACTIONS, 'actions']); + + return ( +
+ {networks.length ? ( + + ) : ( + + + + {t('pci_instances_actions_instance_network_network_empty_message')} + + + )} +
+ ); +}; + +export default NetworkSelector; diff --git a/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/ReverseDNS.component.tsx b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/ReverseDNS.component.tsx new file mode 100644 index 000000000000..f9b82e236f0e --- /dev/null +++ b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/ReverseDNS.component.tsx @@ -0,0 +1,24 @@ +import { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Text } from '@ovhcloud/ods-react'; +import { LoadingCell } from '@/pages/instances/datagrid/components/cell/LoadingCell.component'; +import { useReverseDns } from '@/data/hooks/network/useNetwork'; + +const ReverseDNS: FC<{ ip: string }> = ({ ip }) => { + const { t } = useTranslation('dashboard'); + const { data = [], isPending } = useReverseDns(ip); + + if (isPending || data.length > 0) + return ( + + {t('pci_instances_dashboard_network_dns')} + {data.map((dns) => ( + {dns} + ))} + + ); + + return null; +}; + +export default ReverseDNS; diff --git a/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/VolumeSelector.component.tsx b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/VolumeSelector.component.tsx new file mode 100644 index 000000000000..61997de89b0c --- /dev/null +++ b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/components/VolumeSelector.component.tsx @@ -0,0 +1,72 @@ +import { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + FormField, + FormFieldLabel, + Icon, + Link, + Message, + MessageBody, + MessageIcon, + Select, + SelectContent, + SelectControl, + Text, +} from '@ovhcloud/ods-react'; +import { useProjectUrl } from '@ovh-ux/manager-react-components'; +import { TUnattachedResource } from '../view-models/selectUnattachedResource'; + +type TVolumeSelectorProps = { + volumes: TUnattachedResource[]; + onValueChange: (id: string) => void; +}; + +const VolumeSelector: FC = ({ + volumes, + onValueChange, +}) => { + const { t } = useTranslation('actions'); + const projectUrl = useProjectUrl('public-cloud'); + const createVolumeUrl = `${projectUrl}/storages/blocks`; + + return ( +
+ {volumes.length ? ( + <> + + + {t('pci_instances_actions_instance_volume_attach_selector_label')} + + + + + {t('pci_instances_actions_instance_volume_attach_information')} + + {t('pci_instances_actions_instance_volume_attach_create_link')} + + + + + ) : ( + + + + {t('pci_instances_actions_instance_volume_attach_empty_message')} + + + )} +
+ ); +}; + +export default VolumeSelector; diff --git a/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/hooks/useDashboard.ts b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/hooks/useDashboard.ts new file mode 100644 index 000000000000..846d794643d5 --- /dev/null +++ b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/hooks/useDashboard.ts @@ -0,0 +1,31 @@ +import { useMemo } from 'react'; +import { useProjectUrl } from '@ovh-ux/manager-react-components'; +import { useInstance } from '@/data/hooks/instance/useInstance'; +import { selectInstanceDashboard } from '../view-models/selectInstanceDashboard'; + +type TUseDashboardArgs = { + region: string | null; + instanceId: string; +}; + +export const useDashboard = ({ region, instanceId }: TUseDashboardArgs) => { + const projectUrl = useProjectUrl('public-cloud'); + const { data: instance, isPending, error, pendingTasks } = useInstance({ + region, + instanceId, + params: ['withBackups', 'withImage', 'withNetworks', 'withVolumes'], + queryOptions: { + gcTime: 0, + }, + }); + + return useMemo( + () => ({ + instance: selectInstanceDashboard(projectUrl, instance), + pendingTasks, + isPending, + error, + }), + [instance, isPending, projectUrl, error, pendingTasks], + ); +}; diff --git a/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/hooks/useDashboardAction.ts b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/hooks/useDashboardAction.ts new file mode 100644 index 000000000000..e50392e5edd6 --- /dev/null +++ b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/hooks/useDashboardAction.ts @@ -0,0 +1,68 @@ +import { useMemo } from 'react'; +import { useNetworks } from '@/data/hooks/network/useNetwork'; +import { useDashboard } from './useDashboard'; +import { + selectUnattachedPrivateNetworks, + selectUnattachedVolumes, +} from '../view-models/selectUnattachedResource'; +import { useVolumes } from '@/data/hooks/volume/useVolume'; + +type TUseUnattachedResource = { + projectId: string; + region: string; + instanceId: string; +}; + +export const useUnattachedPrivateNetworks = ({ + projectId, + region, + instanceId, +}: TUseUnattachedResource) => { + const { instance, isPending: isInstancePending } = useDashboard({ + region, + instanceId, + }); + const { data: networks, isPending: isNetworkPending } = useNetworks( + projectId, + region, + ); + + return useMemo( + () => ({ + instance: instance?.name ?? '', + networks: + instance?.addresses && networks + ? selectUnattachedPrivateNetworks(networks, instance.addresses) + : [], + isPending: isInstancePending || isNetworkPending, + }), + [instance, networks, isInstancePending, isNetworkPending], + ); +}; + +export const useUnattachedVolumes = ({ + projectId, + region, + instanceId, +}: TUseUnattachedResource) => { + const { instance, isPending: isInstancePending } = useDashboard({ + region, + instanceId, + }); + const { data: volumes, isPending: isVolumePending } = useVolumes( + projectId, + region, + ); + + return useMemo( + () => ({ + instance: instance?.name ?? '', + volumes: + instance?.volumes && volumes + ? selectUnattachedVolumes(volumes, instance.volumes) + : [], + isPending: isInstancePending || isVolumePending, + }), + [instance, volumes, isInstancePending, isVolumePending], + ); +}; diff --git a/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/hooks/useDashboardPolling.ts b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/hooks/useDashboardPolling.ts new file mode 100644 index 000000000000..1a80bfc6b3c4 --- /dev/null +++ b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/hooks/useDashboardPolling.ts @@ -0,0 +1,107 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { useCallback } from 'react'; +import { useNotifications } from '@ovh-ux/manager-react-components'; +import { ApiError } from '@ovh-ux/manager-core-api'; +import { useTranslation } from 'react-i18next'; +import { TInstance, TPartialInstance } from '@/types/instance/entity.type'; +import { useProjectId } from '@/hooks/project/useProjectId'; +import { + shouldRetryAfter404Error, + TPendingTask, + useInstancesPolling, +} from '@/data/hooks/instance/polling/useInstancesPolling'; +import { updateAllInstancesFromCache } from '@/adapters/tanstack-query/store/instances/updaters'; + +type TDashboardPollingArgs = { + instanceId: string; + region: string; + pendingTasks: TPendingTask[]; +}; + +export const useDashboardPolling = ({ + instanceId, + region, + pendingTasks, +}: TDashboardPollingArgs) => { + const queryClient = useQueryClient(); + const projectId = useProjectId(); + const { clearNotifications, addSuccess } = useNotifications(); + const { t } = useTranslation('actions'); + + const handlePollingSuccess = useCallback( + (instance?: TInstance) => { + if (!instance) return; + const { task, actions, status, pricings } = instance; + const isDeleted = !task.isPending && status === 'DELETED'; + const deletedInstance = { addresses: new Map(), volumes: [] }; + const newInstance: TPartialInstance = { + id: instanceId, + actions, + status, + task, + pricings, + }; + + updateAllInstancesFromCache(queryClient, { + projectId, + region, + instance: isDeleted + ? { ...newInstance, ...deletedInstance } + : newInstance, + }); + + if (!task.isPending) { + clearNotifications(); + addSuccess( + t(`pci_instances_actions_instance_success_message`, { + name: instance.name, + }), + true, + ); + } + }, + [ + queryClient, + projectId, + instanceId, + clearNotifications, + addSuccess, + t, + region, + ], + ); + + const handlePollingError = useCallback( + (error: ApiError) => { + if (error.response?.status === 404) { + updateAllInstancesFromCache(queryClient, { + projectId, + region, + instance: { + id: instanceId, + actions: [], + status: 'DELETED', + task: { + isPending: false, + status: null, + }, + addresses: new Map(), + volumes: [], + }, + }); + } + }, + [projectId, queryClient, instanceId, region], + ); + + const pollingData = useInstancesPolling( + pendingTasks, + { + onSuccess: handlePollingSuccess, + onError: handlePollingError, + }, + { retry: shouldRetryAfter404Error }, + ); + + return pollingData; +}; diff --git a/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/view-models/selectInstanceDashboard.ts b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/view-models/selectInstanceDashboard.ts new file mode 100644 index 000000000000..4341208de587 --- /dev/null +++ b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/view-models/selectInstanceDashboard.ts @@ -0,0 +1,213 @@ +import { getInstanceStatus } from '@/pages/instances/mapper/status.mapper'; +import { TActionName } from '@/types/instance/common.type'; +import { + TInstance, + TInstanceAction, + TInstanceAddresses, + TInstanceFlavor, + TInstancePrice, + TInstanceRegion, + TInstanceStatus, + TInstanceStatusSeverity, + TInstanceTaskStatus, + TInstanceVolume, +} from '@/types/instance/entity.type'; + +type TPrice = { + label: string; + type: string; + value: number; +}; + +type TFlavor = { + name: string; + ram: string; + cpu: string; + storage: string; + publicBandwidth: string; +}; + +export type TAction = { + label: string; + link: { + path: string; + isExternal: boolean; + isTargetBlank?: boolean; + }; +}; + +type TInstanceActions = Map; + +export type TInstanceDashboardViewModel = { + id: string; + name: string; + flavor: TFlavor | null; + region: TInstanceRegion; + addresses: TInstanceAddresses; + pricings: TPrice[]; + status: { + label: TInstanceStatus; + severity: TInstanceStatusSeverity; + }; + task: TInstanceTaskStatus; + image: string; + volumes: TInstanceVolume[]; + sshKey: string | null; + login: string | null; + actions: TInstanceActions; + canActivateMonthlyBilling: boolean; + isDeleteEnabled: boolean; + isEditEnabled: boolean; +} | null; + +const isEditionEnabled = (actions: TInstanceAction[]) => + actions.some(({ name }) => name === 'edit'); + +const mapFlavor = ({ name, specs }: TInstanceFlavor) => ({ + name, + ram: specs ? `${specs.ram.value} ${specs.ram.unit}` : '-', + cpu: specs ? `${specs.cpu.value} ${specs.cpu.unit}` : '-', + storage: specs ? `${specs.storage.value} ${specs.storage.unit}` : '-', + publicBandwidth: specs + ? `${specs.bandwidth.public.value} ${specs.bandwidth.public.unit}` + : '-', +}); + +const mapPricings = (pricings: TInstancePrice[]) => + pricings + .filter((pricing) => pricing.status === 'enabled') + .map((pricing) => ({ + label: pricing.type !== 'licence' ? 'instance' : 'licence', // label will be a translation key + type: pricing.type, + value: pricing.priceInUcents, + })); + +const canActivateMonthlyBilling = (actions: TInstanceAction[]) => + actions.some(({ name }) => name === 'activate_monthly_billing'); + +const canDeleteInstance = (actions: TInstanceAction[]) => + actions.some(({ name }) => name === 'delete'); + +// TODO: find a way to handle this properly (where to build path and translated label) +const getActionHrefByName = ( + projectUrl: string, + name: TActionName, + { region: { name: region }, id }: Pick, +): TAction['link'] => { + if (name === 'edit') { + return { + path: `../${id}/edit`, + isExternal: false, + }; + } + + if (name === 'create_autobackup') { + return { path: `${projectUrl}/workflow/new`, isExternal: true }; + } + + if (name === 'assign_floating_ip') { + const searchParams = new URLSearchParams({ + ipType: 'floating_ip', + region, + instance: id, + }); + + return { + path: `${projectUrl}/public-ips/order?${searchParams.toString()}`, + isExternal: true, + }; + } + + if (name === 'soft_reboot') { + return { + path: `../${id}/soft-reboot?region=${region}`, + isExternal: false, + }; + } + + if (name === 'hard_reboot') { + return { + path: `../${id}/hard-reboot?region=${region}`, + isExternal: false, + }; + } + + if (name === 'rescue') { + return { + path: `../${id}/rescue/start?region=${region}`, + isExternal: false, + }; + } + + if (name === 'unrescue') { + return { + path: `../${id}/rescue/end?region=${region}`, + isExternal: false, + }; + } + + if (name === 'create_backup') { + return { + path: `../${id}/backup?region=${region}`, + isExternal: false, + }; + } + + const actions = new Set(['stop', 'start', 'shelve', 'unshelve', 'reinstall']); + + if (actions.has(name)) { + return { + path: `../${id}/${name}?region=${region}`, + isExternal: false, + }; + } + + return { path: '', isExternal: false }; +}; + +const mapActions = ( + instance: TInstance, + projectUrl: string, +): TInstanceActions => + instance.actions + .filter( + ({ name }) => + !['details', 'delete', 'activate_monthly_billing'].includes(name), + ) + .reduce((acc, action) => { + const { group, name } = action; + const newAction = { + label: `pci_instances_list_action_${name}`, + link: getActionHrefByName(projectUrl, name, instance), + }; + const foundAction = acc.get(group); + if (!foundAction) return acc.set(group, [newAction]); + foundAction.push(newAction); + return acc; + }, new Map() as TInstanceActions); + +export const selectInstanceDashboard = ( + projectUrl: string, + instance?: TInstance, +): TInstanceDashboardViewModel => { + if (!instance) return null; + + return { + id: instance.id, + name: instance.name, + flavor: instance.flavor ? mapFlavor(instance.flavor) : null, + region: instance.region, + addresses: instance.addresses, + pricings: mapPricings(instance.pricings || []), + task: instance.task, + status: getInstanceStatus(instance.status), + image: instance.image?.name ?? '', + volumes: instance.volumes ?? [], + sshKey: instance.sshKey, + login: instance.login, + actions: mapActions(instance, projectUrl), + canActivateMonthlyBilling: canActivateMonthlyBilling(instance.actions), + isDeleteEnabled: canDeleteInstance(instance.actions), + isEditEnabled: isEditionEnabled(instance.actions), + }; +}; diff --git a/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/view-models/selectUnattachedResource.ts b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/view-models/selectUnattachedResource.ts new file mode 100644 index 000000000000..08121cfa03b5 --- /dev/null +++ b/packages/manager/apps/pci-instances/src/pages/instances/instance/dashboard/view-models/selectUnattachedResource.ts @@ -0,0 +1,39 @@ +import { TNetwork } from '@/types/network/entity.type'; +import { + TInstanceAddresses, + TInstanceVolume, +} from '@/types/instance/entity.type'; +import { TVolume } from '@/types/volume/common.type'; + +export type TUnattachedResource = { + label: string; + value: string; +}; + +export const selectUnattachedPrivateNetworks = ( + networks: TNetwork[], + addresses: TInstanceAddresses, +): TUnattachedResource[] => + networks + .filter( + ({ id, visibility }) => + visibility === 'private' && + !addresses + .get('private') + ?.find((address) => address.subnet?.network.id === id), + ) + .map((network) => ({ label: network.name, value: network.id })); + +export const selectUnattachedVolumes = ( + volumes: TVolume[], + instanceVolumes: TInstanceVolume[], +): TUnattachedResource[] => + volumes + .filter( + ({ id }) => + !instanceVolumes.some((instanceVolume) => id === instanceVolume.id), + ) + .map(({ id, name }) => ({ + label: name, + value: id, + })); diff --git a/packages/manager/apps/pci-instances/src/pages/instances/mapper/mapper.spec.tsx b/packages/manager/apps/pci-instances/src/pages/instances/mapper/mapper.spec.tsx index cd24d1fcbe61..17fb187220a4 100644 --- a/packages/manager/apps/pci-instances/src/pages/instances/mapper/mapper.spec.tsx +++ b/packages/manager/apps/pci-instances/src/pages/instances/mapper/mapper.spec.tsx @@ -1,6 +1,8 @@ -import { describe, expect, test } from 'vitest'; +import { describe, expect, test, it } from 'vitest'; import { mapAddressesToListItems } from '.'; import { TAggregatedInstanceAddress } from '@/types/instance/entity.type'; +import { getInstanceStatus, severityToStatus } from './status.mapper'; +import { TStatus } from '@/types/instance/common.type'; describe('Mapper functions', () => { describe('Considering the mapAddressesToListItems function', () => { @@ -22,4 +24,30 @@ describe('Mapper functions', () => { ]); }); }); + + describe('Considering the getInstanceStatus function', () => { + it.each( + Object.entries(severityToStatus).flatMap(([key, values]) => + values.map((value) => [value, key]), + ), + )( + 'should return severity "%s" for status "%s', + (status, expectedSeverity) => { + const result = getInstanceStatus(status as TStatus); + expect(result).toEqual({ + label: status, + severity: expectedSeverity, + }); + }, + ); + + it('should return severity "info" for unknown status', () => { + const unknownStatus = 'NOT_A_REAL_STATUS' as TStatus; + const result = getInstanceStatus(unknownStatus); + expect(result).toEqual({ + label: unknownStatus, + severity: 'info', + }); + }); + }); }); diff --git a/packages/manager/apps/pci-instances/src/pages/instances/mapper/status.mapper.ts b/packages/manager/apps/pci-instances/src/pages/instances/mapper/status.mapper.ts new file mode 100644 index 000000000000..2b6f500674bf --- /dev/null +++ b/packages/manager/apps/pci-instances/src/pages/instances/mapper/status.mapper.ts @@ -0,0 +1,48 @@ +import { + TSeverity, + TStatus, + TStatusSeverity, +} from '@/types/instance/common.type'; + +export const severityToStatus: Record = { + warning: [ + 'BUILDING', + 'REBOOT', + 'REBUILD', + 'REVERT_RESIZE', + 'SOFT_DELETED', + 'VERIFY_RESIZE', + 'MIGRATING', + 'RESIZE', + 'BUILD', + 'SHUTOFF', + 'RESCUE', + 'SHELVED', + 'SHELVED_OFFLOADED', + 'RESCUING', + 'UNRESCUING', + 'SNAPSHOTTING', + 'RESUMING', + 'HARD_REBOOT', + 'PASSWORD', + 'PAUSED', + ], + error: ['DELETED', 'ERROR', 'STOPPED', 'SUSPENDED', 'UNKNOWN'], + success: ['ACTIVE', 'RESCUED', 'RESIZED'], + info: [], +}; + +const getInstanceStatusSeverity = (status: TStatus): TSeverity => { + const severities = Object.keys(severityToStatus) as TSeverity[]; + + return ( + severities.find((severity) => + severityToStatus[severity].some((s) => s === status), + ) ?? 'info' + ); +}; + +export const getInstanceStatus = (status: TStatus): TStatusSeverity => ({ + label: status, + severity: getInstanceStatusSeverity(status), +}); diff --git a/packages/manager/apps/pci-instances/src/pages/instances/onboarding/Onboarding.page.tsx b/packages/manager/apps/pci-instances/src/pages/instances/onboarding/Onboarding.page.tsx index 3b67e5c1916c..839808156b97 100644 --- a/packages/manager/apps/pci-instances/src/pages/instances/onboarding/Onboarding.page.tsx +++ b/packages/manager/apps/pci-instances/src/pages/instances/onboarding/Onboarding.page.tsx @@ -7,6 +7,7 @@ import { } from '@ovh-ux/manager-react-components'; import { Navigate, useHref, useRouteLoaderData } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; +import { NAMESPACES } from '@ovh-ux/manager-common-translations'; import { OsdsText } from '@ovhcloud/ods-components/react'; import { ODS_THEME_COLOR_INTENT, @@ -23,7 +24,7 @@ import { useInstances } from '@/data/hooks/instance/useInstances'; import { Spinner } from '@/components/spinner/Spinner.component'; const Onboarding: FC = () => { - const { t } = useTranslation(['onboarding', 'common']); + const { t } = useTranslation(['onboarding', 'common', NAMESPACES.ONBOARDING]); const context = useContext(ShellContext); const { ovhSubsidiary } = context.environment.getUser() as { ovhSubsidiary: OvhSubsidiary; @@ -104,7 +105,7 @@ const Onboarding: FC = () => { }, ), - category: t('pci_instances_onboarding_guide_title'), + category: t(`${NAMESPACES.ONBOARDING}:tutorial`), }} /> ))} diff --git a/packages/manager/apps/pci-instances/src/pages/instances/datagrid/components/cell/StatusCell.component.tsx b/packages/manager/apps/pci-instances/src/pages/instances/task/TaskStatus.component.tsx similarity index 77% rename from packages/manager/apps/pci-instances/src/pages/instances/datagrid/components/cell/StatusCell.component.tsx rename to packages/manager/apps/pci-instances/src/pages/instances/task/TaskStatus.component.tsx index 5af14c01d42e..2ef4f6200616 100644 --- a/packages/manager/apps/pci-instances/src/pages/instances/datagrid/components/cell/StatusCell.component.tsx +++ b/packages/manager/apps/pci-instances/src/pages/instances/task/TaskStatus.component.tsx @@ -6,29 +6,31 @@ import { LoadingCell } from '@/pages/instances/datagrid/components/cell/LoadingC import StatusChip, { TStatusChipStatus, } from '@/components/statusChip/StatusChip.component'; -import { TAggregatedInstance } from '@/types/instance/entity.type'; +import { TStatusSeverity } from '@/types/instance/common.type'; type TStatusCellProps = { - instance: TAggregatedInstance; + status: TStatusSeverity; + taskState: string | null; isLoading: boolean; isPolling: boolean; }; -export const StatusCell: FC = ({ +export const TaskStatus: FC = ({ isLoading, - instance, + status, + taskState, isPolling, }) => { const { t, i18n } = useTranslation('status'); - const statusLabel = instance.status.label.toLowerCase(); + const statusLabel = status.label.toLowerCase(); const pollingTaskLabel = - instance.taskState ?? t('common:pci_instances_common_pending'); + taskState ?? t('common:pci_instances_common_pending'); - const status: TStatusChipStatus = isPolling + const chipStatus: TStatusChipStatus = isPolling ? { label: pollingTaskLabel, severity: 'default' } : { - ...instance.status, + ...status, label: t(`pci_instances_status_${statusLabel}`), }; @@ -46,7 +48,7 @@ export const StatusCell: FC = ({ return ( { test('Should render component correctly', () => { render( - , diff --git a/packages/manager/apps/pci-instances/src/routes/Router.tsx b/packages/manager/apps/pci-instances/src/routes/Router.tsx index 3aa80335f7d7..958f09f06320 100644 --- a/packages/manager/apps/pci-instances/src/routes/Router.tsx +++ b/packages/manager/apps/pci-instances/src/routes/Router.tsx @@ -1,9 +1,14 @@ -import { FC } from 'react'; +import { FC, useContext, useMemo } from 'react'; import { createHashRouter, RouterProvider } from 'react-router-dom'; -import routes from '@/routes/routes'; +import { ShellContext } from '@ovh-ux/manager-react-shell-client'; +import { getRoutes } from '@/routes/routes'; -const router = createHashRouter(routes); +const Router: FC = () => { + const shell = useContext(ShellContext); -const Router: FC = () => ; + const router = useMemo(() => createHashRouter(getRoutes(shell)), [shell]); + + return ; +}; export default Router; diff --git a/packages/manager/apps/pci-instances/src/routes/loaders/instanceAction/instanceActionLegacy.loader.ts b/packages/manager/apps/pci-instances/src/routes/loaders/instanceAction/instanceActionLegacy.loader.ts deleted file mode 100644 index 17e759d87703..000000000000 --- a/packages/manager/apps/pci-instances/src/routes/loaders/instanceAction/instanceActionLegacy.loader.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { LoaderFunction, redirect } from 'react-router-dom'; -import { TSectionType } from '@/types/instance/action/action.type'; -import { actionSectionRegex } from '@/constants'; -import { getPathMatch } from '@/utils'; - -export const instanceActionLegacyLoader: LoaderFunction = ({ request }) => { - const url = new URL(request.url); - const { pathname, searchParams } = url; - - const instanceId = searchParams.get('instanceId'); - const region = searchParams.get('region') ?? null; - - const section = getPathMatch(pathname, actionSectionRegex); - - const canRedirect = !!section && !!instanceId; - - if (canRedirect) { - return redirect(`../region/${region}/instance/${instanceId}/${section}`); - } - - return { notFoundAction: true }; -}; diff --git a/packages/manager/apps/pci-instances/src/routes/loaders/instanceLegacy.loader.ts b/packages/manager/apps/pci-instances/src/routes/loaders/instanceLegacy.loader.ts new file mode 100644 index 000000000000..2e1d77ebe7a4 --- /dev/null +++ b/packages/manager/apps/pci-instances/src/routes/loaders/instanceLegacy.loader.ts @@ -0,0 +1,102 @@ +import { + generatePath, + LoaderFunction, + matchPath, + PathMatch, + redirect, +} from 'react-router-dom'; +import { validate as uuidValidate } from 'uuid'; + +type RedirectPattern = { + expectedPath: string; + redirectPath: string; + queryParams?: string[]; +}; + +const REDIRECT_ROUTES: RedirectPattern[] = [ + { + expectedPath: '/pci/projects/:projectId/instances/:instanceId', + redirectPath: + '/pci/projects/:projectId/instances/region/:region/instance/:instanceId', + }, + { + expectedPath: + '/pci/projects/:projectId/instances/:instanceId/billing/monthly/activate', + redirectPath: + '/pci/projects/:projectId/instances/region/:region/instance/:instanceId/billing/monthly/activate', + }, + { + expectedPath: '/pci/projects/:projectId/instances/:instanceId/rescue/start', + redirectPath: + '/pci/projects/:projectId/instances/region/:region/instance/:instanceId/rescue/start', + }, + { + expectedPath: '/pci/projects/:projectId/instances/:instanceId/rescue/end', + redirectPath: + '/pci/projects/:projectId/instances/region/:region/instance/:instanceId/rescue/end', + }, + { + expectedPath: '/pci/projects/:projectId/instances/:instanceId/:action', + redirectPath: + '/pci/projects/:projectId/instances/region/:region/instance/:instanceId/:action', + }, + // use this regex to migrate listing actions if necessary + // {expectedPath: "/pci/projects/:projectId/instances/:action", redirectPath: "/pci/projects/:projectId/instances/region/:region/:action", queryParams: ['instanceId']}, + // We sort by size to get the more precise first +].sort((a, b) => b.expectedPath.length - a.expectedPath.length); + +const PARAMS_REGEX = /\/:([\w-]+)(\?)?/g; + +const validateInstanceId = (match: PathMatch) => { + if ('instanceId' in match.params) { + return ( + typeof match.params.instanceId === 'string' && + uuidValidate(match.params.instanceId) + ); + } + + return true; +}; + +export const instanceLegacyRedirectionLoader: LoaderFunction = ({ + request, +}) => { + const { pathname, searchParams } = new URL(request.url); + + const matchedRoute = REDIRECT_ROUTES.find(({ expectedPath }) => { + const match = matchPath(expectedPath, pathname); + return match !== null && validateInstanceId(match); + }); + + if (matchedRoute) { + const { expectedPath, redirectPath, queryParams } = matchedRoute; + const match = matchPath(expectedPath, pathname); + + const redirectAvailableParams = { + // Set all required parameters to null by default + ...Object.fromEntries( + [...redirectPath.matchAll(PARAMS_REGEX)].map( + ([, paramName]) => [paramName, 'null'] as [string, string], + ), + ), + // If available from search params we use it + ...Object.fromEntries(searchParams.entries()), + // Match params has the bigger priority + ...((match as PathMatch).params as Record), + }; + + const search = new URLSearchParams( + queryParams + ?.map((param) => [param, redirectAvailableParams[param]]) + .filter((param): param is [string, string] => !!param[1]), + ); + const redirectUrl = generatePath( + redirectPath, + redirectAvailableParams, + ).concat(search.size > 0 ? `?${search.toString()}` : ''); + + return redirect(redirectUrl); + } + + return null; +}; diff --git a/packages/manager/apps/pci-instances/src/routes/routes.tsx b/packages/manager/apps/pci-instances/src/routes/routes.tsx index 85b2faf5b2ac..b865f3b090c5 100644 --- a/packages/manager/apps/pci-instances/src/routes/routes.tsx +++ b/packages/manager/apps/pci-instances/src/routes/routes.tsx @@ -1,23 +1,35 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */ import { RouteObject } from 'react-router-dom'; import { getProjectQuery } from '@ovh-ux/manager-pci-common'; +import { ShellContextType } from '@ovh-ux/manager-react-shell-client'; import queryClient from '@/queryClient'; -import { withSuspendedMigrateRoutes } from '@/hooks/migration/useSuspendNonMigratedRoutes'; -import { instanceActionLegacyLoader } from './loaders/instanceAction/instanceActionLegacy.loader'; +import { + suspendNonMigratedRoutesLoader, + withSuspendedMigrateRoutes, +} from '@/hooks/migration/useSuspendNonMigratedRoutes'; +import { instanceLegacyRedirectionLoader } from './loaders/instanceLegacy.loader'; +import { ComponentType } from 'react'; +import { TSectionType } from '@/types/instance/action/action.type'; -const lazyRouteConfig = (importFn: CallableFunction) => ({ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +const lazyRouteConfig = ( + importFn: () => Promise<{ default: ComponentType } & Omit>, + componentParameters: ComponentProps = {} as ComponentProps, +): Pick => ({ lazy: async () => { - const { default: moduleDefault, ...moduleExports } = await importFn(); + const { default: ComponentDefault, ...moduleExports } = await importFn(); - /* eslint-disable @typescript-eslint/no-unsafe-return */ return { - Component: moduleDefault, + element: , ...moduleExports, }; }, }); -function getRouteWithMigrationWrapper(route: R): R { +const getRouteWithMigrationWrapper = (shell: ShellContextType) => < + R extends RouteObject +>( + route: R, +): R => { const newRoute = { ...route, }; @@ -25,35 +37,64 @@ function getRouteWithMigrationWrapper(route: R): R { if (route.Component) { newRoute.Component = withSuspendedMigrateRoutes(route.Component); } + if (route.element) { + const routeElement = route.element; + newRoute.Component = withSuspendedMigrateRoutes(() => routeElement); + delete newRoute.element; + } + + newRoute.loader = suspendNonMigratedRoutesLoader(shell, newRoute.loader); - if (route.lazy) { + const parentLazy = route.lazy; + if (parentLazy) { newRoute.lazy = async () => { - const lazyValues = await (route.lazy as NonNullable)(); + const lazyValues = await parentLazy(); + + if (lazyValues.loader) { + lazyValues.loader = suspendNonMigratedRoutesLoader( + shell, + lazyValues.loader, + ); + } if (lazyValues.Component) { lazyValues.Component = withSuspendedMigrateRoutes(lazyValues.Component); } + if (lazyValues.element) { + const lazyElement = lazyValues.element; + lazyValues.Component = withSuspendedMigrateRoutes(() => lazyElement); + delete lazyValues.element; + } + + if ( + !route.Component && + !route.element && + !lazyValues.Component && + !lazyValues.element + ) { + lazyValues.Component = withSuspendedMigrateRoutes(); + } return lazyValues; }; + } else if (!route.Component && !route.element) { + newRoute.Component = withSuspendedMigrateRoutes(); } if (route.children) { newRoute.children = route.children.map((subRoute) => - getRouteWithMigrationWrapper(subRoute), + getRouteWithMigrationWrapper(shell)(subRoute), ); } return newRoute; -} +}; export const ROOT_PATH = '/pci/projects/:projectId/instances'; -export const REGION_PATH = 'region/:regionId'; +export const REGION_PATH = 'region/:region'; export const INSTANCE_PATH = 'instance/:instanceId'; -export const SECTIONS = { - onboarding: 'onboarding', - new: 'new', - instance: ':instanceId', + +const ACTIONS_SECTIONS = { delete: 'delete', stop: 'stop', start: 'start', @@ -65,41 +106,49 @@ export const SECTIONS = { rescue: 'rescue/start', rescueEnd: 'rescue/end', createBackup: 'backup', - edit: ':instanceId/edit', activateMonthlyBilling: 'billing/monthly/activate', + attachNetwork: 'network/private/attach', + attachVolume: 'attach', +} satisfies Record; + +export const SECTIONS = { + ...ACTIONS_SECTIONS, + onboarding: 'onboarding', + new: 'new', + instanceLegacy: ':instanceId', + instance: `${REGION_PATH}/${INSTANCE_PATH}`, + edit: ':instanceId/edit', }; -const instanceActionsSections = [ - SECTIONS.delete, - SECTIONS.start, - SECTIONS.stop, - SECTIONS.shelve, - SECTIONS.unshelve, - SECTIONS.softReboot, - SECTIONS.hardReboot, - SECTIONS.reinstall, - SECTIONS.rescue, - SECTIONS.rescueEnd, - SECTIONS.createBackup, - SECTIONS.activateMonthlyBilling, -]; +const getActionComponentBySection = (section: string) => { + switch (section) { + case SECTIONS.createBackup: + return import('@/pages/instances/action/BackupAction.page'); + case SECTIONS.activateMonthlyBilling: + return import('@/pages/instances/action/BillingMonthlyAction.page'); + case SECTIONS.rescue: + case SECTIONS.rescueEnd: + return import('@/pages/instances/action/RescueAction.page'); + case SECTIONS.reinstall: + return import('@/pages/instances/action/ReinstallAction.page'); + case SECTIONS.attachNetwork: + return import('@/pages/instances/instance/dashboard/action/AttachNetwork.page'); + case SECTIONS.attachVolume: + return import('@/pages/instances/instance/dashboard/action/AttachVolume.page'); + default: + return import('@/pages/instances/action/BaseAction.page'); + } +}; -const instanceActionLegacyRoutes: RouteObject[] = instanceActionsSections.map( - (section) => ({ - id: section, - path: section, - loader: instanceActionLegacyLoader, - }), -); - -const instanceActionRoutes = instanceActionsSections.map((section) => ({ - path: `${REGION_PATH}/${INSTANCE_PATH}/${section}`, - ...lazyRouteConfig(() => - import('@/pages/instances/action/InstanceAction.page'), - ), +const instanceActionsRoutes: RouteObject[] = Object.values( + ACTIONS_SECTIONS, +).map((section) => ({ + path: section, + ...lazyRouteConfig<{section: TSectionType}>(() => getActionComponentBySection(section), { section }), + loader: instanceLegacyRedirectionLoader, })); -const routes: RouteObject[] = [ +export const getRoutes = (shell: ShellContextType): RouteObject[] => [ { path: '/', ...lazyRouteConfig(() => import('@/components/layout/Layout.component')), @@ -114,7 +163,7 @@ const routes: RouteObject[] = [ { path: '', ...lazyRouteConfig(() => import('@/pages/instances/Instances.page')), - children: [...instanceActionLegacyRoutes, ...instanceActionRoutes], + children: instanceActionsRoutes, }, { path: SECTIONS.onboarding, @@ -128,11 +177,31 @@ const routes: RouteObject[] = [ import('@/pages/instances/create/CreateInstance.page'), ), }, + { + path: SECTIONS.instanceLegacy, + children: [ + { + path: '*', + loader: instanceLegacyRedirectionLoader, + }, + ], + loader: instanceLegacyRedirectionLoader, + ...lazyRouteConfig(() => import('@/pages/404/NotFound.page')), + }, { path: SECTIONS.instance, ...lazyRouteConfig(() => import('@/pages/instances/instance/Instance.page'), ), + children: [ + { + path: '', + ...lazyRouteConfig(() => + import('@/pages/instances/instance/dashboard/Dashboard.page'), + ), + children: instanceActionsRoutes, + }, + ], }, { path: SECTIONS.edit, @@ -144,12 +213,10 @@ const routes: RouteObject[] = [ path: '*', ...lazyRouteConfig(() => import('@/pages/404/NotFound.page')), }, - ].map(getRouteWithMigrationWrapper), + ].map(getRouteWithMigrationWrapper(shell)), }, { path: '*', ...lazyRouteConfig(() => import('@/pages/404/NotFound.page')), }, ]; - -export default routes; diff --git a/packages/manager/apps/pci-instances/src/types/instance/action/action.type.ts b/packages/manager/apps/pci-instances/src/types/instance/action/action.type.ts index 9c325a3bc581..9c5c9a16e706 100644 --- a/packages/manager/apps/pci-instances/src/types/instance/action/action.type.ts +++ b/packages/manager/apps/pci-instances/src/types/instance/action/action.type.ts @@ -10,4 +10,6 @@ export type TSectionType = | 'rescue/start' | 'rescue/end' | 'backup' - | 'billing/monthly/activate'; + | 'billing/monthly/activate' + | 'network/private/attach' + | 'attach'; diff --git a/packages/manager/apps/pci-instances/src/types/instance/api.type.ts b/packages/manager/apps/pci-instances/src/types/instance/api.type.ts index c997eca2cd07..79006ed80b07 100644 --- a/packages/manager/apps/pci-instances/src/types/instance/api.type.ts +++ b/packages/manager/apps/pci-instances/src/types/instance/api.type.ts @@ -53,7 +53,7 @@ export type TAggregatedInstanceDto = { isImageDeprecated: boolean; }; -export type TPartialInstanceDto = Pick & +export type TPartialAggregatedInstanceDto = Pick & Partial; export type TRetrieveInstancesQueryParams = DeepReadonly<{ @@ -104,7 +104,7 @@ export type TInstancePriceDto = { includeVat: boolean; price: { currencyCode: string; - priceInUCents: number; + priceInUcents: number; text: string; value: number; }; diff --git a/packages/manager/apps/pci-instances/src/types/instance/common.type.ts b/packages/manager/apps/pci-instances/src/types/instance/common.type.ts index 0ab5fe9afab9..b51aa22f86b2 100644 --- a/packages/manager/apps/pci-instances/src/types/instance/common.type.ts +++ b/packages/manager/apps/pci-instances/src/types/instance/common.type.ts @@ -63,7 +63,7 @@ export type TPrice = { status: 'enabled' | 'available' | 'eligible'; value: number; text: string; - priceInUCents: number; + priceInUcents: number; currencyCode: string; includeVat: boolean; }; @@ -96,3 +96,10 @@ export type TBackup = { name: string; createdAt: string; }; + +export type TSeverity = 'success' | 'error' | 'warning' | 'info'; + +export type TStatusSeverity = { + label: TStatus; + severity: TSeverity; +}; diff --git a/packages/manager/apps/pci-instances/src/types/instance/entity.type.ts b/packages/manager/apps/pci-instances/src/types/instance/entity.type.ts index 31296eaf5445..38d7caafaf2d 100644 --- a/packages/manager/apps/pci-instances/src/types/instance/entity.type.ts +++ b/packages/manager/apps/pci-instances/src/types/instance/entity.type.ts @@ -10,19 +10,18 @@ import { TFlavorSpec, TImage, TBackup, + TStatusSeverity, + TSeverity, } from './common.type'; export type TInstanceAddressType = TAddressType; -export type TInstanceStatusSeverity = 'success' | 'error' | 'warning' | 'info'; +export type TInstanceStatusSeverity = TSeverity; export type TInstanceStatus = TStatus; export type TInstanceActionName = TActionName; -export type TAggregatedInstanceStatus = { - label: TInstanceStatus; - severity: TInstanceStatusSeverity; -}; +export type TAggregatedInstanceStatus = TStatusSeverity; export type TInstanceTaskStatus = { isPending: boolean; @@ -100,7 +99,7 @@ export type TInstanceAction = { type TInstanceFlavorSpec = TFlavorSpec; -type TInstanceFlavor = { +export type TInstanceFlavor = { id: string; name: string; specs: { @@ -116,10 +115,12 @@ type TInstanceFlavor = { export type TInstancePrice = TPrice; -type TInstanceImageDto = TImage; +export type TInstanceImage = TImage; type TInstanceBackup = TBackup; +export type TInstanceAddresses = Map; + export type TInstance = { id: string; name: string; @@ -127,12 +128,14 @@ export type TInstance = { status: TInstanceStatus; task: TInstanceTaskStatus; actions: TInstanceAction[]; - addresses: Map; + addresses: TInstanceAddresses; volumes: TInstanceVolume[] | null; flavor: TInstanceFlavor | null; pricings: TInstancePrice[] | null; - image: TInstanceImageDto | null; + image: TInstanceImage | null; backups: TInstanceBackup[] | null; sshKey: string | null; login: string | null; }; + +export type TPartialInstance = Pick & Partial; diff --git a/packages/manager/apps/pci-instances/src/types/network/api.type.ts b/packages/manager/apps/pci-instances/src/types/network/api.type.ts new file mode 100644 index 000000000000..1ba33f7acf87 --- /dev/null +++ b/packages/manager/apps/pci-instances/src/types/network/api.type.ts @@ -0,0 +1,11 @@ +import { DeepReadonly } from '../utils.type'; +import { TVisibility } from './common.type'; + +type TNetworkVisibilityDto = TVisibility; + +export type TNetworkDto = DeepReadonly<{ + id: string; + name: string; + region: string; + visibility: TNetworkVisibilityDto; +}>; diff --git a/packages/manager/apps/pci-instances/src/types/network/common.type.ts b/packages/manager/apps/pci-instances/src/types/network/common.type.ts new file mode 100644 index 000000000000..97ec3180c6f5 --- /dev/null +++ b/packages/manager/apps/pci-instances/src/types/network/common.type.ts @@ -0,0 +1 @@ +export type TVisibility = 'private' | 'public'; diff --git a/packages/manager/apps/pci-instances/src/types/network/entity.type.ts b/packages/manager/apps/pci-instances/src/types/network/entity.type.ts new file mode 100644 index 000000000000..10fc64359fa5 --- /dev/null +++ b/packages/manager/apps/pci-instances/src/types/network/entity.type.ts @@ -0,0 +1,11 @@ +import { DeepReadonly } from '../utils.type'; +import { TVisibility } from './common.type'; + +type TNetworkVisibility = TVisibility; + +export type TNetwork = DeepReadonly<{ + id: string; + name: string; + region: string; + visibility: TNetworkVisibility; +}>; diff --git a/packages/manager/apps/pci-instances/src/types/volume/api.type.ts b/packages/manager/apps/pci-instances/src/types/volume/api.type.ts new file mode 100644 index 000000000000..541161608878 --- /dev/null +++ b/packages/manager/apps/pci-instances/src/types/volume/api.type.ts @@ -0,0 +1,3 @@ +import { TVolume } from './common.type'; + +export type TVolumeDto = TVolume; diff --git a/packages/manager/apps/pci-instances/src/types/volume/common.type.ts b/packages/manager/apps/pci-instances/src/types/volume/common.type.ts new file mode 100644 index 000000000000..24a438362da3 --- /dev/null +++ b/packages/manager/apps/pci-instances/src/types/volume/common.type.ts @@ -0,0 +1,5 @@ +export type TVolume = { + id: string; + name: string; + size: number; +}; diff --git a/packages/manager/apps/pci-instances/src/utils/index.ts b/packages/manager/apps/pci-instances/src/utils/index.ts index 02f3997ff864..1ba6a22ffc0b 100644 --- a/packages/manager/apps/pci-instances/src/utils/index.ts +++ b/packages/manager/apps/pci-instances/src/utils/index.ts @@ -49,11 +49,3 @@ export const replaceToSnakeCase = (str: string): string => export const isCustomUrlSection = (str: string): boolean => str.includes('-') || str.includes('/'); - -export const getPathMatch = ( - pathname: string, - regex: RegExp, -): T | null => { - const match = pathname.match(regex); - return match ? (match[0] as T) : null; -}; diff --git a/packages/manager/apps/pci-instances/src/utils/utils.spec.tsx b/packages/manager/apps/pci-instances/src/utils/utils.spec.tsx index d98ce06d5191..7852a55aa8ed 100644 --- a/packages/manager/apps/pci-instances/src/utils/utils.spec.tsx +++ b/packages/manager/apps/pci-instances/src/utils/utils.spec.tsx @@ -3,7 +3,6 @@ import { describe, expect, test } from 'vitest'; import { ErrorBannerProps } from '@ovh-ux/manager-react-components'; import { ApiError } from '@ovh-ux/manager-core-api'; import { - getPathMatch, instancesQueryKey, isCustomUrlSection, mapUnknownErrorToBannerError, @@ -169,26 +168,4 @@ describe('Utility functions', () => { }); }); }); - - describe('Considering the getPathMatch function', () => { - type Data = { - pathname: string; - regex: RegExp; - expectedResult: string | null; - }; - - test.each` - pathname | regex | expectedResult - ${'/foo/bar'} | ${/foo/} | ${'foo'} - ${'/foo/bar'} | ${/bar/} | ${'bar'} - ${'/foo/bar'} | ${/baz/} | ${null} - ${'/foo/bar/baz'} | ${/foo\/bar/} | ${'foo/bar'} - ${'/foo/bar/baz'} | ${/bar\/baz/} | ${'bar/baz'} - `( - `Given a pathname '$pathname' and a regex '$regex', then expect result to be '$expectedResult'`, - ({ pathname, regex, expectedResult }: Data) => { - expect(getPathMatch(pathname, regex)).toBe(expectedResult); - }, - ); - }); }); diff --git a/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_de_DE.json b/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_de_DE.json index ef107d36ab3a..ec5f325d4d81 100644 --- a/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_de_DE.json +++ b/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_de_DE.json @@ -28,5 +28,11 @@ "dashboard_graph_y_axis_label_vcpu": "Anzahl der vCPU(s)", "dashboard_graph_x_axis_label": "Tag", "dashboard_graph_included": "In den Savings Plans inklusive", - "dashboard_graph_excluded": "Von den Savings Plans ausgeschlossen" + "dashboard_graph_excluded": "Von den Savings Plans ausgeschlossen", + "dashboard_resource_list_title": "Verwendete Ressourcen", + "dashboard_resource_list_search": "ID suchen", + "dashboard_resource_list_no_results": "Keine Ergebnisse für „{{searchQuery}}“ gefunden", + "dashboard_resource_list_no_resources": "Keine Ressource für diese Geschmacksrichtung verwendet", + "dashboard_resource_list_details": "Details", + "dashboard_resource_list_view_resources": "Ressourcen anzeigen" } diff --git a/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_en_GB.json b/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_en_GB.json index 20ed265075f7..1873b4da6ddd 100644 --- a/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_en_GB.json +++ b/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_en_GB.json @@ -28,5 +28,11 @@ "dashboard_graph_y_axis_label_vcpu": "Number of vCPUs", "dashboard_graph_x_axis_label": "Day", "dashboard_graph_included": "Included in Savings Plans", - "dashboard_graph_excluded": "Excluded from Savings Plans" + "dashboard_graph_excluded": "Excluded from Savings Plans", + "dashboard_resource_list_title": "Resources used", + "dashboard_resource_list_search": "Search for ID", + "dashboard_resource_list_no_results": "No results found for \"{{searchQuery}}\"", + "dashboard_resource_list_no_resources": "No resources used for this flavor", + "dashboard_resource_list_details": "Details", + "dashboard_resource_list_view_resources": "View resources" } diff --git a/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_es_ES.json b/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_es_ES.json index 05141c1759f6..41b5951ce749 100644 --- a/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_es_ES.json +++ b/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_es_ES.json @@ -28,5 +28,11 @@ "dashboard_graph_y_axis_label_vcpu": "Número de vCPU", "dashboard_graph_x_axis_label": "Día", "dashboard_graph_included": "Incluido en los Savings Plans", - "dashboard_graph_excluded": "Excluido de los Savings Plans" + "dashboard_graph_excluded": "Excluido de los Savings Plans", + "dashboard_resource_list_title": "Recursos utilizados", + "dashboard_resource_list_search": "Buscar un ID", + "dashboard_resource_list_no_results": "No se han encontrado resultados para \"{{searchQuery}}\"", + "dashboard_resource_list_no_resources": "No hay recursos utilizados para este flavor", + "dashboard_resource_list_details": "Detalles", + "dashboard_resource_list_view_resources": "Ver los recursos" } diff --git a/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_fr_CA.json b/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_fr_CA.json index dd377a017f9c..b1df76ed91e2 100644 --- a/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_fr_CA.json +++ b/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_fr_CA.json @@ -37,5 +37,11 @@ "dashboard_graph_y_axis_label_vcpu": "Nombre de vCPU(s)", "dashboard_graph_x_axis_label": "Jour", "dashboard_graph_included": "Inclus dans les Savings Plans", - "dashboard_graph_excluded": "Exclus des Savings Plans" + "dashboard_graph_excluded": "Exclus des Savings Plans", + "dashboard_resource_list_title": "Ressources utilisées", + "dashboard_resource_list_search": "Rechercher un ID", + "dashboard_resource_list_no_results": "Aucun résultat trouvé pour \"{{searchQuery}}\"", + "dashboard_resource_list_no_resources": "Aucune ressource utilisée pour cette flavor", + "dashboard_resource_list_details": "Détails", + "dashboard_resource_list_view_resources": "Voir les ressources" } diff --git a/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_fr_FR.json b/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_fr_FR.json index dd377a017f9c..b1df76ed91e2 100644 --- a/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_fr_FR.json +++ b/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_fr_FR.json @@ -37,5 +37,11 @@ "dashboard_graph_y_axis_label_vcpu": "Nombre de vCPU(s)", "dashboard_graph_x_axis_label": "Jour", "dashboard_graph_included": "Inclus dans les Savings Plans", - "dashboard_graph_excluded": "Exclus des Savings Plans" + "dashboard_graph_excluded": "Exclus des Savings Plans", + "dashboard_resource_list_title": "Ressources utilisées", + "dashboard_resource_list_search": "Rechercher un ID", + "dashboard_resource_list_no_results": "Aucun résultat trouvé pour \"{{searchQuery}}\"", + "dashboard_resource_list_no_resources": "Aucune ressource utilisée pour cette flavor", + "dashboard_resource_list_details": "Détails", + "dashboard_resource_list_view_resources": "Voir les ressources" } diff --git a/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_it_IT.json b/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_it_IT.json index bf4a1a509bb8..c42e46933b75 100644 --- a/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_it_IT.json +++ b/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_it_IT.json @@ -28,5 +28,11 @@ "dashboard_graph_y_axis_label_vcpu": "Numero di vCPU", "dashboard_graph_x_axis_label": "Giorno", "dashboard_graph_included": "Incluso nei Savings Plan", - "dashboard_graph_excluded": "Escluso dai Savings Plan" + "dashboard_graph_excluded": "Escluso dai Savings Plan", + "dashboard_resource_list_title": "Risorse utilizzate", + "dashboard_resource_list_search": "Cerca un ID", + "dashboard_resource_list_no_results": "Nessun risultato trovato per \"{{searchQuery}}\"", + "dashboard_resource_list_no_resources": "Nessuna risorsa utilizzata per questo flavor", + "dashboard_resource_list_details": "Dettagli", + "dashboard_resource_list_view_resources": "Visualizza le risorse" } diff --git a/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_pl_PL.json b/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_pl_PL.json index ac3a8956d54f..4725df5b5881 100644 --- a/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_pl_PL.json +++ b/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_pl_PL.json @@ -28,5 +28,11 @@ "dashboard_graph_y_axis_label_vcpu": "Liczba vCPU", "dashboard_graph_x_axis_label": "Dzień", "dashboard_graph_included": "Objęte Savings Plans", - "dashboard_graph_excluded": "Wyłączone z Savings Plans" + "dashboard_graph_excluded": "Wyłączone z Savings Plans", + "dashboard_resource_list_title": "Wykorzystane zasoby", + "dashboard_resource_list_search": "Wyszukaj ID", + "dashboard_resource_list_no_results": "Brak wyników dla \"{{searchQuery}}\"", + "dashboard_resource_list_no_resources": "Brak zasobów używanych dla tego flavor", + "dashboard_resource_list_details": "Dane", + "dashboard_resource_list_view_resources": "Wyświetl zasoby" } diff --git a/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_pt_PT.json b/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_pt_PT.json index 0fc046f856fa..ea24c8c2bcf4 100644 --- a/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_pt_PT.json +++ b/packages/manager/apps/pci-savings-plan/public/translations/dashboard/Messages_pt_PT.json @@ -28,5 +28,11 @@ "dashboard_graph_y_axis_label_vcpu": "Número de vCPU(s)", "dashboard_graph_x_axis_label": "Dia", "dashboard_graph_included": "Incluído nos Savings Plans", - "dashboard_graph_excluded": "Excluído dos Savings Plans" + "dashboard_graph_excluded": "Excluído dos Savings Plans", + "dashboard_resource_list_title": "Recursos utilizados", + "dashboard_resource_list_search": "Procurar um ID", + "dashboard_resource_list_no_results": "Não foram encontrados resultados para \"{{searchQuery}}\"", + "dashboard_resource_list_no_resources": "Nenhum recurso utilizado para este \"flavor\"", + "dashboard_resource_list_details": "Detalhes", + "dashboard_resource_list_view_resources": "Ver os recursos" } diff --git a/packages/manager/apps/pci-savings-plan/src/components/Dashboard/ConsumptionDatagrid/ConsumptionDatagrid.tsx b/packages/manager/apps/pci-savings-plan/src/components/Dashboard/ConsumptionDatagrid/ConsumptionDatagrid.tsx index 20e4f727a6ed..de2c7cf9686c 100644 --- a/packages/manager/apps/pci-savings-plan/src/components/Dashboard/ConsumptionDatagrid/ConsumptionDatagrid.tsx +++ b/packages/manager/apps/pci-savings-plan/src/components/Dashboard/ConsumptionDatagrid/ConsumptionDatagrid.tsx @@ -4,15 +4,17 @@ import { ShellContext, useOvhTracking, } from '@ovh-ux/manager-react-shell-client'; -import { useNavigate, useParams } from 'react-router-dom'; +import { ODS_BUTTON_VARIANT } from '@ovhcloud/ods-components'; +import { useNavigate } from 'react-router-dom'; import { + ActionMenu, Datagrid, DataGridTextCell, useDataGrid, useNotifications, } from '@ovh-ux/manager-react-components'; import { OdsText, OdsButton } from '@ovhcloud/ods-components/react'; -import React, { useContext, useEffect, useMemo } from 'react'; +import React, { useContext, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { SavingsPlanFlavorConsumption, @@ -21,10 +23,12 @@ import { import { toLocalDateUTC } from '@/utils/formatter/date'; import { paginateResults } from '@/utils/paginate/utils'; import { useProjectId } from '@/hooks/useProject'; +import ConsumptionResourceList from './ConsumptionResourceList'; type ConsumptionDatagridProps = { isLoading: boolean; consumption: SavingsPlanFlavorConsumption; + isInstanceFlavor: boolean; }; const CellText = ({ text }: { text: string }) => ( @@ -34,16 +38,19 @@ const CellText = ({ text }: { text: string }) => ( const ConsumptionDatagrid = ({ isLoading, consumption, + isInstanceFlavor, }: ConsumptionDatagridProps) => { const { pagination, setPagination } = useDataGrid(); const { environment } = useContext(ShellContext); const locale = environment.getUserLocale(); const { t } = useTranslation(['dashboard', 'listing']); const { trackClick } = useOvhTracking(); - + const [isDrawerOpen, setDrawerOpen] = useState(false); + const [selectedResources, setSelectedResources] = useState([]); const navigate = useNavigate(); const projectId = useProjectId(); const { clearNotifications } = useNotifications(); + const columns = [ { label: t('dashboard_columns_start'), @@ -73,6 +80,28 @@ const ConsumptionDatagrid = ({ ), }, + { + label: '', + id: 'action', + cell: (props: SavingsPlanPeriodConsumption) => ( + { + setSelectedResources(props.resourceIds); + setDrawerOpen(true); + }, + }, + ]} + isCompact + variant={ODS_BUTTON_VARIANT.ghost} + /> + ), + }, ]; const handleClick = () => { @@ -102,8 +131,19 @@ const ConsumptionDatagrid = ({ } }, [items]); + const handleCloseDrawer = () => { + setSelectedResources([]); + setDrawerOpen(false); + }; + return (
+ {t('dashboard_table_title')} diff --git a/packages/manager/apps/pci-savings-plan/src/components/Dashboard/ConsumptionDatagrid/ConsumptionResourceList.tsx b/packages/manager/apps/pci-savings-plan/src/components/Dashboard/ConsumptionDatagrid/ConsumptionResourceList.tsx new file mode 100644 index 000000000000..9a7c0b16d205 --- /dev/null +++ b/packages/manager/apps/pci-savings-plan/src/components/Dashboard/ConsumptionDatagrid/ConsumptionResourceList.tsx @@ -0,0 +1,104 @@ +import { usePciUrl } from '@ovh-ux/manager-pci-common'; +import { Drawer } from '@ovh-ux/manager-react-components'; +import { ODS_ICON_NAME, OdsInputChangeEvent } from '@ovhcloud/ods-components'; +import React, { useMemo, useState } from 'react'; +import { OdsText, OdsInput, OdsLink } from '@ovhcloud/ods-components/react'; +import { useTranslation } from 'react-i18next'; + +type ConsumptionResourceListProps = { + isDrawerOpen: boolean; + resources: string[]; + handleCloseDrawer: () => void; + isInstanceFlavor: boolean; +}; + +const ConsumptionResourceList = ({ + isDrawerOpen, + resources, + isInstanceFlavor, + handleCloseDrawer, +}: ConsumptionResourceListProps) => { + const pciUrl = usePciUrl(); + const { t } = useTranslation('dashboard'); + + const [searchQuery, setSearchQuery] = useState(''); + + const handleSearchChange = (event: OdsInputChangeEvent) => { + const { value } = event.detail; + setSearchQuery(typeof value === 'string' ? value : ''); + }; + + const filteredPlanIds = useMemo(() => { + if (!resources) return []; + if (!searchQuery) return resources; + return resources.filter((id) => + id.toLowerCase().includes(searchQuery.toLowerCase()), + ); + }, [resources, searchQuery]); + + const onClose = () => { + setSearchQuery(''); + handleCloseDrawer(); + }; + + const getResourceUrl = (planId: string) => + isInstanceFlavor + ? `${pciUrl}/instances/${planId}` + : `${pciUrl}/rancher/${planId}`; + + return ( + +
+
+ + {t('dashboard_resource_list_title')} + +
+ + {resources.length > 0 ? ( + <> +
+ {t('dashboard_resource_list_search')} + +
+ +
+ {filteredPlanIds.length > 0 ? ( + filteredPlanIds.map((planId) => ( +
+ +
+ )) + ) : ( + + {t('dashboard_resource_list_no_results', { searchQuery })} + + )} +
+ + ) : ( + + {t('dashboard_resource_list_no_resources')} + + )} +
+
+ ); +}; + +export default ConsumptionResourceList; diff --git a/packages/manager/apps/pci-savings-plan/src/index.scss b/packages/manager/apps/pci-savings-plan/src/index.scss index 7ae3d9be3e64..381073f15522 100644 --- a/packages/manager/apps/pci-savings-plan/src/index.scss +++ b/packages/manager/apps/pci-savings-plan/src/index.scss @@ -33,6 +33,10 @@ ods-tag.org-tag::part(tag) { transform: translateY(-10%); } +button[data-testid='drawer-backdrop'] { + opacity: 0.8; +} + .dns-field { width: 100%; diff --git a/packages/manager/apps/pci-savings-plan/src/pages/dashboard/index.tsx b/packages/manager/apps/pci-savings-plan/src/pages/dashboard/index.tsx index e5c046d8da59..6bd64d5babf9 100644 --- a/packages/manager/apps/pci-savings-plan/src/pages/dashboard/index.tsx +++ b/packages/manager/apps/pci-savings-plan/src/pages/dashboard/index.tsx @@ -1,7 +1,7 @@ import { ErrorBanner } from '@ovh-ux/manager-react-components'; import React, { useContext, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { ShellContext } from '@ovh-ux/manager-react-shell-client'; import { OdsText, OdsMessage } from '@ovhcloud/ods-components/react'; import ConsumptionDatagrid from '@/components/Dashboard/ConsumptionDatagrid/ConsumptionDatagrid'; @@ -13,6 +13,7 @@ import GenericChart from '@/components/Chart/Chart'; import { useFilteredConsumption } from '@/hooks/useFilteredConsumption'; import Header from '@/components/Header/Header'; import { useProjectId } from '@/hooks/useProject'; +import { isInstanceFlavor } from '@/utils/savingsPlan'; const Dashboard: React.FC = () => { const projectId = useProjectId(); @@ -44,6 +45,7 @@ const Dashboard: React.FC = () => { () => consumption?.flavors?.find((f) => f.flavor === flavor), [consumption, flavor], ); + const isInstance = useMemo(() => isInstanceFlavor(flavor), [flavor]); useEffect(() => { if (!isLoading && !isPending && savingsPlan?.length === 0) { @@ -65,6 +67,7 @@ const Dashboard: React.FC = () => { return ( <>
+ {t('dashboard_description')} @@ -102,6 +105,7 @@ const Dashboard: React.FC = () => { /> )} diff --git a/packages/manager/modules/pci/.eslintrc.json b/packages/manager/modules/pci/.eslintrc.json index c12d22beb416..b5c87a06876b 100644 --- a/packages/manager/modules/pci/.eslintrc.json +++ b/packages/manager/modules/pci/.eslintrc.json @@ -1,5 +1,8 @@ { "globals": { "moment": true + }, + "rules": { + "class-methods-use-this": "off" } } diff --git a/packages/manager/modules/pci/src/components/project/images-list/images-list.controller.js b/packages/manager/modules/pci/src/components/project/images-list/images-list.controller.js index 0bfd1361e51f..259a19c189e1 100644 --- a/packages/manager/modules/pci/src/components/project/images-list/images-list.controller.js +++ b/packages/manager/modules/pci/src/components/project/images-list/images-list.controller.js @@ -5,21 +5,23 @@ import keys from 'lodash/keys'; import partition from 'lodash/partition'; import reduce from 'lodash/reduce'; import some from 'lodash/some'; +import groupBy from 'lodash/groupBy'; import { IMAGE_ASSETS } from './images.constants'; +import { DISTANT_BACKUP_FEATURE } from '../../../projects/project/instances/instances.constants'; export default class ImagesListController { /* @ngInject */ constructor( $q, - OvhApiCloudProjectImage, - OvhApiCloudProjectSnapshot, + PciProjectsProjectInstanceService, PciProjectImages, + ovhFeatureFlipping, ) { this.$q = $q; - this.OvhApiCloudProjectImage = OvhApiCloudProjectImage; - this.OvhApiCloudProjectSnapshot = OvhApiCloudProjectSnapshot; + this.PciProjectsProjectInstanceService = PciProjectsProjectInstanceService; this.PciProjectImages = PciProjectImages; + this.ovhFeatureFlipping = ovhFeatureFlipping; this.IMAGE_ASSETS = IMAGE_ASSETS; } @@ -33,8 +35,11 @@ export default class ImagesListController { this.isLoading = true; return this.$q - .all([this.getImages(), this.getSnapshots()]) - .then(() => this.findDefaultImage()) + .all([this.getImages(), this.getSnapshots(), this.loadSnapshotRegions()]) + .then(() => { + this.computeUnavailableSnapshots(); + this.findDefaultImage(); + }) .finally(() => { this.isLoading = false; }); @@ -112,15 +117,48 @@ export default class ImagesListController { getSnapshots() { return this.PciProjectImages.getSnapshots(this.serviceName).then( (snapshots) => { - this.snapshots = snapshots; - this.unavailableSnapshotsPresent = some( - snapshots, - (snapshot) => !this.isCompatible(snapshot), + this.snapshots = groupBy(snapshots, (s) => + s.region === this.region ? 'local' : 'distant', ); }, ); } + loadSnapshotRegions() { + return this.$q + .all([ + this.PciProjectsProjectInstanceService.getProductAvailability( + this.serviceName, + undefined, + 'snapshot', + ), + this.ovhFeatureFlipping.checkFeatureAvailability( + DISTANT_BACKUP_FEATURE, + ), + ]) + .then(([productAvailability, feature]) => { + this.snapshotsPlans = productAvailability.plans.filter((p) => + p.code.startsWith('snapshot.consumption'), + ); + this.distantBackupAvailability = feature.isFeatureAvailable( + DISTANT_BACKUP_FEATURE, + ); + }); + } + + computeUnavailableSnapshots() { + this.unavailableSnapshotsPresent = { + local: some( + this.snapshots.local, + (snapshot) => !this.isCompatible(snapshot), + ), + distant: some( + this.snapshots.distant, + (snapshot) => !this.isCompatible(snapshot), + ), + }; + } + findDefaultImage() { if (this.defaultImageId) { // To cover case where image is deprecated @@ -162,12 +200,12 @@ export default class ImagesListController { } if (!this.image) { - const snapshot = find(this.snapshots, { + const snapshot = find(this.snapshots.local, { region: this.region, id: this.defaultImageId, }); if (snapshot) { - this.selectedTab = 'snapshots'; + this.selectedTab = 'snapshots-local'; this.image = snapshot; this.onImageChange(snapshot); @@ -199,10 +237,13 @@ export default class ImagesListController { } isCompatible(image) { - return ( - image.isAvailableInRegion(this.region) && - image.isCompatibleWithFlavor(this.flavorType) && - image.isCompatibleWithOsTypes(this.osTypes) + return this.PciProjectImages.isImageCompatible( + image, + this.region, + this.flavorType, + this.osTypes, + this.snapshotsPlans, + this.distantBackupAvailability, ); } } diff --git a/packages/manager/modules/pci/src/components/project/images-list/images-list.html b/packages/manager/modules/pci/src/components/project/images-list/images-list.html index 72ce3d9f5648..89a72b573f02 100644 --- a/packages/manager/modules/pci/src/components/project/images-list/images-list.html +++ b/packages/manager/modules/pci/src/components/project/images-list/images-list.html @@ -116,15 +116,16 @@
- image.isAvailableInRegion(region) && - image.isCompatibleWithFlavor(flavorType), + // eslint-disable-next-line class-methods-use-this + isImageCompatible( + image, + targetRegionName, + flavorType, + osTypes, + snapshotsPlans, + distantBackupAvailability, + ) { + if (image.isBackup()) { + const targetRegion = snapshotsPlans + .flatMap((p) => p.regions) + .find((r) => r.name === targetRegionName); + + if (targetRegionName !== image.region) { + if (!distantBackupAvailability) return false; + + if (![ONE_AZ_REGION, THREE_AZ_REGION].includes(targetRegion.type)) + return false; + + const imageRegion = snapshotsPlans + .find((p) => p.code === image.planCode) + .regions.find((r) => r.name === image.region); + + if (![ONE_AZ_REGION, THREE_AZ_REGION].includes(imageRegion.type)) + return false; + } + } else if (!image.isAvailableInRegion(targetRegionName)) { + return false; + } + + return ( + image.isCompatibleWithFlavor(flavorType) && + image.isCompatibleWithOsTypes(osTypes) ); } } diff --git a/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_de_DE.json b/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_de_DE.json index dc9031d83f2a..07c4caad5aeb 100644 --- a/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_de_DE.json +++ b/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_de_DE.json @@ -29,5 +29,7 @@ "pci_project_instances_distribution_baremetal_ubuntu": "Ubuntu", "pci_project_instances_distribution_baremetal_windows_server_2012": "Windows Server 2012", "pci_project_instances_distribution_baremetal_windows_server_2016": "Windows Server 2016", - "pci_project_instances_distribution_baremetal_windows_other": "Andere Windows-Distributionen" + "pci_project_instances_distribution_baremetal_windows_other": "Andere Windows-Distributionen", + "pci_project_instances_shapshots_local": "Lokales Backup", + "pci_project_instances_shapshots_distant": "Remote-Backup" } diff --git a/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_en_GB.json b/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_en_GB.json index 44edf0935826..0f86fa5048fe 100644 --- a/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_en_GB.json +++ b/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_en_GB.json @@ -29,5 +29,7 @@ "pci_project_instances_distribution_baremetal_ubuntu": "Ubuntu", "pci_project_instances_distribution_baremetal_windows_server_2012": "Windows Server 2012", "pci_project_instances_distribution_baremetal_windows_server_2016": "Windows Server 2016", - "pci_project_instances_distribution_baremetal_windows_other": "Other Windows operating systems" + "pci_project_instances_distribution_baremetal_windows_other": "Other Windows operating systems", + "pci_project_instances_shapshots_local": "Local Backup", + "pci_project_instances_shapshots_distant": "Remote Backup" } diff --git a/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_es_ES.json b/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_es_ES.json index f1e9f79dc5c8..8af5e620d113 100644 --- a/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_es_ES.json +++ b/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_es_ES.json @@ -29,5 +29,7 @@ "pci_project_instances_distribution_baremetal_ubuntu": "Ubuntu", "pci_project_instances_distribution_baremetal_windows_server_2012": "Windows Server 2012", "pci_project_instances_distribution_baremetal_windows_server_2016": "Windows Server 2016", - "pci_project_instances_distribution_baremetal_windows_other": "Otras distribuciones Windows" + "pci_project_instances_distribution_baremetal_windows_other": "Otras distribuciones Windows", + "pci_project_instances_shapshots_local": "Backup local", + "pci_project_instances_shapshots_distant": "Backup remoto" } diff --git a/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_fr_CA.json b/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_fr_CA.json index 12d114deacb2..9fda0e7e3e20 100644 --- a/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_fr_CA.json +++ b/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_fr_CA.json @@ -5,7 +5,8 @@ "pci_project_instances_os_baremetal-linux": "Bare Metal distributions", "pci_project_instances_distribution_unknown": "Bare Metal distributions", "pci_project_instances_apps": "Distributions + Apps", - "pci_project_instances_shapshots": "Backups", + "pci_project_instances_shapshots_local": "Backup Local", + "pci_project_instances_shapshots_distant": "Backup Distant", "pci_project_instances_licence": "L'utilisation d'une image est soumise à l'acceptation préalable du Contrat de license utilisateur du fournisseur.", "pci_project_instances_distribution_linux": "Linux", "pci_project_instances_distribution_linux_other": "Arch Linux", diff --git a/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_fr_FR.json b/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_fr_FR.json index 12d114deacb2..9fda0e7e3e20 100644 --- a/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_fr_FR.json +++ b/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_fr_FR.json @@ -5,7 +5,8 @@ "pci_project_instances_os_baremetal-linux": "Bare Metal distributions", "pci_project_instances_distribution_unknown": "Bare Metal distributions", "pci_project_instances_apps": "Distributions + Apps", - "pci_project_instances_shapshots": "Backups", + "pci_project_instances_shapshots_local": "Backup Local", + "pci_project_instances_shapshots_distant": "Backup Distant", "pci_project_instances_licence": "L'utilisation d'une image est soumise à l'acceptation préalable du Contrat de license utilisateur du fournisseur.", "pci_project_instances_distribution_linux": "Linux", "pci_project_instances_distribution_linux_other": "Arch Linux", diff --git a/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_it_IT.json b/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_it_IT.json index 8c168f6b3581..5f7823770752 100644 --- a/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_it_IT.json +++ b/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_it_IT.json @@ -29,5 +29,7 @@ "pci_project_instances_distribution_baremetal_ubuntu": "Ubuntu", "pci_project_instances_distribution_baremetal_windows_server_2012": "Windows Server 2012", "pci_project_instances_distribution_baremetal_windows_server_2016": "Windows Server 2016", - "pci_project_instances_distribution_baremetal_windows_other": "Altre distribuzioni Windows" + "pci_project_instances_distribution_baremetal_windows_other": "Altre distribuzioni Windows", + "pci_project_instances_shapshots_local": "Backup locale", + "pci_project_instances_shapshots_distant": "Backup remoto" } diff --git a/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_pl_PL.json b/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_pl_PL.json index 8de2f9ff7016..2ba97dec993e 100644 --- a/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_pl_PL.json +++ b/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_pl_PL.json @@ -29,5 +29,7 @@ "pci_project_instances_distribution_baremetal_ubuntu": "Ubuntu", "pci_project_instances_distribution_baremetal_windows_server_2012": "Windows Server 2012", "pci_project_instances_distribution_baremetal_windows_server_2016": "Windows Server 2012", - "pci_project_instances_distribution_baremetal_windows_other": "Inne dystrybucje Windows" + "pci_project_instances_distribution_baremetal_windows_other": "Inne dystrybucje Windows", + "pci_project_instances_shapshots_local": "Lokalny backup", + "pci_project_instances_shapshots_distant": "Zdalny backup" } diff --git a/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_pt_PT.json b/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_pt_PT.json index 988c2648556d..5656f895cb61 100644 --- a/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_pt_PT.json +++ b/packages/manager/modules/pci/src/components/project/images-list/translations/Messages_pt_PT.json @@ -29,5 +29,7 @@ "pci_project_instances_distribution_baremetal_ubuntu": "Ubuntu", "pci_project_instances_distribution_baremetal_windows_server_2012": "Windows Server 2012", "pci_project_instances_distribution_baremetal_windows_server_2016": "Windows Server 2016", - "pci_project_instances_distribution_baremetal_windows_other": "Outras distribuições Windows" + "pci_project_instances_distribution_baremetal_windows_other": "Outras distribuições Windows", + "pci_project_instances_shapshots_local": "Backup local", + "pci_project_instances_shapshots_distant": "Backup remoto" } diff --git a/packages/manager/modules/pci/src/projects/project/instances/add/add.component.js b/packages/manager/modules/pci/src/projects/project/instances/add/add.component.js index 71c12bc79256..ecd2b9fb4b89 100644 --- a/packages/manager/modules/pci/src/projects/project/instances/add/add.component.js +++ b/packages/manager/modules/pci/src/projects/project/instances/add/add.component.js @@ -14,7 +14,6 @@ export default { includeCategories: ' { @@ -809,21 +797,24 @@ export default class PciInstancesAddController { onImageChange() { this.displaySelectedImage = true; + if (this.model.image.isBackup()) { this.instance.imageId = this.model.image.id; + this.instance.imageRegionName = this.model.image.region; } else { this.instance.imageId = this.model.image.getIdByRegion( this.instance.region, ); + this.instance.imageRegionName = this.instance.region; } this.onFlexChange(false); if (!this.isLinuxImageType()) { this.model.sshKey = null; - this.instance.sshKeyId = null; + this.instance.sshKey = null; } else { - this.instance.sshKeyId = get(this.model.sshKey, 'id'); + this.instance.sshKey = this.model.sshKey; } this.trackAddInstance([ @@ -1220,33 +1211,7 @@ export default class PciInstancesAddController { } } - getPrivateNetworkOnChange() { - if (get(this.selectedPrivateNetwork, 'id')) { - this.instance.networks = [ - { - networkId: get(this.selectedPrivateNetwork, 'id'), - }, - ]; - if (!this.isPrivateMode() && !this.isLocalPrivateMode()) { - this.instance.networks = [ - ...this.instance.networks, - { - networkId: this.publicNetwork.find((network) => { - if (FLAVORS_BAREMETAL.test(this.flavor.type)) { - return network.name === PUBLIC_NETWORK_BAREMETAL; - } - return network.name === PUBLIC_NETWORK; - })?.id, - }, - ]; - } - } else { - this.instance.networks = []; - } - } - onPrivateNetworkChange(modelValue) { - this.getPrivateNetworkOnChange(); if ( modelValue && modelValue.subnet && @@ -1505,17 +1470,9 @@ export default class PciInstancesAddController { } onCreateFormStepperSubmit() { - if (this.isPrivateMode()) { - this.confirmPrivateInstanceCreation = true; - return null; - } return this.create(); } - onCancelCreateInstanceConfirmation() { - this.confirmPrivateInstanceCreation = false; - } - create() { this.isLoading = true; this.trackAction('create'); @@ -1523,52 +1480,6 @@ export default class PciInstancesAddController { if (!this.isLinuxImageType()) { this.instance.userData = null; } - if (this.isLocalPrivateMode()) { - const publicNetworkAlreadyExist = this.instance?.networks?.some( - (instanceNetwork) => { - return this.publicNetwork?.find( - (network) => network.id === instanceNetwork.networkId, - ); - }, - ); - - if ( - this.isLocalPrivateModeLocalZone && - this.isCreatingNewPrivateNetwork && - this.privateNetworkName - ) { - this.instance.networks = [ - ...(this.instance.networks || []), - { - networkId: this.availableLocalPrivateNetworks.find(({ name }) => { - return name === this.privateNetworkName; - })?.id, - }, - ]; - } - - if (this.isAttachPublicNetwork && !publicNetworkAlreadyExist) { - // if attach check box is ticked, add public network if not added one - this.instance.networks = [ - ...(this.instance.networks || []), - { - networkId: this.publicNetwork?.find( - (network) => network.name === PUBLIC_NETWORK, - )?.id, - }, - ]; - } - if (!this.isAttachPublicNetwork && publicNetworkAlreadyExist) { - // if attach check box is not ticked, remove public network if already added one - this.instance.networks = this.instance?.networks?.filter( - (instanceNetwork) => { - return !this.publicNetwork?.find( - (network) => network.id === instanceNetwork.networkId, - ); - }, - ); - } - } if ( this.PciProjectsProjectInstanceService.automatedBackupIsAvailable( @@ -1587,108 +1498,76 @@ export default class PciInstancesAddController { this.instance.availabilityZone = this.model.threeAzRegion.zone; } - // @TODO: GS Use post /cloud/project/{serviceName}/region/{regionName}/instance - // for local zone instance creation + const network = { + private: + this.isPrivateMode() || this.isLocalPrivateMode() + ? { + floatingIp: this.selectedFloatingIP?.id + ? { + id: this.selectedFloatingIP.id, + } + : null, + floatingIpCreate: + !!this.selectedFloatingIP && !this.selectedFloatingIP.id + ? { + description: '', + } + : null, + gatewayCreate: + this.subnetGateways?.length === 0 && this.isAttachFloatingIP + ? { + model: this.defaultGateway.size, + name: getAutoGeneratedName( + `gateway-${this.model.datacenter.name.toLowerCase()}`, + ), + } + : null, + ip: null, + + network: { + id: this.selectedPrivateNetwork.regions.find( + (r) => r.region === this.instance.region, + ).openstackId, + subnetId: this.selectedPrivateNetwork.subnet?.[0]?.id, + }, + } + : null, + public: + this.selectedMode.name === PUBLIC_NETWORK_MODE || + this.isAttachPublicNetwork, + }; return this.PciProjectsProjectInstanceService.save( this.projectId, - this.instance, + this.instance.region, + { ...this.instance, network }, this.model.number, - this.isPrivateMode(), ) - .then((result) => { - const availabilityZone = - result.availabilityZone ?? this.instance.availabilityZone; - let message; - if (this.model.number === 1) { - if (availabilityZone) { - message = this.$translate.instant( - this.addInstance3azSuccessMessage, - { - instance: this.instance.name, - zone: availabilityZone, - }, - ); - } else { - message = this.$translate.instant(this.addInstanceSuccessMessage, { - instance: this.instance.name, - }); - } - } else { - message = this.$translate.instant(this.addInstancesSuccessMessage); - } - - if (Array.isArray(result) && result.length > 1) { - return this.goBack(message, 'success'); + .then(() => { + let messageType = null; + if ( + this.model.image.isBackup() && + !this.model.image.isAvailableInRegion(this.instance.region) + ) { + messageType = 'distant_backup'; + } else if (this.isPrivateMode()) { + messageType = 'private_network'; } - const { id: instanceId, ipAddresses: ips } = result; - if (!this.isPrivateMode()) { - return this.goBack(message, 'success'); - } - if (this.subnetGateways?.length === 0 && this.isAttachFloatingIP) { - return this.createGateway(instanceId, ips, message); - } - return this.onCreateInstanceSuccess(instanceId, ips, message); + return this.goBack( + this.$translate.instant( + `pci_projects_project_instances_add_success_message${ + messageType ? `_${messageType}` : '' + }`, + ), + 'success', + ); }) .catch((error) => { this.createInstanceError(error); }) .finally(() => { - if (!this.isPrivateMode()) { - this.isLoading = false; - } else { - this.confirmPrivateInstanceCreation = false; - } - }); - } - - onCreateInstanceSuccess(instanceId, ips, message) { - const filteredIp = ips.find(({ version: ipv4 }) => ipv4 === 4); - if (this.isAttachFloatingIP && filteredIp) { - if (this.isCreateFloatingIPClicked && !this.selectedFloatingIP.id) { - return this.createAndAttachFloatingIp( - instanceId, - filteredIp.ip, - message, - ); - } - if (this.selectedFloatingIP) { - return this.associateFloatingIp( - instanceId, - this.selectedFloatingIP.id, - filteredIp.ip, - message, - ); - } - } - return this.goBack(message, 'success'); - } - - createGateway(instanceId, ips, message) { - this.gatewayName = getAutoGeneratedName( - `gateway-${this.model.datacenter.name.toLowerCase()}`, - ); - - this.selectedGatewaySize = this.defaultGateway.size; - this.gatewayModel = { - name: this.gatewayName, - model: this.selectedGatewaySize, - }; - const network = this.selectedPrivateNetwork.regions.find( - (networkRegion) => networkRegion.region === this.model.datacenter.name, - ); - return this.PciProjectsProjectInstanceService.createGateway( - this.projectId, - this.model.datacenter.name, - network.openstackId, - this.selectedPrivateNetwork?.subnet[0]?.id, - this.gatewayModel, - ) - .then(() => this.onCreateInstanceSuccess(instanceId, ips, message)) - .catch((err) => { this.isLoading = false; - return this.handleError(err); }); } @@ -1732,47 +1611,6 @@ export default class PciInstancesAddController { ); } - createAndAttachFloatingIp(instanceId, ip, message) { - this.isLoading = true; - this.floatingIpModel = { - ip, - }; - return this.PciProjectsProjectInstanceService.createAndAttachFloatingIp( - this.projectId, - this.model.datacenter.name, - instanceId, - this.floatingIpModel, - ) - .then(() => this.goBack(message, 'success')) - .catch((err) => { - this.handleError(err); - }) - .finally(() => { - this.isLoading = false; - }); - } - - associateFloatingIp(instanceId, floatingIpId, ip, message) { - this.isLoading = true; - this.floatingIpModel = { - floatingIpId, - ip, - }; - return this.PciProjectsProjectInstanceService.associateFloatingIp( - this.projectId, - this.model.datacenter.name, - instanceId, - this.floatingIpModel, - ) - .then(() => this.goBack(message, 'success')) - .catch((err) => { - this.handleError(err); - }) - .finally(() => { - this.isLoading = false; - }); - } - cancel() { this.trackAction('cancel'); return this.goBack(); @@ -1836,12 +1674,16 @@ export default class PciInstancesAddController { this.isAddingPrivateNetwork = true; return this.createPrivateNetwork() - .then(() => { + .then((networkId) => { return this.PciProjectsProjectInstanceService.getLocalPrivateNetworks( this.projectId, this.model.datacenter.name, ).then((networks) => { this.availableLocalPrivateNetworks = networks; + this.selectedPrivateNetwork = networks.find( + (n) => n.id === networkId, + ); + this.isCreatingNewPrivateNetwork = false; return this.getLocalPrivateNetworkSubnet() .then(() => { this.CucCloudMessage.success( diff --git a/packages/manager/modules/pci/src/projects/project/instances/add/add.html b/packages/manager/modules/pci/src/projects/project/instances/add/add.html index c338da7cec37..48d4713263a7 100644 --- a/packages/manager/modules/pci/src/projects/project/instances/add/add.html +++ b/packages/manager/modules/pci/src/projects/project/instances/add/add.html @@ -437,6 +437,16 @@

+ + +

+
+
@@ -1252,45 +1262,3 @@

>
- -
- -
-
diff --git a/packages/manager/modules/pci/src/projects/project/instances/add/add.routing.js b/packages/manager/modules/pci/src/projects/project/instances/add/add.routing.js index 45607a165f73..66b6ca6402fa 100644 --- a/packages/manager/modules/pci/src/projects/project/instances/add/add.routing.js +++ b/packages/manager/modules/pci/src/projects/project/instances/add/add.routing.js @@ -23,11 +23,6 @@ export default /* @ngInject */ ($stateProvider) => { projectId, ) => PciProjectsProjectInstanceService.getPrivateNetworks(projectId), - publicNetwork: /* @ngInject */ ( - PciProjectsProjectInstanceService, - projectId, - ) => PciProjectsProjectInstanceService.getPublicNetwork(projectId), - regions: /* @ngInject */ ( PciProjectsProjectInstanceService, projectId, diff --git a/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_de_DE.json b/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_de_DE.json index 5c007dff65d3..426951906867 100644 --- a/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_de_DE.json +++ b/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_de_DE.json @@ -140,5 +140,6 @@ "pci_projects_project_network_private_create_announce_first_address": "Die erste Adresse eines CIDR als Standard-Gateway anzeigen (DHCP-Option 3)", "pci_projects_project_network_private_create_configure_api": "Sie können Ihre IP-Bereiche über die API anpassen.", "pci_projects_project_instances_add_numInstances_3az_help": "Wenn Sie mehrere Instanzen in einer 3AZ-Region erstellen, befinden sich alle in derselben Availability Zone, unabhängig davon, ob Sie eine Zone oder die Option „Für mich auswählen“ wählen.", - "pci_projects_project_instances_add_billing_floating_ip_detail_price_hourly": "Ihnen werden {{ floatingIpPrice }} zzgl. MwSt./Stunde berechnet." + "pci_projects_project_instances_add_billing_floating_ip_detail_price_hourly": "Ihnen werden {{ floatingIpPrice }} zzgl. MwSt./Stunde berechnet.", + "pci_projects_project_instances_add_image_distant_warning": "Die Erstellung Ihrer Instanz von einem Remote-Backup aus erfordert eine lokale Kopie des Remote-Images, die zum üblichen Preis abgerechnet wird." } diff --git a/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_en_GB.json b/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_en_GB.json index a4996e17adec..4f45592a5dd0 100644 --- a/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_en_GB.json +++ b/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_en_GB.json @@ -140,5 +140,6 @@ "pci_projects_project_network_private_create_announce_first_address": "Declare the first address of a CIDR given as the default gateway (DHCP option 3)", "pci_projects_project_network_private_create_configure_api": "You can customise your ranges via the API.", "pci_projects_project_instances_add_numInstances_3az_help": "Creating multiple instances in a 3-AZ region places them within the same availability zone, regardless of whether you choose a zone or the “choose for me” option.", - "pci_projects_project_instances_add_billing_floating_ip_detail_price_hourly": "You will be charged {{ floatingIpPrice }} ex. VAT/hour." + "pci_projects_project_instances_add_billing_floating_ip_detail_price_hourly": "You will be charged {{ floatingIpPrice }} ex. VAT/hour.", + "pci_projects_project_instances_add_image_distant_warning": "Creating your instance from a remote backup will require a local copy of the remote image, which will be invoiced at the usual rate." } diff --git a/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_es_ES.json b/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_es_ES.json index e44f26462b56..4c7562ea4bb1 100644 --- a/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_es_ES.json +++ b/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_es_ES.json @@ -140,5 +140,6 @@ "pci_projects_project_network_private_create_announce_first_address": "Anunciar la primera dirección de un CIDR determinado como puerta de enlace predeterminada (DHCP opción 3)", "pci_projects_project_network_private_create_configure_api": "Puede personalizar los rangos desde la API.", "pci_projects_project_instances_add_numInstances_3az_help": "Al crear varias instancias en una región 3AZ, todas estarán en la misma zona de disponibilidad, independientemente de que elija una zona o la opción «elegir por mí».", - "pci_projects_project_instances_add_billing_floating_ip_detail_price_hourly": "Se facturará {{ floatingIpPrice }}/hora + IVA." + "pci_projects_project_instances_add_billing_floating_ip_detail_price_hourly": "Se facturará {{ floatingIpPrice }}/hora + IVA.", + "pci_projects_project_instances_add_image_distant_warning": "Para crear su instancia a partir de un backup remoto, deberá realizar una copia local de la imagen remota, que se facturará al precio habitual." } diff --git a/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_fr_CA.json b/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_fr_CA.json index a1a83a8a1b0b..9b0d654fadc5 100644 --- a/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_fr_CA.json +++ b/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_fr_CA.json @@ -20,6 +20,7 @@ "pci_projects_project_instances_add_image_title": "Sélectionnez une image", "pci_projects_project_instances_add_image_not_available": "Ces images sont disponibles sur d'autre modèles.", + "pci_projects_project_instances_add_image_distant_warning": "La création de votre instance à partir d’un backup distant nécessitera une copie locale de l’image distante qui sera facturée au tarif habituel.", "pci_projects_project_instances_add_instance_is_iops": "La sauvegarde automatique s’applique strictement aux données du disque local de votre instance. Les données des disques NVMe ne seront pas sauvegardées.", "pci_projects_project_instances_add_instance_title": "Configurez votre instance", diff --git a/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_fr_FR.json b/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_fr_FR.json index a1a83a8a1b0b..d404c3827b77 100644 --- a/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_fr_FR.json +++ b/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_fr_FR.json @@ -20,6 +20,7 @@ "pci_projects_project_instances_add_image_title": "Sélectionnez une image", "pci_projects_project_instances_add_image_not_available": "Ces images sont disponibles sur d'autre modèles.", + "pci_projects_project_instances_add_image_distant_warning": "La création de votre instance à partir d’un backup distant nécessitera une copie locale de l’image distante qui sera facturée au tarif habituel.", "pci_projects_project_instances_add_instance_is_iops": "La sauvegarde automatique s’applique strictement aux données du disque local de votre instance. Les données des disques NVMe ne seront pas sauvegardées.", "pci_projects_project_instances_add_instance_title": "Configurez votre instance", @@ -113,11 +114,9 @@ "pci_projects_project_instances_add_private_mode1": "Votre instance est en cours de création. La configuration de tous les composants peut prendre quelques minutes.", "pci_projects_project_instances_add_private_mode2": "Veuillez ne pas quitter ou rafraîchir cette page car cela pourrait avoir un impact sur le processus de création.", - "pci_projects_project_instances_add_success_message": "L'instance {{ instance }} a été ajoutée.", - "pci_projects_project_instances_3az_add_success_message": "L'instance {{ instance }} a été ajoutée avec succès dans la zone de disponibilité : {{ zone }}", - "pci_projects_project_instances_add_success_multiple_message": "Les instances ont été ajoutées.", - "pci_projects_project_instances_add_error_save": "Une erreur est survenue lors de l'ajout de l'instance {{ instance }} : {{ message }}", - "pci_projects_project_instances_add_error_multiple_save": "Une erreur est survenue lors de l'ajout des instances : {{ message }}", + "pci_projects_project_instances_add_success_message_private_network": "L'opération de création d'instance(s) est en cours, la création de l'instance(s) peut prendre quelques minutes", + "pci_projects_project_instances_add_success_message_distant_backup": "L'opération de création d'instance est en cours, la création de l'instance à partir d'un backup distant entraîne un temps de traitement plus long en fonction de la taille de l'image et de la région distante", + "pci_projects_project_instances_add_success_message": "L(es) opération(s) de création d'instance(s) est en cours", "pci_projects_project_instances_add_modal_title": "Créer un réseau privé", "pci_projects_project_instances_add_modal_description": "Merci de selectionner un nom de réseau privé et votre sous-réseau afin de créer votre nouveau réseau privé.", diff --git a/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_it_IT.json b/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_it_IT.json index ba05aef66d9a..41209ae086be 100644 --- a/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_it_IT.json +++ b/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_it_IT.json @@ -140,5 +140,6 @@ "pci_projects_project_network_private_create_announce_first_address": "Annunciare il primo indirizzo di un CIDR come gateway predefinito (DHCP opzione 3)", "pci_projects_project_network_private_create_configure_api": "È possibile personalizzare le classi via API.", "pci_projects_project_instances_add_numInstances_3az_help": "Quando crei più istanze in una Region 3-AZ, saranno tutte nella stessa zona di disponibilità, sia che tu scelga una zona o l'opzione \"Scegli per me\".", - "pci_projects_project_instances_add_billing_floating_ip_detail_price_hourly": "Sarà fatturato un importo di {{ floatingIpPrice }} +IVA/ora." + "pci_projects_project_instances_add_billing_floating_ip_detail_price_hourly": "Sarà fatturato un importo di {{ floatingIpPrice }} +IVA/ora.", + "pci_projects_project_instances_add_image_distant_warning": "La creazione della tua istanza a partire da un backup remoto richiederà una copia locale dell'immagine remota, che sarà fatturata alla tariffa abituale." } diff --git a/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_pl_PL.json b/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_pl_PL.json index 79ece0c503da..dd803dd9b101 100644 --- a/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_pl_PL.json +++ b/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_pl_PL.json @@ -140,5 +140,6 @@ "pci_projects_project_network_private_create_announce_first_address": "Ustaw pierwszy adres danego CIDR jako bramę domyślną (DHCP opcja 3)", "pci_projects_project_network_private_create_configure_api": "Możesz dostosować zakresy adresów za pomocą API.", "pci_projects_project_instances_add_numInstances_3az_help": "Jeśli tworzysz wiele instancji w regionie 3AZ, wszystkie znajdą się w tej samej strefie – niezależnie od tego, czy wybierzesz ją samodzielnie, czy skorzystasz z opcji „wybierz za mnie”.", - "pci_projects_project_instances_add_billing_floating_ip_detail_price_hourly": "Otrzymasz fakturę na kwotę {{floatingIpPrice}} netto/godz." + "pci_projects_project_instances_add_billing_floating_ip_detail_price_hourly": "Otrzymasz fakturę na kwotę {{floatingIpPrice}} netto/godz.", + "pci_projects_project_instances_add_image_distant_warning": "Utworzenie instancji z kopii zapasowej w zdalnym regionie wymaga wcześniejszego skopiowania obrazu do lokalnej przestrzeni. Operacja ta będzie zafakturowana zgodnie ze standardowym cennikiem." } diff --git a/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_pt_PT.json b/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_pt_PT.json index cde61cd194b0..f9ca006d0abd 100644 --- a/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_pt_PT.json +++ b/packages/manager/modules/pci/src/projects/project/instances/add/translations/Messages_pt_PT.json @@ -140,5 +140,6 @@ "pci_projects_project_network_private_create_announce_first_address": "Anunciar o primeiro endereço de um CIDR dado como gateway predefinido (DHCP opção 3)", "pci_projects_project_network_private_create_configure_api": "Pode personalizar os seus intervalos através da API.", "pci_projects_project_instances_add_numInstances_3az_help": "Quando criar várias instâncias numa região 3-AZ, todas elas estarão na mesma zona de disponibilidade, quer opte por uma zona ou pela opção \"escolher por mim\".", - "pci_projects_project_instances_add_billing_floating_ip_detail_price_hourly": "Será faturada a {{ floatingIpPrice }} + IVA/hora." + "pci_projects_project_instances_add_billing_floating_ip_detail_price_hourly": "Será faturada a {{ floatingIpPrice }} + IVA/hora.", + "pci_projects_project_instances_add_image_distant_warning": "A criação da sua instância a partir de um backup remoto exigirá uma cópia local da imagem remota, que será faturada ao preço habitual." } diff --git a/packages/manager/modules/pci/src/projects/project/instances/instance/backup/backup.controller.js b/packages/manager/modules/pci/src/projects/project/instances/instance/backup/backup.controller.js index 8196156e4d36..889216f94ae8 100644 --- a/packages/manager/modules/pci/src/projects/project/instances/instance/backup/backup.controller.js +++ b/packages/manager/modules/pci/src/projects/project/instances/instance/backup/backup.controller.js @@ -1,21 +1,35 @@ import get from 'lodash/get'; +import { formatISO } from 'date-fns'; import { FLAVORS_TYPE, HOURS_PER_MONTH } from './backup.contants'; +import { + LOCAL_ZONE_REGION, + ONE_AZ_REGION, + THREE_AZ_REGION, +} from '../../../project.constants'; export default class PciInstanceBackupController { /* @ngInject */ constructor( $filter, $translate, + $scope, + $q, CucCloudMessage, PciProjectsProjectInstanceService, + OvhApiCloudProjectRegion, + ovhManagerRegionService, atInternet, coreConfig, ) { this.$filter = $filter; this.$translate = $translate; + this.$scope = $scope; + this.$q = $q; this.CucCloudMessage = CucCloudMessage; this.PciProjectsProjectInstanceService = PciProjectsProjectInstanceService; + this.OvhApiCloudProjectRegion = OvhApiCloudProjectRegion; + this.ovhManagerRegionService = ovhManagerRegionService; this.atInternet = atInternet; this.FLAVORS_TYPE = FLAVORS_TYPE; this.coreConfig = coreConfig; @@ -23,28 +37,116 @@ export default class PciInstanceBackupController { $onInit() { this.backup = { - name: `${this.instance.name} ${this.$filter('date')( - new Date(), - 'short', - )}`, + name: `${this.instance.name} ${formatISO(new Date())}`, + distantRegion: null, + distantSnapshotName: `${this.instance.name} ${formatISO(new Date())}`, }; - this.isLoading = false; + this.distantSnapshot = false; + + this.isLoading = true; + this.$q + .all([ + this.PciProjectsProjectInstanceService.getDistantBackupAvailableRegions( + this.projectId, + this.instance.region, + ).then((regions) => { + this.distantBackupRegions = regions + .map((r) => { + let badgeClass = ''; + let badgeName = ''; + + if (r.type === THREE_AZ_REGION) { + badgeClass = 'oui-3az'; + badgeName = 'pci_project_flavors_zone_3az_region'; + } else if (r.type === ONE_AZ_REGION) { + badgeClass = 'oui-1az'; + badgeName = 'pci_project_flavors_zone_global_region'; + } else if (r.type === LOCAL_ZONE_REGION) { + badgeClass = 'oui-localzone'; + badgeName = 'pci_project_flavors_zone_localzone'; + } + + return { + ...r, + badgeClass, + badgeName, + }; + }) + .sort((a, b) => + a.microRegion.text.localeCompare(b.microRegion.text), + ); + }), + ]) + .then(() => { + this.isLoading = false; + }); + + const updateShowActivateRegionWarning = () => { + this.showActivateRegionWarning = + this.distantSnapshot && + this.shouldEnableRegion(this.backup.distantRegion); + }; + + this.$scope.$watch( + '$ctrl.backup.distantRegion', + updateShowActivateRegionWarning, + ); + this.$scope.$watch( + '$ctrl.distantSnapshot', + updateShowActivateRegionWarning, + ); } createBackup() { this.isLoading = true; this.trackBackupCreate(); - return this.PciProjectsProjectInstanceService.createBackup( - this.projectId, - this.instance, - this.backup, + + const backupToCreate = { + ...this.backup, + }; + if (!this.distantSnapshot) { + delete backupToCreate.distantSnapshotName; + delete backupToCreate.distantRegion; + } + + return (this.shouldEnableRegion(backupToCreate.distantRegion) + ? this.OvhApiCloudProjectRegion.v6() + .addRegion( + { serviceName: this.projectId }, + { region: backupToCreate.distantRegion }, + ) + .$promise.then(() => { + return this.$q.all([ + this.OvhApiCloudProjectRegion.AvailableRegions() + .v6() + .resetQueryCache(), + this.PciProjectsProjectInstanceService.getSnapshotAvailability( + this.projectId, + this.catalogEndpoint, + ).then((snapshotAvailability) => { + this.snapshotAvailability[backupToCreate.distantRegion] = + snapshotAvailability[backupToCreate.distantRegion]; + }), + ]); + }) + : Promise.resolve() ) + .then(() => { + return this.PciProjectsProjectInstanceService.createBackup( + this.projectId, + this.instance, + backupToCreate, + ); + }) .then(() => this.goBack( this.$translate.instant( - 'pci_projects_project_instances_instance_backup_success_message', + backupToCreate.distantRegion + ? 'pci_projects_project_instances_instance_backup_with_distant_success_message' + : 'pci_projects_project_instances_instance_backup_success_message', { - backup: this.backup.name, + backup: backupToCreate.name, + distantName: backupToCreate.distantSnapshotName, }, ), ), @@ -55,7 +157,7 @@ export default class PciInstanceBackupController { 'pci_projects_project_instances_instance_backup_error_backup', { message: get(err, 'data.message', null), - backup: this.backup.name, + backup: backupToCreate.name, }, ), 'error', @@ -86,4 +188,16 @@ export default class PciInstanceBackupController { }, ).format(value * HOURS_PER_MONTH)}`; } + + getRegionGroup(region) { + return region.continent; + } + + shouldEnableRegion(regionName) { + if (!regionName) return false; + + const region = this.distantBackupRegions.find((r) => r.name === regionName); + + return !!region && !region.enabled; + } } diff --git a/packages/manager/modules/pci/src/projects/project/instances/instance/backup/backup.html b/packages/manager/modules/pci/src/projects/project/instances/instance/backup/backup.html index c3106589951a..8563763a1b9e 100644 --- a/packages/manager/modules/pci/src/projects/project/instances/instance/backup/backup.html +++ b/packages/manager/modules/pci/src/projects/project/instances/instance/backup/backup.html @@ -15,12 +15,10 @@ > + + +
+ + + + + + +
+ + + + + +
+
+ + +
+
+ + + + + diff --git a/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_de_DE.json b/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_de_DE.json index 2511c8fd24f3..b3c0020eab87 100644 --- a/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_de_DE.json +++ b/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_de_DE.json @@ -9,5 +9,11 @@ "pci_projects_project_instances_instance_backup_error_backup": "Bei der Erstellung des Backups {{ backup }} ist ein Fehler aufgetreten: {{message}}", "pci_projects_project_instances_instance_backup_warning_iops_message": "Das Backup betrifft nur Ihre Root-Festplatte (Betriebssystem). Für Daten auf NVMe-Geräten wird kein Backup erstellt. Bitte verwenden Sie hierfür Ihre eigenen Tools.", "pci_projects_project_instances_instance_backup_trusted_zone_extra_amount_charge_message": "Der angezeigte Preis entspricht dem Preis der Dienstleistung. Der Aufpreis für die Trusted Zone wird der monatlichen Gesamtabrechnung hinzugefügt.", - "pci_projects_project_instances_instance_backup_warning_message_baremetal": "Ihre Instanz wird angehalten und in den Rescue-Modus versetzt, um das Backup zu erstellen. Sobald das Backup erstellt ist, wird Ihre Instanz neu gestartet. Je nach Größe Ihrer Festplatten kann dieser Vorgang einige Zeit in Anspruch nehmen." + "pci_projects_project_instances_instance_backup_warning_message_baremetal": "Ihre Instanz wird angehalten und in den Rescue-Modus versetzt, um das Backup zu erstellen. Sobald das Backup erstellt ist, wird Ihre Instanz neu gestartet. Je nach Größe Ihrer Festplatten kann dieser Vorgang einige Zeit in Anspruch nehmen.", + "pci_projects_project_instances_instance_backup_distant_label": "Ein Remote-Backup hinzufügen (optional)", + "pci_projects_project_instances_instance_backup_distant_name_label": "Name des Remote-Backups", + "pci_projects_project_instances_instance_backup_distant_region_label": "Wählen Sie einen Standort aus.", + "pci_projects_project_instances_instance_backup_distant_region_price": "Jedes Backup wird in Rechnung gestellt: {{ price }} inkl. MwSt./Monat/GB", + "pci_projects_project_instances_instance_backup_region_enable_warning": "Der ausgewählte Standort ist nicht aktiviert. Wenn Sie auf „Bestätigen“ klicken, wird er Ihrem Projekt hinzugefügt.", + "pci_projects_project_instances_instance_backup_with_distant_success_message": "Das Backup {{ backup }} wird erstellt. Sobald dieses lokale Backup erstellt wurde, erstellen wir das Remote-Backup mit dem Namen {{ distantName }}. Um sich den Status und die Details des Vorgangs anzusehen, wechseln Sie zur Seite „Instance Backup“. Wenn das Backup nicht sofort angezeigt wird, können Sie Ihre Seite aktualisieren, damit auch die Liste aktualisiert wird." } diff --git a/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_en_GB.json b/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_en_GB.json index 251abd20c2a1..564f3fcffa89 100644 --- a/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_en_GB.json +++ b/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_en_GB.json @@ -9,5 +9,11 @@ "pci_projects_project_instances_instance_backup_error_backup": "An error has occurred creating the {{ backup }} backup: {{ message }}", "pci_projects_project_instances_instance_backup_warning_iops_message": "The backup only includes your root disk (operating system). None of the data stored on your NVMe devices will be backed up. Please use your own tools for this.", "pci_projects_project_instances_instance_backup_trusted_zone_extra_amount_charge_message": "The price displayed corresponds to the price of the service. The trust zone surcharge is added to the overall monthly bill.", - "pci_projects_project_instances_instance_backup_warning_message_baremetal": "Your instance will be stopped and put in rescue mode, so that the backup can be created. Once the backup is complete, your instance will restart. Depending on the size of your disks, this may take a while." + "pci_projects_project_instances_instance_backup_warning_message_baremetal": "Your instance will be stopped and put in rescue mode, so that the backup can be created. Once the backup is complete, your instance will restart. Depending on the size of your disks, this may take a while.", + "pci_projects_project_instances_instance_backup_distant_label": "Add a remote backup (Option)", + "pci_projects_project_instances_instance_backup_distant_name_label": "Remote backup name", + "pci_projects_project_instances_instance_backup_distant_region_label": "Select a region", + "pci_projects_project_instances_instance_backup_distant_region_price": "Each backup will be billed at {{ price }} ex. VAT/month/GB", + "pci_projects_project_instances_instance_backup_region_enable_warning": "The selected location is not currently active. Click ‘confirm’ to add it to your project.", + "pci_projects_project_instances_instance_backup_with_distant_success_message": "Creating the {{ backup }} backup... Once the local backup is created, we will create a remote backup and rename it {{ distantName }}. To view the status and details of the operation, please go to the Instance Backup page. If the backup doesn’t appear right away, please refresh the page to update list." } diff --git a/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_es_ES.json b/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_es_ES.json index 15f7f1a2409e..22fb6f19d30e 100644 --- a/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_es_ES.json +++ b/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_es_ES.json @@ -9,5 +9,11 @@ "pci_projects_project_instances_instance_backup_error_backup": "Se ha producido un error al crear el backup {{ backup }}: {{ message }}.", "pci_projects_project_instances_instance_backup_warning_iops_message": "Solo se realizará la copia de seguridad del disco raíz (sistema operativo). No se guardará ningún dato desde los periféricos NVMe. Para realizar copias de seguridad de estos datos deberá utilizar sus propias herramientas.", "pci_projects_project_instances_instance_backup_trusted_zone_extra_amount_charge_message": "El precio mostrado corresponde al precio del servicio. El recargo correspondiente a la zona de confianza (Trusted Zone) se añade a la facturación mensual global.", - "pci_projects_project_instances_instance_backup_warning_message_baremetal": "La instancia se detendrá y entrará en modo de rescate para poder crear la copia de seguridad. Una vez realizada la copia de seguridad, la instancia se reiniciará. La duración de esta operación dependerá del tamaño de los discos." + "pci_projects_project_instances_instance_backup_warning_message_baremetal": "La instancia se detendrá y entrará en modo de rescate para poder crear la copia de seguridad. Una vez realizada la copia de seguridad, la instancia se reiniciará. La duración de esta operación dependerá del tamaño de los discos.", + "pci_projects_project_instances_instance_backup_distant_label": "Añadir un backup remoto (opcional)", + "pci_projects_project_instances_instance_backup_distant_name_label": "Nombre del backup remoto", + "pci_projects_project_instances_instance_backup_distant_region_label": "Seleccione una localización", + "pci_projects_project_instances_instance_backup_distant_region_price": "Cada backup tiene un precio de: {{ price }}/mes + IVA por GB", + "pci_projects_project_instances_instance_backup_region_enable_warning": "La localización seleccionada no está activada. Al hacer clic en «Confirmar», se añadirá a su proyecto.", + "pci_projects_project_instances_instance_backup_with_distant_success_message": "Creando el backup {{ backup }}... Una vez creado el backup local, procederemos a crear el backup remoto denominado {{ distantName }}. Para consultar el estado y los detalles de la operación, acceda a la página Instance Backup. Si el backup no aparece inmediatamente, recargue la página para actualizar la lista." } diff --git a/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_fr_CA.json b/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_fr_CA.json index 10b36f0ec95b..5272600b7d62 100644 --- a/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_fr_CA.json +++ b/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_fr_CA.json @@ -9,5 +9,11 @@ "pci_projects_project_instances_instance_backup_success_message": "Le backup {{ backup }} est en cours de création. Pour visualiser son statut et les détails de l'opération, veuillez vous rendre sur la page 'Instance Backup'. Si le backup ne s'affiche pas immédiatement, n'hésitez pas à rafraîchir votre page pour mettre à jour la liste.", "pci_projects_project_instances_instance_backup_error_backup": "Une erreur est survenue lors de la création du backup {{ backup }} : {{ message }}", "pci_projects_project_instances_instance_backup_warning_iops_message": "La sauvegarde ne concerne que votre disque racine (système d'exploitation). Aucune donnée ne sera sauvegardée à partir de périphériques NVMe. Veuillez utiliser vos propres outils pour cela.", - "pci_projects_project_instances_instance_backup_trusted_zone_extra_amount_charge_message": "Le prix affiché correspond au prix du service. La majoration liée à la zone de confiance est ajoutée à la facturation mensuelle globale." + "pci_projects_project_instances_instance_backup_trusted_zone_extra_amount_charge_message": "Le prix affiché correspond au prix du service. La majoration liée à la zone de confiance est ajoutée à la facturation mensuelle globale.", + "pci_projects_project_instances_instance_backup_distant_label": "Ajouter un backup distant (Option)", + "pci_projects_project_instances_instance_backup_distant_name_label": "Nom du backup distant", + "pci_projects_project_instances_instance_backup_distant_region_label": "Sélectionnez une localisation", + "pci_projects_project_instances_instance_backup_distant_region_price": "Chaque sauvegarde sera facturée : {{price}} HT/mois/Go", + "pci_projects_project_instances_instance_backup_region_enable_warning": "La localisation sélectionnée n’est pas activée. En cliquant sur \"confirmer\" vous l'ajouterez à votre projet.", + "pci_projects_project_instances_instance_backup_with_distant_success_message": "Le backup {{ backup }} est en cours de création. Une fois ce backup local créé, nous procéderons à la création du backup distant nommé {{ distantName }}. Pour visualiser le statut et les détails de l'opération, veuillez vous rendre sur la page 'Instance Backup'. Si le backup ne s'affiche pas immédiatement, n'hésitez pas à rafraîchir votre page pour mettre à jour la liste." } diff --git a/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_fr_FR.json b/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_fr_FR.json index 10b36f0ec95b..5272600b7d62 100644 --- a/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_fr_FR.json +++ b/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_fr_FR.json @@ -9,5 +9,11 @@ "pci_projects_project_instances_instance_backup_success_message": "Le backup {{ backup }} est en cours de création. Pour visualiser son statut et les détails de l'opération, veuillez vous rendre sur la page 'Instance Backup'. Si le backup ne s'affiche pas immédiatement, n'hésitez pas à rafraîchir votre page pour mettre à jour la liste.", "pci_projects_project_instances_instance_backup_error_backup": "Une erreur est survenue lors de la création du backup {{ backup }} : {{ message }}", "pci_projects_project_instances_instance_backup_warning_iops_message": "La sauvegarde ne concerne que votre disque racine (système d'exploitation). Aucune donnée ne sera sauvegardée à partir de périphériques NVMe. Veuillez utiliser vos propres outils pour cela.", - "pci_projects_project_instances_instance_backup_trusted_zone_extra_amount_charge_message": "Le prix affiché correspond au prix du service. La majoration liée à la zone de confiance est ajoutée à la facturation mensuelle globale." + "pci_projects_project_instances_instance_backup_trusted_zone_extra_amount_charge_message": "Le prix affiché correspond au prix du service. La majoration liée à la zone de confiance est ajoutée à la facturation mensuelle globale.", + "pci_projects_project_instances_instance_backup_distant_label": "Ajouter un backup distant (Option)", + "pci_projects_project_instances_instance_backup_distant_name_label": "Nom du backup distant", + "pci_projects_project_instances_instance_backup_distant_region_label": "Sélectionnez une localisation", + "pci_projects_project_instances_instance_backup_distant_region_price": "Chaque sauvegarde sera facturée : {{price}} HT/mois/Go", + "pci_projects_project_instances_instance_backup_region_enable_warning": "La localisation sélectionnée n’est pas activée. En cliquant sur \"confirmer\" vous l'ajouterez à votre projet.", + "pci_projects_project_instances_instance_backup_with_distant_success_message": "Le backup {{ backup }} est en cours de création. Une fois ce backup local créé, nous procéderons à la création du backup distant nommé {{ distantName }}. Pour visualiser le statut et les détails de l'opération, veuillez vous rendre sur la page 'Instance Backup'. Si le backup ne s'affiche pas immédiatement, n'hésitez pas à rafraîchir votre page pour mettre à jour la liste." } diff --git a/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_it_IT.json b/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_it_IT.json index 1cce7fe8a8f2..4e51e9c8af3e 100644 --- a/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_it_IT.json +++ b/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_it_IT.json @@ -9,5 +9,11 @@ "pci_projects_project_instances_instance_backup_error_backup": "Si è verificato un errore durante la creazione del backup {{ backup }}: {{ message }}", "pci_projects_project_instances_instance_backup_warning_iops_message": "Il backup sarà relativo esclusivamente al disco di root (sistema operativo). Non verrà salvato nessun dato dalle periferiche NVMe. Per eseguire questa operazione, utilizza i tool a tua disposizione.", "pci_projects_project_instances_instance_backup_trusted_zone_extra_amount_charge_message": "il prezzo visualizzato corrisponde al prezzo del servizio. Il costo supplementare per la Trusted Zone viene aggiunto alla fatturazione mensile totale.", - "pci_projects_project_instances_instance_backup_warning_message_baremetal": "Per poter creare il backup, l’istanza verrà arrestata e attivata in modalità di ripristino. Una volta effettuato il backup, l'istanza verrà riavviata. La durata dell’operazione varierà in base alla dimensione dei dischi." + "pci_projects_project_instances_instance_backup_warning_message_baremetal": "Per poter creare il backup, l’istanza verrà arrestata e attivata in modalità di ripristino. Una volta effettuato il backup, l'istanza verrà riavviata. La durata dell’operazione varierà in base alla dimensione dei dischi.", + "pci_projects_project_instances_instance_backup_distant_label": "Aggiungere un backup remoto (opzionale)", + "pci_projects_project_instances_instance_backup_distant_name_label": "Nome del backup remoto", + "pci_projects_project_instances_instance_backup_distant_region_label": "Seleziona una localizzazione", + "pci_projects_project_instances_instance_backup_distant_region_price": "Ogni backup viene fatturato {{price}} +IVA/mese/GB", + "pci_projects_project_instances_instance_backup_region_enable_warning": "La localizzazione selezionata non è attivata. Cliccando su \"Confermare\" la aggiungerai al tuo progetto.", + "pci_projects_project_instances_instance_backup_with_distant_success_message": "Creazione del backup {{ backup }} in corso... Una volta creato il backup locale, procederemo alla creazione del backup remoto {{ distantName }}. Per visualizzare lo stato e i dettagli dell'operazione, accedi alla pagina “Instance Backup”. Se il backup non compare immediatamente, fai il refresh della pagina per aggiornare la lista." } diff --git a/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_pl_PL.json b/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_pl_PL.json index 1e5d29d6e02a..9623f0bd52e4 100644 --- a/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_pl_PL.json +++ b/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_pl_PL.json @@ -9,5 +9,11 @@ "pci_projects_project_instances_instance_backup_error_backup": "Wystąpił błąd podczas tworzenia kopii zapasowej {{ backup }}: {{ message }}", "pci_projects_project_instances_instance_backup_warning_iops_message": "Zostanie wykonana tylko kopia zapasowa dysku (systemu operacyjnego). Nie zostanie wykonana żadna kopia zapasowa danych z urządzeń NVMe. Użyj do tego własnych narzędzi.", "pci_projects_project_instances_instance_backup_trusted_zone_extra_amount_charge_message": "Podana cena odpowiada cenie usługi. Dopłata za opcję Trusted Zone jest doliczana do kwoty miesięcznej faktury.", - "pci_projects_project_instances_instance_backup_warning_message_baremetal": "Twoja instancja zostanie zatrzymana i przełączona w tryb awaryjny, aby można było utworzyć kopię zapasową. Po wykonaniu kopii zapasowej Twoja instancja zostanie ponownie uruchomiona. W zależności od rozmiaru dysków może to potrwać kilka minut." + "pci_projects_project_instances_instance_backup_warning_message_baremetal": "Twoja instancja zostanie zatrzymana i przełączona w tryb awaryjny, aby można było utworzyć kopię zapasową. Po wykonaniu kopii zapasowej Twoja instancja zostanie ponownie uruchomiona. W zależności od rozmiaru dysków może to potrwać kilka minut.", + "pci_projects_project_instances_instance_backup_distant_label": "Dodaj zdalny backup (opcjonalnie)", + "pci_projects_project_instances_instance_backup_distant_name_label": "Nazwa zdalnego backupu", + "pci_projects_project_instances_instance_backup_distant_region_label": "Wybierz lokalizację", + "pci_projects_project_instances_instance_backup_distant_region_price": "Każda kopia zapasowa będzie płatna: {{price}} netto/m-c/GB", + "pci_projects_project_instances_instance_backup_region_enable_warning": "Wybrana lokalizacja nie jest aktywna. Klikając „Zatwierdź”, dodasz ją do swojego projektu.", + "pci_projects_project_instances_instance_backup_with_distant_success_message": "Trwa tworzenie kopii instancji {{backup}}. Po utworzeniu lokalnego backupu utworzymy backup zdalny {{distantName}}. Aby sprawdzić status i szczegóły operacji, przejdź do strony „Instance Backup”. Jeśli kopia zapasowa nie wyświetla się natychmiast, odśwież stronę, aby zaktualizować listę." } diff --git a/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_pt_PT.json b/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_pt_PT.json index 67d6711742dd..329121a42431 100644 --- a/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_pt_PT.json +++ b/packages/manager/modules/pci/src/projects/project/instances/instance/backup/translations/Messages_pt_PT.json @@ -9,5 +9,11 @@ "pci_projects_project_instances_instance_backup_error_backup": "Ocorreu um erro durante a criação do backup {{ backup }}: {{ message }}", "pci_projects_project_instances_instance_backup_warning_iops_message": "A cópia de segurança só inclui o seu disco de raiz (sistema operativo). Não será feito nenhum backup dos dados armazenados nos seus dispositivos NVMe. Utilize as suas próprias ferramentas para o fazer.", "pci_projects_project_instances_instance_backup_trusted_zone_extra_amount_charge_message": "O preço indicado corresponde ao preço do serviço. O custo adicional relativo ao Trusted Zone é adicionado à faturação mensal global.", - "pci_projects_project_instances_instance_backup_warning_message_baremetal": "Para poder criar o backup, a sua instância será interrompida e entrará em modo rescue. Depois de efetuar o backup, a instância será reiniciada. A duração desta operação poderá variar em função do tamanho dos discos." + "pci_projects_project_instances_instance_backup_warning_message_baremetal": "Para poder criar o backup, a sua instância será interrompida e entrará em modo rescue. Depois de efetuar o backup, a instância será reiniciada. A duração desta operação poderá variar em função do tamanho dos discos.", + "pci_projects_project_instances_instance_backup_distant_label": "Adicionar um backup remoto (opcional)", + "pci_projects_project_instances_instance_backup_distant_name_label": "Nome do backup remoto", + "pci_projects_project_instances_instance_backup_distant_region_label": "Selecione uma localização", + "pci_projects_project_instances_instance_backup_distant_region_price": "Cada backup será faturado: {{ price }}/mês + IVA por GB", + "pci_projects_project_instances_instance_backup_region_enable_warning": "A localização escolhida não está ativada. Ao clicar em \"Confirmar\", será adicionado ao seu projeto.", + "pci_projects_project_instances_instance_backup_with_distant_success_message": "O backup {{ backup }} está em processo de criação. Uma vez criado este backup local, procederemos à criação do backup remoto designado {{ distantName }}. Para visualizar o estado e os detalhes da operação, aceda à página “Instance Backup”. Se o backup não for apresentado imediatamente, não hesite em atualizar a página para obter a lista mais recente." } diff --git a/packages/manager/modules/pci/src/projects/project/instances/instance/edit/edit.component.js b/packages/manager/modules/pci/src/projects/project/instances/instance/edit/edit.component.js index b8e91e4e5af7..85ca31672cf9 100644 --- a/packages/manager/modules/pci/src/projects/project/instances/instance/edit/edit.component.js +++ b/packages/manager/modules/pci/src/projects/project/instances/instance/edit/edit.component.js @@ -8,7 +8,6 @@ export default { catalogEndpoint: '<', excludeCategories: '<', goBack: '<', - imageEditMessage: '<', imageEditSuccessMessage: '<', instance: '<', hasComingSoonFlavorTag: '<', diff --git a/packages/manager/modules/pci/src/projects/project/instances/instance/edit/edit.controller.js b/packages/manager/modules/pci/src/projects/project/instances/instance/edit/edit.controller.js index b207e17437ec..3d5bbd2dc742 100644 --- a/packages/manager/modules/pci/src/projects/project/instances/instance/edit/edit.controller.js +++ b/packages/manager/modules/pci/src/projects/project/instances/instance/edit/edit.controller.js @@ -53,9 +53,6 @@ export default class PciInstanceEditController { }; this.regionsTypesAvailability = {}; this.fetchRegionsTypesAvailability(); - this.imageEditMessage = - this.imageEditMessage || - 'pci_projects_project_instances_instance_edit_reboot_message'; this.imageEditSuccessMessage = this.imageEditSuccessMessage || 'pci_projects_project_instances_instance_edit_image_success_message'; @@ -125,8 +122,10 @@ export default class PciInstanceEditController { } if (image.isBackup()) { this.editInstance.imageId = image.id; + this.editInstance.region = image.region; } else { this.editInstance.imageId = image.getIdByRegion(this.instance.region); + this.editInstance.region = image.region; } } else { this.editInstance.imageId = null; @@ -212,8 +211,10 @@ export default class PciInstanceEditController { reinstallInstance() { this.isLoading = true; - return this.PciProjectsProjectInstanceService.reinstall( + return this.PciProjectsProjectInstanceService.reinstallFromRegion( this.projectId, + this.instance.region, + this.instance.id, this.editInstance, ) .then(() => diff --git a/packages/manager/modules/pci/src/projects/project/instances/instance/edit/edit.html b/packages/manager/modules/pci/src/projects/project/instances/instance/edit/edit.html index 2de4ecffec70..e16afe4eb80b 100644 --- a/packages/manager/modules/pci/src/projects/project/instances/instance/edit/edit.html +++ b/packages/manager/modules/pci/src/projects/project/instances/instance/edit/edit.html @@ -237,14 +237,10 @@

data-type="warning" data-ng-if="$ctrl.editInstance.imageId && $ctrl.instance.image.id !== $ctrl.editInstance.imageId && $ctrl.model.isImageCompatible" > -
    -
  • -
  • -
+

i.isShelving(), diff --git a/packages/manager/modules/pci/src/projects/project/instances/instances.service.js b/packages/manager/modules/pci/src/projects/project/instances/instances.service.js index f1e0df13f2e7..33548c06e3f4 100644 --- a/packages/manager/modules/pci/src/projects/project/instances/instances.service.js +++ b/packages/manager/modules/pci/src/projects/project/instances/instances.service.js @@ -17,12 +17,14 @@ import { BANDWIDTH_LIMIT, BANDWIDTH_OUT_INVOICE, DEFAULT_IP, + DISTANT_BACKUP_FEATURE, FLAVORS_WITHOUT_ADDITIONAL_IPS, FLAVORS_WITHOUT_AUTOMATED_BACKUP, FLAVORS_WITHOUT_SOFT_REBOOT, FLAVORS_WITHOUT_SUSPEND, FLAVORS_WITHOUT_VNC, } from './instances.constants'; +import { ONE_AZ_REGION, THREE_AZ_REGION } from '../project.constants'; /* eslint class-methods-use-this: ["error", { "exceptMethods": ["getBaseApiRoute"] }] */ export default class PciProjectInstanceService { @@ -47,6 +49,7 @@ export default class PciProjectInstanceService { OvhApiOrderCatalogPublic, PciProjectRegions, PciProject, + ovhFeatureFlipping, ) { this.$http = $http; this.$q = $q; @@ -71,6 +74,7 @@ export default class PciProjectInstanceService { this.FLAVORS_WITHOUT_VNC = FLAVORS_WITHOUT_VNC; this.FLAVORS_WITHOUT_ADDITIONAL_IPS = FLAVORS_WITHOUT_ADDITIONAL_IPS; this.FLAVORS_WITHOUT_AUTOMATED_BACKUP = FLAVORS_WITHOUT_AUTOMATED_BACKUP; + this.ovhFeatureFlipping = ovhFeatureFlipping; this.licensePriceFormatter = new Intl.NumberFormat( this.coreConfig.getUserLocale().replace('_', '-'), @@ -251,6 +255,18 @@ export default class PciProjectInstanceService { ).$promise; } + reinstallFromRegion( + projectId, + instanceRegion, + instanceId, + { imageId, region }, + ) { + return this.$http.post( + `/cloud/project/${projectId}/region/${instanceRegion}/instance/${instanceId}/reinstall`, + { imageId, imageRegionName: region }, + ); + } + start(projectId, instance) { return this.$http.post( `${this.getBaseApiRoute(projectId, instance)}/instance/${ @@ -378,7 +394,11 @@ export default class PciProjectInstanceService { ); } - createBackup(projectId, { id: instanceId }, { name: snapshotName }) { + createBackup( + projectId, + { id: instanceId }, + { name: snapshotName, distantRegion, distantSnapshotName }, + ) { return this.OvhApiCloudProjectInstance.v6().backup( { serviceName: projectId, @@ -386,6 +406,8 @@ export default class PciProjectInstanceService { }, { snapshotName, + distantRegionName: distantRegion, + distantSnapshotName, }, ).$promise; } @@ -618,6 +640,47 @@ export default class PciProjectInstanceService { }); } + getDistantBackupAvailableRegions(projectId, instanceRegionName) { + return Promise.all([ + this.getProductAvailability(projectId, undefined, 'snapshot'), + this.ovhFeatureFlipping.checkFeatureAvailability(DISTANT_BACKUP_FEATURE), + ]).then(([productAvailability, feature]) => { + if (!feature.isFeatureAvailable(DISTANT_BACKUP_FEATURE)) return []; + + const instancePlan = productAvailability.plans.find( + (p) => + p.code.startsWith('snapshot.consumption') && + p.regions.some((r) => r.name === instanceRegionName), + ); + + const instanceRegion = instancePlan?.regions.find( + (r) => r.name === instanceRegionName, + ); + + if ( + !instanceRegion || + (instanceRegion.type !== THREE_AZ_REGION && + instanceRegion.type !== ONE_AZ_REGION) + ) + return []; + + const regions = productAvailability.plans + .filter((p) => p.code.startsWith('snapshot.consumption')) + .flatMap((p) => p.regions); + + return regions + .filter( + (r) => + r.name !== instanceRegionName && + (r.type === THREE_AZ_REGION || r.type === ONE_AZ_REGION), + ) + .map((r) => ({ + ...r, + ...this.ovhManagerRegionService.getRegion(r.name), + })); + }); + } + getProductAvailability( projectId, ovhSubsidiary = this.coreConfig.getUser().ovhSubsidiary, @@ -650,67 +713,40 @@ export default class PciProjectInstanceService { save( serviceName, + region, { autobackup, flavorId, imageId, + imageRegionName, monthlyBilling, name, - networks, - region, - sshKeyId, + network, + sshKey, userData, availabilityZone, }, number = 1, - isPrivateMode, ) { - const saveInstanceNamespace = 'instance-creation'; - const status = 'ACTIVE'; - if (number > 1) { - return this.$http - .post(`/cloud/project/${serviceName}/instance/bulk`, { - autobackup, - flavorId, - imageId, - monthlyBilling, - name, - networks, - region, - sshKeyId, - userData, - number, - availabilityZone, - }) - .then(({ data }) => { - return data; - }); - } return this.$http - .post(`/cloud/project/${serviceName}/instance`, { + .post(`/cloud/project/${serviceName}/region/${region}/instance`, { + bulk: number, autobackup, - flavorId, - imageId, - monthlyBilling, + flavor: { + id: flavorId, + }, + bootFrom: { + imageId, + imageRegionName, + }, + billingPeriod: monthlyBilling ? 'monthly' : 'hourly', name, - networks, - region, - sshKeyId, + network, + sshKey: sshKey ? { name: sshKey.name } : null, userData, availabilityZone, }) .then(({ data }) => { - if (isPrivateMode) { - const url = `/cloud/project/${serviceName}/instance/${data.id}`; - return this.checkOperationStatus( - url, - saveInstanceNamespace, - status, - ).then((res) => { - this.Poller.kill({ namespace: saveInstanceNamespace }); - return res; - }); - } return data; }); } @@ -1007,7 +1043,7 @@ export default class PciProjectInstanceService { ) .then(({ resourceId }) => { this.Poller.kill({ namespace: 'private-network-creation' }); - return this.getCreatedSubnet(projectId, region, resourceId); + return resourceId; }); } @@ -1025,14 +1061,6 @@ export default class PciProjectInstanceService { ); } - getCreatedSubnet(projectId, region, networkId) { - return this.$http - .get( - `/cloud/project/${projectId}/region/${region}/network/${networkId}/subnet`, - ) - .then(({ data }) => data); - } - associateGatewayToNetwork(projectId, region, gatewayId, subnetId) { return this.$http .post( diff --git a/packages/manager/modules/pci/src/projects/project/storages/instance-backups/add/add.component.js b/packages/manager/modules/pci/src/projects/project/storages/instance-backups/add/add.component.js index 31acfca2af64..aba8e8f5bd38 100644 --- a/packages/manager/modules/pci/src/projects/project/storages/instance-backups/add/add.component.js +++ b/packages/manager/modules/pci/src/projects/project/storages/instance-backups/add/add.component.js @@ -8,7 +8,6 @@ export default { excludeCategories: '<', backup: '<', goBack: '<', - publicNetworks: '<', privateNetworks: '<', projectId: '<', quota: '<', diff --git a/packages/manager/modules/pci/src/projects/project/storages/instance-backups/add/add.controller.js b/packages/manager/modules/pci/src/projects/project/storages/instance-backups/add/add.controller.js index b576c4422789..74c1e6681aa0 100644 --- a/packages/manager/modules/pci/src/projects/project/storages/instance-backups/add/add.controller.js +++ b/packages/manager/modules/pci/src/projects/project/storages/instance-backups/add/add.controller.js @@ -5,7 +5,6 @@ import Datacenter from '../../../../../components/project/regions-list/datacente import Quota from '../../../../../components/project/instance/quota/quota.class'; import { PATTERN } from '../../../../../components/project/instance/name/constants'; import Instance from '../../../../../components/project/instance/instance.class'; -import { BAREMETAL_LABEL, PUBLIC_NETWORK_TYPE_NAMES } from './add.constants'; import { THREE_AZ_REGION } from '../../../project.constants'; export default class PciInstancesAddController { @@ -28,7 +27,7 @@ export default class PciInstancesAddController { imageId: this.backup.id, region: this.backup.region, monthlyBilling: false, - sshKeyId: null, + sshKey: null, }); this.region = new Datacenter({ @@ -141,30 +140,6 @@ export default class PciInstancesAddController { this.messages = this.messageHandler.getMessages(); } - isBareMetalBackup() { - return !!this.backup.type.includes(BAREMETAL_LABEL); - } - - onPrivateNetworkChange(modelValue) { - const networkId = get(modelValue, 'id'); - const publicNetwork = this.publicNetworks?.find((network) => { - const networkName = this.isBareMetalBackup() - ? PUBLIC_NETWORK_TYPE_NAMES.BAREMETAL - : PUBLIC_NETWORK_TYPE_NAMES.CLASSIC; - return network.name === networkName; - }); - this.instance.networks = networkId - ? [ - { - networkId, - }, - { - networkId: publicNetwork?.id, - }, - ] - : []; - } - create() { this.isLoading = true; @@ -172,26 +147,54 @@ export default class PciInstancesAddController { this.instance.userData = null; } - this.instance.sshKeyId = get(this.model.sshKey, 'id'); + this.instance.sshKey = this.model.sshKey; - return this.PciProjectsProjectInstanceService.save( - this.projectId, - this.instance, - this.model.number, + return (this.model.privateNetwork.id + ? this.PciProjectsProjectInstanceService.getSubnets( + this.projectId, + this.model.privateNetwork.id, + ) + : Promise.resolve() ) - .then(() => { - const message = - this.model.number === 1 - ? this.$translate.instant( - 'pci_projects_project_instances_backup_add_success_message', - { - instance: this.instance.name, + .then((data) => { + const network = { + private: this.model.privateNetwork.id + ? { + network: { + id: this.model.privateNetwork.regions.find( + (r) => r.region === this.instance.region, + ).openstackId, + subnetId: data.find( + (subnet) => + subnet.ipPools[0].region === this.instance.region, + )?.id, }, - ) - : this.$translate.instant( - 'pci_projects_project_instances_backup_add_success_multiple_message', - ); - return this.goBack(message); + } + : null, + public: true, + }; + + return this.PciProjectsProjectInstanceService.save( + this.projectId, + this.instance.region, + { ...this.instance, network }, + this.model.number, + ); + }) + .then(() => { + let messageType = null; + if (this.model.privateNetwork.id) { + messageType = 'private_network'; + } + + return this.goBack( + this.$translate.instant( + `pci_projects_project_instances_backup_add_success_message${ + messageType ? `_${messageType}` : '' + }`, + ), + 'success', + ); }) .catch((error) => { let message; diff --git a/packages/manager/modules/pci/src/projects/project/storages/instance-backups/add/add.html b/packages/manager/modules/pci/src/projects/project/storages/instance-backups/add/add.html index a8bbd256556c..6908b2863a32 100644 --- a/packages/manager/modules/pci/src/projects/project/storages/instance-backups/add/add.html +++ b/packages/manager/modules/pci/src/projects/project/storages/instance-backups/add/add.html @@ -143,7 +143,6 @@

data-items="$ctrl.availablePrivateNetworks" data-match="name" data-searchable - data-on-change="$ctrl.onPrivateNetworkChange(modelValue)" > diff --git a/packages/manager/modules/pci/src/projects/project/storages/instance-backups/add/add.routing.js b/packages/manager/modules/pci/src/projects/project/storages/instance-backups/add/add.routing.js index f6dd3e924bda..d87665cbf342 100644 --- a/packages/manager/modules/pci/src/projects/project/storages/instance-backups/add/add.routing.js +++ b/packages/manager/modules/pci/src/projects/project/storages/instance-backups/add/add.routing.js @@ -35,11 +35,6 @@ export default /* @ngInject */ ($stateProvider) => { projectId, }), - publicNetworks: /* @ngInject */ ( - PciProjectsProjectInstanceService, - projectId, - ) => PciProjectsProjectInstanceService.getPublicNetwork(projectId), - privateNetworks: /* @ngInject */ ( backup, PciProjectsProjectInstanceService, @@ -64,7 +59,7 @@ export default /* @ngInject */ ($stateProvider) => { }); }, - goBack: /* @ngInject */ (goToInstanceBackups) => goToInstanceBackups, + goBack: /* @ngInject */ (goToInstances) => goToInstances, }, }); }; diff --git a/packages/manager/modules/pci/src/projects/project/storages/instance-backups/add/translations/Messages_fr_FR.json b/packages/manager/modules/pci/src/projects/project/storages/instance-backups/add/translations/Messages_fr_FR.json index 4dca2ac813cb..a21d4f4d2e91 100644 --- a/packages/manager/modules/pci/src/projects/project/storages/instance-backups/add/translations/Messages_fr_FR.json +++ b/packages/manager/modules/pci/src/projects/project/storages/instance-backups/add/translations/Messages_fr_FR.json @@ -17,8 +17,10 @@ "pci_projects_project_instances_backup_add_cancel": "Annuler", "pci_projects_project_instances_backup_add_save_form": "Création de l'instance en cours", "pci_projects_project_instances_backup_add_save_multiple_form": "Création des instances en cours", - "pci_projects_project_instances_backup_add_success_message": "L'instance {{ instance }} a été ajoutée.", - "pci_projects_project_instances_backup_add_success_multiple_message": "Les instances ont été ajoutées.", + + "pci_projects_project_instances_backup_add_success_message_private_network": "L'opération de création d'instance(s) est en cours, la création de l'instance(s) peut prendre quelques minutes", + "pci_projects_project_instances_backup_add_success_message": "L(es) opération(s) de création d'instance(s) est en cours", + "pci_projects_project_instances_backup_add_error_save": "Une erreur est survenue lors de l'ajout de l'instance {{ instance }} : {{ message }}", "pci_projects_project_instances_backup_add_error_multiple_save": "Une erreur est survenue lors de l'ajout des instances : {{ message }}", "pci_projects_project_instances_backup_add_flex": "Instance flexible", diff --git a/packages/manager/modules/pci/src/projects/project/storages/instance-backups/instance-backups.routing.js b/packages/manager/modules/pci/src/projects/project/storages/instance-backups/instance-backups.routing.js index dff61bbc28f0..c4a69b9c179e 100644 --- a/packages/manager/modules/pci/src/projects/project/storages/instance-backups/instance-backups.routing.js +++ b/packages/manager/modules/pci/src/projects/project/storages/instance-backups/instance-backups.routing.js @@ -68,6 +68,30 @@ export default /* @ngInject */ ($stateProvider) => { return promise; }, + goToInstances: /* @ngInject */ (CucCloudMessage, $state, projectId) => ( + message = false, + type = 'success', + ) => { + const reload = message && type === 'success'; + + const promise = $state.go( + 'pci.projects.project.instances', + { + projectId, + }, + { + reload, + }, + ); + + if (message) { + promise.then(() => + CucCloudMessage[type](message, 'pci.projects.project.instances'), + ); + } + + return promise; + }, breadcrumb: /* @ngInject */ ($translate) => $translate .refresh()