feat: redesign device group management interface

- Complete UI/UX redesign of device groups table
- Enhanced member addition interface
- Add user management directly from device groups
- Add firmware group actions (update and upgrade)
- Improved bulk operations for device groups
This commit is contained in:
sepehr 2025-10-16 17:33:19 +03:00
parent 5822fb5d1d
commit 996c189076
4 changed files with 742 additions and 86 deletions

View file

@ -25,29 +25,67 @@
<ng-container *ngIf="item.id==1 ; then Default;else NotDefault">
</ng-container>
<ng-template #Default>
<c-badge color="info">All Devices</c-badge>
<c-badge color="info"><i class="fa-solid fa-network-wired me-1"></i>All Devices</c-badge>
</ng-template>
<ng-template #NotDefault>
<c-badge color="info" *ngIf="value[0]==null && item.id!=1">0 Members</c-badge>
<c-badge color="info" *ngIf="value[0]!=null">{{value.length}} Members</c-badge>
<c-badge color="info" *ngIf="value[0]==null && item.id!=1"
[cTooltip]="'No devices assigned'" style="cursor: help">
<i class="fa-solid fa-server me-1"></i>0
</c-badge>
<c-badge color="info" *ngIf="value[0]!=null"
[cTooltip]="getDevicesTooltip(item)"
[cTooltipPlacement]="'top'" style="cursor: help">
<i class="fa-solid fa-server me-1"></i>{{value.length}}
</c-badge>
</ng-template>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Create Time" field="created">
<ng-template let-value="item.created" let-item="item" let-index="index">
{{value}}
{{formatCreateTime(value)}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Actions" field="action">
<ng-template let-value="item.id" let-item="item" let-index="index">
<button [disabled]="value==1" cButton color="warning" size="sm" (click)="editAddGroup(item,'showedit');"
class="mx-1"><i class="fa-regular fa-pen-to-square"></i></button>
<button [disabled]="value==1" cButton color="info" size="sm" (click)="show_members(item.id);"
class="mx-1"><i class="fa-regular fa-eye"></i></button>
<button [disabled]="value==1" cButton color="danger" size="sm" (click)="show_delete_group(item);"
class="mx-1"><i class="fa-regular fa-trash-can"></i></button>
<gui-grid-column header="Users" field="assigned_users" width="80" align="CENTER">
<ng-template let-value="item.assigned_users" let-item="item" let-index="index">
<c-badge color="info"
[cTooltip]="getUsersTooltip(value)"
[cTooltipPlacement]="'top'"
style="cursor: help">
<i class="fa-solid fa-users me-1"></i>{{value.length}}
</c-badge>
</ng-template>
</gui-grid-column>
<gui-grid-column align="center" [cellEditing]="false" [sorting]="false" header="Action">
<ng-template let-value="value" let-item="item">
<button size="sm" shape="rounded-0" variant="outline" cButton color="primary" (click)="show_members(item.id)"
style="border: none;padding: 4px 7px;"><i class="fa-regular fa-eye"></i><small> View devices</small>
</button>
<button color="primary" shape="rounded-0" variant="ghost" style="padding: 4px 7px;"
[matMenuTriggerFor]="menu" cButton>
<i class="fa-solid fa-bars"></i>
</button>
<mat-menu #menu="matMenu">
<div cListGroup>
<li cListGroupItem [active]="false" color="dark">Actions Menu</li>
<button size="sm" (click)="editAddGroup(item,'showedit')" style="padding: 4px 7px;"
[disabled]="item.id==1" cListGroupItem><i class="fa-solid fa-pencil"></i><small>
Edit Group</small></button>
<button size="sm" (click)="manageUsers(item)" style="padding: 4px 7px;"
cListGroupItem><i class="fa-solid fa-users-gear"></i><small>
Manage Users</small></button>
<button size="sm" (click)="groupFirmwareAction(item, 'update')" style="padding: 4px 7px;"
cListGroupItem><i class="text-success fa-solid fa-upload"></i><small>
Update Firmware</small></button>
<button size="sm" (click)="groupFirmwareAction(item, 'upgrade')" style="padding: 4px 7px;"
cListGroupItem><i class="text-secondary fa-solid fa-microchip"></i><small>
Upgrade Firmware</small></button>
<button size="sm" (click)="show_delete_group(item)" style="padding: 4px 7px;"
[disabled]="item.id==1" cListGroupItem><i class="text-danger fa-solid fa-trash"></i><small>
Delete Group</small></button>
</div>
</mat-menu>
</ng-template>
</gui-grid-column>
</gui-grid>
@ -59,92 +97,146 @@
<c-modal #EditGroupModal backdrop="static" size="lg" [(visible)]="EditGroupModalVisible" id="EditGroupModal">
<c-modal-header>
<h5 cModalTitle> Group Edit</h5>
<c-modal #EditGroupModal backdrop="static" size="xl" [(visible)]="EditGroupModalVisible" id="EditGroupModal">
<c-modal-header class="bg-light">
<h5 cModalTitle><i class="fa-solid fa-edit me-2"></i>Edit Device Group</h5>
<button [cModalToggle]="EditGroupModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body>
<c-input-group class="mb-3">
<div [cFormFloating]="true" class="mb-3">
<input cFormControl id="floatingInput" placeholder="Group Name" [(ngModel)]="currentGroup['name']" />
<label cLabel for="floatingInput">Group Name</label>
<c-modal-body class="p-4">
<!-- Group Basic Info -->
<div class="bg-light p-3 rounded mb-3 d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center flex-grow-1 me-3">
<label class="form-label me-2 mb-0 fw-semibold">Group Name:</label>
<input cFormControl [(ngModel)]="currentGroup['name']" class="form-control-sm" style="max-width: 300px;" />
</div>
</c-input-group>
<c-input-group class="mb-3">
<h5>Group Members :</h5>
<c-badge color="info" class="fs-6 p-2">
<i class="fa-solid fa-server me-1"></i>{{groupMembers.length}} Devices
</c-badge>
</div>
<!-- Current Members -->
<c-card class="mb-3">
<c-card-header class="d-flex justify-content-between align-items-center">
<h6 class="mb-0"><i class="fa-solid fa-list me-2"></i>Current Group Members</h6>
<div>
<button *ngIf="MemberRows.length > 0" cButton color="danger" size="sm" variant="outline" class="me-2">
<i class="fa-solid fa-trash me-1"></i>Remove {{MemberRows.length}} Selected
</button>
<button cButton color="success" size="sm" (click)="show_new_member_form()">
<i class="fa-solid fa-plus me-1"></i>Add Devices
</button>
</div>
</c-card-header>
<c-card-body class="p-0">
<div *ngIf="groupMembers.length === 0" class="text-center p-4 text-muted">
<i class="fa-solid fa-server fa-3x mb-3 opacity-50"></i>
<h6>No devices in this group</h6>
<p class="mb-0">Click "Add Devices" to start adding devices to this group</p>
</div>
<div *ngIf="groupMembers.length > 0">
<gui-grid [autoResizeWidth]="true" [searching]="searching" [source]="groupMembers" [columnMenu]="columnMenu"
[sorting]="sorting" [infoPanel]="infoPanel" [rowSelection]="rowSelection"
(selectedRows)="onSelectedRowsMembers($event)" [autoResizeWidth]=true [paging]="paging">
<gui-grid-column header="Member Name" field="name">
(selectedRows)="onSelectedRowsMembers($event)" [paging]="paging">
<gui-grid-column header="Device Name" field="name">
<ng-template let-value="item.name" let-item="item" let-index="index">
&nbsp; {{value}} </ng-template>
</gui-grid-column>
<gui-grid-column header="perm Name" field="ip">
<ng-template let-value="item.ip" let-item="item" let-index="index">
{{value}}
<div class="d-flex align-items-center">
<i class="fa-solid fa-server me-2 text-primary"></i>
<strong>{{value}}</strong>
</div>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Actions" width="120" field="action">
<gui-grid-column header="IP Address" field="ip">
<ng-template let-value="item.ip" let-item="item" let-index="index">
<c-badge color="secondary">{{value}}</c-badge>
</ng-template>
</gui-grid-column>
<gui-grid-column header="Actions" width="100" field="action" align="CENTER">
<ng-template let-value="item.id" let-item="item" let-index="index">
<button cButton color="danger" size="sm" (click)="remove_from_group(item.id)"><i
class="fa-regular fa-trash-can"></i></button>
<button cButton color="danger" size="sm" variant="outline" (click)="remove_from_group(item.id)" title="Remove from group">
<i class="fa-solid fa-times"></i>
</button>
</ng-template>
</gui-grid-column>
</gui-grid>
<br />
<button *ngIf="MemberRows.length!= 0" style="margin: 10px 0;" cButton color="danger" size="sm"><i
class="fa-regular fa-trash-can"></i>Delete {{MemberRows.length}} Selected</button>
</c-input-group>
<hr />
<button cButton color="primary" (click)="show_new_member_form()">+ Add new Members</button>
</div>
</c-card-body>
</c-card>
</c-modal-body>
<c-modal-footer>
<button cButton color="primary" (click)="save_group()">save</button>
<c-modal-footer class="bg-light">
<button cButton color="primary" (click)="save_group()">
<i class="fa-solid fa-save me-1"></i>Save Changes
</button>
<button [cModalToggle]="EditGroupModal.id" cButton color="secondary">
Close
<i class="fa-solid fa-times me-1"></i>Cancel
</button>
</c-modal-footer>
</c-modal>
<c-modal #NewMemberModal backdrop="static" size="lg" [(visible)]="NewMemberModalVisible" id="NewMemberModal">
<c-modal-header>
<h5 cModalTitle>Members not in Group</h5>
<c-modal #NewMemberModal [backdrop]="true" size="xl" [(visible)]="NewMemberModalVisible" id="NewMemberModal" style="z-index: 1060; backdrop-filter: blur(2px);">
<c-modal-header class="bg-success text-white">
<h5 cModalTitle><i class="fa-solid fa-plus-circle me-2"></i>Add Devices to Group</h5>
<button (click)="NewMemberModalVisible=!NewMemberModalVisible" cButtonClose></button>
</c-modal-header>
<c-modal-body>
<c-input-group class="mb-3">
<h5>Members Availble to add:</h5>
<c-modal-body class="p-4">
<!-- Selection Summary -->
<div class="mb-3" style="min-height: 58px;">
<c-alert *ngIf="NewMemberRows.length > 0" color="info" class="d-flex align-items-center mb-0">
<i class="fa-solid fa-info-circle me-2"></i>
<span><strong>{{NewMemberRows.length}}</strong> device(s) selected for addition</span>
</c-alert>
</div>
<!-- Available Devices -->
<c-card>
<c-card-header>
<h6 class="mb-0"><i class="fa-solid fa-server me-2"></i>Available Devices ({{availbleMembers.length}} total)</h6>
</c-card-header>
<c-card-body class="p-0">
<div *ngIf="availbleMembers.length === 0" class="text-center p-4 text-muted">
<i class="fa-solid fa-check-circle fa-3x mb-3 text-success opacity-50"></i>
<h6>All devices are already in groups</h6>
<p class="mb-0">No available devices to add to this group</p>
</div>
<div *ngIf="availbleMembers.length > 0">
<gui-grid [autoResizeWidth]="true" *ngIf="NewMemberModalVisible" [searching]="searching"
[source]="availbleMembers" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel"
[rowSelection]="rowSelection" (selectedRows)="onSelectedRowsNewMembers($event)" [autoResizeWidth]=true
[paging]="paging">
<gui-grid-column header="Group Name" field="name">
[rowSelection]="rowSelection" (selectedRows)="onSelectedRowsNewMembers($event)" [paging]="paging">
<gui-grid-column header="Device Name" field="name">
<ng-template let-value="item.name" let-item="item" let-index="index">
&nbsp; {{value}} </ng-template>
</gui-grid-column>
<gui-grid-column header="perm Name" field="ip">
<ng-template let-value="item.ip" let-item="item" let-index="index">
{{value}}
<div class="d-flex align-items-center">
<i class="fa-solid fa-server me-2 text-primary"></i>
<strong>{{value}}</strong>
</div>
</ng-template>
</gui-grid-column>
<gui-grid-column header="perm Name" field="mac">
<gui-grid-column header="IP Address" field="ip">
<ng-template let-value="item.ip" let-item="item" let-index="index">
<c-badge color="secondary">{{value}}</c-badge>
</ng-template>
</gui-grid-column>
<gui-grid-column header="MAC Address" field="mac">
<ng-template let-value="item.mac" let-item="item" let-index="index">
{{value}}
<small class="text-muted">{{value}}</small>
</ng-template>
</gui-grid-column>
</gui-grid>
<br />
</c-input-group>
<hr />
</div>
</c-card-body>
</c-card>
</c-modal-body>
<c-modal-footer>
<button *ngIf="NewMemberRows.length!= 0" (click)="add_new_members()" cButton color="primary">Add {{
NewMemberRows.length }}</button>
<button (click)="NewMemberModalVisible=!NewMemberModalVisible" cButton color="secondary">
Close
<c-modal-footer class="bg-light d-flex justify-content-between">
<div>
<small class="text-muted">Select devices from the list above to add them to the group</small>
</div>
<div>
<button *ngIf="NewMemberRows.length > 0" (click)="add_new_members()" cButton color="success">
<i class="fa-solid fa-plus me-1"></i>Add {{NewMemberRows.length}} Device(s)
</button>
<button (click)="NewMemberModalVisible=!NewMemberModalVisible" cButton color="secondary" class="ms-2">
<i class="fa-solid fa-times me-1"></i>Cancel
</button>
</div>
</c-modal-footer>
</c-modal>
@ -176,3 +268,199 @@
</button>
</c-modal-footer>
</c-modal>
<c-modal #UserManagementModal backdrop="static" size="xl" [(visible)]="UserManagementModalVisible" id="UserManagementModal">
<c-modal-header>
<h5 cModalTitle><i class="fa-solid fa-users-gear me-2"></i>User Permissions - {{selectedGroup?.name}}</h5>
<button [cModalToggle]="UserManagementModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body>
<c-card class="mb-3">
<c-card-header class="bg-light">
<h6 class="mb-0"><i class="fa-solid fa-user-plus me-2"></i>Add User Permission</h6>
</c-card-header>
<c-card-body>
<c-row class="g-3">
<c-col md="4">
<div class="search-select-wrapper">
<label class="search-label">User</label>
<input cFormControl
[(ngModel)]="userSearch"
(input)="filterUsers($event)"
(focus)="showUserDropdown = true"
(blur)="hideUserDropdown()"
placeholder="Search and select user..."
class="search-input compact-select"
autocomplete="off" />
<div *ngIf="showUserDropdown && filteredUsers.length > 0" class="search-dropdown">
<div *ngFor="let user of filteredUsers"
class="search-option"
(mousedown)="selectUser(user)">
{{user.username}} ({{user.first_name}} {{user.last_name}})
</div>
</div>
<div *ngIf="showUserDropdown && filteredUsers.length === 0 && userSearch" class="search-no-results">
No users found
</div>
</div>
</c-col>
<c-col md="4">
<div class="search-select-wrapper">
<label class="search-label">Permission</label>
<input cFormControl
[(ngModel)]="permissionSearch"
(input)="filterPermissions($event)"
(focus)="showPermissionDropdown = true"
(blur)="hidePermissionDropdown()"
placeholder="Search and select permission..."
class="search-input compact-select"
autocomplete="off" />
<div *ngIf="showPermissionDropdown && filteredPermissions.length > 0" class="search-dropdown">
<div *ngFor="let perm of filteredPermissions"
class="search-option"
(mousedown)="selectPermission(perm)">
{{perm.name}}
</div>
</div>
<div *ngIf="showPermissionDropdown && filteredPermissions.length === 0 && permissionSearch" class="search-no-results">
No permissions found
</div>
</div>
</c-col>
<c-col md="4" class="d-flex align-items-end">
<button cButton color="success" (click)="addUserPermission()" [disabled]="!selectedUser || !selectedPermission">
<i class="fa-solid fa-plus me-1"></i>Add Permission
</button>
</c-col>
</c-row>
</c-card-body>
</c-card>
<c-card>
<c-card-header class="bg-light">
<h6 class="mb-0"><i class="fa-solid fa-list-check me-2"></i>Current Permissions ({{selectedGroup?.assigned_users?.length || 0}} users)</h6>
</c-card-header>
<c-card-body class="p-0">
<div *ngIf="selectedGroup?.assigned_users?.length === 0" class="text-center p-4 text-muted">
<i class="fa-solid fa-users-slash fa-2x mb-2"></i>
<p class="mb-0">No users have permissions for this group</p>
</div>
<div *ngIf="selectedGroup?.assigned_users?.length > 0" class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th width="40">#</th>
<th>User</th>
<th>Name</th>
<th>Permission</th>
<th width="200">Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let user of selectedGroup.assigned_users; let i = index">
<td class="text-muted">{{i + 1}}</td>
<td>
<div class="d-flex align-items-center">
<i class="fa-solid fa-user me-2 text-primary"></i>
<strong>{{user.username}}</strong>
</div>
</td>
<td>{{user.first_name}} {{user.last_name}}</td>
<td>
<c-badge [color]="getPermissionColor(user.perm_name)">{{user.perm_name}}</c-badge>
</td>
<td>
<div class="btn-group" role="group">
<button cButton color="warning" size="sm" variant="outline"
(click)="editUserPermission(user)" title="Change Permission"
[disabled]="selectedGroup.id === 1 && user.username === 'mikrowizard'">
<i class="fa-solid fa-edit"></i>
</button>
<button cButton color="danger" size="sm" variant="outline"
(click)="removeUserPermission(user)" title="Remove Permission"
[disabled]="selectedGroup.id === 1 && user.username === 'mikrowizard'">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</c-card-body>
</c-card>
</c-modal-body>
<c-modal-footer>
<button [cModalToggle]="UserManagementModal.id" cButton color="secondary">
<i class="fa-solid fa-times me-1"></i>Close
</button>
</c-modal-footer>
</c-modal>
<c-modal #EditPermissionModal backdrop="static" [(visible)]="EditPermissionModalVisible" id="EditPermissionModal">
<c-modal-header>
<h5 cModalTitle>Change Permission for {{editingUser?.username}}</h5>
<button [cModalToggle]="EditPermissionModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body>
<div class="mb-3">
<label class="form-label">Current Permission: <c-badge [color]="getPermissionColor(editingUser?.perm_name)">{{editingUser?.perm_name}}</c-badge></label>
</div>
<div class="mb-3">
<label class="form-label">New Permission</label>
<select cSelect [(ngModel)]="newPermissionId" class="form-select">
<option value="">Select Permission...</option>
<option *ngFor="let perm of availablePermissions" [value]="perm.id">{{perm.name}}</option>
</select>
</div>
</c-modal-body>
<c-modal-footer>
<button cButton color="primary" (click)="updateUserPermission()" [disabled]="!newPermissionId">
<i class="fa-solid fa-save me-1"></i>Update
</button>
<button [cModalToggle]="EditPermissionModal.id" cButton color="secondary">
Cancel
</button>
</c-modal-footer>
</c-modal>
<c-modal #RemovePermissionModal backdrop="static" [(visible)]="RemovePermissionModalVisible" id="RemovePermissionModal">
<c-modal-header>
<h5 cModalTitle>Remove Permission</h5>
<button [cModalToggle]="RemovePermissionModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body>
<div class="text-center">
<i class="fa-solid fa-exclamation-triangle fa-3x text-warning mb-3"></i>
<p>Are you sure you want to remove <strong>{{removingUser?.username}}</strong>'s permission from group <strong>{{selectedGroup?.name}}</strong>?</p>
<p class="text-muted small">This action cannot be undone.</p>
</div>
</c-modal-body>
<c-modal-footer>
<button cButton color="danger" (click)="confirmRemovePermission()">
<i class="fa-solid fa-trash me-1"></i>Remove
</button>
<button [cModalToggle]="RemovePermissionModal.id" cButton color="secondary">
Cancel
</button>
</c-modal-footer>
</c-modal>
<c-modal #FirmwareConfirmModal backdrop="static" [(visible)]="FirmwareConfirmModalVisible" id="FirmwareConfirmModal">
<c-modal-header>
<h5 cModalTitle>Confirm Firmware {{firmwareAction | titlecase}}</h5>
<button [cModalToggle]="FirmwareConfirmModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body>
<div class="text-center">
<i class="fa-solid fa-exclamation-triangle fa-3x text-warning mb-3"></i>
<p>Are you sure you want to <strong>{{firmwareAction}}</strong> firmware for all devices in group <strong>{{selectedGroupForFirmware?.name}}</strong>?</p>
<p class="text-muted small">This action will affect all devices in this group and may take some time to complete.</p>
</div>
</c-modal-body>
<c-modal-footer>
<button cButton color="primary" (click)="confirmGroupFirmwareAction()">
<i class="fa-solid fa-{{firmwareAction === 'update' ? 'upload' : 'microchip'}} me-1"></i>{{firmwareAction | titlecase}} Firmware
</button>
<button [cModalToggle]="FirmwareConfirmModal.id" cButton color="secondary">
Cancel
</button>
</c-modal-footer>
</c-modal>

View file

@ -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);
}

View file

@ -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;
});
}
}

View file

@ -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],
})