Skip to content

Commit c065c41

Browse files
committed
feat: add env group rendering and sync nodes preview #1268
1 parent bc4cab5 commit c065c41

File tree

20 files changed

+523
-120
lines changed

20 files changed

+523
-120
lines changed

api/cluster/group.go

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,74 @@ import (
55

66
"github.com/0xJacky/Nginx-UI/model"
77
"github.com/gin-gonic/gin"
8+
"github.com/samber/lo"
89
"github.com/uozi-tech/cosy"
910
"gorm.io/gorm"
1011
)
1112

13+
type APIRespEnvGroup struct {
14+
model.EnvGroup
15+
SyncNodes []*model.Environment `json:"sync_nodes,omitempty" gorm:"-"`
16+
}
17+
1218
func GetGroup(c *gin.Context) {
13-
cosy.Core[model.EnvGroup](c).Get()
19+
cosy.Core[model.EnvGroup](c).
20+
SetTransformer(func(m *model.EnvGroup) any {
21+
db := cosy.UseDB(c)
22+
23+
var nodes []*model.Environment
24+
if len(m.SyncNodeIds) > 0 {
25+
db.Model(&model.Environment{}).
26+
Where("id IN (?)", m.SyncNodeIds).
27+
Find(&nodes)
28+
}
29+
30+
return &APIRespEnvGroup{
31+
EnvGroup: *m,
32+
SyncNodes: nodes,
33+
}
34+
}).
35+
Get()
1436
}
1537

1638
func GetGroupList(c *gin.Context) {
1739
cosy.Core[model.EnvGroup](c).GormScope(func(tx *gorm.DB) *gorm.DB {
1840
return tx.Order("order_id ASC")
19-
}).PagingList()
41+
}).
42+
SetScan(func(tx *gorm.DB) any {
43+
var groups []*APIRespEnvGroup
44+
45+
var nodeIDs []uint64
46+
tx.Find(&groups)
47+
48+
for _, group := range groups {
49+
nodeIDs = append(nodeIDs, group.SyncNodeIds...)
50+
}
51+
52+
var nodes []*model.Environment
53+
nodeIDs = lo.Uniq(nodeIDs)
54+
if len(nodeIDs) > 0 {
55+
db := cosy.UseDB(c)
56+
db.Model(&model.Environment{}).
57+
Where("id IN (?)", nodeIDs).
58+
Find(&nodes)
59+
}
60+
61+
nodeMap := lo.SliceToMap(nodes, func(node *model.Environment) (uint64, *model.Environment) {
62+
return node.ID, node
63+
})
64+
65+
for _, group := range groups {
66+
for _, nodeID := range group.SyncNodeIds {
67+
if node, ok := nodeMap[nodeID]; ok {
68+
group.SyncNodes = append(group.SyncNodes, node)
69+
}
70+
}
71+
}
72+
73+
return groups
74+
}).
75+
PagingList()
2076
}
2177

2278
func ReloadNginx(c *gin.Context) {

app/components.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ declare module 'vue' {
8282
CodeEditorCodeEditor: typeof import('./src/components/CodeEditor/CodeEditor.vue')['default']
8383
ConfigHistoryConfigHistory: typeof import('./src/components/ConfigHistory/ConfigHistory.vue')['default']
8484
ConfigHistoryDiffViewer: typeof import('./src/components/ConfigHistory/DiffViewer.vue')['default']
85+
EnvGroupRenderEnvGroupRender: typeof import('./src/components/EnvGroupRender/EnvGroupRender.vue')['default']
86+
EnvGroupRenderEnvGroupRenderer: typeof import('./src/components/EnvGroupRender/EnvGroupRenderer.vue')['default']
87+
EnvGroupRendererEnvGroupRenderer: typeof import('./src/components/EnvGroupRenderer/EnvGroupRenderer.vue')['default']
8588
EnvGroupTabsEnvGroupTabs: typeof import('./src/components/EnvGroupTabs/EnvGroupTabs.vue')['default']
8689
EnvIndicatorEnvIndicator: typeof import('./src/components/EnvIndicator/EnvIndicator.vue')['default']
8790
FooterToolbarFooterToolBar: typeof import('./src/components/FooterToolbar/FooterToolBar.vue')['default']
@@ -98,6 +101,7 @@ declare module 'vue' {
98101
NgxConfigEditorNgxConfigEditor: typeof import('./src/components/NgxConfigEditor/NgxConfigEditor.vue')['default']
99102
NgxConfigEditorNgxServer: typeof import('./src/components/NgxConfigEditor/NgxServer.vue')['default']
100103
NgxConfigEditorNgxUpstream: typeof import('./src/components/NgxConfigEditor/NgxUpstream.vue')['default']
104+
NodeCardNodeCard: typeof import('./src/components/NodeCard/NodeCard.vue')['default']
101105
NodeSelectorNodeSelector: typeof import('./src/components/NodeSelector/NodeSelector.vue')['default']
102106
NotificationNotification: typeof import('./src/components/Notification/Notification.vue')['default']
103107
OTPInputOTPInput: typeof import('./src/components/OTPInput/OTPInput.vue')['default']
@@ -115,6 +119,7 @@ declare module 'vue' {
115119
SwitchAppearanceIconsVPIconMoon: typeof import('./src/components/SwitchAppearance/icons/VPIconMoon.vue')['default']
116120
SwitchAppearanceIconsVPIconSun: typeof import('./src/components/SwitchAppearance/icons/VPIconSun.vue')['default']
117121
SwitchAppearanceSwitchAppearance: typeof import('./src/components/SwitchAppearance/SwitchAppearance.vue')['default']
122+
SyncNodesPreviewSyncNodesPreview: typeof import('./src/components/SyncNodesPreview/SyncNodesPreview.vue')['default']
118123
SystemRestoreSystemRestoreContent: typeof import('./src/components/SystemRestore/SystemRestoreContent.vue')['default']
119124
TwoFAAuthorization: typeof import('./src/components/TwoFA/Authorization.vue')['default']
120125
VPSwitchVPSwitch: typeof import('./src/components/VPSwitch/VPSwitch.vue')['default']

app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"@fingerprintjs/fingerprintjs": "^4.6.2",
1919
"@formkit/auto-animate": "^0.8.2",
2020
"@simplewebauthn/browser": "^13.1.2",
21-
"@uozi-admin/curd": "^4.5.8",
21+
"@uozi-admin/curd": "^4.5.9",
2222
"@uozi-admin/request": "^2.8.4",
2323
"@vue/reactivity": "^3.5.18",
2424
"@vue/shared": "^3.5.18",

app/pnpm-lock.yaml

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<script setup lang="ts">
2+
import type { EnvGroup } from '@/api/env_group'
3+
import NodeCard from '@/components/NodeCard'
4+
5+
defineProps<{
6+
envGroup: EnvGroup | null
7+
}>()
8+
9+
const modalVisible = ref(false)
10+
11+
function showModal() {
12+
modalVisible.value = true
13+
}
14+
15+
function handleCancel() {
16+
modalVisible.value = false
17+
}
18+
</script>
19+
20+
<template>
21+
<div v-if="envGroup">
22+
<span
23+
class="cursor-pointer text-blue-500 hover:text-blue-700"
24+
@click="showModal"
25+
>
26+
{{ envGroup.name }}
27+
</span>
28+
29+
<AModal
30+
v-model:open="modalVisible"
31+
:title="envGroup.name"
32+
:footer="null"
33+
width="680px"
34+
@cancel="handleCancel"
35+
>
36+
<div class="py-4">
37+
<div class="mb-4">
38+
<strong class="text-gray-900 dark:text-gray-100">{{ $gettext('Post-sync Action') }}:</strong>
39+
<span class="ml-2 text-gray-700 dark:text-gray-300">
40+
<template v-if="!envGroup.post_sync_action || envGroup.post_sync_action === 'none'">
41+
{{ $gettext('No Action') }}
42+
</template>
43+
<template v-else-if="envGroup.post_sync_action === 'reload_nginx'">
44+
{{ $gettext('Reload Nginx') }}
45+
</template>
46+
<template v-else>
47+
{{ envGroup.post_sync_action }}
48+
</template>
49+
</span>
50+
</div>
51+
52+
<div>
53+
<strong class="text-gray-900 dark:text-gray-100">{{ $gettext('Sync Nodes') }}</strong>
54+
<div v-if="!envGroup.sync_node_ids || envGroup.sync_node_ids.length === 0" class="mt-2 text-gray-400 dark:text-gray-500">
55+
{{ $gettext('No nodes selected') }}
56+
</div>
57+
<div v-else class="mt-2">
58+
<div class="grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
59+
<NodeCard
60+
v-for="nodeId in envGroup.sync_node_ids"
61+
:key="nodeId"
62+
:node-id="nodeId"
63+
size="sm"
64+
/>
65+
</div>
66+
</div>
67+
</div>
68+
</div>
69+
</AModal>
70+
</div>
71+
<span v-else class="text-gray-400">-</span>
72+
</template>
73+
74+
<style lang="less" scoped>
75+
</style>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './EnvGroupRender.vue'

app/src/components/EnvGroupTabs/EnvGroupTabs.vue

Lines changed: 4 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,21 @@
11
<script setup lang="ts">
2-
import type ReconnectingWebSocket from 'reconnecting-websocket'
32
import type { EnvGroup } from '@/api/env_group'
4-
import type { Environment } from '@/api/environment'
53
import { message } from 'ant-design-vue'
64
import nodeApi from '@/api/node'
7-
import ws from '@/lib/websocket'
5+
import { useNodeAvailabilityStore } from '@/pinia/moudule/nodeAvailability'
86
97
const props = defineProps<{
108
envGroups: EnvGroup[]
119
}>()
1210
1311
const modelValue = defineModel<string | number>('activeKey')
12+
const nodeStore = useNodeAvailabilityStore()
1413
15-
const environments = ref<Environment[]>([])
16-
const environmentsMap = ref<Record<number, Environment>>({})
1714
const loading = ref({
1815
reload: false,
1916
restart: false,
2017
})
2118
22-
// WebSocket connection for environment monitoring
23-
let socket: ReconnectingWebSocket | WebSocket | null = null
24-
25-
// Get node data when tab is not 'All'
26-
watch(modelValue, newVal => {
27-
if (newVal && newVal !== 0) {
28-
connectWebSocket()
29-
}
30-
else {
31-
disconnectWebSocket()
32-
}
33-
}, { immediate: true })
34-
35-
function connectWebSocket() {
36-
if (socket) {
37-
socket.close()
38-
}
39-
40-
socket = ws('/api/cluster/environments/enabled/ws', true)
41-
42-
socket.onmessage = event => {
43-
try {
44-
const message = JSON.parse(event.data)
45-
46-
if (message.event === 'message') {
47-
const data: Environment[] = message.data
48-
environments.value = data
49-
environmentsMap.value = environments.value.reduce((acc, node) => {
50-
acc[node.id] = node
51-
return acc
52-
}, {} as Record<number, Environment>)
53-
}
54-
}
55-
catch (error) {
56-
console.error('Error parsing WebSocket message:', error)
57-
}
58-
}
59-
60-
socket.onerror = error => {
61-
console.warn('Failed to connect to environments WebSocket endpoint', error)
62-
}
63-
}
64-
65-
function disconnectWebSocket() {
66-
if (socket) {
67-
socket.close()
68-
socket = null
69-
}
70-
}
71-
72-
// Cleanup on unmount
73-
onUnmounted(() => {
74-
disconnectWebSocket()
75-
})
76-
7719
// Get the current Node Group data
7820
const currentEnvGroup = computed(() => {
7921
if (!modelValue.value || modelValue.value === 0)
@@ -90,8 +32,8 @@ const syncNodes = computed(() => {
9032
return []
9133
9234
return currentEnvGroup.value.sync_node_ids
93-
.map(id => environmentsMap.value[id])
94-
.filter(Boolean)
35+
.map(id => nodeStore.getNodeStatus(id))
36+
.filter((node): node is NonNullable<typeof node> => Boolean(node))
9537
})
9638
9739
// Handle reload Nginx on all sync nodes

0 commit comments

Comments
 (0)