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 ( import (
"github.com/0xJacky/Nginx-UI/api" "github.com/0xJacky/Nginx-UI/api"
"github.com/0xJacky/Nginx-UI/internal/analytic" "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/model"
"github.com/0xJacky/Nginx-UI/query" "github.com/0xJacky/Nginx-UI/query"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/spf13/cast" "github.com/spf13/cast"
"net/http" "net/http"
"regexp"
) )
func GetEnvironment(c *gin.Context) { func GetEnvironment(c *gin.Context) {
@ -27,99 +26,34 @@ func GetEnvironment(c *gin.Context) {
} }
func GetEnvironmentList(c *gin.Context) { func GetEnvironmentList(c *gin.Context) {
data, err := environment.RetrieveEnvironmentList() cosy.Core[model.Environment](c).
if err != nil { SetFussy("name").
api.ErrHandler(c, err) SetEqual("enabled").
return SetTransformer(func(m *model.Environment) any {
} return analytic.GetNode(m)
c.JSON(http.StatusOK, gin.H{ }).PagingList()
"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
} }
func AddEnvironment(c *gin.Context) { func AddEnvironment(c *gin.Context) {
var json EnvironmentManageJson cosy.Core[model.Environment](c).SetValidRules(gin.H{
if !api.BindAndValid(c, &json) { "name": "required",
return "url": "required,url",
} "token": "required",
if err := validateRegex(json); err != nil { "enabled": "omitempty,boolean",
api.ErrHandler(c, err) }).ExecutedHook(func(c *cosy.Ctx[model.Environment]) {
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() go analytic.RestartRetrieveNodesStatus()
}).Create()
c.JSON(http.StatusOK, env)
} }
func EditEnvironment(c *gin.Context) { func EditEnvironment(c *gin.Context) {
id := cast.ToInt(c.Param("id")) cosy.Core[model.Environment](c).SetValidRules(gin.H{
"name": "required",
var json EnvironmentManageJson "url": "required,url",
if !api.BindAndValid(c, &json) { "token": "required",
return "enabled": "omitempty,boolean",
} }).ExecutedHook(func(c *cosy.Ctx[model.Environment]) {
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() go analytic.RestartRetrieveNodesStatus()
}).Modify()
GetEnvironment(c)
} }
func DeleteEnvironment(c *gin.Context) { func DeleteEnvironment(c *gin.Context) {

View file

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

View file

@ -66,6 +66,7 @@ async function request() {
let hasCodeBlockIndicator = false let hasCodeBlockIndicator = false
while (true) { while (true) {
try {
const { done, value } = await reader.read() const { done, value } = await reader.read()
if (done) { if (done) {
setTimeout(() => { setTimeout(() => {
@ -77,6 +78,10 @@ async function request() {
} }
apply(value!) apply(value!)
} }
catch (e) {
break
}
}
function apply(input: Uint8Array) { function apply(input: Uint8Array) {
const decoder = new TextDecoder('utf-8') const decoder = new TextDecoder('utf-8')

View file

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

View file

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

View file

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

View file

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

View file

@ -42,6 +42,7 @@ onMounted(async () => {
users.value = [] users.value = []
let page = 1 let page = 1
while (true) { while (true) {
try {
const r = await acme_user.get_list({ page }) const r = await acme_user.get_list({ page })
users.value.push(...r.data) users.value.push(...r.data)
@ -49,6 +50,10 @@ onMounted(async () => {
break break
page++ page++
} }
catch (e) {
break
}
}
init() init()

View file

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

View file

@ -25,10 +25,21 @@ const node_map = computed(() => {
let websocket: ReconnectingWebSocket | WebSocket let websocket: ReconnectingWebSocket | WebSocket
onMounted(() => { onMounted(async () => {
environment.get_list().then(r => { let hasMore = true
data.value = r.data 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(() => {
websocket = analytic.nodes() websocket = analytic.nodes()
websocket.onmessage = async m => { websocket.onmessage = async m => {
const nodes = JSON.parse(m.data) const nodes = JSON.parse(m.data)

View file

@ -1,11 +1,11 @@
<script setup lang="tsx"> <script setup lang="tsx">
import { h } from 'vue' 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 type { customRender } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
import { datetime } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer' import { datetime } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
import environment from '@/api/environment' import environment from '@/api/environment'
import StdCurd from '@/components/StdDesign/StdDataDisplay/StdCurd.vue' 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' import type { Column, JSXElements } from '@/components/StdDesign/types'
const columns: Column[] = [{ const columns: Column[] = [{
@ -16,6 +16,7 @@ const columns: Column[] = [{
edit: { edit: {
type: input, type: input,
}, },
search: true,
}, },
{ {
title: () => $gettext('URL'), title: () => $gettext('URL'),
@ -77,6 +78,7 @@ const columns: Column[] = [{
customRender: (args: customRender) => { customRender: (args: customRender) => {
const template: JSXElements = [] const template: JSXElements = []
const { text } = args const { text } = args
if (args.record.enabled) {
if (text === true || text > 0) { if (text === true || text > 0) {
template.push(<Badge status="success"/>) template.push(<Badge status="success"/>)
template.push($gettext('Online')) template.push($gettext('Online'))
@ -85,12 +87,37 @@ const columns: Column[] = [{
template.push(<Badge status="error"/>) template.push(<Badge status="error"/>)
template.push($gettext('Offline')) template.push($gettext('Offline'))
} }
}
else {
template.push(<Badge status="default"/>)
template.push($gettext('Disabled'))
}
return h('div', template) return h('div', template)
}, },
sortable: true, sortable: true,
pithy: 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'), title: () => $gettext('Updated at'),
dataIndex: 'updated_at', dataIndex: 'updated_at',

View file

@ -113,7 +113,13 @@ const errors: Record<string, Record<string, string>> = inject('errors') as Recor
</template> </template>
</Draggable> </Draggable>
</AFormItem> </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" /> <AInput v-model:value="data.server.name" />
</AFormItem> </AFormItem>
</AForm> </AForm>

View file

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

View file

@ -52,9 +52,15 @@ func init() {
func GetNode(env *model.Environment) (n *Node) { func GetNode(env *model.Environment) (n *Node) {
if env == nil { if env == nil {
// this should never happen
logger.Error("env is nil") logger.Error("env is nil")
return return
} }
if !env.Enabled {
return &Node{
Environment: env,
}
}
n, ok := NodeMap[env.ID] n, ok := NodeMap[env.ID]
if !ok { if !ok {
n = &Node{} n = &Node{}
@ -75,16 +81,18 @@ func InitNode(env *model.Environment) (n *Node) {
return return
} }
if err != nil {
logger.Error(err)
return
}
client := http.Client{ client := http.Client{
Transport: &http.Transport{ Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}, },
} }
req, err := http.NewRequest("GET", u, nil) req, err := http.NewRequest("GET", u, nil)
if err != nil {
logger.Error(err)
return
}
req.Header.Set("X-Node-Secret", env.Token) req.Header.Set("X-Node-Secret", env.Token)
resp, err := client.Do(req) resp, err := client.Do(req)
@ -97,7 +105,7 @@ func InitNode(env *model.Environment) (n *Node) {
defer resp.Body.Close() defer resp.Body.Close()
bytes, _ := io.ReadAll(resp.Body) bytes, _ := io.ReadAll(resp.Body)
if resp.StatusCode != 200 { if resp.StatusCode != http.StatusOK {
logger.Error(string(bytes)) logger.Error(string(bytes))
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"` Name string `json:"name"`
URL string `json:"url"` URL string `json:"url"`
Token string `json:"token"` Token string `json:"token"`
Enabled bool `json:"enabled" gorm:"default:true"`
OperationSync bool `json:"operation_sync"` OperationSync bool `json:"operation_sync"`
SyncApiRegex string `json:"sync_api_regex"` 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"`
}