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

This commit is contained in:
Jacky 2024-05-07 16:31:47 +08:00
parent cc5d2be1bd
commit b429c15893
No known key found for this signature in database
GPG key ID: 215C21B10DF38B4D
17 changed files with 250 additions and 240 deletions

View file

@ -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) {

View file

@ -45,3 +45,6 @@ RedirectUri =
Enabled = false
CMD = logrotate /etc/logrotate.d/nginx
Interval = 1440
[cluster]
Node =

View file

@ -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) {

View file

@ -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>

View file

@ -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>

View file

@ -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;
}

View file

@ -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: [],
};

View file

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

View file

@ -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>

View file

@ -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)

View file

@ -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',

View file

@ -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>

View file

@ -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 = {}

View file

@ -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,
}
n = &Node{
Environment: env,
}
u, err := url.JoinPath(env.URL, "/api/node")
u, err := url.JoinPath(env.URL, "/api/node")
if err != nil {
logger.Error(err)
return
}
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)
client := http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
resp, err := client.Do(req)
req, err := http.NewRequest("GET", u, nil)
if err != nil {
logger.Error(err)
return
}
if err != nil {
logger.Error(err)
return
}
req.Header.Set("X-Node-Secret", env.Token)
defer resp.Body.Close()
bytes, _ := io.ReadAll(resp.Body)
resp, err := client.Do(req)
if resp.StatusCode != 200 {
logger.Error(string(bytes))
return
}
if err != nil {
logger.Error(err)
return
}
err = json.Unmarshal(bytes, &n.NodeInfo)
if err != nil {
logger.Error(err)
return
}
defer resp.Body.Close()
bytes, _ := io.ReadAll(resp.Body)
return
if resp.StatusCode != http.StatusOK {
logger.Error(string(bytes))
return
}
err = json.Unmarshal(bytes, &n.NodeInfo)
if err != nil {
logger.Error(err)
return
}
return
}

View file

@ -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
}

View file

@ -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
settings/cluster.go Normal file
View file

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