Skip to content

Commit 4151996

Browse files
CopilotrobertsLando
andcommitted
feat(frontend): add Groups management UI and virtual node support
- Add Groups.vue view with CRUD operations table and export functionality - Create DialogGroupEdit component for creating/editing groups - Add Groups route and navigation menu item with group_work icon - Update nodes table to display virtual nodes with VIRTUAL chip badge - Add virtual column with rich value display (cloud icon for virtual nodes) - Modify ExpandedNode to hide advanced features for virtual nodes: - Hide device ID and config DB link for virtual nodes - Hide statistics, ping, and advanced buttons for virtual nodes - Hide help, groups, and OTA update tabs for virtual nodes - Show only group name in name column for virtual nodes - Filter out virtual nodes from group member selection Co-authored-by: robertsLando <11502495+robertsLando@users.noreply.github.com>
1 parent eb26fef commit 4151996

File tree

7 files changed

+440
-11
lines changed

7 files changed

+440
-11
lines changed

src/App.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,12 @@ export default {
493493
path: Routes.scenes,
494494
})
495495
496+
pages.splice(4, 0, {
497+
icon: 'group_work',
498+
title: 'Groups',
499+
path: Routes.groups,
500+
})
501+
496502
pages.push({
497503
icon: 'share',
498504
title: 'Network graph',
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<template>
2+
<v-dialog v-model="_value" max-width="600px" persistent>
3+
<v-card>
4+
<v-card-title>
5+
<span class="text-h5">{{ title }}</span>
6+
</v-card-title>
7+
8+
<v-card-text>
9+
<v-container grid-list-md>
10+
<v-form v-model="valid" ref="form" validate-on="lazy">
11+
<v-row>
12+
<v-col cols="12">
13+
<v-text-field
14+
v-model="editedGroup.name"
15+
label="Group Name"
16+
required
17+
:rules="[required]"
18+
hint="Enter a descriptive name for this multicast group"
19+
></v-text-field>
20+
</v-col>
21+
<v-col cols="12">
22+
<v-select
23+
v-model="editedGroup.nodeIds"
24+
label="Nodes"
25+
required
26+
multiple
27+
item-title="_name"
28+
item-value="id"
29+
:items="nodes"
30+
:rules="[required, minNodes]"
31+
hint="Select at least 2 nodes for the multicast group"
32+
chips
33+
closable-chips
34+
>
35+
<template #chip="{ item }">
36+
<v-chip
37+
size="small"
38+
closable
39+
@click:close="
40+
removeNode(item.raw.id)
41+
"
42+
>
43+
{{ item.raw._name }}
44+
</v-chip>
45+
</template>
46+
<template
47+
#item="{ item, props: itemProps }"
48+
>
49+
<v-list-item
50+
v-bind="itemProps"
51+
:title="item.raw._name"
52+
:subtitle="`Node ID: ${item.raw.id}${item.raw.loc ? ' - ' + item.raw.loc : ''}`"
53+
>
54+
<template #prepend>
55+
<v-icon
56+
:color="
57+
item.raw.ready
58+
? 'success'
59+
: 'error'
60+
"
61+
>
62+
{{
63+
item.raw.ready
64+
? 'check_circle'
65+
: 'error'
66+
}}
67+
</v-icon>
68+
</template>
69+
</v-list-item>
70+
</template>
71+
</v-select>
72+
</v-col>
73+
</v-row>
74+
</v-form>
75+
</v-container>
76+
</v-card-text>
77+
78+
<v-card-actions>
79+
<v-spacer></v-spacer>
80+
<v-btn
81+
color="blue-darken-1"
82+
variant="text"
83+
@click="$emit('close')"
84+
>
85+
Cancel
86+
</v-btn>
87+
<v-btn
88+
color="blue-darken-1"
89+
variant="text"
90+
@click="handleSave"
91+
:disabled="!valid"
92+
>
93+
Save
94+
</v-btn>
95+
</v-card-actions>
96+
</v-card>
97+
</v-dialog>
98+
</template>
99+
100+
<script>
101+
export default {
102+
props: {
103+
modelValue: Boolean,
104+
title: String,
105+
editedGroup: Object,
106+
nodes: Array,
107+
},
108+
watch: {
109+
modelValue(val) {
110+
if (val && this.$refs.form) {
111+
this.$refs.form.resetValidation()
112+
}
113+
},
114+
},
115+
computed: {
116+
_value: {
117+
get() {
118+
return this.modelValue
119+
},
120+
set(val) {
121+
this.$emit('update:modelValue', val)
122+
},
123+
},
124+
},
125+
data() {
126+
return {
127+
valid: true,
128+
required: (v) => !!v || 'This field is required',
129+
minNodes: (v) => (v && v.length >= 1) || 'Select at least 1 node',
130+
}
131+
},
132+
methods: {
133+
removeNode(nodeId) {
134+
const index = this.editedGroup.nodeIds.indexOf(nodeId)
135+
if (index > -1) {
136+
this.editedGroup.nodeIds.splice(index, 1)
137+
}
138+
},
139+
async handleSave() {
140+
const result = await this.$refs.form.validate()
141+
if (result.valid) {
142+
this.$emit('save')
143+
}
144+
},
145+
},
146+
}
147+
</script>

src/components/nodes-table/ExpandedNode.vue

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,19 @@
88
>
99
<v-row class="mt-2" align="center">
1010
<v-col style="min-width: 200px" class="ml-4">
11-
<span class="text-h6 text-grey">Device </span>
11+
<span class="text-h6 text-grey"
12+
>{{ node.virtual ? 'Virtual Node' : 'Device' }}
13+
</span>
1214
<br />
13-
<span class="subtitle font-weight-bold font-monospace">
15+
<span
16+
class="subtitle font-weight-bold font-monospace"
17+
v-if="!node.virtual"
18+
>
1419
{{ node.hexId }}
1520
</span>
1621

1722
<v-icon
23+
v-if="!node.virtual"
1824
@click="openLink(node.dbLink)"
1925
class="ml-2"
2026
v-tooltip:bottom="'See device config'"
@@ -23,7 +29,7 @@
2329
</v-icon>
2430
<br />
2531
<span
26-
v-if="$vuetify.display.smAndDown"
32+
v-if="$vuetify.display.smAndDown && !node.virtual"
2733
class="comment font-weight-bold text-primary"
2834
>
2935
{{
@@ -38,7 +44,7 @@
3844
<v-col
3945
:class="$vuetify.display.smAndDown ? 'text-center' : 'text-end'"
4046
>
41-
<v-btn-group class="ml-2" multiple>
47+
<v-btn-group v-if="!node.virtual" class="ml-2" multiple>
4248
<v-btn
4349
color="primary"
4450
variant="outlined"
@@ -110,43 +116,43 @@
110116
text="Node"
111117
/>
112118
<v-tab
113-
v-if="nodeMetadata"
119+
v-if="nodeMetadata && !node.virtual"
114120
prepend-icon="help"
115121
value="manual"
116122
class="justify-start"
117123
text="Help"
118-
W
119124
/>
120125
<v-tab
121-
v-if="showHass"
126+
v-if="showHass && !node.virtual"
122127
prepend-icon="home"
123128
value="homeassistant"
124129
class="justify-start"
125130
text="Home Assistant"
126131
/>
127132
<v-tab
133+
v-if="!node.virtual"
128134
prepend-icon="device_hub"
129135
value="groups"
130136
class="justify-start"
131137
text="Groups"
132138
/>
133139
<v-tab
134-
v-if="node.schedule"
140+
v-if="node.schedule && !node.virtual"
135141
prepend-icon="group"
136142
value="users"
137143
class="justify-start"
138144
text="Users"
139145
/>
140146
<v-tab
141147
value="ota"
142-
v-if="!node.isControllerNode"
148+
v-if="!node.isControllerNode && !node.virtual"
143149
prepend-icon="auto_mode"
144150
class="justify-start"
145151
text="OTA Updates"
146152
/>
147153
<v-tab
148154
value="otw"
149-
v-if="node.isControllerNode"
155+
v-if="node.isControllerNode && !node.virtual"
150156
prepend-icon="auto_mode"
151157
class="justify-start"
152158
text="Firmware Updates"

src/components/nodes-table/index.vue

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,20 @@
169169
{{ item.productLabel }}
170170
</template>
171171
<template #[`item.name`]="{ item }">
172-
{{ item.name || '' }}
172+
<div class="d-flex align-center">
173+
<span>{{ item.virtual ? item.name : item.name || '' }}</span>
174+
<v-chip
175+
v-if="item.virtual"
176+
size="x-small"
177+
color="purple"
178+
class="ml-2"
179+
>
180+
VIRTUAL
181+
</v-chip>
182+
</div>
183+
</template>
184+
<template #[`item.virtual`]="{ item }">
185+
<rich-value :value="richValue(item, 'virtual')" />
173186
</template>
174187
<template #[`item.loc`]="{ item }">
175188
{{ item.loc || '' }}

src/components/nodes-table/nodes-table.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,28 @@ export default {
9191
productDescription: { type: 'string', label: 'Product' },
9292
productLabel: { type: 'string', label: 'Product code' },
9393
name: { type: 'string', label: 'Name' },
94+
virtual: {
95+
type: 'boolean',
96+
label: 'Virtual',
97+
richValue: (node) =>
98+
this.booleanRichValue(node.virtual, {
99+
true: {
100+
icon: 'cloud',
101+
iconStyle: 'color: purple',
102+
description: 'Virtual Node',
103+
},
104+
false: {
105+
icon: 'device_hub',
106+
iconStyle: 'color: green',
107+
description: 'Physical Node',
108+
},
109+
default: {
110+
icon: 'device_hub',
111+
iconStyle: 'color: green',
112+
description: 'Physical Node',
113+
},
114+
}),
115+
},
94116
loc: { type: 'string', label: 'Location' },
95117
security: {
96118
type: 'string',

src/router/index.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const Settings = () => import('@/views/Settings.vue')
66
const Mesh = () => import('@/views/Mesh.vue')
77
const Store = () => import('@/views/Store.vue')
88
const Scenes = () => import('@/views/Scenes.vue')
9+
const Groups = () => import('@/views/Groups.vue')
910
const Debug = () => import('@/views/Debug.vue')
1011
const Login = () => import('@/views/Login.vue')
1112
const ErrorPage = () => import('@/views/ErrorPage.vue')
@@ -22,6 +23,7 @@ export const Routes = {
2223
controlPanel: '/control-panel',
2324
settings: '/settings',
2425
scenes: '/scenes',
26+
groups: '/groups',
2527
debug: '/debug',
2628
store: '/store',
2729
mesh: '/mesh',
@@ -90,6 +92,15 @@ const router = createRouter({
9092
requiresAuth: true,
9193
},
9294
},
95+
{
96+
path: Routes.groups,
97+
name: 'Groups',
98+
component: Groups,
99+
props: true,
100+
meta: {
101+
requiresAuth: true,
102+
},
103+
},
93104
{
94105
path: Routes.debug,
95106
name: 'Debug',

0 commit comments

Comments
 (0)