diff --git a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_de_DE.json b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_de_DE.json index e72bd4571650..599ae4638ca2 100644 --- a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_de_DE.json +++ b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_de_DE.json @@ -8,5 +8,6 @@ "status-FAILED": "Fehlgeschlagen", "status-ERROR": "Fehler", "status-STOPPED": "Angehalten", - "status-RUNNING": "In Betrieb" + "status-RUNNING": "In Betrieb", + "status-STANDBY": "Bereit" } diff --git a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_en_GB.json b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_en_GB.json index babfedc4e823..0689f36b6df1 100644 --- a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_en_GB.json +++ b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_en_GB.json @@ -8,5 +8,6 @@ "status-FAILED": "Failed", "status-ERROR": "Error", "status-STOPPED": "Stopped", - "status-RUNNING": "Running" + "status-RUNNING": "Running", + "status-STANDBY": "Standby" } diff --git a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_es_ES.json b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_es_ES.json index 02ac6f3161da..2232584cacec 100644 --- a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_es_ES.json +++ b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_es_ES.json @@ -8,5 +8,6 @@ "status-FAILED": "Fallido", "status-ERROR": "Error", "status-STOPPED": "Detenida", - "status-RUNNING": "En ejecución" + "status-RUNNING": "En ejecución", + "status-STANDBY": "En espera" } diff --git a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_fr_CA.json b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_fr_CA.json index 9a233b24d9b9..00a06aec1749 100644 --- a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_fr_CA.json +++ b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_fr_CA.json @@ -8,5 +8,6 @@ "status-FAILED": "Echec", "status-ERROR": "Erreur", "status-STOPPED": "Arrêtée", - "status-RUNNING": "En service" + "status-RUNNING": "En service", + "status-STANDBY": "En veille" } diff --git a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_fr_FR.json b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_fr_FR.json index 9a233b24d9b9..00a06aec1749 100644 --- a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_fr_FR.json +++ b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_fr_FR.json @@ -8,5 +8,6 @@ "status-FAILED": "Echec", "status-ERROR": "Erreur", "status-STOPPED": "Arrêtée", - "status-RUNNING": "En service" + "status-RUNNING": "En service", + "status-STANDBY": "En veille" } diff --git a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_it_IT.json b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_it_IT.json index a1eae47b9561..797d6a58b47e 100644 --- a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_it_IT.json +++ b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_it_IT.json @@ -8,5 +8,6 @@ "status-FAILED": "Fallito", "status-ERROR": "Errore", "status-STOPPED": "Arrestato", - "status-RUNNING": "In servizio" + "status-RUNNING": "In servizio", + "status-STANDBY": "In standby" } diff --git a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_pl_PL.json b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_pl_PL.json index b46d5cf69a20..2589cb93e2e1 100644 --- a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_pl_PL.json +++ b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_pl_PL.json @@ -8,5 +8,6 @@ "status-FAILED": "Błąd", "status-ERROR": "Błąd", "status-STOPPED": "Zatrzymano", - "status-RUNNING": "Uruchomiony" + "status-RUNNING": "Uruchomiony", + "status-STANDBY": "W gotowości" } diff --git a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_pt_PT.json b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_pt_PT.json index 8b87a9b0e2c9..7c52f08c8fee 100644 --- a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_pt_PT.json +++ b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/apps/status/Messages_pt_PT.json @@ -8,5 +8,6 @@ "status-FAILED": "Falhado", "status-ERROR": "Erro", "status-STOPPED": "Interrompida", - "status-RUNNING": "Em serviço" + "status-RUNNING": "Em serviço", + "status-STANDBY": "Em espera" } diff --git a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_de_DE.json b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_de_DE.json index 1e2c27c09c83..1c0c65cd18a9 100644 --- a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_de_DE.json +++ b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_de_DE.json @@ -4,32 +4,48 @@ "scalingStratActiveLabel": "Autoscaling aktiviert", "noScalingStratActiveLabel": "Autoscaling deaktiviert", "replicasInputLabel": "Anzahl der Replikas, auf denen Ihre App deployt wird", - "haInfoHelper": "Deployen Sie Ihre App für Hochverfügbarkeit auf mindestens 2 Replikas", + "haInfoHelper": "Deployen Sie Ihre App für Hochverfügbarkeit auf mindestens 2 Replikas.", "replicasMinInputLabel": "Mindestanzahl an Replikas", "replicasMaxInputLabel": "Höchstzahl an Replikas", "resourceTypeLabel": "Überwachte Metrik", "resourceTypeInfo": "Metrik für das Auslösen von Autoscaling.", "treshholderTargetLabel": "Schwellenwert (%)", - "treshholderTargetInfo": "Wert der Metrik in %", + "treshholderTargetInfo": "Zielwert der durchschnittlichen Ressourcennutzung (%) zur Berechnung der gewünschten Anzahl von Replikaten.", "scalingBillingInfo": "Bitte beachten Sie, dass die ausgewählte Anzahl an Replikas die Anzahl der bezogenen Ressourcen entsprechend erhöht.", - "errorFormMinMaxRepField": "Die Mindestanzahl an Replikas muss kleiner als die Höchstzahl an Replikas sein", + "errorFormMinMaxRepField": "Die Mindestanzahl an Replikas muss kleiner als die Höchstzahl an Replikas sein.", + "replicasMinRangeError": "Der Wert muss zwischen 0 und 10 liegen.", + "replicasMaxRangeError": "Der Wert muss zwischen 1 und 10 liegen.", "metricUrlLabel": "URL der Metrik", "metricUrlPlaceholder": "Beispiel: http://:6000/metrics", - "metricUrlInfo": "Vollständige URL des API-Vorgangs, die zum Abrufen des Metrikwerts aufgerufen werden soll", + "metricUrlInfo": "Vollständige URL des API-Vorgangs, die zum Abrufen des Metrikwerts aufgerufen werden soll.", "dataFormatLabel": "Datenformat", - "dataFormatInfo": "Der Dateityp, den Sie aufrufen", + "dataFormatInfo": "Der Dateityp, den Sie aufrufen.", "dataLocationLabel": "Datenspeicherort", "dataLocationInfo": "Ort des Metrikwerts in der Payload der Antwort. Der Wert hängt vom Ort ab.", "targetMetricValueLabel": "Zielwert der Metrik", - "targetMetricValueInfo": "Zielwert für die Skalierung. Wenn die von der API bereitgestellte Metrik diesem Wert entspricht oder diesen übersteigt, wird die Skalierung nach oben ausgeführt. Wenn die Metrik kleiner oder gleich 0 ist, wird die Skalierung auf 0 reduziert. (Dieser Wert kann eine Dezimalzahl sein.)", + "targetMetricValueInfo": "Zielwert für die Skalierung. Der Autoscaler passt die Anzahl der Replikate an, um diesen Zielwert beizubehalten. Das bedeutet: Liegt der von der Metrics-API zurückgegebene Metrikwert unter diesem Wert, werden Replikate hochskaliert; liegt er darüber, werden sie herunterskaliert.", "targetMetricValueMinimum": "Der Zielwert für die Metrik muss größer als 0 sein.", - "targetMetricValueRequired": "Metrikzielwert erforderlich", - "dataLocationRequired": "Datenspeicherort erforderlich", + "targetMetricValueRequired": "Metrikzielwert erforderlich.", + "dataLocationRequired": "Datenspeicherort erforderlich.", "dataLocationPlaceholder": "Beispiel: components.worker.tasks", - "dataFormatRequired": "Datenformat erforderlich", - "metricUrlRequired": "Metrik-URL erforderlich", + "dataFormatRequired": "Datenformat erforderlich.", + "metricUrlRequired": "Metrik-URL erforderlich.", "aggregationTypeLabel": "Aggregationstyp", - "aggregationTypeInfo": " Der Aggregationstyp, der vor dem Vergleichen des Werts der aggregierten Metrik mit dem Zielwert ausgeführt werden soll. Wenn Sie zum Beispiel AVERAGE wählen, ist der für das Scaling mit dem Zielwert verglichene Wert der Durchschnitt der Metrikwerte jedes Replikats Ihrer AI Deploy Anwendung", - "aggregationTypeRequired": "Aggregationstyp erforderlich", - "metricUrlInvalid": "Die Metrik-URL muss eine gültige URL sein, die mit http:// oder https:// beginnt." + "aggregationTypeInfo": " Der Aggregationstyp, der vor dem Vergleichen des Werts der aggregierten Metrik mit dem Zielwert ausgeführt werden soll. Wenn Sie zum Beispiel AVERAGE wählen, ist der für das Scaling mit dem Zielwert verglichene Wert der Durchschnitt der Metrikwerte jedes Replikats Ihrer AI Deploy Anwendung.", + "aggregationTypeRequired": "Aggregationstyp erforderlich.", + "metricUrlInvalid": "Die Metrik-URL muss eine gültige URL sein, die mit http:// oder https:// beginnt.", + "replicasSectionTitle": "Anzahl an Replikas", + "replicasMaxInputInfo": "Höchstzahl an Replikas.", + "scaleToZeroInputLabel": "Zeit vor Skalierung auf 0 (s)", + "scaleToZeroInputInfo": "Anzahl der Sekunden, die gewartet wird, bevor die App auf 0 skaliert wird, wenn sie keine HTTP/gRPC-Anfragen mehr erhält. Muss größer oder gleich 60 sein.", + "scaleUpDelayInputLabel": "Zeit vor Hochskalierung (s)", + "scaleUpDelayInputInfo": "Anzahl der Sekunden, die vor dem Hochskalieren von N auf N+1 Replikas gewartet wird. Muss größer oder gleich 0 und kleiner oder gleich 3600 sein.", + "scaleDownDelayInputLabel": "Zeit vor Herunterskalierung (s)", + "scaleDownDelayInputInfo": "Anzahl der Sekunden, die vor dem Herunterskalieren von N auf N-1 Replikas gewartet wird. Muss größer oder gleich 0 und kleiner oder gleich 3600 sein.", + "scaleDelayRangeError": "Der Wert muss zwischen 0 und 3600 Sekunden liegen.", + "scaleToZeroDelayRangeError": "Der Wert muss größer oder gleich 60 Sekunden sein.", + "scaleToZeroWarningFlavor": "Sie haben die Skalierung auf 0 ausgewählt. Wenn Ihre App auf 0 skaliert ist und der Flavor nicht mehr verfügbar ist, können wir Ihre App möglicherweise nicht neu starten.", + "scaleToZeroWarningTraffic": "Die Anzahl der Replikas wird auf 0 reduziert, wenn Ihre Anwendung während des definierten Zeitraums keine Aufrufe mehr erhält.", + "triggerSectionTitle": "Auslöser", + "scaleWindowSectionTitle": "Skalierungsfenster" } diff --git a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_en_GB.json b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_en_GB.json index f73226fd3747..deecb56d3242 100644 --- a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_en_GB.json +++ b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_en_GB.json @@ -4,32 +4,48 @@ "scalingStratActiveLabel": "Auto-scaling enabled", "noScalingStratActiveLabel": "Auto-scaling disabled", "replicasInputLabel": "Number of replicas your app will be deployed on", - "haInfoHelper": "Deploy on a minimum of 2 replicas for high availability", + "haInfoHelper": "Deploy on a minimum of 2 replicas for high availability.", "replicasMinInputLabel": "Minimum replicas", "replicasMaxInputLabel": "Maximum replicas", "resourceTypeLabel": "Metric monitored", - "resourceTypeInfo": "The metric that will act as the trigger for auto-scaling", + "resourceTypeInfo": "The metric that will act as the trigger for auto-scaling.", "treshholderTargetLabel": "Trigger threshold (%)", - "treshholderTargetInfo": "The metric value, in percent.", + "treshholderTargetInfo": "Target average resource utilization (%) used to calculate the desired number of replicas.", "scalingBillingInfo": "Please note that the number of replica nodes you choose will increase the number of resources you use.", - "errorFormMinMaxRepField": "Minimum Replicas must be less than Maximum Replicas", + "errorFormMinMaxRepField": "Minimum Replicas must be less than Maximum Replicas.", + "replicasMinRangeError": "The value must be between 0 and 10.", + "replicasMaxRangeError": "The value must be between 1 and 10.", "metricUrlLabel": "Metric URL", "metricUrlPlaceholder": "e.g. http://:6000/metrics", - "metricUrlInfo": "The full URL to fetch the metric value via an API call", + "metricUrlInfo": "The full URL to fetch the metric value via an API call.", "dataFormatLabel": "Data format", - "dataFormatInfo": "The type of file you are accessing", + "dataFormatInfo": "The type of file you are accessing.", "dataLocationLabel": "Data location", "dataLocationInfo": "The location of the metric value inside the response payload. Value dependent on location.", "targetMetricValueLabel": "Metric target value", - "targetMetricValueInfo": "Scaling target value. When the metric provided by the API is equal to or greater than this value, it scales up. Scaling is set to 0 when the metric is less than or equal to 0. (This value can be a decimal number.)", - "targetMetricValueMinimum": "Metric target value must be greater than 0", - "targetMetricValueRequired": "Metric target value is required", - "dataLocationRequired": "Data location is required", + "targetMetricValueInfo": "Target value to scale on. Autoscaler will adjust the number of replicas to maintain this target value, meaning that if metric value returned by metrics API is below this value then replicas will be scaled up, and scaled down when above.", + "targetMetricValueMinimum": "Metric target value must be greater than 0.", + "targetMetricValueRequired": "Metric target value is required.", + "dataLocationRequired": "Data location is required.", "dataLocationPlaceholder": "ex: components.worker.tasks", - "dataFormatRequired": "Data format is required", - "metricUrlRequired": "Metric URL is required", + "dataFormatRequired": "Data format is required.", + "metricUrlRequired": "Metric URL is required.", "aggregationTypeLabel": "Aggregation type", "aggregationTypeInfo": " The aggregation type to perform before comparing the aggregated metric value to the target value. For example, if you choose AVERAGE, the scaling will use the average of the metric values across all replicas of your AI Deploy application to compare with the target value.", - "aggregationTypeRequired": "Aggregation type is required", - "metricUrlInvalid": "The metric URL must be a valid URL starting with http:// or https://" + "aggregationTypeRequired": "Aggregation type is required.", + "metricUrlInvalid": "The metric URL must be a valid URL starting with http:// or https://.", + "replicasSectionTitle": "Number of replicas", + "replicasMaxInputInfo": "Maximum number of replicas.", + "scaleToZeroInputLabel": "Time before scaling to 0 (s)", + "scaleToZeroInputInfo": "Number of seconds to wait before scaling the app to 0 when it no longer receives HTTP/gRPC requests. Must be greater than or equal to 60.", + "scaleUpDelayInputLabel": "Time before scaling up (s)", + "scaleUpDelayInputInfo": "Number of seconds to wait before scaling up from N to N+1 replicas. Must be greater than or equal to 0 and less than or equal to 3600.", + "scaleDownDelayInputLabel": "Time before scaling down (s)", + "scaleDownDelayInputInfo": "Number of seconds to wait before scaling down from N to N-1 replicas. Must be greater than or equal to 0 and less than or equal to 3600.", + "scaleDelayRangeError": "The value must be between 0 and 3600 seconds.", + "scaleToZeroDelayRangeError": "The value must be greater than or equal to 60 seconds.", + "scaleToZeroWarningFlavor": "You have selected scaling to 0. If your app is scaled to 0 and if the flavor is not available anymore, we might not be able to restart your app.", + "scaleToZeroWarningTraffic": "The number of replicas will be reduced to 0 when your application no longer receives calls during the defined period.", + "triggerSectionTitle": "Trigger", + "scaleWindowSectionTitle": "Scaling window" } diff --git a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_es_ES.json b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_es_ES.json index 3603a50c0eb4..63e45d9ab3e5 100644 --- a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_es_ES.json +++ b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_es_ES.json @@ -4,32 +4,48 @@ "scalingStratActiveLabel": "Autoscaling activado", "noScalingStratActiveLabel": "Autoscaling desactivado", "replicasInputLabel": "Número de réplicas en las que se desplegará su app", - "haInfoHelper": "Despliegue en un mínimo de 2 réplicas para disfrutar de alta disponibilidad", + "haInfoHelper": "Despliegue en un mínimo de 2 réplicas para disfrutar de alta disponibilidad.", "replicasMinInputLabel": "Número mínimo de réplicas", "replicasMaxInputLabel": "Número máximo de réplicas", "resourceTypeLabel": "Métrica vigilada", "resourceTypeInfo": "Métrica que activará el autoscaling.", "treshholderTargetLabel": "Umbral de activación (%)", - "treshholderTargetInfo": "Valor de la métrica, en porcentaje.", + "treshholderTargetInfo": "Utilización media objetivo de recursos (%) utilizada para calcular el número deseado de réplicas.", "scalingBillingInfo": "Tenga en cuenta que el número de réplicas seleccionado multiplicará el número de recursos utilizados.", - "errorFormMinMaxRepField": "El número mínimo de réplicas debe ser inferior al número máximo de réplicas", + "errorFormMinMaxRepField": "El número mínimo de réplicas debe ser inferior al número máximo de réplicas.", + "replicasMinRangeError": "El valor debe estar entre 0 y 10.", + "replicasMaxRangeError": "El valor debe estar entre 1 y 10.", "metricUrlLabel": "URL de métrica", "metricUrlPlaceholder": "p. ej.: http://:6000/metrics", - "metricUrlInfo": "Dirección URL completa de la operación de la API a la que se va a llamar para obtener el valor de la métrica", + "metricUrlInfo": "Dirección URL completa de la operación de la API a la que se va a llamar para obtener el valor de la métrica.", "dataFormatLabel": "Formato de datos", - "dataFormatInfo": "El tipo de archivo al que está llamando", + "dataFormatInfo": "El tipo de archivo al que está llamando.", "dataLocationLabel": "Ubicación de los datos", "dataLocationInfo": "Ubicación del valor de la métrica en la carga de respuesta. El valor depende de la ubicación.", "targetMetricValueLabel": "Valor de destino de la métrica", - "targetMetricValueInfo": "Valor de destino para el escalado. Cuando la métrica proporcionada por la API es igual o mayor que este valor, el escalado se realiza hacia arriba. Si la métrica es menor o igual que 0, la escala se reduce a 0. (Este valor puede ser un número decimal.)", - "targetMetricValueMinimum": "El valor de destino de la métrica debe ser mayor que 0", - "targetMetricValueRequired": "Se requiere el valor de destino de la métrica", - "dataLocationRequired": "Se requiere la ubicación de los datos", + "targetMetricValueInfo": "Valor objetivo para el escalado. El autoscaler ajustará el número de réplicas para mantener este valor objetivo; esto significa que, si el valor de la métrica devuelto por la API de métricas está por debajo de este valor, las réplicas aumentarán, y se reducirán cuando esté por encima.", + "targetMetricValueMinimum": "El valor de destino de la métrica debe ser mayor que 0.", + "targetMetricValueRequired": "Se requiere el valor de destino de la métrica.", + "dataLocationRequired": "Se requiere la ubicación de los datos.", "dataLocationPlaceholder": "p. ej.: components.worker.tasks", - "dataFormatRequired": "Se requiere el formato de los datos", - "metricUrlRequired": "Se requiere la dirección URL de la métrica", + "dataFormatRequired": "Se requiere el formato de los datos.", + "metricUrlRequired": "Se requiere la dirección URL de la métrica.", "aggregationTypeLabel": "Tipo de agregación", - "aggregationTypeInfo": " Tipo de agregación que se debe realizar antes de comparar el valor de la métrica agregada con el valor de destino. Por ejemplo, si elige AVERAGE, el valor comparado con el valor de destino para el escalado será el promedio de los valores de métrica de cada réplica de su aplicación AI Deploy", - "aggregationTypeRequired": "Tipo de agregación requerido", - "metricUrlInvalid": "La dirección URL de la métrica debe ser una dirección URL válida que empiece por http:// o https://" + "aggregationTypeInfo": " Tipo de agregación que se debe realizar antes de comparar el valor de la métrica agregada con el valor de destino. Por ejemplo, si elige AVERAGE, el valor comparado con el valor de destino para el escalado será el promedio de los valores de métrica de cada réplica de su aplicación AI Deploy.", + "aggregationTypeRequired": "Tipo de agregación requerido.", + "metricUrlInvalid": "La dirección URL de la métrica debe ser una dirección URL válida que empiece por http:// o https://.", + "replicasSectionTitle": "Número de réplicas", + "replicasMaxInputInfo": "Número máximo de réplicas.", + "scaleToZeroInputLabel": "Tiempo antes del escalado a 0 (s)", + "scaleToZeroInputInfo": "Número de segundos que se debe esperar antes de escalar la app a 0 cuando ya no reciba solicitudes HTTP/gRPC. Debe ser mayor o igual a 60.", + "scaleUpDelayInputLabel": "Tiempo antes del escalado hacia arriba (s)", + "scaleUpDelayInputInfo": "Número de segundos que se debe esperar antes de escalar de N a N+1 réplicas. Debe ser mayor o igual a 0 y menor o igual a 3600.", + "scaleDownDelayInputLabel": "Tiempo antes del escalado hacia abajo (s)", + "scaleDownDelayInputInfo": "Número de segundos que se debe esperar antes de escalar de N a N-1 réplicas. Debe ser mayor o igual a 0 y menor o igual a 3600.", + "scaleDelayRangeError": "El valor debe estar entre 0 y 3600 segundos.", + "scaleToZeroDelayRangeError": "El valor debe ser mayor o igual a 60 segundos.", + "scaleToZeroWarningFlavor": "Ha seleccionado el escalado a 0. Si su app se escala a 0 y el flavor ya no está disponible, es posible que no podamos reiniciar su app.", + "scaleToZeroWarningTraffic": "El número de réplicas se reducirá a 0 cuando su aplicación ya no reciba llamadas durante el periodo definido.", + "triggerSectionTitle": "Desencadenador", + "scaleWindowSectionTitle": "Ventana de escalado" } diff --git a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_fr_CA.json b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_fr_CA.json index bf726a5f573b..5df24678d05d 100644 --- a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_fr_CA.json +++ b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_fr_CA.json @@ -4,32 +4,48 @@ "scalingStratActiveLabel": "Autoscaling activé", "noScalingStratActiveLabel": "Autoscaling désactivé", "replicasInputLabel": "Nombre de réplicas sur lesquels sera déployée votre app", - "haInfoHelper": "Déployez sur un minimum de 2 réplicas pour disposer de haute-disponibilité", + "haInfoHelper": "Déployez sur un minimum de 2 réplicas pour disposer de haute-disponibilité.", "replicasMinInputLabel": "Minimum de réplicas", "replicasMaxInputLabel": "Maximum de réplicas", "resourceTypeLabel": "Métrique surveillée", "resourceTypeInfo": "Métrique qui agira comme élément déclencheur de l'Autoscaling.", "treshholderTargetLabel": "Seuil de déclenchement (%)", - "treshholderTargetInfo": "Valeur de la métrique, en pourcentage.", + "treshholderTargetInfo": "Utilisation moyenne cible des ressources (%) utilisée pour calculer le nombre souhaité de réplicas.", "scalingBillingInfo": "Veuillez noter que le nombre de réplicas choisi multipliera d'autant le nombre de ressources prises.", - "errorFormMinMaxRepField": "Minimum réplicas doit être inférieur à Maximum réplicas", + "errorFormMinMaxRepField": "Minimum réplicas doit être inférieur à Maximum réplicas.", + "replicasMinRangeError": "La valeur doit être comprise entre 0 et 10.", + "replicasMaxRangeError": "La valeur doit être comprise entre 1 et 10.", "metricUrlLabel": "URL de la métrique", "metricUrlPlaceholder": "ex: http://:6000/metrics", - "metricUrlInfo": "URL complète de l'opération API à appeler pour obtenir la valeur de la métrique", + "metricUrlInfo": "URL complète de l'opération API à appeler pour obtenir la valeur de la métrique.", "dataFormatLabel": "Format des données", - "dataFormatInfo": "Le type de fichier que vous appelez", + "dataFormatInfo": "Le type de fichier que vous appelez.", "dataLocationLabel": "Emplacement des données", "dataLocationInfo": "Emplacement de la valeur de la métrique dans la charge utile de la réponse. La valeur dépend de l'emplacement.", "targetMetricValueLabel": "Valeur cible de la métrique", - "targetMetricValueInfo": "Valeur cible pour la mise à l'échelle. Lorsque la métrique fournie par l'API est égale ou supérieure à cette valeur, la mise à l'échelle s'effectue vers le haut. Si la métrique est inférieure ou égale à 0, la mise à l'échelle est ramenée à 0. (Cette valeur peut être un nombre décimal.)", - "targetMetricValueMinimum": "Valeur cible de la métrique doit être supérieure à 0", - "targetMetricValueRequired": "Valeur cible de la métrique est requise", - "dataLocationRequired": "Emplacement des données est requis", + "targetMetricValueInfo": "Valeur cible de mise à l'échelle. L'autoscaler ajustera le nombre de réplicas pour maintenir cette valeur cible, ce qui signifie que si la valeur de la métrique renvoyée par l'API de métriques est inférieure à cette valeur, les réplicas seront augmentés, et ils seront réduits lorsqu'elle est supérieure.", + "targetMetricValueMinimum": "Valeur cible de la métrique doit être supérieure à 0.", + "targetMetricValueRequired": "Valeur cible de la métrique est requise.", + "dataLocationRequired": "Emplacement des données est requis.", "dataLocationPlaceholder": "ex: components.worker.tasks", - "dataFormatRequired": "Format des données est requis", - "metricUrlRequired": "URL de la métrique est requise", - "metricUrlInvalid": "L'URL de la métrique doit être une URL valide commençant par http:// ou https://", + "dataFormatRequired": "Format des données est requis.", + "metricUrlRequired": "URL de la métrique est requise.", + "metricUrlInvalid": "L'URL de la métrique doit être une URL valide commençant par http:// ou https://.", "aggregationTypeLabel": "Type d'agrégation", - "aggregationTypeInfo": " Type d'aggregation à effectuer avant de comparer la valeur de métrique aggrégée à la valeur cible. Par exemple, si vous choisissez AVERAGE, la valeur comparée à la valeur cible pour le scaling sera la moyenne des valeurs de métrique de chaque réplica de votre app AI Deploy", - "aggregationTypeRequired": "Type d'agrégation est requis" + "aggregationTypeInfo": " Type d'aggregation à effectuer avant de comparer la valeur de métrique aggrégée à la valeur cible. Par exemple, si vous choisissez AVERAGE, la valeur comparée à la valeur cible pour le scaling sera la moyenne des valeurs de métrique de chaque réplica de votre app AI Deploy.", + "aggregationTypeRequired": "Type d'agrégation est requis.", + "replicasSectionTitle": "Nombre de réplicas", + "replicasMaxInputInfo": "Nombre maximum de réplicas.", + "scaleToZeroInputLabel": "Temps avant mise à l'échelle à 0 (s)", + "scaleToZeroInputInfo": "Nombre de secondes à attendre avant de mettre l'app à l'échelle à 0 lorsqu'elle ne reçoit plus de requêtes HTTP/gRPC. Doit être supérieur ou égal à 60.", + "scaleUpDelayInputLabel": "Temps avant mise à l'échelle vers le haut (s)", + "scaleUpDelayInputInfo": "Nombre de secondes à attendre avant une mise à l'échelle vers le haut de N à N+1 réplicas. Doit être supérieur ou égal à 0 et inférieur ou égal à 3600.", + "scaleDownDelayInputLabel": "Temps avant mise à l'échelle vers le bas (s)", + "scaleDownDelayInputInfo": "Nombre de secondes à attendre avant une mise à l'échelle vers le bas de N à N-1 réplicas. Doit être supérieur ou égal à 0 et inférieur ou égal à 3600.", + "scaleDelayRangeError": "La valeur doit être comprise entre 0 et 3600 secondes.", + "scaleToZeroDelayRangeError": "La valeur doit être supérieure ou égale à 60 secondes.", + "scaleToZeroWarningFlavor": "Vous avez sélectionné la mise à l'échelle à 0. Si votre app est mise à l'échelle à 0 et si le flavor n'est plus disponible, nous ne pourrons peut-être pas redémarrer votre app.", + "scaleToZeroWarningTraffic": "Le nombre de réplicas sera réduit à 0 lorsque votre application ne recevra plus d'appels pendant la période définie.", + "triggerSectionTitle": "Déclencheur", + "scaleWindowSectionTitle": "Fenêtre de mise à l'échelle" } diff --git a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_fr_FR.json b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_fr_FR.json index bf726a5f573b..d353cbffe0b4 100644 --- a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_fr_FR.json +++ b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_fr_FR.json @@ -3,33 +3,49 @@ "scalingLink": "Plus d'informations sur les stratégies de mise à l'échelle", "scalingStratActiveLabel": "Autoscaling activé", "noScalingStratActiveLabel": "Autoscaling désactivé", + "replicasSectionTitle": "Nombre de réplicas", "replicasInputLabel": "Nombre de réplicas sur lesquels sera déployée votre app", - "haInfoHelper": "Déployez sur un minimum de 2 réplicas pour disposer de haute-disponibilité", + "haInfoHelper": "Nombre minimum de réplicas.", "replicasMinInputLabel": "Minimum de réplicas", + "replicasMaxInputInfo": "Nombre maximum de réplicas.", "replicasMaxInputLabel": "Maximum de réplicas", + "scaleToZeroInputLabel": "Temps avant mise à l'échelle à 0 (s)", + "scaleToZeroInputInfo": "Nombre de secondes à attendre avant de mettre l'app à l'échelle à 0 lorsqu'elle ne reçoit plus de requêtes HTTP/gRPC. Doit être supérieur ou égal à 60.", + "scaleUpDelayInputLabel": "Temps avant mise à l'échelle vers le haut (s)", + "scaleUpDelayInputInfo": "Nombre de secondes à attendre avant une mise à l'échelle vers le haut de N à N+1 réplicas. Doit être supérieur ou égal à 0 et inférieur ou égal à 3600.", + "scaleDownDelayInputLabel": "Temps avant mise à l'échelle vers le bas (s)", + "scaleDownDelayInputInfo": "Nombre de secondes à attendre avant une mise à l'échelle vers le bas de N à N-1 réplicas. Doit être supérieur ou égal à 0 et inférieur ou égal à 3600.", + "scaleDelayRangeError": "La valeur doit être comprise entre 0 et 3600 secondes.", + "scaleToZeroDelayRangeError": "La valeur doit être supérieure ou égale à 60 secondes.", + "scaleToZeroWarningFlavor": "Vous avez sélectionné la mise à l'échelle à 0. Si votre app est mise à l'échelle à 0 et si le flavor n'est plus disponible, nous ne pourrons peut-être pas redémarrer votre app.", + "scaleToZeroWarningTraffic": "Le nombre de réplicas sera réduit à 0 lorsque votre application ne recevra plus d'appels pendant la période définie.", + "triggerSectionTitle": "Déclencheur", "resourceTypeLabel": "Métrique surveillée", "resourceTypeInfo": "Métrique qui agira comme élément déclencheur de l'Autoscaling.", "treshholderTargetLabel": "Seuil de déclenchement (%)", - "treshholderTargetInfo": "Valeur de la métrique, en pourcentage.", + "treshholderTargetInfo": "Utilisation moyenne cible des ressources (%) utilisée pour calculer le nombre souhaité de réplicas.", + "scaleWindowSectionTitle": "Fenêtre de mise à l'échelle", "scalingBillingInfo": "Veuillez noter que le nombre de réplicas choisi multipliera d'autant le nombre de ressources prises.", - "errorFormMinMaxRepField": "Minimum réplicas doit être inférieur à Maximum réplicas", + "errorFormMinMaxRepField": "Minimum réplicas doit être inférieur à Maximum réplicas.", + "replicasMinRangeError": "La valeur doit être comprise entre 0 et 10.", + "replicasMaxRangeError": "La valeur doit être comprise entre 1 et 10.", "metricUrlLabel": "URL de la métrique", "metricUrlPlaceholder": "ex: http://:6000/metrics", - "metricUrlInfo": "URL complète de l'opération API à appeler pour obtenir la valeur de la métrique", + "metricUrlInfo": "URL complète à appeler via l'API pour récupérer la valeur de la métrique.", "dataFormatLabel": "Format des données", - "dataFormatInfo": "Le type de fichier que vous appelez", + "dataFormatInfo": "Type de fichier auquel vous accédez.", "dataLocationLabel": "Emplacement des données", - "dataLocationInfo": "Emplacement de la valeur de la métrique dans la charge utile de la réponse. La valeur dépend de l'emplacement.", + "dataLocationInfo": "Emplacement de la valeur de la métrique dans la réponse API. La valeur dépend de cet emplacement.", "targetMetricValueLabel": "Valeur cible de la métrique", - "targetMetricValueInfo": "Valeur cible pour la mise à l'échelle. Lorsque la métrique fournie par l'API est égale ou supérieure à cette valeur, la mise à l'échelle s'effectue vers le haut. Si la métrique est inférieure ou égale à 0, la mise à l'échelle est ramenée à 0. (Cette valeur peut être un nombre décimal.)", - "targetMetricValueMinimum": "Valeur cible de la métrique doit être supérieure à 0", - "targetMetricValueRequired": "Valeur cible de la métrique est requise", - "dataLocationRequired": "Emplacement des données est requis", + "targetMetricValueInfo": "Valeur cible de mise à l'échelle. L'autoscaler ajustera le nombre de réplicas pour maintenir cette valeur cible, ce qui signifie que si la valeur de la métrique renvoyée par l'API de métriques est inférieure à cette valeur, les réplicas seront augmentés, et ils seront réduits lorsqu'elle est supérieure.", + "targetMetricValueMinimum": "Valeur cible de la métrique doit être supérieure à 0.", + "targetMetricValueRequired": "Valeur cible de la métrique est requise.", + "dataLocationRequired": "Emplacement des données est requis.", "dataLocationPlaceholder": "ex: components.worker.tasks", - "dataFormatRequired": "Format des données est requis", - "metricUrlRequired": "URL de la métrique est requise", - "metricUrlInvalid": "L'URL de la métrique doit être une URL valide commençant par http:// ou https://", + "dataFormatRequired": "Format des données est requis.", + "metricUrlRequired": "URL de la métrique est requise.", + "metricUrlInvalid": "L'URL de la métrique doit être une URL valide commençant par http:// ou https://.", "aggregationTypeLabel": "Type d'agrégation", - "aggregationTypeInfo": " Type d'aggregation à effectuer avant de comparer la valeur de métrique aggrégée à la valeur cible. Par exemple, si vous choisissez AVERAGE, la valeur comparée à la valeur cible pour le scaling sera la moyenne des valeurs de métrique de chaque réplica de votre app AI Deploy", - "aggregationTypeRequired": "Type d'agrégation est requis" + "aggregationTypeInfo": "Type d'agrégation à appliquer avant de comparer la valeur de métrique agrégée à la valeur cible. Par exemple, si vous choisissez AVERAGE, la mise à l'échelle utilisera la moyenne des valeurs de métrique de tous les réplicas de votre app AI Deploy pour la comparaison avec la valeur cible.", + "aggregationTypeRequired": "Type d'agrégation est requis." } diff --git a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_it_IT.json b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_it_IT.json index 3794d2f535c4..bca61edee12b 100644 --- a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_it_IT.json +++ b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_it_IT.json @@ -4,32 +4,48 @@ "scalingStratActiveLabel": "Autoscaling attivato", "noScalingStratActiveLabel": "Autoscaling disattivato", "replicasInputLabel": "Numero di repliche su cui verrà eseguita la tua App", - "haInfoHelper": "Esegui su almeno 2 repliche per disporre di alta disponibilità", + "haInfoHelper": "Esegui su almeno 2 repliche per disporre di alta disponibilità.", "replicasMinInputLabel": "Numero minimo di repliche", "replicasMaxInputLabel": "Numero massimo di repliche", "resourceTypeLabel": "Metrica monitorata", "resourceTypeInfo": "Metrica che attiverà l'autoscaling.", "treshholderTargetLabel": "Soglia di attivazione (%)", - "treshholderTargetInfo": "Valore della metrica, in percentuale", + "treshholderTargetInfo": "Utilizzo medio target delle risorse (%) utilizzato per calcolare il numero desiderato di repliche.", "scalingBillingInfo": "Ricordiamo che il numero di repliche scelto moltiplicherà il numero di risorse selezionate.", - "errorFormMinMaxRepField": "Il numero minimo di repliche deve essere inferiore al numero massimo", + "errorFormMinMaxRepField": "Il numero minimo di repliche deve essere inferiore al numero massimo.", + "replicasMinRangeError": "Il valore deve essere compreso tra 0 e 10.", + "replicasMaxRangeError": "Il valore deve essere compreso tra 1 e 10.", "metricUrlLabel": "URL della metrica", "metricUrlPlaceholder": "es.: http://:6000/metrics", - "metricUrlInfo": "URL completo dell'API da chiamare per ottenere il valore della metrica", + "metricUrlInfo": "URL completo dell'API da chiamare per ottenere il valore della metrica.", "dataFormatLabel": "Formato dei dati", - "dataFormatInfo": "Tipo di file che stai chiamando", + "dataFormatInfo": "Tipo di file che stai chiamando.", "dataLocationLabel": "Localizzazione dei dati", "dataLocationInfo": "Localizzazione del valore della metrica nel payload della risposta. Il valore dipende dalla localizzazione.", "targetMetricValueLabel": "Valore target della metrica", - "targetMetricValueInfo": "Valore target per lo scaling. Quando la metrica fornita dall'API è uguale o superiore a questo valore, viene eseguito uno scaling verso l'alto. Se la metrica è minore o uguale a 0, lo scaling è riportato a 0. (Questo valore può essere un numero decimale.)", - "targetMetricValueMinimum": "Il valore target della metrica deve essere maggiore di 0", - "targetMetricValueRequired": "Il valore target della metrica è obbligatorio", - "dataLocationRequired": "La localizzazione dei dati è obbligatoria", + "targetMetricValueInfo": "Valore target per la scalabilità. L'autoscaler regolerà il numero di repliche per mantenere questo valore target: se il valore della metrica restituito dall'API delle metriche è inferiore a questo valore, le repliche verranno aumentate; se è superiore, verranno ridotte.", + "targetMetricValueMinimum": "Il valore target della metrica deve essere maggiore di 0.", + "targetMetricValueRequired": "Il valore target della metrica è obbligatorio.", + "dataLocationRequired": "La localizzazione dei dati è obbligatoria.", "dataLocationPlaceholder": "es.: components.worker.tasks", - "dataFormatRequired": "Il formato dei dati è obbligatorio", - "metricUrlRequired": "L'URL della metrica è obbligatorio", + "dataFormatRequired": "Il formato dei dati è obbligatorio.", + "metricUrlRequired": "L'URL della metrica è obbligatorio.", "aggregationTypeLabel": "Tipo di aggregazione", - "aggregationTypeInfo": " Tipo di aggregazione da effettuare prima di confrontare il valore aggregato della metrica con il valore target. Per esempio, se scegli AVERAGE, il valore confrontato al valore target per lo scaling sarà la media dei valori della metrica di ciascuna replica della tua app AI Deploy", - "aggregationTypeRequired": "Il tipo di aggregazione è obbligatorio", - "metricUrlInvalid": "L'URL della metrica deve essere un URL valido che inizia con http:// o https://" + "aggregationTypeInfo": " Tipo di aggregazione da effettuare prima di confrontare il valore aggregato della metrica con il valore target. Per esempio, se scegli AVERAGE, il valore confrontato al valore target per lo scaling sarà la media dei valori della metrica di ciascuna replica della tua app AI Deploy.", + "aggregationTypeRequired": "Il tipo di aggregazione è obbligatorio.", + "metricUrlInvalid": "L'URL della metrica deve essere un URL valido che inizia con http:// o https://.", + "replicasSectionTitle": "Numero di repliche", + "replicasMaxInputInfo": "Numero massimo di repliche.", + "scaleToZeroInputLabel": "Tempo prima della scalabilità a 0 (s)", + "scaleToZeroInputInfo": "Numero di secondi da attendere prima di portare la app a 0 quando non riceve più richieste HTTP/gRPC. Deve essere maggiore o uguale a 60.", + "scaleUpDelayInputLabel": "Tempo prima della scalabilità verso l'alto (s)", + "scaleUpDelayInputInfo": "Numero di secondi da attendere prima di scalare da N a N+1 repliche. Deve essere maggiore o uguale a 0 e minore o uguale a 3600.", + "scaleDownDelayInputLabel": "Tempo prima della scalabilità verso il basso (s)", + "scaleDownDelayInputInfo": "Numero di secondi da attendere prima di scalare da N a N-1 repliche. Deve essere maggiore o uguale a 0 e minore o uguale a 3600.", + "scaleDelayRangeError": "Il valore deve essere compreso tra 0 e 3600 secondi.", + "scaleToZeroDelayRangeError": "Il valore deve essere maggiore o uguale a 60 secondi.", + "scaleToZeroWarningFlavor": "Hai selezionato la scalabilità a 0. Se la tua app viene scalata a 0 e il flavor non è più disponibile, potremmo non essere in grado di riavviare la tua app.", + "scaleToZeroWarningTraffic": "Il numero di repliche verrà ridotto a 0 quando la tua applicazione non riceverà più chiamate durante il periodo definito.", + "triggerSectionTitle": "Attivatore", + "scaleWindowSectionTitle": "Finestra di scalabilità" } diff --git a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_pl_PL.json b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_pl_PL.json index 21b770bd38ee..01398c2361a7 100644 --- a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_pl_PL.json +++ b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_pl_PL.json @@ -4,32 +4,48 @@ "scalingStratActiveLabel": "Autoscaling włączony", "noScalingStratActiveLabel": "Autoscaling wyłączony", "replicasInputLabel": "Liczba replik, na których zostanie uruchomiona aplikacja", - "haInfoHelper": "Uruchom minimum 2 repliki, aby uzyskać wysoką dostępność", + "haInfoHelper": "Uruchom minimum 2 repliki, aby uzyskać wysoką dostępność.", "replicasMinInputLabel": "Minimalna liczba replik", "replicasMaxInputLabel": "Maksymalna liczba replik", "resourceTypeLabel": "Monitorowana metryka", "resourceTypeInfo": "Metryka, która będzie działać jako element uruchamiający Autoscaling.", "treshholderTargetLabel": "Próg uruchomienia (%)", - "treshholderTargetInfo": "Wartość metryki, w procentach.", + "treshholderTargetInfo": "Docelowe średnie wykorzystanie zasobów (%) używane do obliczenia żądanej liczby replik.", "scalingBillingInfo": "Pamiętaj, że wybrana liczba replik zwielokrotni pobrane zasoby o tę samą liczbę.", - "errorFormMinMaxRepField": "Minimalna liczba replik musi być mniejsza niż maksymalna", + "errorFormMinMaxRepField": "Minimalna liczba replik musi być mniejsza niż maksymalna.", + "replicasMinRangeError": "Wartość musi wynosić od 0 do 10.", + "replicasMaxRangeError": "Wartość musi wynosić od 1 do 10.", "metricUrlLabel": "URL metryki", "metricUrlPlaceholder": "np.: http://:6000/metrics", - "metricUrlInfo": "Kompletny adres URL operacji API do wywołania w celu otrzymania wartości metryki", + "metricUrlInfo": "Kompletny adres URL operacji API do wywołania w celu otrzymania wartości metryki.", "dataFormatLabel": "Format danych", - "dataFormatInfo": "Typ plików do wywołania", + "dataFormatInfo": "Typ plików do wywołania.", "dataLocationLabel": "Lokalizacja danych", "dataLocationInfo": "Lokalizacja wartości metryki ładunku (payload) odpowiedzi. Wartość zależy od lokalizacji.", "targetMetricValueLabel": "Wartość docelowa metryki", - "targetMetricValueInfo": "Wartość docelowa do skalowania. Jeżeli dostarczona przez API metryka jest równa lub większa od tej wartości, skalowanie ma miejsce w górę. Jeżeli metryka jest mniejsza lub równa 0, skalowanie jest wyrównywane do zera. (Wartość ta może być liczbą dziesiętną.)", - "targetMetricValueMinimum": "Wartość docelowa metryki musi być większa niż 0", - "targetMetricValueRequired": "Wartość docelowa metryki jest wymagana", - "dataLocationRequired": "Lokalizacja danych jest wymagana", + "targetMetricValueInfo": "Wartość docelowa skalowania. Autoskaler dostosuje liczbę replik, aby utrzymać tę wartość docelową, co oznacza, że jeśli wartość metryki zwrócona przez API metryk jest niższa od tej wartości, liczba replik zostanie zwiększona, a gdy jest wyższa, zostanie zmniejszona.", + "targetMetricValueMinimum": "Wartość docelowa metryki musi być większa niż 0.", + "targetMetricValueRequired": "Wartość docelowa metryki jest wymagana.", + "dataLocationRequired": "Lokalizacja danych jest wymagana.", "dataLocationPlaceholder": "np: components.worker.tasks", - "dataFormatRequired": "Format danych jest wymagany", - "metricUrlRequired": "URL metryki jest wymagany", + "dataFormatRequired": "Format danych jest wymagany.", + "metricUrlRequired": "URL metryki jest wymagany.", "aggregationTypeLabel": "Typ agregacji", - "aggregationTypeInfo": " Typ agregacji do wykonania przed porównaniem zagregowanej wartości metryki z wartością docelową. Na przykład, jeśli wybierzesz AVERAGE, wartość porównana z wartością docelową dla skalowania będzie średnią wartości metryki każdej repliki aplikacji AI Deploy", - "aggregationTypeRequired": "Typ agregacji jest wymagany", - "metricUrlInvalid": "URL metryki musi być ważnym adresem URL rozpoczynającym się od http:// lub https://" + "aggregationTypeInfo": " Typ agregacji do wykonania przed porównaniem zagregowanej wartości metryki z wartością docelową. Na przykład, jeśli wybierzesz AVERAGE, wartość porównana z wartością docelową dla skalowania będzie średnią wartości metryki każdej repliki aplikacji AI Deploy.", + "aggregationTypeRequired": "Typ agregacji jest wymagany.", + "metricUrlInvalid": "URL metryki musi być ważnym adresem URL rozpoczynającym się od http:// lub https://.", + "replicasSectionTitle": "Liczba replik", + "replicasMaxInputInfo": "Maksymalna liczba replik.", + "scaleToZeroInputLabel": "Czas przed skalowaniem do 0 (s)", + "scaleToZeroInputInfo": "Liczba sekund oczekiwania przed przeskalowaniem aplikacji do 0, gdy nie otrzymuje już żądań HTTP/gRPC. Wartość musi być większa lub równa 60.", + "scaleUpDelayInputLabel": "Czas przed skalowaniem w górę (s)", + "scaleUpDelayInputInfo": "Liczba sekund oczekiwania przed skalowaniem z N do N+1 replik. Wartość musi być większa lub równa 0 oraz mniejsza lub równa 3600.", + "scaleDownDelayInputLabel": "Czas przed skalowaniem w dół (s)", + "scaleDownDelayInputInfo": "Liczba sekund oczekiwania przed skalowaniem z N do N-1 replik. Wartość musi być większa lub równa 0 oraz mniejsza lub równa 3600.", + "scaleDelayRangeError": "Wartość musi mieścić się w przedziale od 0 do 3600 sekund.", + "scaleToZeroDelayRangeError": "Wartość musi być większa lub równa 60 sekund.", + "scaleToZeroWarningFlavor": "Wybrano skalowanie do 0. Jeśli aplikacja zostanie przeskalowana do 0 i flavor nie będzie już dostępny, ponowne uruchomienie aplikacji może nie być możliwe.", + "scaleToZeroWarningTraffic": "Liczba replik zostanie zmniejszona do 0, gdy aplikacja przestanie otrzymywać wywołania w zdefiniowanym okresie.", + "triggerSectionTitle": "Wyzwalacz", + "scaleWindowSectionTitle": "Okno skalowania" } diff --git a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_pt_PT.json b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_pt_PT.json index 714aaa50afb3..ed746bfca81b 100644 --- a/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_pt_PT.json +++ b/packages/manager/apps/pci-ai-tools/public/translations/ai-tools/components/scaling/Messages_pt_PT.json @@ -4,32 +4,48 @@ "scalingStratActiveLabel": "Autoscaling ativado", "noScalingStratActiveLabel": "Autoscaling desativado", "replicasInputLabel": "Número de réplicas em que será implementada a sua aplicação", - "haInfoHelper": "Implemente num mínimo de 2 réplicas para dispor de alta disponibilidade", + "haInfoHelper": "Implemente num mínimo de 2 réplicas para dispor de alta disponibilidade.", "replicasMinInputLabel": "Mínimo de réplicas", "replicasMaxInputLabel": "Máximo de réplicas", "resourceTypeLabel": "Métrica monitorizada", "resourceTypeInfo": "Métrica que atuará como elemento desencadeador do Autoscaling.", "treshholderTargetLabel": "Patamar de acionamento (%)", - "treshholderTargetInfo": "Valor da métrica, em percentagem.", + "treshholderTargetInfo": "Utilização média alvo de recursos (%) utilizada para calcular o número pretendido de réplicas.", "scalingBillingInfo": "Tenha em conta que o número de réplicas escolhidas multiplicará o número de recursos consumidos.", "errorFormMinMaxRepField": "O número mínimo de réplicas deve ser inferior ao número máximo de réplicas.", + "replicasMinRangeError": "O valor deve estar entre 0 e 10.", + "replicasMaxRangeError": "O valor deve estar entre 1 e 10.", "metricUrlLabel": "URL da métrica", "metricUrlPlaceholder": "ex: http://:6000/metrics", - "metricUrlInfo": "URL completo da operação API a chamar para obter o valor da métrica", + "metricUrlInfo": "URL completo da operação API a chamar para obter o valor da métrica.", "dataFormatLabel": "Formato dos dados", - "dataFormatInfo": "O tipo de ficheiro que está a chamar", + "dataFormatInfo": "O tipo de ficheiro que está a chamar.", "dataLocationLabel": "Localização dos dados", "dataLocationInfo": "Localização do valor da métrica na carga útil da resposta. O valor depende da localização.", "targetMetricValueLabel": "Valor-alvo da métrica", - "targetMetricValueInfo": "Valor-alvo para a escalabilidade. Quando a métrica fornecida pela API for igual ou superior a este valor, o redimensionamento é feito para cima. Se a métrica for igual ou inferior a 0, o redimensionamento é reduzido a 0. (Este valor pode ser um número decimal.)", - "targetMetricValueMinimum": "O valor-alvo da métrica deve ser superior a 0", - "targetMetricValueRequired": "O valor-alvo da métrica é obrigatório", - "dataLocationRequired": "A localização dos dados é obrigatória", + "targetMetricValueInfo": "Valor-alvo para a escalabilidade. O autoscaler ajustará o número de réplicas para manter este valor-alvo, o que significa que, se o valor da métrica devolvido pela API de métricas estiver abaixo deste valor, as réplicas serão aumentadas e, quando estiver acima, serão reduzidas.", + "targetMetricValueMinimum": "O valor-alvo da métrica deve ser superior a 0.", + "targetMetricValueRequired": "O valor-alvo da métrica é obrigatório.", + "dataLocationRequired": "A localização dos dados é obrigatória.", "dataLocationPlaceholder": "ex: components.worker.tasks", - "dataFormatRequired": "O formato dos dados é obrigatório", - "metricUrlRequired": "O URL da métrica é obrigatório", + "dataFormatRequired": "O formato dos dados é obrigatório.", + "metricUrlRequired": "O URL da métrica é obrigatório.", "aggregationTypeLabel": "Tipo de agregação", "aggregationTypeInfo": " Tipo de agregação a efetuar antes de comparar o valor da métrica agregada com o valor-alvo. Por exemplo, se escolher AVERAGE, o valor comparado com o valor-alvo para o redimensionamento será a média dos valores da métrica de cada réplica da sua aplicação AI Deploy.", - "aggregationTypeRequired": "O tipo de agregação é obrigatório", - "metricUrlInvalid": "O URL da métrica deve ser um URL válido que comece por http:// ou https://" + "aggregationTypeRequired": "O tipo de agregação é obrigatório.", + "metricUrlInvalid": "O URL da métrica deve ser um URL válido que comece por http:// ou https://.", + "replicasSectionTitle": "Número de réplicas", + "replicasMaxInputInfo": "Número máximo de réplicas.", + "scaleToZeroInputLabel": "Tempo antes do redimensionamento para 0 (s)", + "scaleToZeroInputInfo": "Número de segundos de espera antes de redimensionar a app para 0 quando deixar de receber pedidos HTTP/gRPC. Deve ser maior ou igual a 60.", + "scaleUpDelayInputLabel": "Tempo antes do redimensionamento para cima (s)", + "scaleUpDelayInputInfo": "Número de segundos de espera antes do redimensionamento de N para N+1 réplicas. Deve ser maior ou igual a 0 e menor ou igual a 3600.", + "scaleDownDelayInputLabel": "Tempo antes do redimensionamento para baixo (s)", + "scaleDownDelayInputInfo": "Número de segundos de espera antes do redimensionamento de N para N-1 réplicas. Deve ser maior ou igual a 0 e menor ou igual a 3600.", + "scaleDelayRangeError": "O valor deve estar entre 0 e 3600 segundos.", + "scaleToZeroDelayRangeError": "O valor deve ser maior ou igual a 60 segundos.", + "scaleToZeroWarningFlavor": "Selecionou o redimensionamento para 0. Se a sua app for redimensionada para 0 e o flavor deixar de estar disponível, poderemos não conseguir reiniciar a app.", + "scaleToZeroWarningTraffic": "O número de réplicas será reduzido para 0 quando a sua aplicação deixar de receber chamadas durante o período definido.", + "triggerSectionTitle": "Gatilho", + "scaleWindowSectionTitle": "Janela de escalabilidade" } diff --git a/packages/manager/apps/pci-ai-tools/src/__tests__/helpers/mocks/app/appHelper.ts b/packages/manager/apps/pci-ai-tools/src/__tests__/helpers/mocks/app/appHelper.ts index 8a7f4e0d5a61..e90101c90702 100644 --- a/packages/manager/apps/pci-ai-tools/src/__tests__/helpers/mocks/app/appHelper.ts +++ b/packages/manager/apps/pci-ai-tools/src/__tests__/helpers/mocks/app/appHelper.ts @@ -13,6 +13,8 @@ export const mockedOrderScaling: Scaling = { replicasMin: 2, replicasMax: 100, replicas: 2, + scaleUpStabilizationWindowSeconds: 0, + scaleDownStabilizationWindowSeconds: 300, }; export const mockedFixedScaling: ai.app.ScalingStrategy = { @@ -42,5 +44,7 @@ export const mockedAutoScalingInput: ai.app.ScalingStrategyInput = { replicasMax: 100, averageUsageTarget: 75, resourceType: ai.app.ScalingAutomaticStrategyResourceTypeEnum.CPU, + scaleUpStabilizationWindowSeconds: 0, + scaleDownStabilizationWindowSeconds: 300, }, }; diff --git a/packages/manager/apps/pci-ai-tools/src/__tests__/setupTest.ts b/packages/manager/apps/pci-ai-tools/src/__tests__/setupTest.ts index 9a636ed307c7..a527fd56ba75 100644 --- a/packages/manager/apps/pci-ai-tools/src/__tests__/setupTest.ts +++ b/packages/manager/apps/pci-ai-tools/src/__tests__/setupTest.ts @@ -31,9 +31,13 @@ vi.mock('@ovh-ux/manager-core-api', () => { }; }); +const { mockTranslate } = vi.hoisted(() => ({ + mockTranslate: (key: string) => key, +})); + vi.mock('react-i18next', () => ({ useTranslation: () => ({ - t: (key: string) => key, + t: mockTranslate, }), })); diff --git a/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/AutoScalingForm.component.tsx b/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/AutoScalingForm.component.tsx index 6ae6965f5b68..554e6b9a4da3 100644 --- a/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/AutoScalingForm.component.tsx +++ b/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/AutoScalingForm.component.tsx @@ -1,32 +1,51 @@ -import { useFormContext, useWatch } from 'react-hook-form'; +import { Control, FieldValues } from 'react-hook-form'; -import Price from '@/components/price/Price.component'; -import { AppPricing } from '@/types/orderFunnel'; -import { ScalingStrategySchema, ResourceType } from '../scalingHelper'; +import { ScalingStrategySchema } from '../scalingHelper'; import { ReplicaFields } from './ReplicaFields'; import { ResourceTypeSelector } from './ResourceTypeSelector'; import { CpuRamFields } from './CpuRamFields'; import { CustomMetricsFields } from './CustomMetricsFields'; +import { ScalingDelayFields } from './ScalingDelayFields'; -interface AutoScalingFormProps { - pricingFlavor?: AppPricing; +interface AutoScalingFormProps< + TFieldValues extends FieldValues & ScalingStrategySchema, +> { + averageUsageTarget: number; + control: Control; + isCustom: boolean; + syncReplicasMaxFromMin?: (replicasMinValue?: unknown) => void; + showScaleToZero: boolean; } -export function AutoScalingForm({ pricingFlavor }: AutoScalingFormProps) { - const { control } = useFormContext(); - const resType = useWatch({ control, name: 'resourceType' }); - const minRep = useWatch({ control, name: 'replicasMin' }); - - const isCustom = resType === ResourceType.CUSTOM; - +export function AutoScalingForm< + TFieldValues extends FieldValues & ScalingStrategySchema, +>({ + averageUsageTarget, + control, + isCustom, + syncReplicasMaxFromMin, + showScaleToZero, +}: AutoScalingFormProps) { return ( -
-
- - - {!isCustom && } - {isCustom && } +
+
+ +
+
+ + {!isCustom && ( + + )} + {isCustom && }
+
); } diff --git a/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/AutoScalingForm.spec.tsx b/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/AutoScalingForm.spec.tsx index 505a465f96a7..5e063b6d7a9e 100644 --- a/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/AutoScalingForm.spec.tsx +++ b/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/AutoScalingForm.spec.tsx @@ -1,45 +1,26 @@ -import { render, screen, waitFor } from '@testing-library/react'; -import { describe, it, vi, beforeEach, afterEach } from 'vitest'; -import { useForm, FormProvider, Control } from 'react-hook-form'; -import { mockManagerReactShellClient } from '@/__tests__/helpers/mockShellHelper'; -import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { FormProvider, useForm } from 'react-hook-form'; import { AutoScalingForm } from './AutoScalingForm.component'; -import { mockedAppPricing1 } from '@/__tests__/helpers/mocks/app/appHelper'; -import { FullScalingFormValues, ResourceType } from '../scalingHelper'; +import { ResourceType } from '../scalingHelper'; import ai from '@/types/AI'; describe('AutoScalingForm component', () => { - beforeEach(() => { - vi.restoreAllMocks(); - mockManagerReactShellClient(); - vi.mock('@/data/hooks/catalog/useGetCatalog.hook', () => { - return { - useGetCatalog: vi.fn(() => ({ - isSuccess: true, - data: { - locale: { - currencyCode: 'EUR', - }, - }, - })), - }; - }); - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - const TestWrapper = ({ resourceType = ai.app.ScalingAutomaticStrategyResourceTypeEnum .CPU as ResourceType, + replicasMin = 1, }: { resourceType?: ResourceType; + replicasMin?: number; }) => { const methods = useForm({ defaultValues: { - replicasMin: 1, + replicasMin, replicasMax: 5, + cooldownPeriodSeconds: 300, + scaleUpStabilizationWindowSeconds: 0, + scaleDownStabilizationWindowSeconds: 300, resourceType, averageUsageTarget: 75, metricUrl: '', @@ -52,36 +33,43 @@ describe('AutoScalingForm component', () => { return ( - + ); }; - it('should render AutoScalingForm with CPU/RAM fields', async () => { - render(, { wrapper: RouterWithQueryClientWrapper }); + it('should render AutoScalingForm with CPU/RAM fields', () => { + render(); - await waitFor(() => { - expect(screen.getByTestId('auto-scaling-container')).toBeTruthy(); - expect(screen.getByTestId('min-rep-input')).toBeTruthy(); - expect(screen.getByTestId('max-rep-input')).toBeTruthy(); - expect(screen.getByTestId('resource-usage-slider')).toBeTruthy(); - }); + expect(screen.getByTestId('auto-scaling-container')).toBeTruthy(); + expect(screen.getByTestId('min-rep-input')).toBeTruthy(); + expect(screen.getByTestId('max-rep-input')).toBeTruthy(); + expect(screen.getByTestId('scale-up-delay-input')).toBeTruthy(); + expect(screen.getByTestId('scale-down-delay-input')).toBeTruthy(); + expect(screen.getByTestId('resource-usage-slider')).toBeTruthy(); }); - it('should render AutoScalingForm with CUSTOM fields', async () => { - render(, { - wrapper: RouterWithQueryClientWrapper, - }); + it('should render AutoScalingForm with CUSTOM fields', () => { + render(); - await waitFor(() => { - expect(screen.getByTestId('auto-scaling-container')).toBeTruthy(); - expect(screen.getByTestId('min-rep-input')).toBeTruthy(); - expect(screen.getByTestId('max-rep-input')).toBeTruthy(); - expect(screen.getByTestId('metric-url-input')).toBeTruthy(); - expect(screen.getByTestId('data-format-select')).toBeTruthy(); - expect(screen.getByTestId('data-location-input')).toBeTruthy(); - expect(screen.getByTestId('target-metric-value-input')).toBeTruthy(); - expect(screen.getByTestId('aggregation-type-select')).toBeTruthy(); - }); + expect(screen.getByTestId('auto-scaling-container')).toBeTruthy(); + expect(screen.getByTestId('min-rep-input')).toBeTruthy(); + expect(screen.getByTestId('max-rep-input')).toBeTruthy(); + expect(screen.getByTestId('metric-url-input')).toBeTruthy(); + expect(screen.getByTestId('data-format-select')).toBeTruthy(); + expect(screen.getByTestId('data-location-input')).toBeTruthy(); + expect(screen.getByTestId('target-metric-value-input')).toBeTruthy(); + expect(screen.getByTestId('aggregation-type-select')).toBeTruthy(); + }); + + it('should render scale-to-zero fields when minimum replicas is 0', () => { + render(); + + expect(screen.getByTestId('scale-to-zero-input')).toBeTruthy(); }); }); diff --git a/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/CpuRamFields.spec.tsx b/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/CpuRamFields.spec.tsx index e2bee8dff4a8..bc94f0276176 100644 --- a/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/CpuRamFields.spec.tsx +++ b/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/CpuRamFields.spec.tsx @@ -1,8 +1,7 @@ import { render, screen } from '@testing-library/react'; import { describe, expect, it } from 'vitest'; -import { useForm, FormProvider, Control } from 'react-hook-form'; +import { FormProvider, useForm } from 'react-hook-form'; import { CpuRamFields } from './CpuRamFields'; -import { FullScalingFormValues } from '../scalingHelper'; const TestWrapper = ({ defaultValue = 75 }: { defaultValue?: number }) => { const methods = useForm({ @@ -13,7 +12,10 @@ const TestWrapper = ({ defaultValue = 75 }: { defaultValue?: number }) => { return ( - + ); }; diff --git a/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/CpuRamFields.tsx b/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/CpuRamFields.tsx index 6e2b238fb366..28e1f28c37a9 100644 --- a/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/CpuRamFields.tsx +++ b/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/CpuRamFields.tsx @@ -1,6 +1,6 @@ import { HelpCircle } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import { useFormContext, useWatch } from 'react-hook-form'; +import { Control, FieldPath, FieldValues } from 'react-hook-form'; import { FormField, FormItem, @@ -9,18 +9,31 @@ import { PopoverTrigger, Slider, } from '@datatr-ux/uxlib'; -import { ScalingStrategySchema, SCALING_DEFAULTS } from '../scalingHelper'; +import { + SCALING_CONSTRAINTS, + ScalingStrategySchema, +} from '../scalingHelper'; + +interface CpuRamFieldsProps< + TFieldValues extends FieldValues & ScalingStrategySchema, +> { + averageUsageTarget: number; + control: Control; +} -export function CpuRamFields() { +export function CpuRamFields< + TFieldValues extends FieldValues & ScalingStrategySchema, +>({ + averageUsageTarget, + control, +}: CpuRamFieldsProps) { const { t } = useTranslation('ai-tools/components/scaling'); - const { control } = useFormContext(); - const averageUsageTarget = useWatch({ control, name: 'averageUsageTarget' }); return ( -
+
} render={({ field }) => (
@@ -47,8 +60,8 @@ export function CpuRamFields() { onValueChange={([newValue]) => field.onChange(newValue)} id="resource-usage-select" value={[averageUsageTarget]} - min={0} - max={100} + min={SCALING_CONSTRAINTS.AVERAGE_USAGE_TARGET.MIN} + max={SCALING_CONSTRAINTS.AVERAGE_USAGE_TARGET.MAX} step={1} />
- + ); }; @@ -39,10 +38,6 @@ describe('CustomMetricsFields', () => { expect(screen.getByTestId('aggregation-type-select')).toBeTruthy(); }); - // Note: Error messages are now displayed only on form submission (mode: 'onSubmit') - // These tests have been removed as they checked for automatic error display - // The validation now happens when the user submits the form - it('should update metricUrl value', () => { render(); @@ -82,7 +77,7 @@ describe('CustomMetricsFields', () => { ) as HTMLInputElement; expect(input.type).toBe('number'); - expect(input.min).toBe('0'); + expect(input.min).toBe(''); expect(input.step).toBe('0.5'); }); }); diff --git a/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/CustomMetricsFields.tsx b/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/CustomMetricsFields.tsx index 9b296a6fb594..34bb8060a014 100644 --- a/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/CustomMetricsFields.tsx +++ b/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/CustomMetricsFields.tsx @@ -1,6 +1,6 @@ import { HelpCircle } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import { useFormContext } from 'react-hook-form'; +import { Control, FieldPath, FieldValues } from 'react-hook-form'; import { FormControl, FormField, @@ -18,18 +18,28 @@ import { } from '@datatr-ux/uxlib'; import ai from '@/types/AI'; -import { ScalingStrategySchema } from '../scalingHelper'; +import { + SCALING_CONSTRAINTS, + ScalingStrategySchema, +} from '../scalingHelper'; + +interface CustomMetricsFieldsProps< + TFieldValues extends FieldValues & ScalingStrategySchema, +> { + control: Control; +} -export function CustomMetricsFields() { +export function CustomMetricsFields< + TFieldValues extends FieldValues & ScalingStrategySchema, +>({ control }: CustomMetricsFieldsProps) { const { t } = useTranslation('ai-tools/components/scaling'); - const { control } = useFormContext(); return ( - <> -
+
+
} render={({ field }) => (
@@ -58,56 +68,10 @@ export function CustomMetricsFields() { )} />
-
+
( - -
-

{t('dataFormatLabel')}

- - - - - -

{t('dataFormatInfo')}

-
-
-
- - - -
- )} - /> -
-
- } render={({ field }) => (
@@ -136,41 +100,56 @@ export function CustomMetricsFields() { )} />
-
+
} render={({ field }) => (
-

{t('targetMetricValueLabel')}

+

{t('dataFormatLabel')}

-

{t('targetMetricValueInfo')}

+

{t('dataFormatInfo')}

- - - + +
)} />
-
+
} render={({ field }) => (
@@ -222,6 +201,37 @@ export function CustomMetricsFields() { )} />
- +
+ } + render={({ field }) => ( + +
+

{t('targetMetricValueLabel')}

+ + + + + +

{t('targetMetricValueInfo')}

+
+
+
+ + + + +
+ )} + /> +
+
); } diff --git a/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/ReplicaFields.spec.tsx b/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/ReplicaFields.spec.tsx index 01e9aff0aa91..90c80b71bb27 100644 --- a/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/ReplicaFields.spec.tsx +++ b/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/ReplicaFields.spec.tsx @@ -1,8 +1,7 @@ import { fireEvent, render, screen } from '@testing-library/react'; -import { describe, expect, it, vi } from 'vitest'; -import { useForm, FormProvider, Control } from 'react-hook-form'; +import { describe, expect, it } from 'vitest'; +import { FormProvider, useForm } from 'react-hook-form'; import { ReplicaFields } from './ReplicaFields'; -import { FullScalingFormValues } from '../scalingHelper'; const TestWrapper = () => { const methods = useForm({ @@ -14,7 +13,10 @@ const TestWrapper = () => { return ( - + ); }; @@ -37,12 +39,12 @@ describe('ReplicaFields', () => { expect(maxInput.value).toBe('3'); }); - it('should accept numeric input within range', () => { + it('should accept numeric input', () => { render(); const minInput = screen.getByTestId('min-rep-input') as HTMLInputElement; - fireEvent.change(minInput, { target: { value: '50' } }); + fireEvent.change(minInput, { target: { value: '5' } }); - expect(minInput.value).toBe('50'); + expect(minInput.value).toBe('5'); }); }); diff --git a/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/ReplicaFields.tsx b/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/ReplicaFields.tsx index f5cbdccb6e4d..55466c2bb960 100644 --- a/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/ReplicaFields.tsx +++ b/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/ReplicaFields.tsx @@ -1,79 +1,96 @@ -import { HelpCircle } from 'lucide-react'; +import { AlertCircle } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import { useFormContext } from 'react-hook-form'; +import { Control, FieldPath, FieldValues } from 'react-hook-form'; import { + Alert, FormControl, FormField, FormItem, FormMessage, Input, - Label, - Popover, - PopoverContent, - PopoverTrigger, } from '@datatr-ux/uxlib'; import { ScalingStrategySchema } from '../scalingHelper'; -export function ReplicaFields() { +interface ReplicaFieldsProps< + TFieldValues extends FieldValues & ScalingStrategySchema, +> { + control: Control; + showScaleToZeroWarning: boolean; + syncReplicasMaxFromMin?: (replicasMinValue?: unknown) => void; +} + +export function ReplicaFields< + TFieldValues extends FieldValues & ScalingStrategySchema, +>({ + control, + showScaleToZeroWarning, + syncReplicasMaxFromMin, +}: ReplicaFieldsProps) { const { t } = useTranslation('ai-tools/components/scaling'); - const { control } = useFormContext(); return ( - <> -
- ( - -
-

{t('replicasMinInputLabel')}

- - - - - -

{t('haInfoHelper')}

-
-
-
- - - - -
- )} - /> -
-
- ( - - - - - - - - )} - /> +
+

{t('replicasSectionTitle')}

+ {showScaleToZeroWarning && ( + +
+ +

{t('scaleToZeroWarningFlavor')}

+
+
+ )} +
+
+ } + render={({ field }) => ( + +
+

{t('replicasMinInputLabel')}

+
+ + { + field.onChange(event); + syncReplicasMaxFromMin?.(); + }} + /> + + +
+ )} + /> +
+
+ } + render={({ field }) => ( + +
+

{t('replicasMaxInputLabel')}

+
+ + + + +
+ )} + /> +
- +
); } diff --git a/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/ResourceTypeSelector.spec.tsx b/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/ResourceTypeSelector.spec.tsx index c95c511b3cd8..456e035985fa 100644 --- a/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/ResourceTypeSelector.spec.tsx +++ b/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/ResourceTypeSelector.spec.tsx @@ -1,8 +1,7 @@ import { fireEvent, render, screen } from '@testing-library/react'; -import { describe, expect, it, vi } from 'vitest'; -import { useForm, FormProvider, Control } from 'react-hook-form'; +import { describe, expect, it } from 'vitest'; +import { FormProvider, useForm } from 'react-hook-form'; import { ResourceTypeSelector } from './ResourceTypeSelector'; -import { FullScalingFormValues } from '../scalingHelper'; import ai from '@/types/AI'; const TestWrapper = () => { @@ -14,7 +13,7 @@ const TestWrapper = () => { return ( - + ); }; diff --git a/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/ResourceTypeSelector.tsx b/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/ResourceTypeSelector.tsx index 38d2cb3ec907..314e9cf8556f 100644 --- a/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/ResourceTypeSelector.tsx +++ b/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/ResourceTypeSelector.tsx @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next'; -import { useFormContext } from 'react-hook-form'; +import { Control, FieldPath, FieldValues } from 'react-hook-form'; import { HelpCircle } from 'lucide-react'; import { FormField, @@ -15,15 +15,25 @@ import { import ai from '@/types/AI'; import { ScalingStrategySchema, ResourceType } from '../scalingHelper'; -export function ResourceTypeSelector() { +interface ResourceTypeSelectorProps< + TFieldValues extends FieldValues & ScalingStrategySchema, +> { + control: Control; +} + +export function ResourceTypeSelector< + TFieldValues extends FieldValues & ScalingStrategySchema, +>({ + control, +}: ResourceTypeSelectorProps) { const { t } = useTranslation('ai-tools/components/scaling'); - const { control } = useFormContext(); return ( -
+
+

{t('triggerSectionTitle')}

} render={({ field }) => (
diff --git a/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/ScalingDelayFields.tsx b/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/ScalingDelayFields.tsx new file mode 100644 index 000000000000..1d2efadf40f8 --- /dev/null +++ b/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/AutoScalingForm/ScalingDelayFields.tsx @@ -0,0 +1,154 @@ +import { AlertCircle, HelpCircle } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { Control, FieldPath, FieldValues } from 'react-hook-form'; +import { + Alert, + FormControl, + FormField, + FormItem, + Input, + FormMessage, + Popover, + PopoverContent, + PopoverTrigger, +} from '@datatr-ux/uxlib'; +import { + ScalingStrategySchema, +} from '../scalingHelper'; + +interface ScalingDelayFieldsProps< + TFieldValues extends FieldValues & ScalingStrategySchema, +> { + control: Control; + showScaleToZero: boolean; +} + +export function ScalingDelayFields< + TFieldValues extends FieldValues & ScalingStrategySchema, +>({ + control, + showScaleToZero, +}: ScalingDelayFieldsProps) { + const { t } = useTranslation('ai-tools/components/scaling'); + + return ( +
+

{t('scaleWindowSectionTitle')}

+ {showScaleToZero && ( + +
+ +

{t('scaleToZeroWarningTraffic')}

+
+
+ )} +
+ {showScaleToZero && ( +
+ } + render={({ field }) => ( + +
+

{t('scaleToZeroInputLabel')}

+ + + + + +

{t('scaleToZeroInputInfo')}

+
+
+
+ + + + +
+ )} + /> +
+ )} +
+ + } + render={({ field }) => ( + +
+

{t('scaleDownDelayInputLabel')}

+ + + + + +

{t('scaleDownDelayInputInfo')}

+
+
+
+ + + + +
+ )} + /> +
+
+ + } + render={({ field }) => ( + +
+

{t('scaleUpDelayInputLabel')}

+ + + + + +

{t('scaleUpDelayInputInfo')}

+
+
+
+ + + + +
+ )} + /> +
+
+
+ ); +} diff --git a/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/ScalingStrategy.component.tsx b/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/ScalingStrategy.component.tsx index 36708ff969ff..3cb92b194ae6 100644 --- a/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/ScalingStrategy.component.tsx +++ b/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/ScalingStrategy.component.tsx @@ -1,8 +1,9 @@ import { useTranslation } from 'react-i18next'; import { - useFormContext, - useWatch, + Control, ControllerRenderProps, + FieldPath, + FieldValues, } from 'react-hook-form'; import { ExternalLink, HelpCircle } from 'lucide-react'; import { @@ -17,32 +18,43 @@ import { PopoverTrigger, Switch, } from '@datatr-ux/uxlib'; -import { AppPricing } from '@/types/orderFunnel'; import A from '@/components/links/A.component'; import { GUIDES, getGuideUrl } from '@/configuration/guide'; import { useLocale } from '@/hooks/useLocale'; import { AutoScalingForm } from './AutoScalingForm/AutoScalingForm.component'; -import Price from '@/components/price/Price.component'; -import { ScalingStrategySchema, SCALING_DEFAULTS } from './scalingHelper'; +import { + SCALING_CONSTRAINTS, + ScalingStrategySchema, +} from './scalingHelper'; -interface ScalingStrategyProps { - pricingFlavor?: AppPricing; +interface ScalingStrategyProps< + TFieldValues extends FieldValues & ScalingStrategySchema, +> { + autoScaling?: boolean; + averageUsageTarget: number; + control: Control; + isCustom: boolean; + syncReplicasMaxFromMin: (replicasMinValue?: unknown) => void; + showScaleToZero: boolean; } -export default function ScalingStrategy({ - pricingFlavor, -}: ScalingStrategyProps) { +export default function ScalingStrategy< + TFieldValues extends FieldValues & ScalingStrategySchema, +>({ + autoScaling, + averageUsageTarget, + control, + isCustom, + syncReplicasMaxFromMin, + showScaleToZero, +}: ScalingStrategyProps) { const { t } = useTranslation('ai-tools/components/scaling'); const locale = useLocale(); - const { control } = useFormContext(); - - const autoScaling = useWatch({ control, name: 'autoScaling' }); - const replicas = useWatch({ control, name: 'replicas' }); const renderAutoScalingField = ({ field, }: { - field: ControllerRenderProps; + field: ControllerRenderProps>; }) => (
@@ -67,7 +79,7 @@ export default function ScalingStrategy({ const renderReplicasField = ({ field, }: { - field: ControllerRenderProps; + field: ControllerRenderProps>; }) => (
@@ -119,15 +131,21 @@ export default function ScalingStrategy({
} render={renderAutoScalingField} /> {autoScaling ? ( - + ) : ( } render={renderReplicasField} /> )} diff --git a/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/ScalingStrategy.spec.tsx b/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/ScalingStrategy.spec.tsx index c57b922baf2e..7b5dcc514f8e 100644 --- a/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/ScalingStrategy.spec.tsx +++ b/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/ScalingStrategy.spec.tsx @@ -5,13 +5,23 @@ import { screen, waitFor, } from '@testing-library/react'; -import { describe, it, vi } from 'vitest'; -import { useForm, FormProvider } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { describe, expect, it, vi } from 'vitest'; +import { + useForm, + FormProvider, + Resolver, + UseFormReturn, +} from 'react-hook-form'; import { mockManagerReactShellClient } from '@/__tests__/helpers/mockShellHelper'; import { RouterWithQueryClientWrapper } from '@/__tests__/helpers/wrappers/RouterWithQueryClientWrapper'; import ScalingStrategy from './ScalingStrategy.component'; -import { mockedAppPricing1 } from '@/__tests__/helpers/mocks/app/appHelper'; import ai from '@/types/AI'; +import { + baseScalingSchema, + ScalingStrategySchema, + useScalingStrategyForm, +} from './scalingHelper'; describe('Scaling strategy component', () => { beforeEach(() => { @@ -37,16 +47,26 @@ describe('Scaling strategy component', () => { const TestWrapper = ({ autoScaling = false, replicas = 1, + onSubmit = vi.fn(), }: { autoScaling?: boolean; replicas?: number; + onSubmit?: (data: ScalingStrategySchema) => void; }) => { - const form = useForm({ + const tScaling = (key: string) => key; + const form = useForm({ + resolver: zodResolver( + baseScalingSchema(tScaling), + ) as Resolver, + mode: 'onChange', defaultValues: { autoScaling, replicas, replicasMin: 1, replicasMax: 1, + cooldownPeriodSeconds: 300, + scaleUpStabilizationWindowSeconds: 0, + scaleDownStabilizationWindowSeconds: 300, resourceType: ai.app.ScalingAutomaticStrategyResourceTypeEnum.CPU, averageUsageTarget: 75, metricUrl: '', @@ -59,11 +79,46 @@ describe('Scaling strategy component', () => { return ( - +
+ + +
); }; + const ScalingStrategyHarness = ({ + form, + }: { + form: UseFormReturn; + }) => { + const { + autoScaling, + averageUsageTargetValue, + isCustom, + syncReplicasMaxFromMin, + showScaleToZero, + } = useScalingStrategyForm(form); + + return ( + + ); + }; + it('should display Autoscaling form with value', async () => { render(, { wrapper: RouterWithQueryClientWrapper, @@ -100,4 +155,37 @@ describe('Scaling strategy component', () => { expect(screen.getByTestId('replicas-input')).toBeTruthy(); }); }); + + it('should normalize max replicas when minimum replicas becomes greater', async () => { + const onSubmit = vi.fn(); + + render(, { + wrapper: RouterWithQueryClientWrapper, + }); + + const minInput = await screen.findByTestId( + 'min-rep-input', + ) as HTMLInputElement; + const maxInput = screen.getByTestId('max-rep-input') as HTMLInputElement; + + act(() => { + fireEvent.change(maxInput, { target: { value: '1' } }); + fireEvent.change(minInput, { target: { value: '2' } }); + }); + + await waitFor(() => { + expect(maxInput.value).toBe('2'); + }); + + fireEvent.click(screen.getByRole('button', { name: 'submit' })); + + await waitFor(() => { + expect(onSubmit).toHaveBeenCalled(); + }); + + expect(onSubmit.mock.calls[0][0]).toMatchObject({ + replicasMin: 2, + replicasMax: 2, + }); + }); }); diff --git a/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/scalingHelper.ts b/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/scalingHelper.ts index c8f7872673f5..87b0208a6c7e 100644 --- a/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/scalingHelper.ts +++ b/packages/manager/apps/pci-ai-tools/src/components/order/app-scaling/scalingHelper.ts @@ -1,3 +1,10 @@ +import { + FieldPath, + FieldValues, + Resolver, + UseFormReturn, + useWatch, +} from 'react-hook-form'; import { z } from 'zod'; import { Scaling } from '@/types/orderFunnel'; import ai from '@/types/AI'; @@ -18,6 +25,36 @@ export const SCALING_DEFAULTS = { DATA_FORMAT: ai.app.CustomMetricsFormatEnum.JSON, AGGREGATION_TYPE: ai.app.CustomMetricsAggregationTypeEnum.AVERAGE, REPLICAS: 1, + COOLDOWN_PERIOD_SECONDS: 300, + SCALE_UP_STABILIZATION_WINDOW_SECONDS: 0, + SCALE_DOWN_STABILIZATION_WINDOW_SECONDS: 300, +} as const; + +export const SCALING_CONSTRAINTS = { + FIXED_REPLICAS: { + MIN: 1, + MAX: 10, + }, + AUTO_REPLICAS: { + MIN: 0, + MAX: 10, + }, + AVERAGE_USAGE_TARGET: { + MIN: 0, + MAX: 100, + }, + SCALE_DELAY: { + MIN: 0, + MAX: 3600, + }, + SCALE_TO_ZERO_DELAY: { + MIN: 60, + }, + TARGET_METRIC_VALUE: { + MIN: 0, + MAX: 100, + STEP: 0.5, + }, } as const; export type ScalingStrategySchema = { @@ -26,6 +63,9 @@ export type ScalingStrategySchema = { averageUsageTarget?: number; replicasMax?: number; replicasMin?: number; + cooldownPeriodSeconds?: number; + scaleUpStabilizationWindowSeconds?: number; + scaleDownStabilizationWindowSeconds?: number; resourceType?: ResourceType; metricUrl?: string; dataFormat?: ai.app.CustomMetricsFormatEnum; @@ -35,6 +75,89 @@ export type ScalingStrategySchema = { }; const URL_REGEX = /^https?:\/\/.+/; +const emptyToUndefined = (value: unknown) => + value === '' || value === null || value === undefined ? undefined : value; + +const behaviorScalingNumberSchema = z.preprocess( + emptyToUndefined, + z.coerce + .number() + .catch(undefined) + .optional(), +); +const behaviorScalingIntSchema = z.preprocess( + emptyToUndefined, + z.coerce + .number() + .int() + .catch(undefined) + .optional(), +); +const optionalScalingNumberSchema = z.preprocess( + emptyToUndefined, + z.coerce.number().optional(), +); +const replicasMinBehaviorSchema = behaviorScalingIntSchema; +const replicasMaxBehaviorSchema = behaviorScalingIntSchema; +const replicasBehaviorSchema = z + .object({ + replicasMin: replicasMinBehaviorSchema, + replicasMax: replicasMaxBehaviorSchema, + }) + .transform(({ replicasMin, replicasMax }) => { + const minReplicasMax = Math.max( + SCALING_CONSTRAINTS.FIXED_REPLICAS.MIN, + replicasMin ?? SCALING_CONSTRAINTS.FIXED_REPLICAS.MIN, + ); + const normalizedReplicasMax = + replicasMin !== undefined && + (replicasMax === undefined || replicasMax < minReplicasMax) + ? minReplicasMax + : replicasMax; + + return { + minReplicasMax, + normalizedReplicasMax, + showScaleToZero: replicasMin === 0, + }; + }); + +const getScalingNumberValue = (value: unknown) => { + return behaviorScalingNumberSchema.parse(value); +}; + +interface ScalingStrategyFormStateInput { + averageUsageTarget: unknown; + replicasMin: unknown; + resourceType: unknown; +} + +const getReplicasBehavior = (replicasMin: unknown, replicasMax: unknown) => { + return replicasBehaviorSchema.parse({ + replicasMin, + replicasMax, + }); +}; + +const getScalingStrategyFormState = ({ + averageUsageTarget, + replicasMin, + resourceType, +}: ScalingStrategyFormStateInput) => { + const parsedAverageUsageTarget = optionalScalingNumberSchema.safeParse( + averageUsageTarget, + ); + const replicasBehavior = getReplicasBehavior(replicasMin, undefined); + return { + averageUsageTargetValue: + parsedAverageUsageTarget.success && + parsedAverageUsageTarget.data !== undefined + ? parsedAverageUsageTarget.data + : SCALING_DEFAULTS.AVERAGE_USAGE, + isCustom: resourceType === ResourceType.CUSTOM, + showScaleToZero: replicasBehavior.showScaleToZero, + }; +}; export const baseScalingSchema = (t: (key: string) => string) => z @@ -42,18 +165,74 @@ export const baseScalingSchema = (t: (key: string) => string) => autoScaling: z.boolean(), replicas: z.coerce .number() - .min(1) - .max(10), - replicasMin: z.coerce - .number() - .min(1) - .max(10) - .optional(), - replicasMax: z.coerce - .number() - .min(1) - .max(10) - .optional(), + .int() + .min(SCALING_CONSTRAINTS.FIXED_REPLICAS.MIN) + .max(SCALING_CONSTRAINTS.FIXED_REPLICAS.MAX), + replicasMin: z.preprocess( + emptyToUndefined, + z.coerce + .number() + .int() + .min(SCALING_CONSTRAINTS.AUTO_REPLICAS.MIN, { + message: t('replicasMinRangeError'), + }) + .max(SCALING_CONSTRAINTS.AUTO_REPLICAS.MAX, { + message: t('replicasMinRangeError'), + }) + .optional(), + ), + replicasMax: z.preprocess( + emptyToUndefined, + z.coerce + .number() + .int() + .min(SCALING_CONSTRAINTS.FIXED_REPLICAS.MIN, { + message: t('replicasMaxRangeError'), + }) + .max(SCALING_CONSTRAINTS.FIXED_REPLICAS.MAX, { + message: t('replicasMaxRangeError'), + }) + .optional(), + ), + cooldownPeriodSeconds: z.preprocess( + emptyToUndefined, + z.coerce + .number() + .int() + .min(SCALING_CONSTRAINTS.SCALE_DELAY.MIN, { + message: t('scaleDelayRangeError'), + }) + .max(SCALING_CONSTRAINTS.SCALE_DELAY.MAX, { + message: t('scaleDelayRangeError'), + }) + .optional(), + ), + scaleUpStabilizationWindowSeconds: z.preprocess( + emptyToUndefined, + z.coerce + .number() + .int() + .min(SCALING_CONSTRAINTS.SCALE_DELAY.MIN, { + message: t('scaleDelayRangeError'), + }) + .max(SCALING_CONSTRAINTS.SCALE_DELAY.MAX, { + message: t('scaleDelayRangeError'), + }) + .optional(), + ), + scaleDownStabilizationWindowSeconds: z.preprocess( + emptyToUndefined, + z.coerce + .number() + .int() + .min(SCALING_CONSTRAINTS.SCALE_DELAY.MIN, { + message: t('scaleDelayRangeError'), + }) + .max(SCALING_CONSTRAINTS.SCALE_DELAY.MAX, { + message: t('scaleDelayRangeError'), + }) + .optional(), + ), resourceType: z.nativeEnum(ResourceType).optional(), averageUsageTarget: z.coerce.number().optional(), metricUrl: z.preprocess( @@ -74,8 +253,8 @@ export const baseScalingSchema = (t: (key: string) => string) => z.coerce .number() .finite() - .min(0) - .max(100) + .min(SCALING_CONSTRAINTS.TARGET_METRIC_VALUE.MIN) + .max(SCALING_CONSTRAINTS.TARGET_METRIC_VALUE.MAX) .optional(), ), aggregationType: z @@ -84,11 +263,16 @@ export const baseScalingSchema = (t: (key: string) => string) => }) .superRefine((data, ctx) => { if (!data.autoScaling) return; - if (data.replicasMin > data.replicasMax) { + if ( + data.replicasMin === 0 && + (data.cooldownPeriodSeconds === undefined || + data.cooldownPeriodSeconds < + SCALING_CONSTRAINTS.SCALE_TO_ZERO_DELAY.MIN) + ) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: t('errorFormMinMaxRepField'), - path: ['replicasMin'], + message: t('scaleToZeroDelayRangeError'), + path: ['cooldownPeriodSeconds'], }); } if (data.resourceType === ResourceType.CUSTOM) { @@ -114,17 +298,136 @@ export const baseScalingSchema = (t: (key: string) => string) => }); } } + }) + .transform((data) => { + const replicasBehavior = getReplicasBehavior( + data.replicasMin, + data.replicasMax, + ); + + return { + ...data, + replicasMax: replicasBehavior.normalizedReplicasMax, + }; }); +export function withScalingResolverSync< + TFieldValues extends FieldValues & ScalingStrategySchema +>(resolver: Resolver): Resolver { + return async (values, context, options) => { + const names = options.names ? ([...options.names] as string[]) : undefined; + + if (names?.includes('replicasMin') && !names.includes('replicasMax')) { + names.push('replicasMax'); + } + + return resolver( + values, + context, + names ? { ...options, names: names as typeof options.names } : options, + ); + }; +} + export type FullScalingFormValues = z.infer< ReturnType >; +export function useScalingStrategyForm< + TFieldValues extends FieldValues & ScalingStrategySchema +>(form: UseFormReturn) { + const [ + autoScaling, + averageUsageTarget, + replicasMin, + replicasMax, + resourceType, + ] = useWatch({ + control: form.control, + name: [ + 'autoScaling' as FieldPath, + 'averageUsageTarget' as FieldPath, + 'replicasMin' as FieldPath, + 'replicasMax' as FieldPath, + 'resourceType' as FieldPath, + ], + }); + + const { + averageUsageTargetValue, + isCustom, + showScaleToZero, + } = getScalingStrategyFormState({ + averageUsageTarget, + replicasMin, + resourceType, + }); + + const syncReplicasMaxFromMin = (replicasMinValue?: unknown) => { + const normalizedReplicasMinValue = getScalingNumberValue( + replicasMinValue ?? + form.getValues('replicasMin' as FieldPath), + ); + form.setValue( + 'replicasMin' as FieldPath, + normalizedReplicasMinValue as TFieldValues[FieldPath], + { + shouldDirty: true, + shouldValidate: false, + }, + ); + + const currentReplicasMaxValue = getScalingNumberValue( + form.getValues('replicasMax' as FieldPath), + ); + const replicasBehavior = getReplicasBehavior( + normalizedReplicasMinValue, + currentReplicasMaxValue, + ); + const normalizedReplicasMaxValue = replicasBehavior.normalizedReplicasMax; + + if ( + normalizedReplicasMaxValue !== undefined && + normalizedReplicasMaxValue !== currentReplicasMaxValue + ) { + form.setValue( + 'replicasMax' as FieldPath, + normalizedReplicasMaxValue as TFieldValues[FieldPath], + { + shouldDirty: true, + shouldValidate: false, + }, + ); + } + + void form.trigger([ + 'replicasMin' as FieldPath, + 'replicasMax' as FieldPath, + ]); + }; + + return { + autoScaling: Boolean(autoScaling), + averageUsageTargetValue, + isCustom, + syncReplicasMaxFromMin, + showScaleToZero, + }; +} + export const getInitialValues = (scaling: Scaling): FullScalingFormValues => ({ autoScaling: scaling.autoScaling ?? SCALING_DEFAULTS.AUTO_SCALING, replicas: scaling.replicas ?? SCALING_DEFAULTS.REPLICAS, replicasMin: scaling.replicasMin ?? SCALING_DEFAULTS.MIN_REPLICAS, replicasMax: scaling.replicasMax ?? SCALING_DEFAULTS.MAX_REPLICAS, + cooldownPeriodSeconds: + scaling.cooldownPeriodSeconds ?? SCALING_DEFAULTS.COOLDOWN_PERIOD_SECONDS, + scaleUpStabilizationWindowSeconds: + scaling.scaleUpStabilizationWindowSeconds ?? + SCALING_DEFAULTS.SCALE_UP_STABILIZATION_WINDOW_SECONDS, + scaleDownStabilizationWindowSeconds: + scaling.scaleDownStabilizationWindowSeconds ?? + SCALING_DEFAULTS.SCALE_DOWN_STABILIZATION_WINDOW_SECONDS, resourceType: scaling.resourceType ?? SCALING_DEFAULTS.RESOURCE_TYPE, averageUsageTarget: scaling.averageUsageTarget ?? SCALING_DEFAULTS.AVERAGE_USAGE, diff --git a/packages/manager/apps/pci-ai-tools/src/lib/orderFunnelHelper.ts b/packages/manager/apps/pci-ai-tools/src/lib/orderFunnelHelper.ts index fd7f63fb83d4..f01b843d1c30 100644 --- a/packages/manager/apps/pci-ai-tools/src/lib/orderFunnelHelper.ts +++ b/packages/manager/apps/pci-ai-tools/src/lib/orderFunnelHelper.ts @@ -197,28 +197,37 @@ export function getAppSpec( if (formResult.scaling.autoScaling) { const isCustom = formResult.scaling.resourceType === ResourceType.CUSTOM; + const automaticScaling: ai.app.ScalingStrategyInput['automatic'] = { + replicasMin: formResult.scaling.replicasMin, + replicasMax: formResult.scaling.replicasMax, + scaleUpStabilizationWindowSeconds: + formResult.scaling.scaleUpStabilizationWindowSeconds, + scaleDownStabilizationWindowSeconds: + formResult.scaling.scaleDownStabilizationWindowSeconds, + ...(formResult.scaling.replicasMin === 0 && { + cooldownPeriodSeconds: formResult.scaling.cooldownPeriodSeconds, + }), + ...((!isCustom && { + averageUsageTarget: formResult.scaling.averageUsageTarget, + resourceType: formResult.scaling + .resourceType as ai.app.ScalingAutomaticStrategyResourceTypeEnum, + }) || + {}), + ...(isCustom && { + customMetrics: { + apiUrl: formResult.scaling.metricUrl, + format: formResult.scaling + .dataFormat as ai.app.CustomMetricsFormatEnum, + targetValue: formResult.scaling.targetMetricValue, + valueLocation: formResult.scaling.dataLocation, + aggregationType: formResult.scaling + .aggregationType as ai.app.CustomMetricsAggregationTypeEnum, + }, + }), + }; + appInfos.scalingStrategy = { - automatic: { - replicasMin: formResult.scaling.replicasMin, - replicasMax: formResult.scaling.replicasMax, - ...((!isCustom && { - averageUsageTarget: formResult.scaling.averageUsageTarget, - resourceType: formResult.scaling - .resourceType as ai.app.ScalingAutomaticStrategyResourceTypeEnum, - }) || - {}), - ...(isCustom && { - customMetrics: { - apiUrl: formResult.scaling.metricUrl, - format: formResult.scaling - .dataFormat as ai.app.CustomMetricsFormatEnum, - targetValue: formResult.scaling.targetMetricValue, - valueLocation: formResult.scaling.dataLocation, - aggregationType: formResult.scaling - .aggregationType as ai.app.CustomMetricsAggregationTypeEnum, - }, - }), - }, + automatic: automaticScaling, }; } else { appInfos.scalingStrategy = { diff --git a/packages/manager/apps/pci-ai-tools/src/lib/statusHelper.tsx b/packages/manager/apps/pci-ai-tools/src/lib/statusHelper.tsx index ab435e2ceb8a..c514c469644b 100644 --- a/packages/manager/apps/pci-ai-tools/src/lib/statusHelper.tsx +++ b/packages/manager/apps/pci-ai-tools/src/lib/statusHelper.tsx @@ -57,6 +57,9 @@ export const isStoppedJob = (currentState: ai.job.JobStateEnum) => export const isDeletingApp = (currentState: ai.app.AppStateEnum) => [ai.app.AppStateEnum.DELETING].includes(currentState); +export const isStandbyApp = (currentState: ai.app.AppStateEnum) => + currentState === ai.app.AppStateEnum.STANDBY; + export const isRunningApp = (currentState: ai.app.AppStateEnum) => [ ai.app.AppStateEnum.RUNNING, @@ -65,7 +68,18 @@ export const isRunningApp = (currentState: ai.app.AppStateEnum) => ai.app.AppStateEnum.QUEUED, ].includes(currentState); +export const isRunningOrStandbyApp = (currentState: ai.app.AppStateEnum) => + isRunningApp(currentState) || isStandbyApp(currentState); + export const isStoppedApp = (currentState: ai.app.AppStateEnum) => + [ + ai.app.AppStateEnum.ERROR, + ai.app.AppStateEnum.FAILED, + ai.app.AppStateEnum.STOPPED, + ai.app.AppStateEnum.STANDBY, + ].includes(currentState); + +export const isDeletableApp = (currentState: ai.app.AppStateEnum) => [ ai.app.AppStateEnum.ERROR, ai.app.AppStateEnum.FAILED, diff --git a/packages/manager/apps/pci-ai-tools/src/pages/apps/[appId]/_components/AppHeader.component.tsx b/packages/manager/apps/pci-ai-tools/src/pages/apps/[appId]/_components/AppHeader.component.tsx index 9a680086d03a..0b44543a22f4 100644 --- a/packages/manager/apps/pci-ai-tools/src/pages/apps/[appId]/_components/AppHeader.component.tsx +++ b/packages/manager/apps/pci-ai-tools/src/pages/apps/[appId]/_components/AppHeader.component.tsx @@ -13,7 +13,10 @@ import ai from '@/types/AI'; import AppStatusBadge from '../../_components/AppStatusBadge.component'; import StartApp from './StartApp.component'; import StopApp from './StopApp.component'; -import { isDeletingApp, isRunningApp } from '@/lib/statusHelper'; +import { + isDeletingApp, + isRunningOrStandbyApp, +} from '@/lib/statusHelper'; import A from '@/components/links/A.component'; export const AppHeader = ({ app }: { app: ai.app.App }) => { @@ -37,7 +40,7 @@ export const AppHeader = ({ app }: { app: ai.app.App }) => {

{app.spec.name ?? 'Dashboard'}

- {isRunningApp(app.status.state) || + {isRunningOrStandbyApp(app.status.state) || isDeletingApp(app.status.state) ? ( diff --git a/packages/manager/apps/pci-ai-tools/src/pages/apps/[appId]/dashboard/_components/update-scaling/UpdateScaling.modal.tsx b/packages/manager/apps/pci-ai-tools/src/pages/apps/[appId]/dashboard/_components/update-scaling/UpdateScaling.modal.tsx index 58ee6c27db51..8d7412d608d6 100644 --- a/packages/manager/apps/pci-ai-tools/src/pages/apps/[appId]/dashboard/_components/update-scaling/UpdateScaling.modal.tsx +++ b/packages/manager/apps/pci-ai-tools/src/pages/apps/[appId]/dashboard/_components/update-scaling/UpdateScaling.modal.tsx @@ -1,6 +1,5 @@ import { useTranslation } from 'react-i18next'; -import { useMemo } from 'react'; -import { FormProvider, useForm } from 'react-hook-form'; +import { FormProvider, Resolver, useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { useNavigate } from 'react-router-dom'; import { @@ -19,62 +18,65 @@ import { getAIApiErrorMessage } from '@/lib/apiHelper'; import { useAppData } from '../../../App.context'; import RouteModal from '@/components/route-modal/RouteModal'; import ScalingStrategy from '@/components/order/app-scaling/ScalingStrategy.component'; -import { AppPricing, Scaling } from '@/types/orderFunnel'; -import { getFlavorPricing } from '@/lib/priceFlavorHelper'; -import { useGetCatalog } from '@/data/hooks/catalog/useGetCatalog.hook'; +import { Scaling } from '@/types/orderFunnel'; import { useScalingStrategy } from '@/data/hooks/ai/app/scaling-strategy/useScalingStrategy.hook'; import { getInitialValues, FullScalingFormValues, baseScalingSchema, ResourceType, + useScalingStrategyForm, + withScalingResolverSync, } from '@/components/order/app-scaling/scalingHelper'; const UpdateScaling = () => { const { app, projectId } = useAppData(); - const catalogQuery = useGetCatalog({ refetchOnWindowFocus: false }); const navigate = useNavigate(); const toast = useToast(); const { t } = useTranslation('ai-tools/apps/app/dashboard/update-scaling'); const { t: tScaling } = useTranslation('ai-tools/components/scaling'); - const pricingResource: AppPricing = useMemo(() => { - if (!catalogQuery.isSuccess) return { price: 0, tax: 0 }; - return getFlavorPricing( - app.spec.resources.flavor, - catalogQuery.data, - 'ai-app', - ); - }, [app, catalogQuery.isSuccess]); + const automatic = app.spec.scalingStrategy?.automatic; + const fixed = app.spec.scalingStrategy?.fixed; + const currentScaling: Scaling = { + autoScaling: !!automatic, + replicas: fixed?.replicas || automatic?.replicasMin || 1, + replicasMin: automatic?.replicasMin, + replicasMax: automatic?.replicasMax, + cooldownPeriodSeconds: automatic?.cooldownPeriodSeconds, + scaleUpStabilizationWindowSeconds: + automatic?.scaleUpStabilizationWindowSeconds, + scaleDownStabilizationWindowSeconds: + automatic?.scaleDownStabilizationWindowSeconds, + resourceType: automatic?.customMetrics + ? ResourceType.CUSTOM + : automatic?.resourceType, + averageUsageTarget: automatic?.averageUsageTarget, + metricUrl: automatic?.customMetrics?.apiUrl, + dataFormat: automatic?.customMetrics?.format, + dataLocation: automatic?.customMetrics?.valueLocation, + targetMetricValue: automatic?.customMetrics?.targetValue, + aggregationType: automatic?.customMetrics?.aggregationType, + }; - const currentScaling: Scaling = useMemo(() => { - const automatic = app.spec.scalingStrategy?.automatic; - const fixed = app.spec.scalingStrategy?.fixed; + const scalingSchema = baseScalingSchema(tScaling); - return { - autoScaling: !!automatic, - replicas: fixed?.replicas || automatic?.replicasMin || 1, - replicasMin: automatic?.replicasMin, - replicasMax: automatic?.replicasMax, - resourceType: automatic?.customMetrics - ? ResourceType.CUSTOM - : automatic?.resourceType, - averageUsageTarget: automatic?.averageUsageTarget, - metricUrl: automatic?.customMetrics?.apiUrl, - dataFormat: automatic?.customMetrics?.format, - dataLocation: automatic?.customMetrics?.valueLocation, - targetMetricValue: automatic?.customMetrics?.targetValue, - aggregationType: automatic?.customMetrics?.aggregationType, - }; - }, [app.spec.scalingStrategy]); - - const scalingSchema = useMemo(() => baseScalingSchema(tScaling), [tScaling]); - - const form = useForm({ - resolver: zodResolver(scalingSchema), + const form = useForm({ + resolver: withScalingResolverSync( + zodResolver( + scalingSchema, + ) as Resolver, + ), defaultValues: getInitialValues(currentScaling), - mode: 'onSubmit', + mode: 'onChange', }); + const { + autoScaling, + averageUsageTargetValue, + isCustom, + syncReplicasMaxFromMin, + showScaleToZero, + } = useScalingStrategyForm(form); const { scalingStrategy, isPending } = useScalingStrategy({ onError: (err) => { @@ -96,28 +98,41 @@ const UpdateScaling = () => { const onSubmit = form.handleSubmit((formValues) => { const isCustom = formValues.resourceType === ResourceType.CUSTOM; - const scalingInfo: ai.app.ScalingStrategyInput = formValues.autoScaling - ? { - automatic: { - replicasMin: formValues.replicasMin, - replicasMax: formValues.replicasMax, - ...((!isCustom && { - averageUsageTarget: formValues.averageUsageTarget, - resourceType: formValues.resourceType as ai.app.ScalingAutomaticStrategyResourceTypeEnum, - }) || - {}), - ...(isCustom && { - customMetrics: { - apiUrl: formValues.metricUrl || '', - format: formValues.dataFormat as ai.app.CustomMetricsFormatEnum, - targetValue: formValues.targetMetricValue || 0, - valueLocation: formValues.dataLocation || '', - aggregationType: formValues.aggregationType as ai.app.CustomMetricsAggregationTypeEnum, - }, - }), + let scalingInfo: ai.app.ScalingStrategyInput; + + if (formValues.autoScaling) { + const automaticScaling: ai.app.ScalingStrategyInput['automatic'] = { + replicasMin: formValues.replicasMin, + replicasMax: formValues.replicasMax, + scaleUpStabilizationWindowSeconds: + formValues.scaleUpStabilizationWindowSeconds, + scaleDownStabilizationWindowSeconds: + formValues.scaleDownStabilizationWindowSeconds, + ...(formValues.replicasMin === 0 && { + cooldownPeriodSeconds: formValues.cooldownPeriodSeconds, + }), + ...((!isCustom && { + averageUsageTarget: formValues.averageUsageTarget, + resourceType: + formValues.resourceType as ai.app.ScalingAutomaticStrategyResourceTypeEnum, + }) || + {}), + ...(isCustom && { + customMetrics: { + apiUrl: formValues.metricUrl || '', + format: formValues.dataFormat as ai.app.CustomMetricsFormatEnum, + targetValue: formValues.targetMetricValue || 0, + valueLocation: formValues.dataLocation || '', + aggregationType: + formValues.aggregationType as ai.app.CustomMetricsAggregationTypeEnum, }, - } - : { fixed: { replicas: formValues.replicas } }; + }), + }; + + scalingInfo = { automatic: automaticScaling }; + } else { + scalingInfo = { fixed: { replicas: formValues.replicas } }; + } scalingStrategy({ projectId, appId: app.id, scalingStrat: scalingInfo }); }); @@ -125,7 +140,7 @@ const UpdateScaling = () => { return ( 0)} + isLoading={false} > @@ -136,7 +151,14 @@ const UpdateScaling = () => {
- +