-
-
+
+
+
+
+
+
-
-
- Group Members :
-
-
-
- {{value}}
-
-
-
- {{value}}
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ {{groupMembers.length}} Devices
+
+
+
+
+
+
+ Current Group Members
+
+
+
+
+
+
+
+
+
No devices in this group
+
Click "Add Devices" to start adding devices to this group
+
+ 0">
+
+
+
+
+
+ {{value}}
+
+
+
+
+
+ {{value}}
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
- Members not in Group
+
+
+ Add Devices to Group
-
-
- Members Availble to add:
-
-
-
- {{value}}
-
-
-
- {{value}}
-
-
-
-
- {{value}}
-
-
-
-
-
-
+
+
+
+ 0" color="info" class="d-flex align-items-center mb-0">
+
+ {{NewMemberRows.length}} device(s) selected for addition
+
+
+
+
+
+
+ Available Devices ({{availbleMembers.length}} total)
+
+
+
+
+
All devices are already in groups
+
No available devices to add to this group
+
+ 0">
+
+
+
+
+
+ {{value}}
+
+
+
+
+
+ {{value}}
+
+
+
+
+ {{value}}
+
+
+
+
+
+
-
-
-
+
+
+ Select devices from the list above to add them to the group
+
+
+
+
+
@@ -175,4 +267,200 @@
Close
+
+
+
+ User Permissions - {{selectedGroup?.name}}
+
+
+
+
+
+ Add User Permission
+
+
+
+
+
+
+
+
0" class="search-dropdown">
+
+ {{user.username}} ({{user.first_name}} {{user.last_name}})
+
+
+
+ No users found
+
+
+
+
+
+
+
+
0" class="search-dropdown">
+
+ {{perm.name}}
+
+
+
+ No permissions found
+
+
+
+
+
+
+
+
+
+
+
+ Current Permissions ({{selectedGroup?.assigned_users?.length || 0}} users)
+
+
+
+
+
No users have permissions for this group
+
+ 0" class="table-responsive">
+
+
+
+ | # |
+ User |
+ Name |
+ Permission |
+ Actions |
+
+
+
+
+ | {{i + 1}} |
+
+
+
+ {{user.username}}
+
+ |
+ {{user.first_name}} {{user.last_name}} |
+
+ {{user.perm_name}}
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Change Permission for {{editingUser?.username}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Remove Permission
+
+
+
+
+
+
Are you sure you want to remove {{removingUser?.username}}'s permission from group {{selectedGroup?.name}}?
+
This action cannot be undone.
+
+
+
+
+
+
+
+
+
+ Confirm Firmware {{firmwareAction | titlecase}}
+
+
+
+
+
+
Are you sure you want to {{firmwareAction}} firmware for all devices in group {{selectedGroupForFirmware?.name}}?
+
This action will affect all devices in this group and may take some time to complete.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/views/devices_group/devgroup.component.scss b/src/app/views/devices_group/devgroup.component.scss
new file mode 100644
index 0000000..c845baa
--- /dev/null
+++ b/src/app/views/devices_group/devgroup.component.scss
@@ -0,0 +1,108 @@
+.users-summary {
+ min-height: 60px;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.user-badges {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.25rem;
+}
+
+.user-badges c-badge {
+ font-size: 0.7rem;
+ max-width: 80px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+@media (max-width: 768px) {
+ .users-summary {
+ min-height: auto;
+ }
+
+ .user-badges c-badge {
+ max-width: 60px;
+ font-size: 0.65rem;
+ }
+}
+
+/* Search Select Components */
+.search-select-wrapper {
+ position: relative;
+}
+
+.search-label {
+ display: block;
+ font-size: 0.8rem;
+ color: #6c757d;
+ margin-bottom: 0.25rem;
+ font-weight: 500;
+}
+
+.search-input {
+ width: 100%;
+}
+
+.search-dropdown {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ background: white;
+ border: 1px solid #ced4da;
+ border-top: none;
+ border-radius: 0 0 0.375rem 0.375rem;
+ max-height: 200px;
+ overflow-y: auto;
+ z-index: 1000;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.search-option {
+ padding: 0.5rem 0.75rem;
+ cursor: pointer;
+ border-bottom: 1px solid #f1f3f4;
+ font-size: 0.85rem;
+ transition: background-color 0.2s ease;
+}
+
+.search-option:hover {
+ background-color: #f8f9fa;
+}
+
+.search-option:last-child {
+ border-bottom: none;
+}
+
+.search-no-results {
+ padding: 0.75rem;
+ text-align: center;
+ color: #6c757d;
+ font-size: 0.8rem;
+ font-style: italic;
+ background: white;
+ border: 1px solid #ced4da;
+ border-top: none;
+ border-radius: 0 0 0.375rem 0.375rem;
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ z-index: 1000;
+}
+
+.compact-select {
+ font-size: 0.85rem;
+ height: calc(1.8em + 0.75rem + 2px);
+ padding: 0.375rem 0.75rem;
+ border-radius: 0.375rem;
+}
+
+.compact-select:focus {
+ border-color: #0d6efd;
+ box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
+}
\ No newline at end of file
diff --git a/src/app/views/devices_group/devgroup.component.ts b/src/app/views/devices_group/devgroup.component.ts
index 5ee1248..5614bfa 100644
--- a/src/app/views/devices_group/devgroup.component.ts
+++ b/src/app/views/devices_group/devgroup.component.ts
@@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core";
import { dataProvider } from "../../providers/mikrowizard/data";
import { Router } from "@angular/router";
import { loginChecker } from "../../providers/login_checker";
+import { formatInTimeZone } from "date-fns-tz";
import {
GuiSearching,
GuiSelectedRow,
@@ -31,10 +32,12 @@ interface IUser {
@Component({
templateUrl: "devgroup.component.html",
+ styleUrls: ["devgroup.component.scss"]
})
export class DevicesGroupComponent implements OnInit {
public uid: number;
public uname: string;
+ public tz: string;
constructor(
private data_provider: dataProvider,
@@ -50,6 +53,7 @@ export class DevicesGroupComponent implements OnInit {
this.data_provider.getSessionInfo().then((res) => {
_self.uid = res.uid;
_self.uname = res.name;
+ _self.tz = res.tz;
const userId = _self.uid;
if (res.role != "admin") {
@@ -83,6 +87,30 @@ export class DevicesGroupComponent implements OnInit {
id: 0,
name: "",
};
+ public selectedGroup: any = null;
+ public UserManagementModalVisible: boolean = false;
+ public EditPermissionModalVisible: boolean = false;
+ public RemovePermissionModalVisible: boolean = false;
+ public availableUsers: any[] = [];
+ public availablePermissions: any[] = [];
+ public selectedUserId: string = "";
+ public selectedPermId: string = "";
+ public selectedUser: any = null;
+ public selectedPermission: any = null;
+ public userSearch: string = '';
+ public permissionSearch: string = '';
+ public filteredUsers: any[] = [];
+ public filteredPermissions: any[] = [];
+ public showUserDropdown: boolean = false;
+ public showPermissionDropdown: boolean = false;
+ public editingUser: any = null;
+ public removingUser: any = null;
+ public newPermissionId: string = "";
+ private deviceCache: { [key: number]: any[] } = {};
+ private loadingDevices: { [key: number]: boolean } = {};
+ public FirmwareConfirmModalVisible: boolean = false;
+ public firmwareAction: string = "";
+ public selectedGroupForFirmware: any = null;
public DefaultCurrentGroup: any = {
array_agg: [],
created: "",
@@ -182,6 +210,9 @@ export class DevicesGroupComponent implements OnInit {
save_group() {
var _self = this;
this.data_provider.update_save_group(this.currentGroup).then((res) => {
+ // Clear device cache for this group
+ delete this.deviceCache[this.currentGroup.id];
+ delete this.loadingDevices[this.currentGroup.id];
_self.initGridTable();
_self.EditGroupModalVisible = false;
});
@@ -237,4 +268,225 @@ export class DevicesGroupComponent implements OnInit {
this.loading = false;
});
}
+
+ manageUsers(group: any): void {
+ this.selectedGroup = { ...group };
+ this.loadAvailableUsers();
+ this.loadAvailablePermissions();
+ this.UserManagementModalVisible = true;
+ }
+
+ loadAvailableUsers(): void {
+ this.data_provider.get_users(1, 1000, "").then((res) => {
+ this.availableUsers = res.filter((user: any) =>
+ !this.selectedGroup.assigned_users.some((assignedUser: any) => assignedUser.user_id === user.id)
+ );
+ this.filteredUsers = [...this.availableUsers];
+ });
+ }
+
+ loadAvailablePermissions(): void {
+ this.data_provider.get_perms(1, 1000, "").then((res) => {
+ this.availablePermissions = res;
+ this.filteredPermissions = [...this.availablePermissions];
+ });
+ }
+
+ filterUsers(event: any): void {
+ const query = event.target.value.toLowerCase();
+ this.filteredUsers = this.availableUsers.filter((user: any) =>
+ user.username.toLowerCase().includes(query) ||
+ (user.first_name + ' ' + user.last_name).toLowerCase().includes(query)
+ );
+ }
+
+ filterPermissions(event: any): void {
+ const query = event.target.value.toLowerCase();
+ this.filteredPermissions = this.availablePermissions.filter((perm: any) =>
+ perm.name.toLowerCase().includes(query)
+ );
+ }
+
+ selectUser(user: any): void {
+ this.selectedUser = user;
+ this.selectedUserId = user.id;
+ this.userSearch = user.username + ' (' + user.first_name + ' ' + user.last_name + ')';
+ this.showUserDropdown = false;
+ }
+
+ selectPermission(perm: any): void {
+ this.selectedPermission = perm;
+ this.selectedPermId = perm.id;
+ this.permissionSearch = perm.name;
+ this.showPermissionDropdown = false;
+ }
+
+ hideUserDropdown(): void {
+ setTimeout(() => this.showUserDropdown = false, 200);
+ }
+
+ hidePermissionDropdown(): void {
+ setTimeout(() => this.showPermissionDropdown = false, 200);
+ }
+
+ addUserPermission(): void {
+ if (!this.selectedUser || !this.selectedPermission) return;
+
+ console.log('Adding user permission:', {
+ userId: this.selectedUser.id,
+ permissionId: this.selectedPermission.id,
+ groupId: this.selectedGroup.id,
+ selectedUser: this.selectedUser,
+ selectedPermission: this.selectedPermission
+ });
+
+ this.data_provider.Add_user_perm(this.selectedUser.id, +this.selectedPermission.id, this.selectedGroup.id)
+ .then((res) => {
+ console.log('Add user permission response:', res);
+ this.initGridTable();
+ this.selectedUserId = "";
+ this.selectedPermId = "";
+ this.selectedUser = null;
+ this.selectedPermission = null;
+ this.userSearch = "";
+ this.permissionSearch = "";
+ // Refresh the selected group data
+ this.data_provider.get_devgroup_list().then((groups) => {
+ this.selectedGroup = groups.find((g: any) => g.id === this.selectedGroup.id);
+ this.loadAvailableUsers();
+ });
+ });
+ }
+
+ editUserPermission(user: any): void {
+ this.editingUser = { ...user };
+ this.newPermissionId = user.perm_id.toString();
+ this.EditPermissionModalVisible = true;
+ }
+
+ updateUserPermission(): void {
+ if (!this.newPermissionId) return;
+
+ // Remove old permission and add new one
+ this.data_provider.Delete_user_perm(this.editingUser.id).then(() => {
+ this.data_provider.Add_user_perm(this.editingUser.user_id, +this.newPermissionId, this.selectedGroup.id)
+ .then(() => {
+ this.EditPermissionModalVisible = false;
+ this.initGridTable();
+ // Refresh the selected group data
+ this.data_provider.get_devgroup_list().then((groups) => {
+ this.selectedGroup = groups.find((g: any) => g.id === this.selectedGroup.id);
+ });
+ });
+ });
+ }
+
+ removeUserPermission(user: any): void {
+ this.removingUser = { ...user };
+ this.RemovePermissionModalVisible = true;
+ }
+
+ confirmRemovePermission(): void {
+ this.data_provider.Delete_user_perm(this.removingUser.id).then(() => {
+ this.RemovePermissionModalVisible = false;
+ this.initGridTable();
+ // Refresh the selected group data
+ this.data_provider.get_devgroup_list().then((groups) => {
+ this.selectedGroup = groups.find((g: any) => g.id === this.selectedGroup.id);
+ this.loadAvailableUsers();
+ });
+ });
+ }
+
+ getPermissionColor(permName: string): string {
+ const colorMap: { [key: string]: string } = {
+ 'full': 'success',
+ 'read': 'info',
+ 'write': 'warning',
+ 'admin': 'danger',
+ 'test': 'secondary'
+ };
+ return colorMap[permName] || 'primary';
+ }
+
+ getUsersTooltip(users: any[]): string {
+ if (users.length === 0) return 'No users assigned';
+
+ const maxShow = 10;
+ const userList = users.slice(0, maxShow).map((user, index) =>
+ `• ${user.username} (${user.perm_name})`
+ ).join('\n');
+
+ return users.length > maxShow
+ ? `${userList}\n━━━━━━━━━━━━━━━━\n+${users.length - maxShow} more users`
+ : userList;
+ }
+
+ getDevicesTooltip(group: any): string {
+ if (group.id === 1) return 'All devices in the system';
+ if (!group.array_agg || group.array_agg[0] === null) return 'No devices assigned';
+
+ // Check if data is cached
+ if (this.deviceCache[group.id]) {
+ const devices = this.deviceCache[group.id];
+ const maxShow = 10;
+ const deviceList = devices.slice(0, maxShow).map(device =>
+ `• ${device.name} (${device.ip})`
+ ).join('\n');
+
+ return devices.length > maxShow
+ ? `${deviceList}\n━━━━━━━━━━━━━━━━\n+${devices.length - maxShow} more devices`
+ : deviceList;
+ }
+
+ // Check if already loading
+ if (this.loadingDevices[group.id]) {
+ return 'Loading devices...';
+ }
+
+ // Start loading
+ this.loadingDevices[group.id] = true;
+ this.data_provider.get_devgroup_members(group.id).then((devices) => {
+ this.deviceCache[group.id] = devices;
+ this.loadingDevices[group.id] = false;
+ }).catch(() => {
+ this.loadingDevices[group.id] = false;
+ });
+
+ return 'Loading devices...';
+ }
+
+ formatCreateTime(dateString: string): string {
+ if (!dateString || !this.tz) return dateString;
+ return formatInTimeZone(
+ dateString.split(".")[0] + ".000Z",
+ this.tz,
+ "yyyy-MM-dd HH:mm:ss XXX"
+ );
+ }
+
+ groupFirmwareAction(group: any, action: string): void {
+ this.selectedGroupForFirmware = group;
+ this.firmwareAction = action;
+ this.FirmwareConfirmModalVisible = true;
+ }
+
+ confirmGroupFirmwareAction(): void {
+ if (!this.selectedGroupForFirmware) return;
+
+ this.data_provider.group_firmware_action(this.selectedGroupForFirmware.id, this.firmwareAction)
+ .then((res) => {
+ if ("error" in res) {
+ console.error('Firmware action failed:', res.error);
+ } else {
+ const actionText = this.firmwareAction === 'update' ? 'Update' : 'Upgrade';
+ console.log(`${actionText} firmware initiated for group: ${this.selectedGroupForFirmware.name}`);
+ }
+ this.FirmwareConfirmModalVisible = false;
+ })
+ .catch((error) => {
+ console.error('Firmware action error:', error);
+ this.FirmwareConfirmModalVisible = false;
+ });
+ }
}
diff --git a/src/app/views/devices_group/devgroup.module.ts b/src/app/views/devices_group/devgroup.module.ts
index 0fe8c12..1dc3e06 100644
--- a/src/app/views/devices_group/devgroup.module.ts
+++ b/src/app/views/devices_group/devgroup.module.ts
@@ -2,6 +2,7 @@ import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import {
+ AlertModule,
ButtonGroupModule,
ButtonModule,
CardModule,
@@ -9,15 +10,19 @@ import {
GridModule,
CollapseModule,
ModalModule,
+ TooltipModule,
+ ListGroupModule,
} from "@coreui/angular";
import { DevicesGroupRoutingModule } from "./devgroup-routing.module";
import { DevicesGroupComponent } from "./devgroup.component";
import { GuiGridModule } from "@generic-ui/ngx-grid";
import { BadgeModule } from "@coreui/angular";
import { FormsModule } from "@angular/forms";
+import { MatMenuModule } from "@angular/material/menu";
@NgModule({
imports: [
DevicesGroupRoutingModule,
+ AlertModule,
CardModule,
CommonModule,
GridModule,
@@ -29,6 +34,9 @@ import { FormsModule } from "@angular/forms";
CollapseModule,
ModalModule,
BadgeModule,
+ TooltipModule,
+ MatMenuModule,
+ ListGroupModule,
],
declarations: [DevicesGroupComponent],
})