diff --git a/internal/confparser/confparser.go b/internal/confparser/confparser.go index c1488f4ee..c1bdb28ab 100644 --- a/internal/confparser/confparser.go +++ b/internal/confparser/confparser.go @@ -413,6 +413,12 @@ func (f *FieldConfig) validateSingleValue(val string, index int) error { if _, err := net.ParseMAC(val); err != nil { return fmt.Errorf("%s is not a valid MAC address: %s", fieldRef, val) } + case "hostname_or_ipv4_or_ipv6": + if net.ParseIP(val) == nil { + if _, err := idna.Lookup.ToASCII(val); err != nil { + return fmt.Errorf("%s is not a valid hostname, IPv4 or IPv6 address: %s", fieldRef, val) + } + } case "hostname": if _, err := idna.Lookup.ToASCII(val); err != nil { return fmt.Errorf("%s is not a valid hostname: %s", fieldRef, val) diff --git a/internal/network/types/config.go b/internal/network/types/config.go index 33afbcc7d..e00392209 100644 --- a/internal/network/types/config.go +++ b/internal/network/types/config.go @@ -49,7 +49,7 @@ type NetworkConfig struct { TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,http_user_provided" default:"ntp,http"` TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"` TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"` - TimeSyncNTPServers []string `json:"time_sync_ntp_servers,omitempty" validate_type:"ipv4_or_ipv6" required_if:"TimeSyncOrdering=ntp_user_provided"` + TimeSyncNTPServers []string `json:"time_sync_ntp_servers,omitempty" validate_type:"hostname_or_ipv4_or_ipv6" required_if:"TimeSyncOrdering=ntp_user_provided"` TimeSyncHTTPUrls []string `json:"time_sync_http_urls,omitempty" validate_type:"url" required_if:"TimeSyncOrdering=http_user_provided"` } diff --git a/ui/localization/messages/da.json b/ui/localization/messages/da.json index d2064532f..5aa55d9f3 100644 --- a/ui/localization/messages/da.json +++ b/ui/localization/messages/da.json @@ -688,11 +688,36 @@ "network_settings_load_error": "Kunne ikke indlæse netværksindstillinger: {error}", "network_static_ipv4_header": "Statisk IPv4-konfiguration", "network_static_ipv6_header": "Statisk IPv6-konfiguration", + "network_time_sync_add_http_url": "Tilføj HTTP URL", + "network_time_sync_add_ntp_server": "Tilføj NTP-server", + "network_time_sync_config_header": "Tidssynkroniseringskonfiguration", + "network_time_sync_custom": "Skik", "network_time_sync_description": "Konfigurer indstillinger for tidssynkronisering", + "network_time_sync_disable_fallback_description": "Undgå brug af indbyggede servere til tidssynkronisering", + "network_time_sync_disable_fallback_title": "Deaktiver Fallback", "network_time_sync_http_only": "Kun HTTP", + "network_time_sync_http_url_invalid": "Ugyldig URL for HTTP-tidssynkronisering", + "network_time_sync_http_url_required": "HTTP-tidssynkroniserings-URL påkrævet med brugerleveret HTTP-URL-kilde", + "network_time_sync_no_matching_sources": "Ingen matchende kilder fundet", "network_time_sync_ntp_and_http": "NTP og HTTP", "network_time_sync_ntp_only": "Kun NTP", + "network_time_sync_ntp_server_invalid": "Ugyldig NTP-server", + "network_time_sync_ntp_server_required": "NTP Time Sync Server påkrævet med brugerleveret NTP-serverkilde", + "network_time_sync_ordering_description": "Prioritering af tidssynkroniseringskilder.", + "network_time_sync_ordering_required": "Der kræves mindst én synkroniseringskilde", + "network_time_sync_ordering_title": "Synkronisering Kildebestilling", + "network_time_sync_parallel_description": "Antal servere, der skal forespørges parallelt", + "network_time_sync_parallel_invalid": "Parallelle forespørgsler skal være større end nul", + "network_time_sync_parallel_title": "Parallelle forespørgsler", + "network_time_sync_select_sources": "Vælg kilder...", + "network_time_sync_source_builtin_http": "Indbyggede HTTP-URL'er", + "network_time_sync_source_builtin_ntp": "Indbyggede NTP-servere", + "network_time_sync_source_dhcp": "DHCP NTP-servere", + "network_time_sync_source_user_http": "Brugerleverede HTTP-URL'er", + "network_time_sync_source_user_ntp": "Brugerleverede NTP-servere", "network_time_sync_title": "Tidssynkronisering", + "network_time_sync_user_http_urls_label": "Brugerleverede HTTP-URL'er", + "network_time_sync_user_ntp_servers_label": "Brugerleverede NTP-servere", "network_title": "Netværk", "never_seen_online": "Aldrig set online", "next": "Næste", diff --git a/ui/localization/messages/de.json b/ui/localization/messages/de.json index 326baa165..f8eb674f5 100644 --- a/ui/localization/messages/de.json +++ b/ui/localization/messages/de.json @@ -688,11 +688,36 @@ "network_settings_load_error": "Netzwerkeinstellungen konnten nicht geladen werden: {error}", "network_static_ipv4_header": "Statische IPv4-Konfiguration", "network_static_ipv6_header": "Statische IPv6-Konfiguration", + "network_time_sync_add_http_url": "HTTP-URL hinzufügen", + "network_time_sync_add_ntp_server": "NTP-Server hinzufügen", + "network_time_sync_config_header": "Konfiguration der Zeitsynchronisation", + "network_time_sync_custom": "Brauch", "network_time_sync_description": "Konfigurieren der Zeitsynchronisierungseinstellungen", + "network_time_sync_disable_fallback_description": "Verhindern Sie die Verwendung integrierter Server zur Zeitsynchronisierung", + "network_time_sync_disable_fallback_title": "Fallback deaktivieren", "network_time_sync_http_only": "Nur HTTP", + "network_time_sync_http_url_invalid": "Ungültige HTTP-Zeitsynchronisierungs-URL", + "network_time_sync_http_url_required": "HTTP-Zeitsynchronisierungs-URL erforderlich mit vom Benutzer bereitgestellter HTTP-URL-Quelle", + "network_time_sync_no_matching_sources": "Keine passenden Quellen gefunden", "network_time_sync_ntp_and_http": "NTP und HTTP", "network_time_sync_ntp_only": "Nur NTP", + "network_time_sync_ntp_server_invalid": "Ungültiger NTP-Server", + "network_time_sync_ntp_server_required": "NTP-Zeitsynchronisierungsserver mit vom Benutzer bereitgestellter NTP-Serverquelle erforderlich", + "network_time_sync_ordering_description": "Priorisierung von Zeitsynchronisationsquellen.", + "network_time_sync_ordering_required": "Es ist mindestens eine Synchronisationsquelle erforderlich", + "network_time_sync_ordering_title": "Reihenfolge der Synchronisationsquellen", + "network_time_sync_parallel_description": "Anzahl der Server, die parallel abgefragt werden sollen", + "network_time_sync_parallel_invalid": "Parallele Abfragen müssen größer als Null sein", + "network_time_sync_parallel_title": "Parallele Abfragen", + "network_time_sync_select_sources": "Quellen auswählen...", + "network_time_sync_source_builtin_http": "Integrierte HTTP-URLs", + "network_time_sync_source_builtin_ntp": "Integrierte NTP-Server", + "network_time_sync_source_dhcp": "DHCP-NTP-Server", + "network_time_sync_source_user_http": "Vom Benutzer bereitgestellte HTTP-URLs", + "network_time_sync_source_user_ntp": "Vom Benutzer bereitgestellte NTP-Server", "network_time_sync_title": "Zeitsynchronisation", + "network_time_sync_user_http_urls_label": "Vom Benutzer bereitgestellte HTTP-URLs", + "network_time_sync_user_ntp_servers_label": "Vom Benutzer bereitgestellte NTP-Server", "network_title": "Netzwerk", "never_seen_online": "Noch nie online gesehen", "next": "Nächste", diff --git a/ui/localization/messages/en.json b/ui/localization/messages/en.json index a36874d0c..74ce31a0d 100644 --- a/ui/localization/messages/en.json +++ b/ui/localization/messages/en.json @@ -693,11 +693,36 @@ "network_settings_load_error": "Failed to load network settings: {error}", "network_static_ipv4_header": "Static IPv4 Configuration", "network_static_ipv6_header": "Static IPv6 Configuration", + "network_time_sync_add_http_url": "Add HTTP URL", + "network_time_sync_add_ntp_server": "Add NTP Server", + "network_time_sync_config_header": "Time Synchronization Configuration", + "network_time_sync_custom": "Custom", "network_time_sync_description": "Configure time synchronization settings", + "network_time_sync_disable_fallback_description": "Prevent use of built-in servers for time synchronization", + "network_time_sync_disable_fallback_title": "Disable Fallback", "network_time_sync_http_only": "HTTP only", + "network_time_sync_http_url_invalid": "Invalid HTTP Time Sync URL", + "network_time_sync_http_url_required": "HTTP Time Sync URL required with User-provided HTTP URLs source", + "network_time_sync_no_matching_sources": "No matching sources found", "network_time_sync_ntp_and_http": "NTP and HTTP", "network_time_sync_ntp_only": "NTP only", - "network_time_sync_title": "Time synchronization", + "network_time_sync_ntp_server_invalid": "Invalid NTP Server", + "network_time_sync_ntp_server_required": "NTP Time Sync Server required with User-provided NTP Servers source", + "network_time_sync_ordering_description": "Prioritization of time synchronization sources.", + "network_time_sync_ordering_title": "Synchronization Source Ordering", + "network_time_sync_ordering_required": "At least one synchronization source is required", + "network_time_sync_parallel_invalid": "Parallel queries must be greater than zero", + "network_time_sync_parallel_description": "Number of servers to query in parallel", + "network_time_sync_parallel_title": "Parallel Queries", + "network_time_sync_select_sources": "Select sources...", + "network_time_sync_source_builtin_http": "Built-in HTTP URLs", + "network_time_sync_source_builtin_ntp": "Built-in NTP Servers", + "network_time_sync_source_dhcp": "DHCP NTP Servers", + "network_time_sync_source_user_http": "User-provided HTTP URLs", + "network_time_sync_source_user_ntp": "User-provided NTP Servers", + "network_time_sync_title": "Time Synchronization", + "network_time_sync_user_http_urls_label": "User-provided HTTP URLs", + "network_time_sync_user_ntp_servers_label": "User-provided NTP Servers", "network_title": "Network", "never_seen_online": "Never seen online", "next": "Next", diff --git a/ui/localization/messages/es.json b/ui/localization/messages/es.json index cec167b1f..87d5bc490 100644 --- a/ui/localization/messages/es.json +++ b/ui/localization/messages/es.json @@ -688,11 +688,36 @@ "network_settings_load_error": "No se pudo cargar la configuración de red: {error}", "network_static_ipv4_header": "Configuración estática de IPv4", "network_static_ipv6_header": "Configuración estática de IPv6", + "network_time_sync_add_http_url": "Agregar URL HTTP", + "network_time_sync_add_ntp_server": "Agregar servidor NTP", + "network_time_sync_config_header": "Configuración de sincronización horaria", + "network_time_sync_custom": "Costumbre", "network_time_sync_description": "Configurar los ajustes de sincronización horaria", + "network_time_sync_disable_fallback_description": "Evite el uso de servidores integrados para la sincronización horaria", + "network_time_sync_disable_fallback_title": "Deshabilitar el respaldo", "network_time_sync_http_only": "Sólo HTTP", + "network_time_sync_http_url_invalid": "URL de sincronización de hora HTTP no válida", + "network_time_sync_http_url_required": "Se requiere la URL de sincronización de hora HTTP con la fuente de URL HTTP proporcionada por el usuario", + "network_time_sync_no_matching_sources": "No se encontraron fuentes coincidentes", "network_time_sync_ntp_and_http": "NTP y HTTP", "network_time_sync_ntp_only": "Sólo NTP", + "network_time_sync_ntp_server_invalid": "Servidor NTP no válido", + "network_time_sync_ntp_server_required": "Se requiere un servidor de sincronización de hora NTP con una fuente de servidores NTP proporcionada por el usuario", + "network_time_sync_ordering_description": "Priorización de fuentes de sincronización horaria.", + "network_time_sync_ordering_required": "Se requiere al menos una fuente de sincronización", + "network_time_sync_ordering_title": "Orden de origen de sincronización", + "network_time_sync_parallel_description": "Número de servidores para consultar en paralelo", + "network_time_sync_parallel_invalid": "Las consultas paralelas deben ser mayores que cero.", + "network_time_sync_parallel_title": "Consultas paralelas", + "network_time_sync_select_sources": "Seleccionar fuentes...", + "network_time_sync_source_builtin_http": "URL HTTP integradas", + "network_time_sync_source_builtin_ntp": "Servidores NTP integrados", + "network_time_sync_source_dhcp": "Servidores DHCP NTP", + "network_time_sync_source_user_http": "URL HTTP proporcionadas por el usuario", + "network_time_sync_source_user_ntp": "Servidores NTP proporcionados por el usuario", "network_time_sync_title": "Sincronización horaria", + "network_time_sync_user_http_urls_label": "URL HTTP proporcionadas por el usuario", + "network_time_sync_user_ntp_servers_label": "Servidores NTP proporcionados por el usuario", "network_title": "Red", "never_seen_online": "Nunca visto en línea", "next": "Siguiente", diff --git a/ui/localization/messages/fr.json b/ui/localization/messages/fr.json index b86bc160b..ebd9b12a7 100644 --- a/ui/localization/messages/fr.json +++ b/ui/localization/messages/fr.json @@ -688,11 +688,36 @@ "network_settings_load_error": "Échec du chargement des paramètres réseau : {error}", "network_static_ipv4_header": "Configuration IPv4 statique", "network_static_ipv6_header": "Configuration IPv6 statique", + "network_time_sync_add_http_url": "Ajouter une URL HTTP", + "network_time_sync_add_ntp_server": "Ajouter un serveur NTP", + "network_time_sync_config_header": "Configuration de la synchronisation de l'heure", + "network_time_sync_custom": "Coutume", "network_time_sync_description": "Configurer les paramètres de synchronisation de l'heure", + "network_time_sync_disable_fallback_description": "Empêcher l'utilisation de serveurs intégrés pour la synchronisation de l'heure", + "network_time_sync_disable_fallback_title": "Désactiver le repli", "network_time_sync_http_only": "HTTP uniquement", + "network_time_sync_http_url_invalid": "URL de synchronisation de l'heure HTTP non valide", + "network_time_sync_http_url_required": "URL HTTP Time Sync requise avec la source d'URL HTTP fournie par l'utilisateur", + "network_time_sync_no_matching_sources": "Aucune source correspondante trouvée", "network_time_sync_ntp_and_http": "NTP et HTTP", "network_time_sync_ntp_only": "NTP uniquement", + "network_time_sync_ntp_server_invalid": "Serveur NTP invalide", + "network_time_sync_ntp_server_required": "Serveur NTP Time Sync requis avec la source de serveurs NTP fournie par l'utilisateur", + "network_time_sync_ordering_description": "Priorisation des sources de synchronisation horaire.", + "network_time_sync_ordering_required": "Au moins une source de synchronisation est requise", + "network_time_sync_ordering_title": "Ordre des sources de synchronisation", + "network_time_sync_parallel_description": "Nombre de serveurs à interroger en parallèle", + "network_time_sync_parallel_invalid": "Les requêtes parallèles doivent être supérieures à zéro", + "network_time_sync_parallel_title": "Requêtes parallèles", + "network_time_sync_select_sources": "Sélectionnez les sources...", + "network_time_sync_source_builtin_http": "URL HTTP intégrées", + "network_time_sync_source_builtin_ntp": "Serveurs NTP intégrés", + "network_time_sync_source_dhcp": "Serveurs DHCP NTP", + "network_time_sync_source_user_http": "URL HTTP fournies par l'utilisateur", + "network_time_sync_source_user_ntp": "Serveurs NTP fournis par l'utilisateur", "network_time_sync_title": "Synchronisation horaire", + "network_time_sync_user_http_urls_label": "URL HTTP fournies par l'utilisateur", + "network_time_sync_user_ntp_servers_label": "Serveurs NTP fournis par l'utilisateur", "network_title": "Réseau", "never_seen_online": "Jamais vu en ligne", "next": "Suivant", diff --git a/ui/localization/messages/it.json b/ui/localization/messages/it.json index 3fe77fedf..76afc00c3 100644 --- a/ui/localization/messages/it.json +++ b/ui/localization/messages/it.json @@ -688,11 +688,36 @@ "network_settings_load_error": "Impossibile caricare le impostazioni di rete: {error}", "network_static_ipv4_header": "Configurazione IPv4 statica", "network_static_ipv6_header": "Configurazione IPv6 statica", + "network_time_sync_add_http_url": "Aggiungi URL HTTP", + "network_time_sync_add_ntp_server": "Aggiungi server NTP", + "network_time_sync_config_header": "Configurazione della sincronizzazione dell'ora", + "network_time_sync_custom": "Costume", "network_time_sync_description": "Configurare le impostazioni di sincronizzazione dell'ora", + "network_time_sync_disable_fallback_description": "Impedisci l'uso di server integrati per la sincronizzazione dell'ora", + "network_time_sync_disable_fallback_title": "Disabilita il fallback", "network_time_sync_http_only": "Solo HTTP", + "network_time_sync_http_url_invalid": "URL di sincronizzazione dell'ora HTTP non valido", + "network_time_sync_http_url_required": "URL di sincronizzazione temporale HTTP richiesto con l'origine URL HTTP fornita dall'utente", + "network_time_sync_no_matching_sources": "Nessuna fonte corrispondente trovata", "network_time_sync_ntp_and_http": "NTP e HTTP", "network_time_sync_ntp_only": "Solo NTP", + "network_time_sync_ntp_server_invalid": "Server NTP non valido", + "network_time_sync_ntp_server_required": "È richiesto il server di sincronizzazione dell'ora NTP con l'origine dei server NTP forniti dall'utente", + "network_time_sync_ordering_description": "Priorità delle fonti di sincronizzazione dell'ora.", + "network_time_sync_ordering_required": "È richiesta almeno una fonte di sincronizzazione", + "network_time_sync_ordering_title": "Ordinamento delle origini di sincronizzazione", + "network_time_sync_parallel_description": "Numero di server da interrogare in parallelo", + "network_time_sync_parallel_invalid": "Le query parallele devono essere maggiori di zero", + "network_time_sync_parallel_title": "Domande parallele", + "network_time_sync_select_sources": "Seleziona fonti...", + "network_time_sync_source_builtin_http": "URL HTTP incorporati", + "network_time_sync_source_builtin_ntp": "Server NTP integrati", + "network_time_sync_source_dhcp": "Server DHCP NTP", + "network_time_sync_source_user_http": "URL HTTP forniti dall'utente", + "network_time_sync_source_user_ntp": "Server NTP forniti dall'utente", "network_time_sync_title": "Sincronizzazione oraria", + "network_time_sync_user_http_urls_label": "URL HTTP forniti dall'utente", + "network_time_sync_user_ntp_servers_label": "Server NTP forniti dall'utente", "network_title": "Rete", "never_seen_online": "Mai visto online", "next": "Avanti", diff --git a/ui/localization/messages/nb.json b/ui/localization/messages/nb.json index 59c8ea64c..7705eb659 100644 --- a/ui/localization/messages/nb.json +++ b/ui/localization/messages/nb.json @@ -688,11 +688,36 @@ "network_settings_load_error": "Kunne ikke laste inn nettverksinnstillinger: {error}", "network_static_ipv4_header": "Statisk IPv4-konfigurasjon", "network_static_ipv6_header": "Statisk IPv6-konfigurasjon", + "network_time_sync_add_http_url": "Legg til HTTP URL", + "network_time_sync_add_ntp_server": "Legg til NTP-server", + "network_time_sync_config_header": "Tidssynkroniseringskonfigurasjon", + "network_time_sync_custom": "Skikk", "network_time_sync_description": "Konfigurer innstillinger for tidssynkronisering", + "network_time_sync_disable_fallback_description": "Forhindre bruk av innebygde servere for tidssynkronisering", + "network_time_sync_disable_fallback_title": "Deaktiver Fallback", "network_time_sync_http_only": "Kun HTTP", + "network_time_sync_http_url_invalid": "Ugyldig URL for HTTP-tidssynkronisering", + "network_time_sync_http_url_required": "HTTP-tidssynkroniserings-URL kreves med brukeroppgitt HTTP-URL-kilde", + "network_time_sync_no_matching_sources": "Fant ingen samsvarende kilder", "network_time_sync_ntp_and_http": "NTP og HTTP", "network_time_sync_ntp_only": "Kun NTP", + "network_time_sync_ntp_server_invalid": "Ugyldig NTP-server", + "network_time_sync_ntp_server_required": "NTP Time Sync Server kreves med brukerlevert NTP-serverkilde", + "network_time_sync_ordering_description": "Prioritering av tidssynkroniseringskilder.", + "network_time_sync_ordering_required": "Minst én synkroniseringskilde kreves", + "network_time_sync_ordering_title": "Synkronisering Kildebestilling", + "network_time_sync_parallel_description": "Antall servere som skal spørres parallelt", + "network_time_sync_parallel_invalid": "Parallelle søk må være større enn null", + "network_time_sync_parallel_title": "Parallelle spørringer", + "network_time_sync_select_sources": "Velg kilder...", + "network_time_sync_source_builtin_http": "Innebygde HTTP URL-er", + "network_time_sync_source_builtin_ntp": "Innebygde NTP-servere", + "network_time_sync_source_dhcp": "DHCP NTP-servere", + "network_time_sync_source_user_http": "Brukeroppgitte HTTP-URLer", + "network_time_sync_source_user_ntp": "Brukerleverte NTP-servere", "network_time_sync_title": "Tidssynkronisering", + "network_time_sync_user_http_urls_label": "Brukeroppgitte HTTP-URLer", + "network_time_sync_user_ntp_servers_label": "Brukerleverte NTP-servere", "network_title": "Nettverk", "never_seen_online": "Aldri sett på nett", "next": "Neste", diff --git a/ui/localization/messages/sv.json b/ui/localization/messages/sv.json index 17c794f30..76fcb505f 100644 --- a/ui/localization/messages/sv.json +++ b/ui/localization/messages/sv.json @@ -688,11 +688,36 @@ "network_settings_load_error": "Misslyckades med att läsa in nätverksinställningar: {error}", "network_static_ipv4_header": "Statisk IPv4-konfiguration", "network_static_ipv6_header": "Statisk IPv6-konfiguration", + "network_time_sync_add_http_url": "Lägg till HTTP URL", + "network_time_sync_add_ntp_server": "Lägg till NTP-server", + "network_time_sync_config_header": "Tidssynkroniseringskonfiguration", + "network_time_sync_custom": "Beställnings", "network_time_sync_description": "Konfigurera inställningar för tidssynkronisering", + "network_time_sync_disable_fallback_description": "Förhindra användning av inbyggda servrar för tidssynkronisering", + "network_time_sync_disable_fallback_title": "Inaktivera reserv", "network_time_sync_http_only": "Endast HTTP", + "network_time_sync_http_url_invalid": "Ogiltig URL för HTTP-tidssynkronisering", + "network_time_sync_http_url_required": "HTTP-tidssynkroniserings-URL krävs med HTTP-URL:s källa som tillhandahålls av användaren", + "network_time_sync_no_matching_sources": "Inga matchande källor hittades", "network_time_sync_ntp_and_http": "NTP och HTTP", "network_time_sync_ntp_only": "Endast NTP", + "network_time_sync_ntp_server_invalid": "Ogiltig NTP-server", + "network_time_sync_ntp_server_required": "NTP Time Sync Server krävs med användartillhandahållen NTP-serverkälla", + "network_time_sync_ordering_description": "Prioritering av tidssynkroniseringskällor.", + "network_time_sync_ordering_required": "Minst en synkroniseringskälla krävs", + "network_time_sync_ordering_title": "Synkronisering Källordning", + "network_time_sync_parallel_description": "Antal servrar att fråga parallellt", + "network_time_sync_parallel_invalid": "Parallella frågor måste vara större än noll", + "network_time_sync_parallel_title": "Parallella frågor", + "network_time_sync_select_sources": "Välj källor...", + "network_time_sync_source_builtin_http": "Inbyggda HTTP-URL:er", + "network_time_sync_source_builtin_ntp": "Inbyggda NTP-servrar", + "network_time_sync_source_dhcp": "DHCP NTP-servrar", + "network_time_sync_source_user_http": "Användarangivna HTTP-URL:er", + "network_time_sync_source_user_ntp": "Användarförsedda NTP-servrar", "network_time_sync_title": "Tidssynkronisering", + "network_time_sync_user_http_urls_label": "Användarangivna HTTP-URL:er", + "network_time_sync_user_ntp_servers_label": "Användarförsedda NTP-servrar", "network_title": "Nätverk", "never_seen_online": "Aldrig sett online", "next": "Nästa", diff --git a/ui/localization/messages/zh.json b/ui/localization/messages/zh.json index 9510b518a..3b990be98 100644 --- a/ui/localization/messages/zh.json +++ b/ui/localization/messages/zh.json @@ -688,11 +688,36 @@ "network_settings_load_error": "加载网络设置失败:{error}", "network_static_ipv4_header": "静态 IPv4 配置", "network_static_ipv6_header": "静态 IPv6 配置", + "network_time_sync_add_http_url": "添加 HTTP 网址", + "network_time_sync_add_ntp_server": "添加 NTP 服务器", + "network_time_sync_config_header": "时间同步配置", + "network_time_sync_custom": "风俗", "network_time_sync_description": "配置时间同步设置。", + "network_time_sync_disable_fallback_description": "防止使用内置服务器进行时间同步", + "network_time_sync_disable_fallback_title": "禁用回退", "network_time_sync_http_only": "仅 HTTP", + "network_time_sync_http_url_invalid": "HTTP 时间同步 URL 无效", + "network_time_sync_http_url_required": "用户提供的 HTTP URL 源需要 HTTP 时间同步 URL", + "network_time_sync_no_matching_sources": "未找到匹配的来源", "network_time_sync_ntp_and_http": "NTP 和 HTTP", "network_time_sync_ntp_only": "仅 NTP", + "network_time_sync_ntp_server_invalid": "NTP 服务器无效", + "network_time_sync_ntp_server_required": "NTP 时间同步服务器需要用户提供的 NTP 服务器源", + "network_time_sync_ordering_description": "时间同步源的优先级。", + "network_time_sync_ordering_required": "至少需要一个同步源", + "network_time_sync_ordering_title": "同步源排序", + "network_time_sync_parallel_description": "并行查询的服务器数量", + "network_time_sync_parallel_invalid": "并行查询必须大于零", + "network_time_sync_parallel_title": "并行查询", + "network_time_sync_select_sources": "选择来源...", + "network_time_sync_source_builtin_http": "内置 HTTP URL", + "network_time_sync_source_builtin_ntp": "内置 NTP 服务器", + "network_time_sync_source_dhcp": "DHCP NTP 服务器", + "network_time_sync_source_user_http": "用户提供的 HTTP URL", + "network_time_sync_source_user_ntp": "用户提供的 NTP 服务器", "network_time_sync_title": "时间同步", + "network_time_sync_user_http_urls_label": "用户提供的 HTTP URL", + "network_time_sync_user_ntp_servers_label": "用户提供的 NTP 服务器", "network_title": "网络", "never_seen_online": "从未在线", "next": "下一步", diff --git a/ui/src/components/Combobox.tsx b/ui/src/components/Combobox.tsx index a85b2c64a..b21fbebc5 100644 --- a/ui/src/components/Combobox.tsx +++ b/ui/src/components/Combobox.tsx @@ -2,13 +2,16 @@ import { useRef } from "react"; import clsx from "clsx"; import { Combobox as HeadlessCombobox, + ComboboxButton, ComboboxInput, ComboboxOption, ComboboxOptions, } from "@headlessui/react"; +import { ChevronDownIcon } from "@heroicons/react/16/solid"; import { m } from "@localizations/messages.js"; import Card from "@components/Card"; +import { FieldError } from "@components/InputField"; import { cva } from "@/cva.config"; export interface ComboboxOption { @@ -37,6 +40,7 @@ interface ComboboxProps extends Omit { emptyMessage?: string; size?: keyof typeof sizes; disabledMessage?: string; + error?: string | null; } export function Combobox({ @@ -49,74 +53,86 @@ export function Combobox({ size = "MD", onChange, disabledMessage = m.input_disabled(), + error, ...otherProps }: ComboboxProps) { const inputRef = useRef(null); const classes = comboboxVariants({ size }); return ( - - {() => ( - <> - - onInputChange(event.target.value)} - disabled={disabled} - /> - - - {options().length > 0 && ( - - {options().map(option => ( - - {option.label} - - ))} - - )} - - {options().length === 0 && inputRef.current?.value && ( -
-
{emptyMessage}
-
- )} - - )} -
+ <> + + {() => ( + <> + + onInputChange(event.target.value)} + disabled={disabled} + /> + + + + + + {options().length > 0 && ( + + {options().map(option => ( + + {option.label} + + ))} + + )} + + {options().length === 0 && inputRef.current?.value && ( +
+
{emptyMessage}
+
+ )} + + )} +
+ {error && } + ); } diff --git a/ui/src/components/CustomTimeConfigurationCard.tsx b/ui/src/components/CustomTimeConfigurationCard.tsx new file mode 100644 index 000000000..36497123a --- /dev/null +++ b/ui/src/components/CustomTimeConfigurationCard.tsx @@ -0,0 +1,388 @@ +import { useEffect, useMemo, useState } from "react"; +import { LuPlus, LuX } from "react-icons/lu"; +import { Controller, useFieldArray, useFormContext } from "react-hook-form"; +import validator from "validator"; + +import { NetworkSettings } from "@hooks/stores"; +import { Button } from "@components/Button"; +import { Checkbox } from "@components/Checkbox"; +import { Combobox, ComboboxOption } from "@components/Combobox"; +import FieldLabel from "@components/FieldLabel"; +import { GridCard } from "@components/Card"; +import InputField, { FieldError } from "@components/InputField"; +import { SettingsItem } from "@components/SettingsItem"; +import { m } from "@localizations/messages.js"; + +const timeSourcesDisplayMap = { + ntp: m.network_time_sync_source_builtin_ntp(), + http: m.network_time_sync_source_builtin_http(), + ntp_dhcp: m.network_time_sync_source_dhcp(), + ntp_user_provided: m.network_time_sync_source_user_ntp(), + http_user_provided: m.network_time_sync_source_user_http(), +} as Record; + +const ensureArray = (arr: T[] | null | undefined): T[] => { + return Array.isArray(arr) ? arr : []; +}; + +const sourceDisplay = (sourceDisplayMap: Record, source: string): string => { + return sourceDisplayMap[source] || source; +}; + +export default function CustomTimeConfigurationCard() { + const formMethods = useFormContext(); + const { control, formState, register, trigger, watch } = formMethods; + + const [sourceQuery, setSourceQuery] = useState(""); + + const time_sync_ordering = watch("time_sync_ordering"); + const time_sync_ntp_servers = watch("time_sync_ntp_servers"); + const time_sync_http_urls = watch("time_sync_http_urls"); + const time_sync_disable_fallback = watch("time_sync_disable_fallback"); + const time_sync_parallel = watch("time_sync_parallel"); + + const validateOrderingMethod = (list: string[] | null, source: string, err: string) => { + if (time_sync_ordering?.includes(source)) { + if (list === null || list?.length <= 0 || list?.every(v => v?.length <= 0)) { + return err; + } + } + return true; + }; + + const { + fields: ntpFields, + append: ntpAppend, + replace: ntpReplace, + } = useFieldArray({ + name: "time_sync_ntp_servers", + rules: { + validate: value => { + return validateOrderingMethod( + value as string[], + "ntp_user_provided", + m.network_time_sync_ntp_server_required(), + ); + }, + }, + }); + + const { + fields: httpFields, + append: httpAppend, + replace: httpReplace, + } = useFieldArray({ + name: "time_sync_http_urls", + rules: { + validate: value => { + return validateOrderingMethod( + value as string[], + "http_user_provided", + m.network_time_sync_http_url_required(), + ); + }, + }, + }); + + const sourceOptions = useMemo( + () => + Object.keys(timeSourcesDisplayMap).map(key => ({ + value: key, + label: sourceDisplay(timeSourcesDisplayMap, key), + })), + [], + ); + + const filteredSources = useMemo(() => { + const selectedSources = ensureArray(time_sync_ordering); + const availableSources = sourceOptions.filter( + option => !selectedSources.includes(option.value), + ); + + if (sourceQuery === "") { + return availableSources; + } else { + return availableSources.filter(option => + option.label.toLowerCase().includes(sourceQuery.toLowerCase()), + ); + } + }, [sourceOptions, sourceQuery, time_sync_ordering]); + + const onSourceSelect = (option: { value: string | null; sources?: string[] }) => { + let sources: string[] = []; + if (option.sources) { + // they gave us a full set of keys (e.g. from deleting one) + sources = [...new Set(ensureArray(option.sources))]; + } else { + // they gave us a single key to add + sources = [...time_sync_ordering]; + if (option.value !== null) { + sources.push(option.value); + } + sources = [...new Set(sources)]; + } + return sources; + }; + + const onSourceQueryChange = (query: string) => { + setSourceQuery(query); + }; + + // If somehow, the saved config is invalid, show validation errors + useEffect(() => { + trigger(); + }, [trigger]); + + return ( + +
+
+

+ {m.network_time_sync_config_header()} +

+
+ {/* Source ordering */} +
+
+ +
+ { + if (value === null || value?.length <= 0) { + return m.network_time_sync_ordering_required(); + } + return true; + }, + }} + render={({ field: orderingField, fieldState: orderingFieldState }) => ( + <> + {orderingField?.value?.length > 0 && ( +
+ {orderingField.value.map((source, sourceIndex) => ( + + + {sourceDisplay(timeSourcesDisplayMap, source)} + +
+ )} +
+ { + const selectedOption = option as ComboboxOption | null; + await orderingField.onChange( + onSourceSelect({ value: selectedOption?.value ?? null }), + ); + onSourceQueryChange(""); + orderingField.onBlur(); + trigger("time_sync_ntp_servers"); + trigger("time_sync_http_urls"); + }} + displayValue={() => sourceQuery} + onInputChange={onSourceQueryChange} + options={() => filteredSources} + size="SM" + immediate + placeholder={m.network_time_sync_select_sources()} + emptyMessage={m.network_time_sync_no_matching_sources()} + error={orderingFieldState?.error?.message} + /> +
+ + )} + /> +
+
+
+ {/* NTP server fields */} +
+ + {ntpFields.map((ntpServer, ntpIndex) => { + return ( +
+
+
+ { + if (value === null || value === "") { + return m.network_time_sync_ntp_server_invalid(); + } + return true; + }, + })} + onBlur={() => { + trigger("time_sync_ntp_servers"); + trigger("time_sync_ordering"); + }} + error={formState.errors.time_sync_ntp_servers?.[ntpIndex]?.message} + /> +
+ {ntpIndex >= 0 && ( +
+
+ )} +
+
+ ); + })} + +
+ {/* HTTP server fields */} +
+ + {httpFields.map((httpServer, httpIndex) => { + return ( +
+
+
+ { + if ( + value == "" || + value === null || + !validator.isURL(value || "", { + protocols: ["http", "https"], + require_tld: false, + require_protocol: true, + require_valid_protocol: true, + }) + ) { + return m.network_time_sync_http_url_invalid(); + } + return true; + }, + })} + onBlur={() => { + trigger("time_sync_http_urls"); + trigger("time_sync_ordering"); + }} + error={formState.errors.time_sync_http_urls?.[httpIndex]?.message} + /> +
+ {httpIndex >= 0 && ( +
+
+ )} +
+
+ ); + })} + +
+
+ + + + + { + return !isNaN(+value) && +value > 0 + ? true + : m.network_time_sync_parallel_invalid(); + }, + })} + error={formState.errors.time_sync_parallel?.message} + /> + +
+
+
+ ); +} diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 20f791848..2471f4929 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -777,6 +777,13 @@ export type IPv4Mode = "disabled" | "static" | "dhcp" | "unknown"; export type LLDPMode = "disabled" | "basic" | "all" | "unknown"; export type mDNSMode = "disabled" | "auto" | "ipv4_only" | "ipv6_only" | "unknown"; export type TimeSyncMode = "ntp_only" | "ntp_and_http" | "http_only" | "custom" | "unknown"; +export type TimeSyncOrdering = + | "ntp" + | "http" + | "ntp_dhcp" + | "ntp_user_provided" + | "http_user_provided" + | "unknown"; export interface IPv4StaticConfig { address: string; @@ -804,6 +811,11 @@ export interface NetworkSettings { lldp_tx_tlvs: string[]; mdns_mode: mDNSMode; time_sync_mode: TimeSyncMode; + time_sync_ordering: string[]; + time_sync_parallel: number; + time_sync_disable_fallback: boolean; + time_sync_ntp_servers: string[]; + time_sync_http_urls: string[]; } export const useNetworkStateStore = create((set, get) => ({ diff --git a/ui/src/routes/devices.$id.settings.network.tsx b/ui/src/routes/devices.$id.settings.network.tsx index 10e4cafab..88c90c621 100644 --- a/ui/src/routes/devices.$id.settings.network.tsx +++ b/ui/src/routes/devices.$id.settings.network.tsx @@ -11,6 +11,7 @@ import { useJsonRpc } from "@hooks/useJsonRpc"; import AutoHeight from "@components/AutoHeight"; import { Button } from "@components/Button"; import { ConfirmDialog } from "@components/ConfirmDialog"; +import CustomTimeConfigurationCard from "@components/CustomTimeConfigurationCard"; import DhcpLeaseCard from "@components/DhcpLeaseCard"; import EmptyCard from "@components/EmptyCard"; import { GridCard } from "@components/Card"; @@ -118,6 +119,11 @@ export default function SettingsNetworkRoute() { domain: settings.domain || "local", // TODO: null means local domain TRUE????? mdns_mode: settings.mdns_mode || "disabled", time_sync_mode: settings.time_sync_mode || "ntp_only", + time_sync_ordering: settings.time_sync_ordering || [], + time_sync_disable_fallback: settings.time_sync_disable_fallback || false, + time_sync_ntp_servers: settings.time_sync_ntp_servers || [], + time_sync_http_urls: settings.time_sync_http_urls || [], + time_sync_parallel: settings.time_sync_parallel || 4, ipv4_static: { address: settings.ipv4_static?.address || state.dhcp_lease?.ip || "", netmask: settings.ipv4_static?.netmask || state.dhcp_lease?.netmask || "", @@ -171,7 +177,33 @@ export default function SettingsNetworkRoute() { [customDomain], ); - const { register, handleSubmit, watch, formState, reset } = formMethods; + const { register, handleSubmit, watch, formState, reset, setValue } = formMethods; + + const onTimeModeSelect = useCallback( + (mode: string) => { + let ordering: string[] = ["ntp", "http"]; + if (mode === "ntp_only") ordering = ["ntp"]; + if (mode === "http_only") ordering = ["http"]; + + const timesyncSettings = { + time_sync_ordering: ordering, + time_sync_parallel: 4, + time_sync_disable_fallback: false, + time_sync_ntp_servers: [], + time_sync_http_urls: [], + }; + + Object.entries(timesyncSettings).forEach(([key, value]) => { + setValue( + key as keyof NetworkSettings, + mode === "custom" && initialSettingsRef.current + ? initialSettingsRef.current[key as keyof NetworkSettings] + : value, + ); + }); + }, + [setValue], + ); const onSubmit = useCallback( async (settings: NetworkSettings) => { @@ -320,6 +352,7 @@ export default function SettingsNetworkRoute() { const ipv4mode = watch("ipv4_mode"); const ipv6mode = watch("ipv6_mode"); const domain = watch("domain"); + const time_sync_mode = watch("time_sync_mode"); const onDhcpLeaseRenew = () => { send("renewDHCPLease", {}, resp => { @@ -482,11 +515,16 @@ export default function SettingsNetworkRoute() { { value: "ntp_only", label: m.network_time_sync_ntp_only() }, { value: "ntp_and_http", label: m.network_time_sync_ntp_and_http() }, { value: "http_only", label: m.network_time_sync_http_only() }, - // { value: "custom", label: "Custom" }, + { value: "custom", label: m.network_time_sync_custom() }, ]} - {...register("time_sync_mode")} + {...register("time_sync_mode", { + onChange: e => { + onTimeModeSelect(e.target.value); + }, + })} /> + {time_sync_mode === "custom" && }