diff --git a/src/app/views/devices_group/devgroup.component.html b/src/app/views/devices_group/devgroup.component.html index fb48f5f..57f80a0 100644 --- a/src/app/views/devices_group/devgroup.component.html +++ b/src/app/views/devices_group/devgroup.component.html @@ -25,29 +25,67 @@ - All Devices + All Devices - 0 Members - {{value.length}} Members + + 0 + + + {{value.length}} + - - {{value}} + {{formatCreateTime(value)}} - - - - - + + + + {{value.length}} + + + + + + + + + +
+
  • Actions Menu
  • + + + + + +
    +
    @@ -59,92 +97,146 @@ - - -
    Group Edit
    + + +
    Edit Device Group
    - - -
    - - + + +
    +
    + +
    - - -
    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

    +
    +
    + + + +
    + + {{value}} +
    +
    +
    + + + {{value}} + + + + + + + +
    +
    +
    +
    - - + + - - -
    Members not in Group
    + + +
    Add Devices to Group
    - - -
    Members Availble to add:
    - - - -   {{value}} - - - - {{value}} - - - - - {{value}} - - - -
    -
    -
    + + +
    + + + {{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

    +
    +
    + + + +
    + + {{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
    +
    + + + +
    + + +
    +
    + {{user.username}} ({{user.first_name}} {{user.last_name}}) +
    +
    +
    + No users found +
    +
    +
    + +
    + + +
    +
    + {{perm.name}} +
    +
    +
    + No permissions found +
    +
    +
    + + + +
    +
    +
    + + +
    Current Permissions ({{selectedGroup?.assigned_users?.length || 0}} users)
    +
    + +
    + +

    No users have permissions for this group

    +
    +
    + + + + + + + + + + + + + + + + + + + +
    #UserNamePermissionActions
    {{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], })