Jelajahi Sumber

feat: add enabled/disabled field to environment model #169

Jacky 1 tahun lalu
induk
melakukan
b429c15893

+ 23 - 89
api/cluster/environment.go

@@ -3,13 +3,12 @@ package cluster
 import (
 	"github.com/0xJacky/Nginx-UI/api"
 	"github.com/0xJacky/Nginx-UI/internal/analytic"
-	"github.com/0xJacky/Nginx-UI/internal/environment"
+	"github.com/0xJacky/Nginx-UI/internal/cosy"
 	"github.com/0xJacky/Nginx-UI/model"
 	"github.com/0xJacky/Nginx-UI/query"
 	"github.com/gin-gonic/gin"
 	"github.com/spf13/cast"
 	"net/http"
-	"regexp"
 )
 
 func GetEnvironment(c *gin.Context) {
@@ -27,99 +26,34 @@ func GetEnvironment(c *gin.Context) {
 }
 
 func GetEnvironmentList(c *gin.Context) {
-	data, err := environment.RetrieveEnvironmentList()
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-	c.JSON(http.StatusOK, gin.H{
-		"data": data,
-	})
-}
-
-type EnvironmentManageJson struct {
-	Name          string `json:"name" binding:"required"`
-	URL           string `json:"url" binding:"required"`
-	Token         string `json:"token"  binding:"required"`
-	OperationSync bool   `json:"operation_sync"`
-	SyncApiRegex  string `json:"sync_api_regex"`
-}
-
-func validateRegex(data EnvironmentManageJson) error {
-	if data.OperationSync {
-		_, err := regexp.Compile(data.SyncApiRegex)
-		return err
-	}
-	return nil
+	cosy.Core[model.Environment](c).
+		SetFussy("name").
+		SetEqual("enabled").
+		SetTransformer(func(m *model.Environment) any {
+			return analytic.GetNode(m)
+		}).PagingList()
 }
 
 func AddEnvironment(c *gin.Context) {
-	var json EnvironmentManageJson
-	if !api.BindAndValid(c, &json) {
-		return
-	}
-	if err := validateRegex(json); err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	env := model.Environment{
-		Name:          json.Name,
-		URL:           json.URL,
-		Token:         json.Token,
-		OperationSync: json.OperationSync,
-		SyncApiRegex:  json.SyncApiRegex,
-	}
-
-	envQuery := query.Environment
-
-	err := envQuery.Create(&env)
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	go analytic.RestartRetrieveNodesStatus()
-
-	c.JSON(http.StatusOK, env)
+	cosy.Core[model.Environment](c).SetValidRules(gin.H{
+		"name":    "required",
+		"url":     "required,url",
+		"token":   "required",
+		"enabled": "omitempty,boolean",
+	}).ExecutedHook(func(c *cosy.Ctx[model.Environment]) {
+		go analytic.RestartRetrieveNodesStatus()
+	}).Create()
 }
 
 func EditEnvironment(c *gin.Context) {
-	id := cast.ToInt(c.Param("id"))
-
-	var json EnvironmentManageJson
-	if !api.BindAndValid(c, &json) {
-		return
-	}
-	if err := validateRegex(json); err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	envQuery := query.Environment
-
-	env, err := envQuery.FirstByID(id)
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	_, err = envQuery.Where(envQuery.ID.Eq(env.ID)).Updates(&model.Environment{
-		Name:          json.Name,
-		URL:           json.URL,
-		Token:         json.Token,
-		OperationSync: json.OperationSync,
-		SyncApiRegex:  json.SyncApiRegex,
-	})
-
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	go analytic.RestartRetrieveNodesStatus()
-
-	GetEnvironment(c)
+	cosy.Core[model.Environment](c).SetValidRules(gin.H{
+		"name":    "required",
+		"url":     "required,url",
+		"token":   "required",
+		"enabled": "omitempty,boolean",
+	}).ExecutedHook(func(c *cosy.Ctx[model.Environment]) {
+		go analytic.RestartRetrieveNodesStatus()
+	}).Modify()
 }
 
 func DeleteEnvironment(c *gin.Context) {

+ 26 - 23
app.example.ini

@@ -1,47 +1,50 @@
 [server]
 HttpPort             = 9000
 RunMode              = debug
-JwtSecret            = 
-Email                = 
+JwtSecret            =
+Email                =
 HTTPChallengePort    = 9180
 StartCmd             = bash
 Database             = database
-CADir                = 
-GithubProxy          = 
-NodeSecret           = 
+CADir                =
+GithubProxy          =
+NodeSecret           =
 Demo                 = false
 PageSize             = 10
 HttpHost             = 0.0.0.0
 CertRenewalInterval  = 7
-RecursiveNameservers = 
+RecursiveNameservers =
 SkipInstallation     = false
-Name                 = 
+Name                 =
 
 [nginx]
 AccessLogPath = /var/log/nginx/access.log
 ErrorLogPath  = /var/log/nginx/error.log
-ConfigDir     = 
-PIDPath       = 
-TestConfigCmd = 
-ReloadCmd     = 
-RestartCmd    = 
+ConfigDir     =
+PIDPath       =
+TestConfigCmd =
+ReloadCmd     =
+RestartCmd    =
 
 [openai]
-Model   = 
-BaseUrl = 
-Proxy   = 
-Token   = 
+Model   =
+BaseUrl =
+Proxy   =
+Token   =
 
 [casdoor]
-Endpoint     = 
-ClientId     = 
-ClientSecret = 
-Certificate  = 
-Organization = 
-Application  = 
-RedirectUri  = 
+Endpoint     =
+ClientId     =
+ClientSecret =
+Certificate  =
+Organization =
+Application  =
+RedirectUri  =
 
 [logrotate]
 Enabled  = false
 CMD      = logrotate /etc/logrotate.d/nginx
 Interval = 1440
+
+[cluster]
+Node =

+ 13 - 8
app/src/components/ChatGPT/ChatGPT.vue

@@ -66,16 +66,21 @@ async function request() {
   let hasCodeBlockIndicator = false
 
   while (true) {
-    const { done, value } = await reader.read()
-    if (done) {
-      setTimeout(() => {
-        scrollToBottom()
-      }, 500)
-      loading.value = false
-      store_record()
+    try {
+      const { done, value } = await reader.read()
+      if (done) {
+        setTimeout(() => {
+          scrollToBottom()
+        }, 500)
+        loading.value = false
+        store_record()
+        break
+      }
+      apply(value!)
+    }
+    catch (e) {
       break
     }
-    apply(value!)
   }
 
   function apply(input: Uint8Array) {

+ 0 - 5
app/src/components/EnvIndicator/EnvIndicator.vue

@@ -4,7 +4,6 @@ import { storeToRefs } from 'pinia'
 import { useRouter } from 'vue-router'
 import { computed, watch } from 'vue'
 import { useSettingsStore } from '@/pinia'
-import settings from '@/api/settings'
 
 const settingsStore = useSettingsStore()
 
@@ -28,10 +27,6 @@ watch(node_id, async () => {
 })
 
 const { server_name } = storeToRefs(useSettingsStore())
-
-settings.get_server_name().then(r => {
-  server_name.value = r.name
-})
 </script>
 
 <template>

+ 27 - 7
app/src/components/NodeSelector/NodeSelector.vue

@@ -14,11 +14,21 @@ const emit = defineEmits(['update:target', 'update:map'])
 const data = ref([]) as Ref<Environment[]>
 const data_map = ref({}) as Ref<Record<number, Environment>>
 
-environment.get_list().then(r => {
-  data.value = r.data
-  r.data?.forEach(node => {
-    data_map.value[node.id] = node
-  })
+onMounted(async () => {
+  let hasMore = true
+  let page = 1
+  while (hasMore) {
+    await environment.get_list({ page, enabled: true }).then(r => {
+      data.value.push(...r.data)
+      r.data?.forEach(node => {
+        data_map.value[node.id] = node
+      })
+      hasMore = r.data.length === r.pagination.per_page
+      page++
+    }).catch(() => {
+      hasMore = false
+    })
+  }
 })
 
 const value = computed({
@@ -35,14 +45,24 @@ const value = computed({
     emit('update:target', v)
   },
 })
+
+const noData = computed(() => {
+  return props.hiddenLocal && !data?.value?.length
+})
 </script>
 
 <template>
   <ACheckboxGroup
     v-model:value="value"
     style="width: 100%"
+    :class="{
+      'justify-center': noData,
+    }"
   >
-    <ARow :gutter="[16, 16]">
+    <ARow
+      v-if="!noData"
+      :gutter="[16, 16]"
+    >
       <ACol
         v-if="!hiddenLocal"
         :span="8"
@@ -76,7 +96,7 @@ const value = computed({
         </ATag>
       </ACol>
     </ARow>
-    <AEmpty v-if="hiddenLocal && data?.length === 0" />
+    <AEmpty v-else />
   </ACheckboxGroup>
 </template>
 

+ 9 - 4
app/src/layouts/BaseLayout.vue

@@ -1,9 +1,12 @@
 <script setup lang="ts">
 import _ from 'lodash'
+import { storeToRefs } from 'pinia'
 import FooterLayout from './FooterLayout.vue'
 import SideBar from './SideBar.vue'
 import HeaderLayout from './HeaderLayout.vue'
 import PageHeader from '@/components/PageHeader/PageHeader.vue'
+import { useSettingsStore } from '@/pinia'
+import settings from '@/api/settings'
 
 const drawer_visible = ref(false)
 const collapsed = ref(collapse())
@@ -19,6 +22,12 @@ function getClientWidth() {
 function collapse() {
   return getClientWidth() < 1280
 }
+
+const { server_name } = storeToRefs(useSettingsStore())
+
+settings.get_server_name().then(r => {
+  server_name.value = r.name
+})
 </script>
 
 <template>
@@ -150,10 +159,6 @@ body {
   font-size: 13px;
 }
 
-.ant-card-bordered {
-
-}
-
 .header-notice-wrapper .ant-tabs-content {
   max-height: 250px;
 }

+ 2 - 2
app/src/layouts/SideBar.vue

@@ -42,7 +42,7 @@ interface meta {
 
 interface sidebar {
   path: string
-  name: () => string
+  name: string
   meta: meta
   children: sidebar[]
 }
@@ -56,7 +56,7 @@ const visible: ComputedRef<sidebar[]> = computed(() => {
 
     const t: sidebar = {
       path: s.path,
-      name: s?.meta?.name ?? (() => ''),
+      name: s.name as string,
       meta: s.meta as unknown as meta,
       children: [],
     };

+ 10 - 5
app/src/views/certificate/ACMEUserSelector.vue

@@ -42,12 +42,17 @@ onMounted(async () => {
   users.value = []
   let page = 1
   while (true) {
-    const r = await acme_user.get_list({ page })
-
-    users.value.push(...r.data)
-    if (r?.data?.length < r?.pagination?.per_page)
+    try {
+      const r = await acme_user.get_list({ page })
+
+      users.value.push(...r.data)
+      if (r?.data?.length < r?.pagination?.per_page)
+        break
+      page++
+    }
+    catch (e) {
       break
-    page++
+    }
   }
 
   init()

+ 7 - 1
app/src/views/dashboard/DashBoard.vue

@@ -1,11 +1,17 @@
 <script setup lang="ts">
 import ServerAnalytic from '@/views/dashboard/ServerAnalytic.vue'
 import Environments from '@/views/dashboard/Environments.vue'
+
+const key = ref(0)
+
+setInterval(() => {
+  key.value++
+}, 5 * 60 * 1000)
 </script>
 
 <template>
   <div>
-    <ServerAnalytic />
+    <ServerAnalytic :key />
     <Environments />
   </div>
 </template>

+ 14 - 3
app/src/views/dashboard/Environments.vue

@@ -25,10 +25,21 @@ const node_map = computed(() => {
 
 let websocket: ReconnectingWebSocket | WebSocket
 
+onMounted(async () => {
+  let hasMore = true
+  let page = 1
+  while (hasMore) {
+    await environment.get_list({ page, enabled: true }).then(r => {
+      data.value.push(...r.data)
+      hasMore = r.data.length === r.pagination.per_page
+      page++
+    }).catch(() => {
+      hasMore = false
+    })
+  }
+})
+
 onMounted(() => {
-  environment.get_list().then(r => {
-    data.value = r.data
-  })
   websocket = analytic.nodes()
   websocket.onmessage = async m => {
     const nodes = JSON.parse(m.data)

+ 34 - 7
app/src/views/environment/Environment.vue

@@ -1,11 +1,11 @@
 <script setup lang="tsx">
 import { h } from 'vue'
-import { Badge } from 'ant-design-vue'
+import { Badge, Tag } from 'ant-design-vue'
 import type { customRender } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
 import { datetime } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
 import environment from '@/api/environment'
 import StdCurd from '@/components/StdDesign/StdDataDisplay/StdCurd.vue'
-import { input } from '@/components/StdDesign/StdDataEntry'
+import { input, switcher } from '@/components/StdDesign/StdDataEntry'
 import type { Column, JSXElements } from '@/components/StdDesign/types'
 
 const columns: Column[] = [{
@@ -16,6 +16,7 @@ const columns: Column[] = [{
   edit: {
     type: input,
   },
+  search: true,
 },
 {
   title: () => $gettext('URL'),
@@ -77,13 +78,19 @@ const columns: Column[] = [{
   customRender: (args: customRender) => {
     const template: JSXElements = []
     const { text } = args
-    if (text === true || text > 0) {
-      template.push(<Badge status="success"/>)
-      template.push($gettext('Online'))
+    if (args.record.enabled) {
+      if (text === true || text > 0) {
+        template.push(<Badge status="success"/>)
+        template.push($gettext('Online'))
+      }
+      else {
+        template.push(<Badge status="error"/>)
+        template.push($gettext('Offline'))
+      }
     }
     else {
-      template.push(<Badge status="error"/>)
-      template.push($gettext('Offline'))
+      template.push(<Badge status="default"/>)
+      template.push($gettext('Disabled'))
     }
 
     return h('div', template)
@@ -91,6 +98,26 @@ const columns: Column[] = [{
   sortable: true,
   pithy: true,
 },
+{
+  title: () => $gettext('Enabled'),
+  dataIndex: 'enabled',
+  customRender: (args: customRender) => {
+    const template: JSXElements = []
+    const { text } = args
+    if (text === true || text > 0)
+      template.push(<Tag color="green">{$gettext('Enabled')}</Tag>)
+
+    else
+      template.push(<Tag color="orange">{$gettext('Disabled')}</Tag>)
+
+    return h('div', template)
+  },
+  edit: {
+    type: switcher,
+  },
+  sortable: true,
+  pithy: true,
+},
 {
   title: () => $gettext('Updated at'),
   dataIndex: 'updated_at',

+ 7 - 1
app/src/views/preference/BasicSettings.vue

@@ -113,7 +113,13 @@ const errors: Record<string, Record<string, string>> = inject('errors') as Recor
         </template>
       </Draggable>
     </AFormItem>
-    <AFormItem :label="$gettext('Server Name')">
+    <AFormItem
+      :label="$gettext('Server Name')"
+      :validate-status="errors?.server?.name ? 'error' : ''"
+      :help="errors?.server?.name.includes('alpha_num_dash_dot')
+        ? $gettext('The server name should only contain letters, numbers, dashes, and dots.')
+        : $gettext('Customize the name of local server to be displayed in the environment indicator.')"
+    >
       <AInput v-model:value="data.server.name" />
     </AFormItem>
   </AForm>

+ 4 - 2
app/src/views/preference/Preference.vue

@@ -52,14 +52,16 @@ settings.get().then(r => {
   data.value = r
 })
 
-const { server_name } = storeToRefs(useSettingsStore())
+const settingsStore = useSettingsStore()
+const { server_name } = storeToRefs(settingsStore)
 const errors = ref({}) as Ref<Record<string, Record<string, string>>>
 
 async function save() {
   // fix type
   data.value.server.http_challenge_port = data.value.server.http_challenge_port.toString()
   settings.save(data.value).then(r => {
-    server_name.value = r?.server?.name ?? ''
+    if (!settingsStore.is_remote)
+      server_name.value = r?.server?.name ?? ''
     data.value = r
     message.success($gettext('Save successfully'))
     errors.value = {}

+ 78 - 70
internal/analytic/node.go

@@ -16,26 +16,26 @@ import (
 )
 
 type NodeInfo struct {
-    NodeRuntimeInfo upgrader.RuntimeInfo `json:"node_runtime_info"`
-    Version         string               `json:"version"`
-    CPUNum          int                  `json:"cpu_num"`
-    MemoryTotal     string               `json:"memory_total"`
-    DiskTotal       string               `json:"disk_total"`
+	NodeRuntimeInfo upgrader.RuntimeInfo `json:"node_runtime_info"`
+	Version         string               `json:"version"`
+	CPUNum          int                  `json:"cpu_num"`
+	MemoryTotal     string               `json:"memory_total"`
+	DiskTotal       string               `json:"disk_total"`
 }
 
 type NodeStat struct {
-    AvgLoad       *load.AvgStat      `json:"avg_load"`
-    CPUPercent    float64            `json:"cpu_percent"`
-    MemoryPercent float64            `json:"memory_percent"`
-    DiskPercent   float64            `json:"disk_percent"`
-    Network       net.IOCountersStat `json:"network"`
-    Status        bool               `json:"status"`
-    ResponseAt    time.Time          `json:"response_at"`
+	AvgLoad       *load.AvgStat      `json:"avg_load"`
+	CPUPercent    float64            `json:"cpu_percent"`
+	MemoryPercent float64            `json:"memory_percent"`
+	DiskPercent   float64            `json:"disk_percent"`
+	Network       net.IOCountersStat `json:"network"`
+	Status        bool               `json:"status"`
+	ResponseAt    time.Time          `json:"response_at"`
 }
 
 type Node struct {
-    EnvironmentID int `json:"environment_id,omitempty"`
-    *model.Environment
+	EnvironmentID int `json:"environment_id,omitempty"`
+	*model.Environment
 	NodeStat
 	NodeInfo
 }
@@ -47,66 +47,74 @@ type TNodeMap map[int]*Node
 var NodeMap TNodeMap
 
 func init() {
-    NodeMap = make(TNodeMap)
+	NodeMap = make(TNodeMap)
 }
 
 func GetNode(env *model.Environment) (n *Node) {
-    if env == nil {
-        logger.Error("env is nil")
-        return
-    }
-    n, ok := NodeMap[env.ID]
-    if !ok {
-        n = &Node{}
-    }
-    n.Environment = env
-    return n
+	if env == nil {
+		// this should never happen
+		logger.Error("env is nil")
+		return
+	}
+	if !env.Enabled {
+		return &Node{
+			Environment: env,
+		}
+	}
+	n, ok := NodeMap[env.ID]
+	if !ok {
+		n = &Node{}
+	}
+	n.Environment = env
+	return n
 }
 
 func InitNode(env *model.Environment) (n *Node) {
-    n = &Node{
-        Environment: env,
-    }
-
-    u, err := url.JoinPath(env.URL, "/api/node")
-
-    if err != nil {
-        logger.Error(err)
-        return
-    }
-
-    if err != nil {
-        logger.Error(err)
-        return
-    }
-    client := http.Client{
-        Transport: &http.Transport{
-            TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
-        },
-    }
-    req, err := http.NewRequest("GET", u, nil)
-    req.Header.Set("X-Node-Secret", env.Token)
-
-    resp, err := client.Do(req)
-
-    if err != nil {
-        logger.Error(err)
-        return
-    }
-
-    defer resp.Body.Close()
-    bytes, _ := io.ReadAll(resp.Body)
-
-    if resp.StatusCode != 200 {
-        logger.Error(string(bytes))
-        return
-    }
-
-    err = json.Unmarshal(bytes, &n.NodeInfo)
-    if err != nil {
-        logger.Error(err)
-        return
-    }
-
-    return
+	n = &Node{
+		Environment: env,
+	}
+
+	u, err := url.JoinPath(env.URL, "/api/node")
+
+	if err != nil {
+		logger.Error(err)
+		return
+	}
+
+	client := http.Client{
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+		},
+	}
+
+	req, err := http.NewRequest("GET", u, nil)
+	if err != nil {
+		logger.Error(err)
+		return
+	}
+
+	req.Header.Set("X-Node-Secret", env.Token)
+
+	resp, err := client.Do(req)
+
+	if err != nil {
+		logger.Error(err)
+		return
+	}
+
+	defer resp.Body.Close()
+	bytes, _ := io.ReadAll(resp.Body)
+
+	if resp.StatusCode != http.StatusOK {
+		logger.Error(string(bytes))
+		return
+	}
+
+	err = json.Unmarshal(bytes, &n.NodeInfo)
+	if err != nil {
+		logger.Error(err)
+		return
+	}
+
+	return
 }

+ 0 - 23
internal/environment/environment.go

@@ -1,23 +0,0 @@
-package environment
-
-import (
-	"github.com/0xJacky/Nginx-UI/internal/analytic"
-	"github.com/0xJacky/Nginx-UI/query"
-)
-
-func RetrieveEnvironmentList() (envs []*analytic.Node, err error) {
-	envQuery := query.Environment
-
-	data, err := envQuery.Find()
-	if err != nil {
-		return
-	}
-
-	for _, v := range data {
-		t := analytic.GetNode(v)
-
-		envs = append(envs, t)
-	}
-
-	return
-}

+ 1 - 0
model/environment.go

@@ -10,6 +10,7 @@ type Environment struct {
 	Name          string `json:"name"`
 	URL           string `json:"url"`
 	Token         string `json:"token"`
+	Enabled       bool   `json:"enabled" gorm:"default:true"`
 	OperationSync bool   `json:"operation_sync"`
 	SyncApiRegex  string `json:"sync_api_regex"`
 }

+ 5 - 0
settings/cluster.go

@@ -0,0 +1,5 @@
+package settings
+
+type Cluster struct {
+	Node []string `ini:",,allowshadow"`
+}