mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2025-05-11 02:15:48 +02:00
feat: add enabled/disabled field to environment model #169
This commit is contained in:
parent
cc5d2be1bd
commit
b429c15893
17 changed files with 250 additions and 240 deletions
|
@ -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
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
c.JSON(http.StatusOK, env)
|
||||
}).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
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
GetEnvironment(c)
|
||||
}).Modify()
|
||||
}
|
||||
|
||||
func DeleteEnvironment(c *gin.Context) {
|
||||
|
|
|
@ -45,3 +45,6 @@ RedirectUri =
|
|||
Enabled = false
|
||||
CMD = logrotate /etc/logrotate.d/nginx
|
||||
Interval = 1440
|
||||
|
||||
[cluster]
|
||||
Node =
|
||||
|
|
|
@ -66,6 +66,7 @@ async function request() {
|
|||
let hasCodeBlockIndicator = false
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) {
|
||||
setTimeout(() => {
|
||||
|
@ -77,6 +78,10 @@ async function request() {
|
|||
}
|
||||
apply(value!)
|
||||
}
|
||||
catch (e) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function apply(input: Uint8Array) {
|
||||
const decoder = new TextDecoder('utf-8')
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
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
|
||||
v-if="!noData"
|
||||
:gutter="[16, 16]"
|
||||
>
|
||||
<ARow :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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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: [],
|
||||
};
|
||||
|
|
|
@ -42,6 +42,7 @@ onMounted(async () => {
|
|||
users.value = []
|
||||
let page = 1
|
||||
while (true) {
|
||||
try {
|
||||
const r = await acme_user.get_list({ page })
|
||||
|
||||
users.value.push(...r.data)
|
||||
|
@ -49,6 +50,10 @@ onMounted(async () => {
|
|||
break
|
||||
page++
|
||||
}
|
||||
catch (e) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
init()
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -25,10 +25,21 @@ const node_map = computed(() => {
|
|||
|
||||
let websocket: ReconnectingWebSocket | WebSocket
|
||||
|
||||
onMounted(() => {
|
||||
environment.get_list().then(r => {
|
||||
data.value = r.data
|
||||
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(() => {
|
||||
websocket = analytic.nodes()
|
||||
websocket.onmessage = async m => {
|
||||
const nodes = JSON.parse(m.data)
|
||||
|
|
|
@ -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,6 +78,7 @@ const columns: Column[] = [{
|
|||
customRender: (args: customRender) => {
|
||||
const template: JSXElements = []
|
||||
const { text } = args
|
||||
if (args.record.enabled) {
|
||||
if (text === true || text > 0) {
|
||||
template.push(<Badge status="success"/>)
|
||||
template.push($gettext('Online'))
|
||||
|
@ -85,12 +87,37 @@ const columns: Column[] = [{
|
|||
template.push(<Badge status="error"/>)
|
||||
template.push($gettext('Offline'))
|
||||
}
|
||||
}
|
||||
else {
|
||||
template.push(<Badge status="default"/>)
|
||||
template.push($gettext('Disabled'))
|
||||
}
|
||||
|
||||
return h('div', template)
|
||||
},
|
||||
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',
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -52,13 +52,15 @@ 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 => {
|
||||
if (!settingsStore.is_remote)
|
||||
server_name.value = r?.server?.name ?? ''
|
||||
data.value = r
|
||||
message.success($gettext('Save successfully'))
|
||||
|
|
|
@ -52,9 +52,15 @@ func init() {
|
|||
|
||||
func GetNode(env *model.Environment) (n *Node) {
|
||||
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{}
|
||||
|
@ -75,16 +81,18 @@ func InitNode(env *model.Environment) (n *Node) {
|
|||
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)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Set("X-Node-Secret", env.Token)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
|
@ -97,7 +105,7 @@ func InitNode(env *model.Environment) (n *Node) {
|
|||
defer resp.Body.Close()
|
||||
bytes, _ := io.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
logger.Error(string(bytes))
|
||||
return
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
5
settings/cluster.go
Normal file
|
@ -0,0 +1,5 @@
|
|||
package settings
|
||||
|
||||
type Cluster struct {
|
||||
Node []string `ini:",,allowshadow"`
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue