mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 04:22:33 +08:00
feat: add NTP server configuration and manual sync server option (#1232)
* Initial plan * feat: add NTP server configuration support for time sync Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * feat: add system NTP server configuration with chrony and timesyncd support Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * fix: improve NTP service restart error handling Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * feat: 优化ntp * fix: logo跳转 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> Co-authored-by: 耗子 <haozi@loli.email>
This commit is contained in:
@@ -30,3 +30,11 @@ type ToolboxSystemHosts struct {
|
||||
type ToolboxSystemPassword struct {
|
||||
Password string `form:"password" json:"password" validate:"required|password"`
|
||||
}
|
||||
|
||||
type ToolboxSystemSyncTime struct {
|
||||
Server string `form:"server" json:"server"` // 可选的 NTP 服务器地址
|
||||
}
|
||||
|
||||
type ToolboxSystemNTPServers struct {
|
||||
Servers []string `form:"servers" json:"servers" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -468,6 +468,8 @@ func (route *Http) Register(r *chi.Mux) {
|
||||
r.Post("/timezone", route.toolboxSystem.UpdateTimezone)
|
||||
r.Post("/time", route.toolboxSystem.UpdateTime)
|
||||
r.Post("/sync_time", route.toolboxSystem.SyncTime)
|
||||
r.Get("/ntp_servers", route.toolboxSystem.GetNTPServers)
|
||||
r.Post("/ntp_servers", route.toolboxSystem.UpdateNTPServers)
|
||||
r.Get("/hostname", route.toolboxSystem.GetHostname)
|
||||
r.Post("/hostname", route.toolboxSystem.UpdateHostname)
|
||||
r.Get("/hosts", route.toolboxSystem.GetHosts)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
@@ -231,7 +232,18 @@ func (s *ToolboxSystemService) UpdateTime(w http.ResponseWriter, r *http.Request
|
||||
|
||||
// SyncTime 同步时间
|
||||
func (s *ToolboxSystemService) SyncTime(w http.ResponseWriter, r *http.Request) {
|
||||
now, err := ntp.Now()
|
||||
req, err := Bind[request.ToolboxSystemSyncTime](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var now time.Time
|
||||
if req.Server != "" {
|
||||
now, err = ntp.Now(req.Server)
|
||||
} else {
|
||||
now, err = ntp.Now()
|
||||
}
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -245,6 +257,38 @@ func (s *ToolboxSystemService) SyncTime(w http.ResponseWriter, r *http.Request)
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// GetNTPServers 获取系统 NTP 服务器配置
|
||||
func (s *ToolboxSystemService) GetNTPServers(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := ntp.GetSystemNTPConfig()
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to get NTP configuration: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, chix.M{
|
||||
"service_type": config.ServiceType,
|
||||
"servers": config.Servers,
|
||||
"builtins": ntp.GetBuiltinServers(),
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateNTPServers 更新系统 NTP 服务器配置
|
||||
func (s *ToolboxSystemService) UpdateNTPServers(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ToolboxSystemNTPServers](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新系统 NTP 配置
|
||||
if err = ntp.SetSystemNTPServers(req.Servers); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to set NTP servers: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// GetHostname 获取主机名
|
||||
func (s *ToolboxSystemService) GetHostname(w http.ResponseWriter, r *http.Request) {
|
||||
hostname, err := shell.Execf("hostnamectl hostname")
|
||||
|
||||
@@ -15,8 +15,8 @@ var ErrNotReachable = errors.New("failed to reach NTP server")
|
||||
|
||||
var ErrNoAvailableServer = errors.New("no available NTP server found")
|
||||
|
||||
var defaultAddresses = []string{
|
||||
//"ntp.ntsc.ac.cn", // 中科院国家授时中心的服务器很快,但是多刷几次就会被封
|
||||
// builtinAddresses 内置的 NTP 服务器地址列表(用于面板同步时间)
|
||||
var builtinAddresses = []string{
|
||||
"ntp.aliyun.com", // 阿里云
|
||||
"ntp1.aliyun.com", // 阿里云2
|
||||
"ntp.tencent.com", // 腾讯云
|
||||
@@ -24,8 +24,15 @@ var defaultAddresses = []string{
|
||||
"time.apple.com", // Apple
|
||||
}
|
||||
|
||||
// GetBuiltinServers 获取内置的 NTP 服务器列表
|
||||
func GetBuiltinServers() []string {
|
||||
result := make([]string, len(builtinAddresses))
|
||||
copy(result, builtinAddresses)
|
||||
return result
|
||||
}
|
||||
|
||||
func Now(address ...string) (time.Time, error) {
|
||||
if len(address) > 0 {
|
||||
if len(address) > 0 && address[0] != "" {
|
||||
if now, err := ntp.Time(address[0]); err != nil {
|
||||
return time.Now(), fmt.Errorf("%w: %s", ErrNotReachable, err)
|
||||
} else {
|
||||
@@ -33,7 +40,7 @@ func Now(address ...string) (time.Time, error) {
|
||||
}
|
||||
}
|
||||
|
||||
best, err := bestServer(defaultAddresses...)
|
||||
best, err := bestServer(builtinAddresses...)
|
||||
if err != nil {
|
||||
return time.Now(), err
|
||||
}
|
||||
@@ -70,7 +77,7 @@ func pingServer(addr string) (time.Duration, error) {
|
||||
// bestServer 返回延迟最低的NTP服务器
|
||||
func bestServer(addresses ...string) (string, error) {
|
||||
if len(addresses) == 0 {
|
||||
addresses = defaultAddresses
|
||||
addresses = builtinAddresses
|
||||
}
|
||||
|
||||
type ntpResult struct {
|
||||
|
||||
299
pkg/ntp/system.go
Normal file
299
pkg/ntp/system.go
Normal file
@@ -0,0 +1,299 @@
|
||||
package ntp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/acepanel/panel/pkg/io"
|
||||
"github.com/acepanel/panel/pkg/shell"
|
||||
)
|
||||
|
||||
// NTPServiceType 表示系统使用的 NTP 服务类型
|
||||
type NTPServiceType string
|
||||
|
||||
const (
|
||||
NTPServiceTimesyncd NTPServiceType = "timesyncd" // systemd-timesyncd (Debian/Ubuntu)
|
||||
NTPServiceChrony NTPServiceType = "chrony" // chrony (RHEL/CentOS/Rocky)
|
||||
NTPServiceUnknown NTPServiceType = "unknown" // 未知或不支持
|
||||
)
|
||||
|
||||
// timesyncd 配置文件路径
|
||||
const timesyncdConfigPath = "/etc/systemd/timesyncd.conf"
|
||||
|
||||
// chrony 配置文件路径(按优先级排序)
|
||||
var chronyConfigPaths = []string{
|
||||
"/etc/chrony.conf",
|
||||
"/etc/chrony/chrony.conf",
|
||||
}
|
||||
|
||||
// SystemNTPConfig 系统 NTP 配置信息
|
||||
type SystemNTPConfig struct {
|
||||
ServiceType NTPServiceType `json:"service_type"` // 服务类型
|
||||
Servers []string `json:"servers"` // NTP 服务器列表
|
||||
}
|
||||
|
||||
// DetectNTPService 检测系统使用的 NTP 服务类型
|
||||
func DetectNTPService() NTPServiceType {
|
||||
// 优先检查 chrony
|
||||
if _, err := shell.Execf("systemctl is-active chronyd 2>/dev/null"); err == nil {
|
||||
return NTPServiceChrony
|
||||
}
|
||||
if _, err := shell.Execf("systemctl is-active chrony 2>/dev/null"); err == nil {
|
||||
return NTPServiceChrony
|
||||
}
|
||||
|
||||
// 检查 systemd-timesyncd
|
||||
if _, err := shell.Execf("systemctl is-active systemd-timesyncd 2>/dev/null"); err == nil {
|
||||
return NTPServiceTimesyncd
|
||||
}
|
||||
|
||||
// 检查配置文件是否存在
|
||||
for _, path := range chronyConfigPaths {
|
||||
if io.Exists(path) {
|
||||
return NTPServiceChrony
|
||||
}
|
||||
}
|
||||
if io.Exists(timesyncdConfigPath) {
|
||||
return NTPServiceTimesyncd
|
||||
}
|
||||
|
||||
return NTPServiceUnknown
|
||||
}
|
||||
|
||||
// GetSystemNTPConfig 获取系统 NTP 配置
|
||||
func GetSystemNTPConfig() (*SystemNTPConfig, error) {
|
||||
serviceType := DetectNTPService()
|
||||
config := &SystemNTPConfig{
|
||||
ServiceType: serviceType,
|
||||
Servers: []string{},
|
||||
}
|
||||
|
||||
switch serviceType {
|
||||
case NTPServiceTimesyncd:
|
||||
servers, err := getTimesyncdServers()
|
||||
if err != nil {
|
||||
return config, err
|
||||
}
|
||||
config.Servers = servers
|
||||
case NTPServiceChrony:
|
||||
servers, err := getChronyServers()
|
||||
if err != nil {
|
||||
return config, err
|
||||
}
|
||||
config.Servers = servers
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// SetSystemNTPServers 设置系统 NTP 服务器
|
||||
func SetSystemNTPServers(servers []string) error {
|
||||
serviceType := DetectNTPService()
|
||||
|
||||
switch serviceType {
|
||||
case NTPServiceTimesyncd:
|
||||
return setTimesyncdServers(servers)
|
||||
case NTPServiceChrony:
|
||||
return setChronyServers(servers)
|
||||
default:
|
||||
return fmt.Errorf("unsupported NTP service type")
|
||||
}
|
||||
}
|
||||
|
||||
// getTimesyncdServers 获取 systemd-timesyncd 的 NTP 服务器配置
|
||||
func getTimesyncdServers() ([]string, error) {
|
||||
if !io.Exists(timesyncdConfigPath) {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
content, err := io.Read(timesyncdConfigPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析配置文件,查找 NTP= 行
|
||||
var servers []string
|
||||
scanner := bufio.NewScanner(strings.NewReader(content))
|
||||
ntpRegex := regexp.MustCompile(`^\s*NTP\s*=\s*(.+)$`)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
if matches := ntpRegex.FindStringSubmatch(line); len(matches) > 1 {
|
||||
// NTP 服务器以空格分隔
|
||||
for _, server := range strings.Fields(matches[1]) {
|
||||
if server != "" {
|
||||
servers = append(servers, server)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
// setTimesyncdServers 设置 systemd-timesyncd 的 NTP 服务器配置
|
||||
func setTimesyncdServers(servers []string) error {
|
||||
var content string
|
||||
if io.Exists(timesyncdConfigPath) {
|
||||
var err error
|
||||
content, err = io.Read(timesyncdConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 构建新的 NTP 配置行
|
||||
ntpLine := "NTP=" + strings.Join(servers, " ")
|
||||
|
||||
// 检查是否已有 [Time] 段和 NTP= 行
|
||||
hasTimeSection := strings.Contains(content, "[Time]")
|
||||
ntpRegex := regexp.MustCompile(`(?m)^\s*#?\s*NTP\s*=.*$`)
|
||||
|
||||
if ntpRegex.MatchString(content) {
|
||||
// 替换现有的 NTP= 行
|
||||
content = ntpRegex.ReplaceAllString(content, ntpLine)
|
||||
} else if hasTimeSection {
|
||||
// 在 [Time] 段后添加 NTP= 行
|
||||
content = strings.Replace(content, "[Time]", "[Time]\n"+ntpLine, 1)
|
||||
} else {
|
||||
// 添加 [Time] 段和 NTP= 行
|
||||
if content != "" && !strings.HasSuffix(content, "\n") {
|
||||
content += "\n"
|
||||
}
|
||||
content += "[Time]\n" + ntpLine + "\n"
|
||||
}
|
||||
|
||||
// 写入配置文件
|
||||
if err := io.Write(timesyncdConfigPath, content, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 重启 systemd-timesyncd 服务
|
||||
_, _ = shell.Execf("systemctl restart systemd-timesyncd 2>/dev/null")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getChronyServers 获取 chrony 的 NTP 服务器配置
|
||||
func getChronyServers() ([]string, error) {
|
||||
var configPath string
|
||||
for _, path := range chronyConfigPaths {
|
||||
if io.Exists(path) {
|
||||
configPath = path
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if configPath == "" {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
content, err := io.Read(configPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析配置文件,查找 server 或 pool 行
|
||||
var servers []string
|
||||
scanner := bufio.NewScanner(strings.NewReader(content))
|
||||
serverRegex := regexp.MustCompile(`^\s*(server|pool)\s+(\S+)`)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
if matches := serverRegex.FindStringSubmatch(line); len(matches) > 2 {
|
||||
servers = append(servers, matches[2])
|
||||
}
|
||||
}
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
// setChronyServers 设置 chrony 的 NTP 服务器配置
|
||||
func setChronyServers(servers []string) error {
|
||||
var configPath string
|
||||
for _, path := range chronyConfigPaths {
|
||||
if io.Exists(path) {
|
||||
configPath = path
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if configPath == "" {
|
||||
// 如果配置文件不存在,使用默认路径
|
||||
configPath = chronyConfigPaths[0]
|
||||
}
|
||||
|
||||
var content string
|
||||
if io.Exists(configPath) {
|
||||
var err error
|
||||
content, err = io.Read(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 移除现有的 server 和 pool 行
|
||||
var newLines []string
|
||||
scanner := bufio.NewScanner(strings.NewReader(content))
|
||||
serverRegex := regexp.MustCompile(`^\s*(server|pool)\s+`)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if !serverRegex.MatchString(line) {
|
||||
newLines = append(newLines, line)
|
||||
}
|
||||
}
|
||||
|
||||
// 在文件开头添加新的 server 行
|
||||
var serverLines []string
|
||||
for _, server := range servers {
|
||||
serverLines = append(serverLines, fmt.Sprintf("server %s iburst", server))
|
||||
}
|
||||
|
||||
// 组合新内容
|
||||
newContent := strings.Join(serverLines, "\n")
|
||||
if len(newLines) > 0 {
|
||||
newContent += "\n" + strings.Join(newLines, "\n")
|
||||
}
|
||||
if !strings.HasSuffix(newContent, "\n") {
|
||||
newContent += "\n"
|
||||
}
|
||||
|
||||
// 写入配置文件
|
||||
if err := io.Write(configPath, newContent, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 重启 chrony 服务
|
||||
_, _ = shell.Execf("systemctl restart chronyd 2>/dev/null")
|
||||
_, _ = shell.Execf("systemctl restart chrony 2>/dev/null")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestartNTPService 重启 NTP 服务
|
||||
func RestartNTPService() error {
|
||||
serviceType := DetectNTPService()
|
||||
|
||||
switch serviceType {
|
||||
case NTPServiceTimesyncd:
|
||||
_, err := shell.Execf("systemctl restart systemd-timesyncd")
|
||||
return err
|
||||
case NTPServiceChrony:
|
||||
if _, err := shell.Execf("systemctl restart chronyd 2>/dev/null"); err != nil {
|
||||
_, err = shell.Execf("systemctl restart chrony")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported NTP service type")
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,13 @@ export default {
|
||||
updateTimezone: (timezone: string): any => http.Post('/toolbox_system/timezone', { timezone }),
|
||||
// 设置时间
|
||||
updateTime: (time: string): any => http.Post('/toolbox_system/time', { time }),
|
||||
// 同步时间
|
||||
syncTime: (): any => http.Post('/toolbox_system/sync_time'),
|
||||
// 同步时间(可选指定 NTP 服务器)
|
||||
syncTime: (server?: string): any => http.Post('/toolbox_system/sync_time', { server }),
|
||||
// 获取 NTP 服务器配置
|
||||
ntpServers: (): any => http.Get('/toolbox_system/ntp_servers'),
|
||||
// 设置 NTP 服务器配置
|
||||
updateNtpServers: (servers: string[]): any =>
|
||||
http.Post('/toolbox_system/ntp_servers', { servers }),
|
||||
// 主机名
|
||||
hostname: (): any => http.Get('/toolbox_system/hostname'),
|
||||
// Hosts
|
||||
|
||||
@@ -7,7 +7,7 @@ const router = useRouter()
|
||||
const logo = computed(() => themeStore.logo || logoImg)
|
||||
|
||||
const toHome = () => {
|
||||
router.push({ name: 'home' })
|
||||
router.push({ name: 'home-index' })
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -21,6 +21,12 @@ const hosts = ref('')
|
||||
const timezone = ref('')
|
||||
const timezones = ref<any[]>([])
|
||||
const time = ref(DateTime.now().toMillis())
|
||||
const syncServer = ref('')
|
||||
const ntpServers = ref<string[]>([])
|
||||
const builtinNtpServers = ref<string[]>([])
|
||||
const ntpServiceType = ref('')
|
||||
const showNtpModal = ref(false)
|
||||
const editingNtpServers = ref<string[]>([])
|
||||
|
||||
const dnsManager = ref('')
|
||||
|
||||
@@ -45,6 +51,11 @@ useRequest(system.timezone()).onSuccess(({ data }) => {
|
||||
timezone.value = data.timezone
|
||||
timezones.value = data.timezones
|
||||
})
|
||||
useRequest(system.ntpServers()).onSuccess(({ data }) => {
|
||||
ntpServers.value = data.servers || []
|
||||
builtinNtpServers.value = data.builtins || []
|
||||
ntpServiceType.value = data.service_type || ''
|
||||
})
|
||||
|
||||
const handleUpdateDNS = () => {
|
||||
useRequest(system.updateDns(dns1.value, dns2.value)).onSuccess(() => {
|
||||
@@ -77,10 +88,41 @@ const handleUpdateTime = async () => {
|
||||
}
|
||||
|
||||
const handleSyncTime = () => {
|
||||
useRequest(system.syncTime()).onSuccess(() => {
|
||||
useRequest(system.syncTime(syncServer.value || undefined)).onSuccess(() => {
|
||||
window.$message.success($gettext('Synchronized successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleOpenNtpSettings = () => {
|
||||
editingNtpServers.value = [...ntpServers.value]
|
||||
showNtpModal.value = true
|
||||
}
|
||||
|
||||
const handleAddNtpServer = () => {
|
||||
editingNtpServers.value.push('')
|
||||
}
|
||||
|
||||
const handleRemoveNtpServer = (index: number) => {
|
||||
editingNtpServers.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const handleResetNtpServers = () => {
|
||||
editingNtpServers.value = [...builtinNtpServers.value]
|
||||
}
|
||||
|
||||
const handleSaveNtpServers = () => {
|
||||
// 过滤空字符串
|
||||
const servers = editingNtpServers.value.filter((s) => s.trim() !== '')
|
||||
if (servers.length === 0) {
|
||||
window.$message.error($gettext('At least one NTP server is required'))
|
||||
return
|
||||
}
|
||||
useRequest(system.updateNtpServers(servers)).onSuccess(() => {
|
||||
ntpServers.value = servers
|
||||
showNtpModal.value = false
|
||||
window.$message.success($gettext('Saved successfully'))
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -172,6 +214,18 @@ const handleSyncTime = () => {
|
||||
<n-form-item :label="$gettext('Modify Time')">
|
||||
<n-date-picker v-model:value="time" type="datetime" clearable />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('NTP Server')">
|
||||
<n-flex :size="8" align="center" style="width: 100%">
|
||||
<n-input
|
||||
v-model:value="syncServer"
|
||||
:placeholder="$gettext('Optional, leave empty to use default servers')"
|
||||
style="flex: 1"
|
||||
/>
|
||||
<n-button @click="handleOpenNtpSettings">
|
||||
{{ $gettext('Configure Default Servers') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-flex>
|
||||
<n-button type="primary" @click="handleUpdateTime">
|
||||
@@ -184,4 +238,72 @@ const handleSyncTime = () => {
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
|
||||
<!-- NTP 服务器配置弹窗 -->
|
||||
<n-modal
|
||||
v-model:show="showNtpModal"
|
||||
preset="card"
|
||||
:title="$gettext('System NTP Server Configuration')"
|
||||
style="width: 60vw"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
>
|
||||
<n-flex vertical>
|
||||
<n-alert v-if="ntpServiceType === 'unknown'" type="warning">
|
||||
{{
|
||||
$gettext(
|
||||
'Unable to detect NTP service. Please ensure chrony or systemd-timesyncd is installed.'
|
||||
)
|
||||
}}
|
||||
</n-alert>
|
||||
<n-alert v-else type="info" :show-icon="false">
|
||||
{{
|
||||
$gettext(
|
||||
'Current NTP service: %{ service }. Changes will be applied to system configuration.',
|
||||
{
|
||||
service: ntpServiceType === 'chrony' ? 'Chrony' : 'systemd-timesyncd'
|
||||
}
|
||||
)
|
||||
}}
|
||||
</n-alert>
|
||||
<n-list>
|
||||
<n-list-item v-for="(_, index) in editingNtpServers" :key="index">
|
||||
<n-flex :size="8" align="center">
|
||||
<n-input
|
||||
v-model:value="editingNtpServers[index]"
|
||||
:placeholder="$gettext('Enter NTP server address')"
|
||||
style="flex: 1"
|
||||
/>
|
||||
<n-button
|
||||
quaternary
|
||||
type="error"
|
||||
:disabled="editingNtpServers.length <= 1"
|
||||
@click="handleRemoveNtpServer(index)"
|
||||
>
|
||||
<template #icon>
|
||||
<i-mdi-delete />
|
||||
</template>
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
<n-flex justify="space-between">
|
||||
<n-flex :size="8">
|
||||
<n-button @click="handleAddNtpServer">
|
||||
<template #icon>
|
||||
<i-mdi-plus />
|
||||
</template>
|
||||
{{ $gettext('Add') }}
|
||||
</n-button>
|
||||
<n-button @click="handleResetNtpServers">
|
||||
{{ $gettext('Reset to Default') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-button type="primary" @click="handleSaveNtpServers">
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user