mirror of
https://github.com/MikroWizard/mikrofront.git
synced 2025-12-06 01:59:29 +00:00
feat: major device management enhancements
- Add direct web access button for device WebFig - Add web proxy-based access with auto-login via MikroWizard - Fix updatable/upgradable filters and add more filter options - Redesign devices table UI with improved UX - Add bulk scan feature with CSV import capability - Add parallel scanning functionality - Add upgrade firmware feature - Add scan history button with enhanced report viewing
This commit is contained in:
parent
10d4cff4a4
commit
5822fb5d1d
3 changed files with 589 additions and 52 deletions
|
|
@ -8,19 +8,25 @@
|
|||
</c-col>
|
||||
<c-col xs [lg]="9">
|
||||
<h6 style="text-align: right;">
|
||||
<button cButton color="danger" class="mx-1" size="sm" style="color: #fff;">{{updates.length}} Updatable
|
||||
</button>
|
||||
<button cButton color="warning" class="mx-1" size="sm" style="color: #fff;">{{upgrades.length}}
|
||||
Upgradable</button>
|
||||
|
|
||||
<button cButton color="success" (click)="openAddDeviceModal()" class="mx-1"
|
||||
size="sm" style="color: #fff;"><i class="fa-solid fa-plus"></i> Bulk Add </button>
|
||||
<button cButton color="dark" (click)="scanwizard(1,'')" [cModalToggle]="ScannerModal.id" class="mx-1"
|
||||
size="sm" style="color: #fff;"><i class="fa-solid fa-magnifying-glass"></i> Scanner</button>
|
||||
size="sm" style="color: #fff;"><i class="fa-solid fa-magnifying-glass"></i> Scan</button>
|
||||
<button cButton color="primary" (click)="show_exec()" class="mx-1"
|
||||
size="sm" style="color: #fff;"><i class="fa-solid fa-history"></i> History</button>
|
||||
</h6>
|
||||
</c-col>
|
||||
</c-row>
|
||||
</c-card-header>
|
||||
<c-card-body>
|
||||
<c-row>
|
||||
<c-col [lg]="9">
|
||||
<button cButton color="danger" class="mx-1" size="sm" style="color: #fff;" (click)="filterUpdatable()">{{updates.length}} Updatable
|
||||
</button>
|
||||
<button cButton color="warning" class="mx-1" size="sm" style="color: #fff;" (click)="filterUpgradable()">{{upgrades.length}}
|
||||
Upgradable</button>
|
||||
<button cButton color="secondary" class="mx-1" size="sm" (click)="clearFilter()">Clear Filter</button>
|
||||
</c-col>
|
||||
<c-col [lg]="3">
|
||||
<c-input-group *ngIf="groups.length>0">
|
||||
<span cInputGroupText>Group</span>
|
||||
|
|
@ -37,6 +43,10 @@
|
|||
(selectedRows)="onSelectedRows($event)" [autoResizeWidth]=true>
|
||||
<gui-grid-column header="Name" field="name">
|
||||
<ng-template let-value="item.name" let-item="item" let-index="index">
|
||||
<button cButton size="sm" variant="ghost" color="primary" (click)="webAccess(item)"
|
||||
style="padding: 2px 4px; margin-right: 5px; border: none;" cTooltip="Web Access">
|
||||
<i class="fas fa-globe" style="font-size: 12px;"></i>
|
||||
</button>
|
||||
<img *ngIf="item.status=='updating'" width="20px" src="assets/img/loading.svg" />
|
||||
<i *ngIf="item.status=='updated'" cTooltip="Tooltip text"
|
||||
style="color: green; margin-right: 3px;font-size: .7em;" class="fa-solid fa-check"></i>
|
||||
|
|
@ -55,7 +65,7 @@
|
|||
<div>{{value}}</div>
|
||||
<i *ngIf="item.update_availble" cTooltip="Firmware Update availble"
|
||||
class="fa-solid fa-up-long text-primary mx-1"></i>
|
||||
<i *ngIf="item.update_availble" cTooltip="Device Firmware not Upgraded"
|
||||
<i *ngIf="item.upgrade_availble" cTooltip="Device Firmware not Upgraded"
|
||||
class="fa-solid fa-microchip text-danger mx-1"></i>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
|
|
@ -115,9 +125,9 @@
|
|||
<button size="sm" (click)="single_device_action(item,'update')" style="padding: 4px 7px;"
|
||||
cListGroupItem><i class="text-primary fa-solid fa-upload"></i><small>
|
||||
Update Firmware</small></button>
|
||||
<!-- <button size="sm" (click)="single_device_action(item,'upgrade')" style="padding: 4px 7px;"
|
||||
<button size="sm" (click)="single_device_action(item,'upgrade')" style="padding: 4px 7px;"
|
||||
cListGroupItem><i class="text-primary fa-solid fa-microchip"></i><small>
|
||||
Upgrade Firmware</small></button> -->
|
||||
Upgrade Firmware</small></button>
|
||||
<button size="sm" (click)="single_device_action(item,'devlogs')" style="padding: 4px 7px;"
|
||||
cListGroupItem><i class="fa-regular fa-rectangle-list"></i><small>
|
||||
Device Logs</small></button>
|
||||
|
|
@ -154,8 +164,9 @@
|
|||
Firmware</button></li>
|
||||
<li><button cDropdownItem
|
||||
(click)="ConfirmAction='update';ConfirmModalVisible=true">Update</button></li>
|
||||
<!-- <li><button cDropdownItem>Upgrade</button></li>
|
||||
<li><button cDropdownItem>Update and Upgrade</button></li> -->
|
||||
<li><button cDropdownItem
|
||||
(click)="ConfirmAction='upgrade';ConfirmModalVisible=true">Upgrade</button></li>
|
||||
<!-- <li><button cDropdownItem>Update and Upgrade</button></li> -->
|
||||
</ul>
|
||||
</c-dropdown>
|
||||
</c-nav-item>
|
||||
|
|
@ -259,8 +270,6 @@
|
|||
</div>
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<h6 style="margin: 0 auto;" *ngIf="scanwizard_step==1"><button cButton color="primary" (click)="show_exec()"
|
||||
style="margin: 0 auto;" variant="outline">Device scan logs</button></h6>
|
||||
<small *ngIf="scan_type=='ip'">Empty username and password means system default
|
||||
configuration</small>
|
||||
</c-modal-footer>
|
||||
|
|
@ -269,39 +278,68 @@
|
|||
|
||||
<c-modal #ExecutedDataModal backdrop="static" size="xl" [(visible)]="ExecutedDataModalVisible" id="ExecutedDataModal">
|
||||
<c-modal-header>
|
||||
<h5 cModalTitle>Scan History : </h5>
|
||||
<h5 cModalTitle>Task History</h5>
|
||||
<button (click)="ExecutedDataModalVisible=!ExecutedDataModalVisible" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
<c-input-group class="mb-3">
|
||||
<c-modal-body style="min-height: 400px; max-height: 70vh; overflow-y: auto;">
|
||||
<div class="mb-3">
|
||||
<c-input-group>
|
||||
<span cInputGroupText>Filter by Type</span>
|
||||
<select [(ngModel)]="selectedTaskType" (change)="filterByTaskType()" cSelect>
|
||||
<option value="all">All Tasks</option>
|
||||
<option value="ip-scan">IP Scan</option>
|
||||
<option value="bulk-add">Bulk Add</option>
|
||||
</select>
|
||||
</c-input-group>
|
||||
</div>
|
||||
<gui-grid [autoResizeWidth]="true" *ngIf="ExecutedDataModalVisible" [searching]="searching"
|
||||
[source]="ExecutedData" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel" [paging]="paging">
|
||||
<gui-grid-column header="Start time" field="start">
|
||||
<ng-template let-value="item['started']" let-item="item" let-index="index">
|
||||
{{value}} </ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Start ip" field="start_ip">
|
||||
<ng-template let-value="item['start_ip']" let-item="item" let-index="index">
|
||||
{{value}} </ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="End ip" field="end_ip">
|
||||
<ng-template let-value="item['end_ip']" let-item="item" let-index="index">
|
||||
{{value}} </ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="End time" field="end">
|
||||
<ng-template let-value="item['ended']" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
[source]="filteredExecutedData" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel" [paging]="paging">
|
||||
<gui-grid-column header="Type" field="task_type" [width]="100">
|
||||
<ng-template let-value="item['task_type']" let-item="item" let-index="index">
|
||||
<i *ngIf="item.task_type === 'ip-scan'" class="fas fa-search" style="margin-right: 3px; color: #0d6efd;"></i>
|
||||
<i *ngIf="item.task_type === 'bulk-add'" class="fas fa-plus-circle" style="margin-right: 3px; color: #198754;"></i>
|
||||
<span style="font-size: 11px;">{{getTaskTypeLabel(value)}}</span>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Logs" field="mac" align="center">
|
||||
<ng-template let-value="item['result']" let-item="item" let-index="index">
|
||||
<button (click)="exportToCsv(value)" color="primary" cButton>download</button>
|
||||
<gui-grid-column header="Time" field="time" [width]="220">
|
||||
<ng-template let-value="value" let-item="item" let-index="index">
|
||||
<div style="font-size: 10px; line-height: 1.3;">
|
||||
<div><strong>Start:</strong> {{item.started}}</div>
|
||||
<div><strong>End:</strong> {{item.ended}}</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Details" field="details">
|
||||
<ng-template let-value="value" let-item="item" let-index="index">
|
||||
<div *ngIf="item.task_type === 'ip-scan'" style="font-size: 12px; line-height: 1.3;">
|
||||
<div>{{item.start_ip}} - {{item.end_ip}}</div>
|
||||
<div>User: {{item.username}}</div>
|
||||
</div>
|
||||
<div *ngIf="item.task_type === 'bulk-add'" style="font-size: 12px; line-height: 1.3;">
|
||||
<div>{{item.device_count}} devices</div>
|
||||
<div style="word-break: break-all;">{{item.task_id}}</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Results" field="results" align="center" [width]="70">
|
||||
<ng-template let-value="value" let-item="item" let-index="index">
|
||||
<div style="font-size: 11px;">
|
||||
<span style="color: green;">✓ {{item.success_count}}</span>
|
||||
<span *ngIf="item.failed_count > 0" style="color: red; margin-left: 5px;">✗ {{item.failed_count}}</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Actions" field="actions" align="center" [width]="160">
|
||||
<ng-template let-value="value" let-item="item" let-index="index">
|
||||
<button (click)="showTaskDetails(item)" color="info" size="sm" cButton class="me-1">
|
||||
<i class="fas fa-eye"></i> Details
|
||||
</button>
|
||||
<button (click)="exportToCsv(item.result)" color="primary" size="sm" cButton>
|
||||
<i class="fas fa-download"></i> CSV
|
||||
</button>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
<br />
|
||||
</c-input-group>
|
||||
<hr />
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button (click)="ExecutedDataModalVisible=!ExecutedDataModalVisible" cButton color="secondary">
|
||||
|
|
@ -320,6 +358,10 @@
|
|||
update?</span>
|
||||
<span *ngIf="ConfirmAction=='update'">Are you sure that You want to <code>update firmware</code> of selected
|
||||
devices?</span>
|
||||
<span *ngIf="ConfirmAction=='upgrade'">Are you sure that You want to <code>upgrade firmware</code> of selected
|
||||
devices?</span>
|
||||
<span *ngIf="ConfirmAction=='reboot'">Are you sure that You want to <code>reboot</code> the selected
|
||||
devices?</span>
|
||||
<ng-container *ngIf="ConfirmAction=='delete'">
|
||||
Are you sure that You want to<code>Delete Device {{selected_device.name}} ?</code><br />
|
||||
<hr>
|
||||
|
|
@ -338,6 +380,12 @@
|
|||
<button *ngIf="ConfirmAction=='update'" (click)="update_firmware()" cButton color="danger">
|
||||
Yes
|
||||
</button>
|
||||
<button *ngIf="ConfirmAction=='upgrade'" (click)="upgrade_firmware()" cButton color="danger">
|
||||
Yes
|
||||
</button>
|
||||
<button *ngIf="ConfirmAction=='reboot'" (click)="upgrade_firmware()" cButton color="danger">
|
||||
Yes
|
||||
</button>
|
||||
<button *ngIf="ConfirmAction=='delete'" (click)="delete_device()" cButton color="danger">
|
||||
Yes,Delete Device
|
||||
</button>
|
||||
|
|
@ -396,4 +444,213 @@
|
|||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
<!-- Add Device Modal -->
|
||||
<c-modal #AddDeviceModal backdrop="static" size="lg" [(visible)]="addDeviceModalVisible" id="AddDeviceModal">
|
||||
<c-modal-header>
|
||||
<h5 cModalTitle>Add Devices from CSV</h5>
|
||||
<button cButtonClose (click)="closeAddDeviceModal()"></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
<div *ngIf="addDeviceStep === 1">
|
||||
<h6>Upload CSV File</h6>
|
||||
<p>Please upload a CSV file containing device information with columns: IP Address, Username, Password, API Port</p>
|
||||
<input type="file" accept=".csv" (change)="onFileSelected($event)" class="form-control mb-3">
|
||||
<div *ngIf="csvPreview.length > 0">
|
||||
<h6>Preview (First 3 rows):</h6>
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th *ngFor="let header of csvHeaders">{{header}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let row of csvPreview">
|
||||
<td *ngFor="let cell of row">{{cell}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h6>Column Mapping:</h6>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label>IP Address Column:</label>
|
||||
<select [(ngModel)]="columnMapping.ip" class="form-select">
|
||||
<option value="">Select Column</option>
|
||||
<option *ngFor="let header of csvHeaders; let i = index" [value]="i">{{header}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label>Username Column:</label>
|
||||
<select [(ngModel)]="columnMapping.username" class="form-select">
|
||||
<option value="">Select Column</option>
|
||||
<option *ngFor="let header of csvHeaders; let i = index" [value]="i">{{header}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-2">
|
||||
<div class="col-md-6">
|
||||
<label>Password Column:</label>
|
||||
<select [(ngModel)]="columnMapping.password" class="form-select">
|
||||
<option value="">Select Column</option>
|
||||
<option *ngFor="let header of csvHeaders; let i = index" [value]="i">{{header}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label>API Port Column:</label>
|
||||
<select [(ngModel)]="columnMapping.port" class="form-select">
|
||||
<option value="">Select Column</option>
|
||||
<option *ngFor="let header of csvHeaders; let i = index" [value]="i">{{header}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="addDeviceStep === 2" class="text-center">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<h5 class="mt-3">{{uploadStatus}}</h5>
|
||||
</div>
|
||||
<div *ngIf="addDeviceStep === 3">
|
||||
<h5>Upload Complete</h5>
|
||||
<div class="alert alert-success">
|
||||
<strong>Success:</strong> {{uploadResult.success}} devices added successfully<br>
|
||||
<strong>Failed:</strong> {{uploadResult.failed}} devices failed to add
|
||||
</div>
|
||||
<button cButton color="primary" (click)="downloadResults()" *ngIf="uploadResult.resultFile">
|
||||
<i class="fas fa-download"></i> Download Results
|
||||
</button>
|
||||
</div>
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button *ngIf="addDeviceStep === 1" cButton color="primary" (click)="uploadDevices()" [disabled]="!isValidMapping()">
|
||||
Add Devices
|
||||
</button>
|
||||
<button *ngIf="addDeviceStep === 3" cButton color="success" (click)="closeAddDeviceModal()">
|
||||
Close
|
||||
</button>
|
||||
<button *ngIf="addDeviceStep !== 2" cButton color="secondary" (click)="closeAddDeviceModal()">
|
||||
Cancel
|
||||
</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
<!-- Web Access Modal -->
|
||||
<c-modal #WebAccessModal backdrop="static" [(visible)]="showWebAccessModal" id="WebAccessModal">
|
||||
<c-modal-header>
|
||||
<h6 cModalTitle>Web Access Options</h6>
|
||||
<button cButtonClose (click)="closeWebAccessModal()"></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
<p>Choose how to access the device:</p>
|
||||
<div class="d-grid gap-2">
|
||||
<button *ngIf="ispro" cButton color="primary" (click)="openProxyAccess()">
|
||||
<i class="fas fa-shield-alt"></i> Proxy Access, through Mikrowizard server
|
||||
</button>
|
||||
<button cButton color="secondary" (click)="openDirectAccess()">
|
||||
<i class="fas fa-external-link-alt"></i> Direct Access
|
||||
</button>
|
||||
</div>
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button cButton color="secondary" (click)="closeWebAccessModal()">Cancel</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
<!-- Task Details Modal -->
|
||||
<c-modal #TaskDetailsModal backdrop="static" size="xl" [(visible)]="detailsModalVisible" id="TaskDetailsModal"
|
||||
style="z-index: 1060; backdrop-filter: blur(2px);">
|
||||
<c-modal-header style="background: rgba(255,255,255,0.95); backdrop-filter: blur(10px);">
|
||||
<h5 cModalTitle>{{getTaskTypeLabel(selectedTaskDetails?.task_type)}} Results</h5>
|
||||
<button (click)="closeDetailsModal()" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body *ngIf="selectedTaskDetails" style="background: rgba(255,255,255,0.98); backdrop-filter: blur(10px); max-height: 70vh; overflow-y: auto;">
|
||||
<div class="mb-3">
|
||||
<c-row>
|
||||
<c-col md="6">
|
||||
<strong>Task Type:</strong> {{getTaskTypeLabel(selectedTaskDetails.task_type)}}<br>
|
||||
<strong>Started:</strong> {{selectedTaskDetails.started}}<br>
|
||||
<strong>Completed:</strong> {{selectedTaskDetails.ended}}
|
||||
</c-col>
|
||||
<c-col md="6">
|
||||
<div *ngIf="selectedTaskDetails.task_type === 'ip-scan'">
|
||||
<strong>IP Range:</strong> {{selectedTaskDetails.start_ip}} - {{selectedTaskDetails.end_ip}}<br>
|
||||
<strong>Username:</strong> {{selectedTaskDetails.username}}
|
||||
</div>
|
||||
<div *ngIf="selectedTaskDetails.task_type === 'bulk-add'">
|
||||
<strong>Task ID:</strong> {{selectedTaskDetails.task_id}}<br>
|
||||
<strong>Total Devices:</strong> {{selectedTaskDetails.device_count}}
|
||||
</div>
|
||||
</c-col>
|
||||
</c-row>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="mb-0">Detailed Results:</h6>
|
||||
<div class="col-md-4">
|
||||
<input type="text" class="form-control form-control-sm" placeholder="Search IP or error..."
|
||||
[(ngModel)]="detailsSearchTerm" (input)="onDetailsSearch()">
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-striped table-hover table-responsive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>IP Address</th>
|
||||
<th>Status</th>
|
||||
<th *ngIf="selectedTaskDetails.task_type === 'bulk-add'">Error Details</th>
|
||||
<th *ngIf="selectedTaskDetails.task_type === 'ip-scan'">Error Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let result of detailsPaginatedResults">
|
||||
<td>{{result.ip}}</td>
|
||||
<td>
|
||||
<c-badge [color]="result.added ? 'success' : 'danger'">
|
||||
{{result.added ? 'Success' : 'Failed'}}
|
||||
</c-badge>
|
||||
</td>
|
||||
<td *ngIf="selectedTaskDetails.task_type === 'bulk-add'">
|
||||
{{result.failures || 'N/A'}}
|
||||
</td>
|
||||
<td *ngIf="selectedTaskDetails.task_type === 'ip-scan'">
|
||||
{{result.faileres || 'N/A'}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<nav *ngIf="getTotalDetailsPages() > 1" class="mt-3">
|
||||
<ul class="pagination pagination-sm justify-content-center">
|
||||
<li class="page-item" [class.disabled]="detailsCurrentPage === 1">
|
||||
<button class="page-link" (click)="onDetailsPageChange(detailsCurrentPage - 1)" [disabled]="detailsCurrentPage === 1">
|
||||
Previous
|
||||
</button>
|
||||
</li>
|
||||
<li class="page-item" *ngFor="let page of [].constructor(getTotalDetailsPages()); let i = index"
|
||||
[class.active]="detailsCurrentPage === i + 1">
|
||||
<button class="page-link" (click)="onDetailsPageChange(i + 1)">
|
||||
{{i + 1}}
|
||||
</button>
|
||||
</li>
|
||||
<li class="page-item" [class.disabled]="detailsCurrentPage === getTotalDetailsPages()">
|
||||
<button class="page-link" (click)="onDetailsPageChange(detailsCurrentPage + 1)"
|
||||
[disabled]="detailsCurrentPage === getTotalDetailsPages()">
|
||||
Next
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="mt-3">
|
||||
<c-badge color="success" class="me-2">Success: {{selectedTaskDetails.success_count}}</c-badge>
|
||||
<c-badge color="danger">Failed: {{selectedTaskDetails.failed_count}}</c-badge>
|
||||
</div>
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button (click)="exportToCsv(selectedTaskDetails.result)" color="primary" cButton class="me-2">
|
||||
<i class="fas fa-download"></i> Download CSV
|
||||
</button>
|
||||
<button (click)="closeDetailsModal()" cButton color="secondary">
|
||||
Close
|
||||
</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
<c-toaster position="fixed" placement="top-end"></c-toaster>
|
||||
|
|
@ -72,6 +72,7 @@ export class DevicesComponent implements OnInit, OnDestroy {
|
|||
@ViewChild("grid", { static: true }) gridComponent: GuiGridComponent;
|
||||
@ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>;
|
||||
public source: Array<any> = [];
|
||||
public originalSource: Array<any> = [];
|
||||
public columns: Array<GuiColumn> = [];
|
||||
public loading: boolean = true;
|
||||
public rows: any = [];
|
||||
|
|
@ -94,6 +95,28 @@ export class DevicesComponent implements OnInit, OnDestroy {
|
|||
public show_pass: boolean = false;
|
||||
public ExecutedDataModalVisible: boolean = false;
|
||||
public ExecutedData: any = [];
|
||||
public filteredExecutedData: any = [];
|
||||
public selectedTaskType: string = 'all';
|
||||
public detailsModalVisible: boolean = false;
|
||||
public selectedTaskDetails: any = null;
|
||||
public detailsCurrentPage: number = 1;
|
||||
public detailsPageSize: number = 10;
|
||||
public detailsPaginatedResults: any[] = [];
|
||||
public detailsSearchTerm: string = '';
|
||||
public filteredDetailsResults: any[] = [];
|
||||
public showWebAccessModal: boolean = false;
|
||||
public currentDeviceInfo: any = null;
|
||||
public addDeviceModalVisible: boolean = false;
|
||||
public addDeviceStep: number = 1;
|
||||
public csvFile: File | null = null;
|
||||
public csvData: any[] = [];
|
||||
public csvHeaders: string[] = [];
|
||||
public csvPreview: any[] = [];
|
||||
public columnMapping = { ip: '', username: '', password: '', port: '' };
|
||||
public uploadStatus: string = 'Processing devices...';
|
||||
public uploadResult = { success: 0, failed: 0, resultFile: null };
|
||||
public currentTaskId: string = '';
|
||||
public statusCheckTimer: any;
|
||||
|
||||
toasterForm = {
|
||||
autohide: true,
|
||||
|
|
@ -165,10 +188,12 @@ export class DevicesComponent implements OnInit, OnDestroy {
|
|||
this.check_firmware();
|
||||
break;
|
||||
case "update":
|
||||
this.update_firmware();
|
||||
this.ConfirmAction = "update";
|
||||
this.ConfirmModalVisible = true;
|
||||
break;
|
||||
case "upgrade":
|
||||
this.upgrade_firmware();
|
||||
this.ConfirmAction = "upgrade";
|
||||
this.ConfirmModalVisible = true;
|
||||
break;
|
||||
case "logauth":
|
||||
this.router.navigate(["/authlog", { devid: dev.id }]);
|
||||
|
|
@ -183,7 +208,8 @@ export class DevicesComponent implements OnInit, OnDestroy {
|
|||
this.router.navigate(["/backups", { devid: dev.id }]);
|
||||
break;
|
||||
case "reboot":
|
||||
this.reboot_devices();
|
||||
this.ConfirmAction = "reboot";
|
||||
this.ConfirmModalVisible = true;
|
||||
break;
|
||||
case "delete":
|
||||
this.ConfirmAction = "delete";
|
||||
|
|
@ -374,6 +400,7 @@ export class DevicesComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
check_firmware() {
|
||||
var _self = this;
|
||||
this.ConfirmModalVisible = false;
|
||||
this.data_provider
|
||||
.check_firmware(this.Selectedrows.toString())
|
||||
.then((res) => {
|
||||
|
|
@ -396,6 +423,7 @@ export class DevicesComponent implements OnInit, OnDestroy {
|
|||
|
||||
update_firmware() {
|
||||
var _self = this;
|
||||
this.ConfirmModalVisible = false;
|
||||
this.data_provider
|
||||
.update_firmware(this.Selectedrows.toString())
|
||||
.then((res) => {
|
||||
|
|
@ -417,6 +445,7 @@ export class DevicesComponent implements OnInit, OnDestroy {
|
|||
|
||||
upgrade_firmware() {
|
||||
var _self = this;
|
||||
this.ConfirmModalVisible = false;
|
||||
this.data_provider
|
||||
.upgrade_firmware(this.Selectedrows.toString())
|
||||
.then((res) => {
|
||||
|
|
@ -436,6 +465,7 @@ export class DevicesComponent implements OnInit, OnDestroy {
|
|||
|
||||
reboot_devices() {
|
||||
var _self = this;
|
||||
this.ConfirmModalVisible = false;
|
||||
this.data_provider
|
||||
.reboot_devices(this.Selectedrows.toString())
|
||||
.then((res) => {
|
||||
|
|
@ -492,11 +522,12 @@ export class DevicesComponent implements OnInit, OnDestroy {
|
|||
);
|
||||
}
|
||||
else{
|
||||
_self.source = res.map((x: any) => {
|
||||
_self.originalSource = res.map((x: any) => {
|
||||
if (x.upgrade_availble) _self.upgrades.push(x);
|
||||
if (x.update_availble) _self.updates.push(x);
|
||||
return x;
|
||||
});
|
||||
_self.source = [..._self.originalSource];
|
||||
_self.device_interval();
|
||||
_self.loading = false;
|
||||
}
|
||||
|
|
@ -637,26 +668,273 @@ export class DevicesComponent implements OnInit, OnDestroy {
|
|||
_self.tz,
|
||||
"yyyy-MM-dd HH:mm:ss XXX"
|
||||
);
|
||||
d.start_ip=d.info.start_ip;
|
||||
d.end_ip=d.info.end_ip;
|
||||
d.start_ip=d.info.start_ip || 'N/A';
|
||||
d.end_ip=d.info.end_ip || 'N/A';
|
||||
d.task_id=d.info.task_id || 'N/A';
|
||||
d.device_count=d.info.device_count || 0;
|
||||
d.username=d.info.username || 'N/A';
|
||||
d.result=JSON.parse(d.result);
|
||||
d.success_count = d.result.filter((r: any) => r.added === true).length;
|
||||
d.failed_count = d.result.filter((r: any) => r.added === false).length;
|
||||
index += 1;
|
||||
return d;
|
||||
});
|
||||
_self.filteredExecutedData = [..._self.ExecutedData];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
filterByTaskType() {
|
||||
if (this.selectedTaskType === 'all') {
|
||||
this.filteredExecutedData = [...this.ExecutedData];
|
||||
} else {
|
||||
this.filteredExecutedData = this.ExecutedData.filter((d: any) => d.task_type === this.selectedTaskType);
|
||||
}
|
||||
}
|
||||
|
||||
showTaskDetails(task: any) {
|
||||
this.selectedTaskDetails = task;
|
||||
this.detailsCurrentPage = 1;
|
||||
this.updateDetailsPagination();
|
||||
this.detailsModalVisible = true;
|
||||
}
|
||||
|
||||
updateDetailsPagination() {
|
||||
if (!this.selectedTaskDetails?.result) return;
|
||||
|
||||
// Filter results based on search term
|
||||
this.filteredDetailsResults = this.selectedTaskDetails.result.filter((result: any) =>
|
||||
result.ip.toLowerCase().includes(this.detailsSearchTerm.toLowerCase()) ||
|
||||
(result.failures && result.failures.toLowerCase().includes(this.detailsSearchTerm.toLowerCase())) ||
|
||||
(result.faileres && result.faileres.toLowerCase().includes(this.detailsSearchTerm.toLowerCase()))
|
||||
);
|
||||
|
||||
const startIndex = (this.detailsCurrentPage - 1) * this.detailsPageSize;
|
||||
const endIndex = startIndex + this.detailsPageSize;
|
||||
this.detailsPaginatedResults = this.filteredDetailsResults.slice(startIndex, endIndex);
|
||||
}
|
||||
|
||||
onDetailsPageChange(page: number) {
|
||||
this.detailsCurrentPage = page;
|
||||
this.updateDetailsPagination();
|
||||
}
|
||||
|
||||
getTotalDetailsPages(): number {
|
||||
if (!this.filteredDetailsResults) return 0;
|
||||
return Math.ceil(this.filteredDetailsResults.length / this.detailsPageSize);
|
||||
}
|
||||
|
||||
onDetailsSearch() {
|
||||
this.detailsCurrentPage = 1;
|
||||
this.updateDetailsPagination();
|
||||
}
|
||||
|
||||
closeDetailsModal() {
|
||||
this.detailsModalVisible = false;
|
||||
this.selectedTaskDetails = null;
|
||||
this.detailsPaginatedResults = [];
|
||||
this.filteredDetailsResults = [];
|
||||
this.detailsCurrentPage = 1;
|
||||
this.detailsSearchTerm = '';
|
||||
}
|
||||
|
||||
filterUpdatable() {
|
||||
this.source = this.originalSource.filter(device => device.update_availble);
|
||||
}
|
||||
|
||||
filterUpgradable() {
|
||||
this.source = this.originalSource.filter(device => device.upgrade_availble);
|
||||
}
|
||||
|
||||
clearFilter() {
|
||||
this.source = [...this.originalSource];
|
||||
}
|
||||
|
||||
webAccess(device: any) {
|
||||
this.currentDeviceInfo = device;
|
||||
if (this.ispro) {
|
||||
this.showWebAccessModal = true;
|
||||
} else {
|
||||
this.openDirectAccess();
|
||||
}
|
||||
}
|
||||
|
||||
openProxyAccess() {
|
||||
if (this.currentDeviceInfo?.id) {
|
||||
window.open(`/api/proxy/init?devid=${this.currentDeviceInfo.id}`, '_blank');
|
||||
} else {
|
||||
const ip = this.currentDeviceInfo?.ip;
|
||||
if (ip) {
|
||||
window.open(`/api/proxy/init?dev_ip=${ip}`, '_blank');
|
||||
}
|
||||
}
|
||||
this.showWebAccessModal = false;
|
||||
}
|
||||
|
||||
openDirectAccess() {
|
||||
const ip = this.currentDeviceInfo?.ip;
|
||||
if (ip) {
|
||||
window.open(`http://${ip}`, '_blank');
|
||||
}
|
||||
this.showWebAccessModal = false;
|
||||
}
|
||||
|
||||
closeWebAccessModal() {
|
||||
this.showWebAccessModal = false;
|
||||
}
|
||||
|
||||
openAddDeviceModal() {
|
||||
this.addDeviceModalVisible = true;
|
||||
this.resetAddDeviceForm();
|
||||
}
|
||||
|
||||
closeAddDeviceModal() {
|
||||
this.addDeviceModalVisible = false;
|
||||
this.resetAddDeviceForm();
|
||||
}
|
||||
|
||||
resetAddDeviceForm() {
|
||||
this.addDeviceStep = 1;
|
||||
this.csvFile = null;
|
||||
this.csvData = [];
|
||||
this.csvHeaders = [];
|
||||
this.csvPreview = [];
|
||||
this.columnMapping = { ip: '', username: '', password: '', port: '' };
|
||||
this.uploadStatus = 'Processing devices...';
|
||||
this.uploadResult = { success: 0, failed: 0, resultFile: null };
|
||||
this.currentTaskId = '';
|
||||
clearTimeout(this.statusCheckTimer);
|
||||
}
|
||||
|
||||
onFileSelected(event: any) {
|
||||
const file = event.target.files[0];
|
||||
if (file && file.type === 'text/csv') {
|
||||
this.csvFile = file;
|
||||
this.parseCSV(file);
|
||||
}
|
||||
}
|
||||
|
||||
parseCSV(file: File) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e: any) => {
|
||||
const csv = e.target.result;
|
||||
const lines = csv.split('\n').filter((line: string) => line.trim());
|
||||
|
||||
if (lines.length > 0) {
|
||||
this.csvHeaders = lines[0].split(',').map((header: string) => header.trim());
|
||||
this.csvData = lines.slice(1).map((line: string) =>
|
||||
line.split(',').map((cell: string) => cell.trim())
|
||||
);
|
||||
this.csvPreview = this.csvData.slice(0, 3);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
isValidMapping(): boolean {
|
||||
return this.columnMapping.ip !== '' &&
|
||||
this.columnMapping.username !== '' &&
|
||||
this.columnMapping.password !== '' &&
|
||||
this.columnMapping.port !== '' &&
|
||||
this.csvData.length > 0;
|
||||
}
|
||||
|
||||
uploadDevices() {
|
||||
if (!this.isValidMapping()) return;
|
||||
|
||||
this.addDeviceStep = 2;
|
||||
|
||||
const devices = this.csvData.map(row => ({
|
||||
ip: row[parseInt(this.columnMapping.ip)],
|
||||
username: row[parseInt(this.columnMapping.username)],
|
||||
password: row[parseInt(this.columnMapping.password)],
|
||||
port: row[parseInt(this.columnMapping.port)]
|
||||
}));
|
||||
|
||||
this.data_provider.bulk_add_devices(devices).then((res) => {
|
||||
if ('error' in res) {
|
||||
this.addDeviceStep = 3;
|
||||
this.show_toast('Error', 'Failed to start device upload', 'danger');
|
||||
this.uploadResult = { success: 0, failed: devices.length, resultFile: null };
|
||||
} else if ('taskId' in res) {
|
||||
this.currentTaskId = res.taskId;
|
||||
this.uploadStatus = 'Processing devices...';
|
||||
this.checkUploadStatus();
|
||||
} else {
|
||||
this.addDeviceStep = 3;
|
||||
this.show_toast('Error', 'Invalid response from server', 'danger');
|
||||
this.uploadResult = { success: 0, failed: devices.length, resultFile: null };
|
||||
}
|
||||
}).catch(() => {
|
||||
this.addDeviceStep = 3;
|
||||
this.uploadResult = { success: 0, failed: devices.length, resultFile: null };
|
||||
this.show_toast('Error', 'Failed to upload devices', 'danger');
|
||||
});
|
||||
}
|
||||
|
||||
checkUploadStatus() {
|
||||
clearTimeout(this.statusCheckTimer);
|
||||
|
||||
this.data_provider.bulk_add_status(this.currentTaskId).then((res) => {
|
||||
if ('error' in res) {
|
||||
this.addDeviceStep = 3;
|
||||
this.show_toast('Error', 'Failed to check upload status', 'danger');
|
||||
this.uploadResult = { success: 0, failed: 0, resultFile: null };
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.status === 'completed') {
|
||||
this.addDeviceStep = 3;
|
||||
this.uploadResult = {
|
||||
success: res.success || 0,
|
||||
failed: res.failed || 0,
|
||||
resultFile: res.resultFile || null
|
||||
};
|
||||
this.show_toast('Success', `${res.success} devices added successfully`, 'success');
|
||||
this.initGridTable();
|
||||
} else if (res.status === 'failed') {
|
||||
this.addDeviceStep = 3;
|
||||
this.show_toast('Error', res.message || 'Upload failed', 'danger');
|
||||
this.uploadResult = { success: 0, failed: 0, resultFile: null };
|
||||
} else {
|
||||
// Still processing
|
||||
this.uploadStatus = res.message || 'Processing devices...';
|
||||
this.statusCheckTimer = setTimeout(() => {
|
||||
this.checkUploadStatus();
|
||||
}, 3000);
|
||||
}
|
||||
}).catch(() => {
|
||||
this.addDeviceStep = 3;
|
||||
this.show_toast('Error', 'Failed to check upload status', 'danger');
|
||||
this.uploadResult = { success: 0, failed: 0, resultFile: null };
|
||||
});
|
||||
}
|
||||
|
||||
downloadResults() {
|
||||
if (this.uploadResult.resultFile) {
|
||||
const link = document.createElement('a');
|
||||
link.href = this.uploadResult.resultFile;
|
||||
link.download = 'device_upload_results.csv';
|
||||
link.click();
|
||||
}
|
||||
}
|
||||
|
||||
getTaskTypeLabel(taskType: string): string {
|
||||
switch(taskType) {
|
||||
case 'ip-scan': return 'IP Scan';
|
||||
case 'bulk-add': return 'Bulk Add';
|
||||
default: return taskType;
|
||||
}
|
||||
}
|
||||
|
||||
getStatusColor(success: number, failed: number): string {
|
||||
if (failed === 0) return 'success';
|
||||
if (success === 0) return 'danger';
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
clearTimeout(this.scan_timer);
|
||||
clearTimeout(this.statusCheckTimer);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
ModalModule,
|
||||
ListGroupModule,
|
||||
TooltipModule,
|
||||
TableModule,
|
||||
} from "@coreui/angular";
|
||||
import { MatMenuModule } from "@angular/material/menu";
|
||||
import { DevicesRoutingModule } from "./devices-routing.module";
|
||||
|
|
@ -44,6 +45,7 @@ import { GuiGridModule } from "@generic-ui/ngx-grid";
|
|||
ListGroupModule,
|
||||
MatMenuModule,
|
||||
TooltipModule,
|
||||
TableModule,
|
||||
],
|
||||
declarations: [DevicesComponent],
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue