feat: redesign user tasks with advanced scheduling

- Complete UI/UX redesign of user tasks interface
- Add cron selector with examples
- Improved task scheduling interface
- Enhanced task management and monitoring
This commit is contained in:
sepehr 2025-10-16 17:33:47 +03:00
parent b20a3d7826
commit 2f68c49936
4 changed files with 884 additions and 184 deletions

View file

@ -57,146 +57,255 @@
</c-row>
<c-modal #EditTaskModal backdrop="static" size="xl" [(visible)]="EditTaskModalVisible" id="EditTaskModal">
<c-modal-header>
<h5 *ngIf="SelectedTask['action']=='edit'" cModalTitle>Editing device {{SelectedTask['name']}}</h5>
<h5 *ngIf="SelectedTask['action']=='add'" cModalTitle>Adding new task</h5>
<c-modal-header class="bg-light">
<h5 *ngIf="SelectedTask['action']=='edit'" cModalTitle>
<i class="fa-solid fa-edit me-2"></i>Edit Task: {{SelectedTask['name']}}
</h5>
<h5 *ngIf="SelectedTask['action']=='add'" cModalTitle>
<i class="fa-solid fa-plus me-2"></i>Create New Task
</h5>
<button [cModalToggle]="EditTaskModal.id" cButtonClose></button>
</c-modal-header>
<c-modal-body>
<div [cFormFloating]="true" class="mb-3">
<input cFormControl id="floatingInput" placeholder="SelectedTask['name']" [(ngModel)]="SelectedTask['name']" />
<label cLabel for="floatingInput">Name</label>
<c-modal-body class="p-4">
<!-- Basic Information Section -->
<div class="task-form-section mb-4">
<div class="section-header mb-3">
<h6 class="section-title mb-1"><i class="fa-solid fa-info-circle me-2"></i>Basic Information</h6>
<small class="text-muted">Define the task name, description and type</small>
</div>
<c-row class="g-3">
<c-col xs="12" md="6">
<input cFormControl placeholder="Task Name (required)" [(ngModel)]="SelectedTask['name']"
class="form-input" title="Unique name for this task" />
</c-col>
<c-col xs="12" md="6">
<select cSelect [(ngModel)]="SelectedTask['task_type']" (change)="onTaskTypeChange()" class="form-select" title="Select task type">
<option value="">Choose Task Type...</option>
<option value="backup">📁 Backup Configuration</option>
<option value="snippet">📝 Execute Script/Snippet</option>
<option value="firmware" *ngIf="ispro">🔄 Firmware Update</option>
</select>
</c-col>
<c-col xs="12">
<textarea cFormControl placeholder="Task Description (optional)" [(ngModel)]="SelectedTask['description']"
class="form-input" rows="2" title="Brief description of what this task does"></textarea>
</c-col>
</c-row>
</div>
<!-- Task Configuration Section -->
<div class="task-config-section mb-4" *ngIf="SelectedTask['task_type']">
<div class="section-header mb-3">
<h6 class="section-title mb-1"><i class="fa-solid fa-cog me-2"></i>Task Configuration</h6>
<small class="text-muted">Configure task-specific settings and parameters</small>
</div>
<!-- Backup Configuration -->
<div *ngIf="SelectedTask['task_type']=='backup'" class="backup-config mb-3">
<c-card class="mb-3">
<c-card-header class="bg-success text-white">
<h6 class="mb-0"><i class="fa-solid fa-database me-2"></i>Backup Configuration</h6>
</c-card-header>
<c-card-body>
<div class="alert alert-info">
<i class="fa-solid fa-info-circle me-2"></i>
This task will create configuration backups of selected devices. Backups are stored securely and can be restored later.
</div>
</c-card-body>
</c-card>
</div>
<!-- Firmware Configuration -->
<c-card *ngIf="SelectedTask['task_type']=='firmware'" class="mb-3">
<c-card-header class="bg-warning text-dark">
<h6 class="mb-0"><i class="fa-solid fa-microchip me-2"></i>Firmware Update Strategy</h6>
</c-card-header>
<c-card-body>
<div class="strategy-buttons mb-3">
<c-button-group role="group" class="w-100">
<button cButton [active]="SelectedTask['data']['strategy']=='system'"
(click)="firmware_type_changed('system')" color="info" variant="outline" class="flex-fill">
<i class="fa-solid fa-cogs me-1"></i>System Default
</button>
<button cButton [active]="SelectedTask['data']['strategy']=='defined'"
(click)="firmware_type_changed('defined')" color="warning" variant="outline" class="flex-fill">
<i class="fa-solid fa-list me-1"></i>Custom Version
</button>
<button cButton [active]="SelectedTask['data']['strategy']=='latest'"
(click)="firmware_type_changed('latest')" color="success" variant="outline" class="flex-fill">
<i class="fa-solid fa-download me-1"></i>Latest Available
</button>
</c-button-group>
</div>
<div *ngIf=" SelectedTask['data']['strategy']=='system'" class="alert alert-info">
<i class="fa-solid fa-info-circle me-2"></i>
Uses global MikroWizard update strategy settings. Check Settings page for configuration.
</div>
<div *ngIf="firms_loaded && SelectedTask['data']['strategy']=='latest'" class="alert alert-success">
<i class="fa-solid fa-cloud-download-alt me-2"></i>
Downloads latest firmware from mikrotik.com. Server needs internet access.
</div>
<div *ngIf="firms_loaded && SelectedTask['data']['strategy']=='defined'">
<c-row class="g-3">
<c-col xs="12" md="6">
<label class="form-label">RouterOS v7+ Version</label>
<select cSelect [(ngModel)]="SelectedTask['data']['version_to_install']" class="form-select">
<option value="">Choose version...</option>
<option *ngFor="let f of available_firmwares" [value]="f">{{f}}</option>
</select>
</c-col>
<c-col xs="12" md="6" *ngIf="updateBehavior=='keep'">
<label class="form-label">RouterOS v6 Version</label>
<select cSelect [(ngModel)]="SelectedTask['data']['version_to_install_6']" class="form-select">
<option value="">Choose version...</option>
<option *ngFor="let f of available_firmwaresv6" [value]="f">{{f}}</option>
</select>
</c-col>
</c-row>
</div>
</c-card-body>
</c-card>
<!-- Snippet Configuration -->
<div *ngIf="SelectedTask['task_type']=='snippet'" class="snippet-config mb-3">
<c-card class="mb-3">
<c-card-header class="bg-primary text-white">
<h6 class="mb-0"><i class="fa-solid fa-code me-2"></i>Script/Snippet Configuration</h6>
</c-card-header>
<c-card-body>
<label class="form-label">Select Script/Snippet to Execute</label>
<ngx-super-select [dataSource]="Snippets" [options]="options"
(selectionChanged)="onSelectValueChanged($event)" [selectedItemValues]="[SelectedTask['snippetid']]"
(searchChanged)="onSnippetsValueChanged($event)" class="styled"></ngx-super-select>
<small class="text-muted mt-2 d-block">
<i class="fa-solid fa-info-circle me-1"></i>
The selected script will be executed on all target devices when this task runs.
</small>
</c-card-body>
</c-card>
</div>
</div>
<div [cFormFloating]="true" class="mb-3">
<input cFormControl id="floatingInput" placeholder="SelectedTask['description']"
[(ngModel)]="SelectedTask['description']" />
<label cLabel for="floatingInput">Description</label>
<!-- Schedule Configuration -->
<div class="schedule-section mb-4">
<div class="section-header mb-3">
<h6 class="section-title mb-1"><i class="fa-solid fa-clock me-2"></i>Schedule Configuration</h6>
<small class="text-muted">Set when this task should run automatically</small>
</div>
<div class="cron-input-wrapper">
<label class="form-label">Execution Schedule (Cron Expression)</label>
<div class="search-select-wrapper">
<div class="input-group">
<input cFormControl
[(ngModel)]="SelectedTask['cron']"
(input)="onCronInputChange($event)"
(focus)="onCronInputFocus()"
(blur)="hideCronDropdown()"
placeholder="Enter cron expression or search presets..."
class="search-input"
autocomplete="off" />
<button class="btn btn-outline-secondary" type="button" (click)="onCronInputFocus()" title="Show presets">
<i class="fa-solid fa-clock"></i>
</button>
</div>
<div *ngIf="showCronDropdown && filteredCrons.length > 0" class="search-dropdown cron-dropdown">
<div *ngFor="let cron of filteredCrons"
class="search-option cron-option"
[class.selected]="selectedCronPreset?.value === cron.value"
(mousedown)="selectCron(cron)">
<div class="cron-label">{{cron.label}}</div>
<div class="cron-value">{{cron.value}}</div>
<div class="cron-description">{{cron.description}}</div>
</div>
</div>
<div *ngIf="showCronDropdown && filteredCrons.length === 0 && cronSearch" class="search-no-results">
No matching cron presets found
</div>
</div>
<small class="text-muted mt-1 d-block">
<i class="fa-solid fa-info-circle me-1"></i>{{getCronDescription()}}
</small>
<div class="mt-2">
<small class="text-muted">
<strong>Quick Examples:</strong>
<code class="me-2">* * * * *</code> = every minute |
<code class="me-2">0 2 * * *</code> = daily at 2 AM |
<code class="me-2">0 */6 * * *</code> = every 6 hours
</small>
</div>
</div>
</div>
<c-input-group class="mb-3">
<label cInputGroupText for="inputGroupSelect01">
Options
</label>
<select cSelect id="inputGroupSelect01" [(ngModel)]="SelectedTask['task_type']">
<option>Choose...</option>
<option value="backup">Backup</option>
<option value="snippet">Snippet</option>
<option value="firmware" *ngIf="ispro">Firmware</option>
</select>
</c-input-group>
<h6 *ngIf="SelectedTask['task_type']=='firmware'" >Update Version Strategy</h6>
<c-card *ngIf="SelectedTask['task_type']=='firmware'">
<c-card-header style="padding: 0;">
<c-input-group>
<c-button-group aria-label="Basic radio toggle button group"
role="group">
<input class="btn-check" type="radio" value="Radio2" />
<label (click)="firmware_type_changed('system')" [active]="SelectedTask['data']['strategy']=='system'"
cButton cFormCheckLabel color="dark" variant="ghost">System setting
defined</label>
<input class="btn-check" type="radio" value="Radio1" />
<label (click)="firmware_type_changed('defined')" [active]="SelectedTask['data']['strategy']=='defined'"
cButton cFormCheckLabel color="dark" variant="ghost">Custom
Version</label>
<input class="btn-check" type="radio" value="Radio3" />
<label (click)="firmware_type_changed('latest')" [active]="SelectedTask['data']['strategy']=='latest'"
cButton cFormCheckLabel color="dark" variant="ghost">Latest
availble</label>
</c-button-group>
</c-input-group>
</c-card-header>
<c-card-body>
<!-- Target Selection -->
<div class="target-section mb-4">
<div class="section-header mb-3">
<h6 class="section-title mb-1"><i class="fa-solid fa-bullseye me-2"></i>Target Selection</h6>
<small class="text-muted">Choose which devices or groups this task will affect</small>
</div>
<div class="target-type-selector mb-3">
<c-button-group role="group" class="w-100">
<button cButton [active]="SelectedTask['selection_type']=='devices'"
(click)="SelectedTask['selection_type']='devices'; form_changed()"
color="primary" variant="outline" class="flex-fill">
<i class="fa-solid fa-server me-1"></i>Individual Devices
</button>
<button cButton [active]="SelectedTask['selection_type']=='groups'"
(click)="SelectedTask['selection_type']='groups'; form_changed()"
color="info" variant="outline" class="flex-fill">
<i class="fa-solid fa-layer-group me-1"></i>Device Groups
</button>
</c-button-group>
</div>
<c-input-group
*ngIf="firms_loaded && SelectedTask['task_type']=='firmware' && SelectedTask['data']['strategy']=='system'">
<c-form-feedback style="display: block;color: #48515a;margin-top: 0;" [valid]="true">
The version of firmware will be selected based on global settings of Mikrowizard Update strategy.
<br/>
Please check settings page for more info and configuration
</c-form-feedback>
</c-input-group>
<c-input-group
*ngIf="firms_loaded && SelectedTask['task_type']=='firmware' && SelectedTask['data']['strategy']=='latest'">
<c-form-feedback style="display: block;color: #48515a;margin-top: 0;" [valid]="true">
The version of firmware will be selected based on latest availble version from Mikrotik website!.
<br/>
<b>V6 Firmware update Behavior</b> and <b>safe install</b> is based on global Mikrowizard setting.(check settings page)
<br/>
<code style="padding: 0!important;">**with this option MikroWizard will download latest availble firmware from mikrotik.com. Please keep in mind that server needs internet access to mikrotik.com</code></c-form-feedback>
</c-input-group>
<c-input-group
*ngIf="firms_loaded && SelectedTask['task_type']=='firmware' && SelectedTask['data']['strategy']=='defined'">
<c-input-group class="mb-3">
<label cInputGroupText for="inputGroupSelect01">
Firmware version to install
</label>
<select cSelect [(ngModel)]="SelectedTask['data']['version_to_install']" id="inputGroupSelect01">
<option>Choose...</option>
<option *ngFor="let f of available_firmwares" [value]="f">{{f}}</option>
</select>
<c-form-feedback style="display: block;color: #979797;margin-top: 0;" [valid]="true">
* The version of firmware to install routers</c-form-feedback>
</c-input-group>
<c-input-group *ngIf="updateBehavior=='keep'" >
<label cInputGroupText for="inputGroupSelect01">
Firmware version v6 to install
</label>
<select cSelect [(ngModel)]="SelectedTask['data']['version_to_install_6']" id="inputGroupSelect01">
<option>Choose...</option>
<option *ngFor="let f of available_firmwaresv6" [value]="f">{{f}}</option>
</select>
<c-form-feedback style="display: block;color: #979797;margin-top: 0;" [valid]="true">
* The version of firmware to install on V6 routers</c-form-feedback>
</c-input-group>
</c-input-group>
</c-card-body>
</c-card>
<c-input-group class="mb-3">
<ngx-super-select *ngIf="SelectedTask['task_type']=='snippet'" [dataSource]="Snippets" [options]="options"
(selectionChanged)="onSelectValueChanged($event)" [selectedItemValues]="[SelectedTask['snippetid']]"
(searchChanged)="onSnippetsValueChanged($event)" class="styled"></ngx-super-select>
</c-input-group>
<div [cFormFloating]="true" class="mb-3">
<input cFormControl id="floatingInput" placeholder="SelectedTask['name']" [(ngModel)]="SelectedTask['cron']" />
<label cLabel for="floatingInput">cron</label>
<!-- Selected Targets -->
<c-card>
<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>Selected {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : 'Groups'}}
</h6>
<div>
<c-badge color="info" class="me-2">{{SelectedMembers.length}} selected</c-badge>
<button cButton color="success" size="sm" (click)="show_new_member_form()">
<i class="fa-solid fa-plus me-1"></i>Add {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : 'Groups'}}
</button>
</div>
</c-card-header>
<c-card-body class="p-0">
<div *ngIf="SelectedMembers.length === 0" class="text-center p-4 text-muted">
<i class="fa-solid fa-{{SelectedTask['selection_type'] === 'devices' ? 'server' : 'layer-group'}} fa-3x mb-3 opacity-50"></i>
<h6>No {{SelectedTask['selection_type']}} selected</h6>
<p class="mb-0">Click "Add {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : 'Groups'}}" to select targets for this task</p>
</div>
<div *ngIf="SelectedMembers.length > 0">
<gui-grid [autoResizeWidth]="true" [source]="SelectedMembers" [columnMenu]="columnMenu" [sorting]="sorting"
[infoPanel]="infoPanel" [autoResizeWidth]=true [paging]="paging">
<gui-grid-column header="Name" field="name">
<ng-template let-value="item.name" let-item="item" let-index="index">
<div class="d-flex align-items-center">
<i class="fa-solid fa-{{SelectedTask['selection_type'] === 'devices' ? 'server' : 'layer-group'}} me-2 text-primary"></i>
<strong>{{value}}</strong>
</div>
</ng-template>
</gui-grid-column>
<gui-grid-column *ngIf="SelectedTask['selection_type']=='devices'" header="MAC Address" field="mac">
<ng-template let-value="item.mac" let-item="item" let-index="index">
<small class="text-muted">{{value}}</small>
</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" variant="outline" (click)="remove_member(item)" title="Remove from task">
<i class="fa-solid fa-times"></i>
</button>
</ng-template>
</gui-grid-column>
</gui-grid>
</div>
</c-card-body>
</c-card>
</div>
<c-input-group class="mb-3">
<label cInputGroupText for="inputGroupSelect01">
Member type
</label>
<select cSelect id="inputGroupSelect01" (change)="form_changed()" [(ngModel)]="SelectedTask['selection_type']">
<option value="devices">Devices</option>
<option value="groups">Groups</option>
</select>
</c-input-group>
<h5>Members :</h5>
<gui-grid [autoResizeWidth]="true" [source]="SelectedMembers" [columnMenu]="columnMenu" [sorting]="sorting"
[infoPanel]="infoPanel" [rowSelection]="rowSelection" [autoResizeWidth]=true [paging]="paging">
<gui-grid-column header="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 *ngIf="SelectedTask['selection_type']=='devices'" header="MAC" field="mac">
<ng-template let-value="item.mac" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column header="Actions" width="120" field="action">
<ng-template let-value="item.id" let-item="item" let-index="index">
<button cButton color="danger" size="sm" (click)="remove_member(item)"><i
class="fa-regular fa-trash-can"></i></button>
</ng-template>
</gui-grid-column>
</gui-grid>
<hr />
<button cButton color="primary" (click)="show_new_member_form()">+ Add new Members</button>
<!--
<c-input-group class="mb-3">
<ngx-super-select
@ -220,54 +329,96 @@
-->
</c-modal-body>
<c-modal-footer>
<button *ngIf="SelectedTask['action']=='add'" (click)="submit('add')" cButton color="primary">Add</button>
<button *ngIf="SelectedTask['action']=='edit'" (click)="submit('edit')" cButton color="primary">save</button>
<button [cModalToggle]="EditTaskModal.id" cButton color="secondary">
Close
</button>
<c-modal-footer class="bg-light d-flex justify-content-between">
<div>
<small class="text-muted">All fields marked with * are required</small>
</div>
<div>
<button *ngIf="SelectedTask['action']=='add'" (click)="submit('add')" cButton color="primary">
<i class="fa-solid fa-plus me-1"></i>Create Task
</button>
<button *ngIf="SelectedTask['action']=='edit'" (click)="submit('edit')" cButton color="primary">
<i class="fa-solid fa-save me-1"></i>Save Changes
</button>
<button [cModalToggle]="EditTaskModal.id" cButton color="secondary" class="ms-2">
<i class="fa-solid fa-times me-1"></i>Cancel
</button>
</div>
</c-modal-footer>
</c-modal>
<c-modal #NewMemberModal backdrop="static" size="lg" [(visible)]="NewMemberModalVisible" id="NewMemberModal">
<c-modal-header>
<h5 cModalTitle>Editing Group </h5>
<c-modal #NewMemberModal backdrop="static" size="xl" [(visible)]="NewMemberModalVisible" id="NewMemberModal">
<c-modal-header class="bg-success text-white">
<h5 cModalTitle>
<i class="fa-solid fa-plus-circle me-2"></i>Add {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : 'Groups'}} to Task
</h5>
<button (click)="NewMemberModalVisible=!NewMemberModalVisible" cButtonClose></button>
</c-modal-header>
<c-modal-body>
<c-input-group class="mb-3">
<h5>Group Members :</h5>
<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="Member 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 *ngIf="SelectedTask['selection_type']=='devices'" header="IP Address" field="ip">
<ng-template let-value="item.ip" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
<gui-grid-column *ngIf="SelectedTask['selection_type']=='devices'" header="MAC Address" field="mac">
<ng-template let-value="item.mac" let-item="item" let-index="index">
{{value}}
</ng-template>
</gui-grid-column>
</gui-grid>
<br />
</c-input-group>
<hr />
<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> {{SelectedTask['selection_type']}}(s) selected for addition to this task</span>
</c-alert>
</div>
<!-- Available Items -->
<c-card>
<c-card-header>
<h6 class="mb-0">
<i class="fa-solid fa-{{SelectedTask['selection_type'] === 'devices' ? 'server' : 'layer-group'}} me-2"></i>
Available {{SelectedTask['selection_type'] === 'devices' ? 'Devices' : 'Groups'}} ({{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 {{SelectedTask['selection_type']}} are already assigned</h6>
<p class="mb-0">No available {{SelectedTask['selection_type']}} to add to this task</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="Name" field="name">
<ng-template let-value="item.name" let-item="item" let-index="index">
<div class="d-flex align-items-center">
<i class="fa-solid fa-{{SelectedTask['selection_type'] === 'devices' ? 'server' : 'layer-group'}} me-2 text-primary"></i>
<strong>{{value}}</strong>
</div>
</ng-template>
</gui-grid-column>
<gui-grid-column *ngIf="SelectedTask['selection_type']=='devices'" 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 *ngIf="SelectedTask['selection_type']=='devices'" header="MAC Address" field="mac">
<ng-template let-value="item.mac" let-item="item" let-index="index">
<small class="text-muted">{{value}}</small>
</ng-template>
</gui-grid-column>
</gui-grid>
</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
</button>
<c-modal-footer class="bg-light d-flex justify-content-between">
<div>
<small class="text-muted">Select {{SelectedTask['selection_type']}} from the list above to add them to this task</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}} {{SelectedTask['selection_type']}}(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>
@ -341,3 +492,5 @@
</button>
</c-modal-footer>
</c-modal>
<c-toaster position="fixed" placement="top-end"></c-toaster>

View file

@ -0,0 +1,371 @@
/* Task Form Sections */
.task-form-section, .task-config-section, .schedule-section, .target-section {
border: 1px solid #e9ecef;
border-radius: 6px;
padding: 0.75rem;
background: #f8f9fa;
}
.section-header {
border-bottom: 1px solid #dee2e6;
padding-bottom: 0.375rem;
margin-bottom: 0.75rem;
}
.section-title {
color: #495057;
font-weight: 600;
font-size: 0.875rem;
}
/* Form Inputs */
.form-input {
border: 1px solid #ced4da;
border-radius: 4px;
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.form-input:focus {
border-color: #80bdff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
/* Strategy Buttons */
.strategy-buttons .btn-group {
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.strategy-buttons button {
font-size: 0.8rem;
padding: 0.375rem 0.75rem;
}
/* Cron Dropdown Styles */
.cron-input-wrapper {
position: relative;
}
.input-group {
position: relative;
display: flex;
flex-wrap: nowrap;
align-items: stretch;
width: 100%;
}
.input-group .search-input {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: none;
flex: 1;
}
.input-group .btn {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left: 1px solid #ced4da;
flex-shrink: 0;
}
.search-select-wrapper {
position: relative;
width: 100%;
}
.search-input {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid #ced4da;
border-radius: 6px;
font-size: 0.9rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.search-input:focus {
border-color: #80bdff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
outline: 0;
}
.search-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #ced4da;
border-top: none;
border-radius: 0 0 6px 6px;
max-height: 300px;
overflow-y: auto;
z-index: 1000;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.search-option {
padding: 0.5rem 0.75rem;
cursor: pointer;
border-bottom: 1px solid #f8f9fa;
transition: background-color 0.15s ease-in-out;
}
.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-style: italic;
}
/* Cron-specific dropdown styles */
.cron-dropdown {
max-height: 400px;
}
.cron-option {
padding: 0.75rem;
border-bottom: 1px solid #e9ecef;
}
.cron-option:hover {
background-color: #e3f2fd;
}
.cron-option.selected {
background-color: #bbdefb;
border-left: 4px solid #2196f3;
}
.cron-option.selected:hover {
background-color: #90caf9;
}
.cron-label {
font-weight: 600;
color: #495057;
font-size: 0.9rem;
}
.cron-value {
font-family: 'Courier New', monospace;
color: #007bff;
font-size: 0.85rem;
margin: 0.25rem 0;
background: #f8f9fa;
padding: 0.25rem 0.5rem;
border-radius: 4px;
display: inline-block;
}
.cron-description {
color: #6c757d;
font-size: 0.8rem;
font-style: italic;
}
/* Target Type Selector */
.target-type-selector .btn-group {
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.target-type-selector button {
font-size: 0.8rem;
padding: 0.375rem 0.75rem;
}
/* Card Enhancements */
.c-card {
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
border: 1px solid #e9ecef;
}
.c-card-header {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-bottom: 1px solid #dee2e6;
font-weight: 600;
}
/* Alert Enhancements */
.alert {
border-radius: 8px;
border: none;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.alert-info {
background: linear-gradient(135deg, #d1ecf1 0%, #bee5eb 100%);
color: #0c5460;
}
.alert-success {
background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
color: #155724;
}
.alert-warning {
background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
color: #856404;
}
/* Badge Enhancements */
.badge {
font-size: 0.7rem;
padding: 0.25em 0.5em;
border-radius: 4px;
}
/* Button Enhancements */
.btn {
border-radius: 4px;
font-weight: 500;
transition: all 0.15s ease-in-out;
}
.btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.8rem;
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
/* Modal Enhancements */
.modal-header {
border-bottom: 1px solid #dee2e6;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
}
.modal-footer {
border-top: 1px solid #dee2e6;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
}
/* Grid Enhancements */
.gui-grid {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* Responsive Design */
@media (max-width: 768px) {
.task-form-section, .task-config-section, .schedule-section, .target-section {
padding: 0.5rem;
}
.section-title {
font-size: 0.8rem;
}
.strategy-buttons button,
.target-type-selector button {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
}
.form-input {
padding: 0.25rem 0.5rem;
font-size: 0.8rem;
}
.cron-label {
font-size: 0.8rem;
}
.cron-value {
font-size: 0.75rem;
}
.cron-description {
font-size: 0.7rem;
}
.c-modal-body {
padding: 0.75rem !important;
}
}
@media (max-width: 576px) {
.task-form-section, .task-config-section, .schedule-section, .target-section {
padding: 0.375rem;
margin-bottom: 0.75rem;
}
.section-header {
margin-bottom: 0.5rem;
}
.strategy-buttons button,
.target-type-selector button {
font-size: 0.7rem;
padding: 0.25rem 0.375rem;
}
.input-group .btn {
padding: 0.25rem 0.5rem;
font-size: 0.8rem;
}
}
/* Animation for smooth transitions */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.search-dropdown {
animation: fadeIn 0.2s ease-out;
}
/* Focus states for accessibility */
.search-option:focus,
.cron-option:focus {
outline: 2px solid #007bff;
outline-offset: -2px;
background-color: #e3f2fd;
}
/* Loading states */
.loading-spinner {
display: inline-block;
width: 1rem;
height: 1rem;
border: 2px solid #f3f3f3;
border-top: 2px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Code examples */
code {
background-color: #f8f9fa;
color: #e83e8c;
padding: 0.2em 0.4em;
border-radius: 3px;
font-size: 0.875em;
font-family: 'Courier New', monospace;
}
/* Improved spacing */
.mt-1 { margin-top: 0.25rem !important; }
.mt-2 { margin-top: 0.5rem !important; }
.me-1 { margin-right: 0.25rem !important; }
.me-2 { margin-right: 0.5rem !important; }
.ms-2 { margin-left: 0.5rem !important; }
.d-block { display: block !important; }

View file

@ -1,4 +1,4 @@
import { Component, OnInit, OnDestroy } from "@angular/core";
import { Component, OnInit, OnDestroy, QueryList, ViewChildren } from "@angular/core";
import { dataProvider } from "../../providers/mikrowizard/data";
import { Router } from "@angular/router";
import { loginChecker } from "../../providers/login_checker";
@ -16,16 +16,28 @@ import {
} from "@generic-ui/ngx-grid";
import { NgxSuperSelectOptions } from "ngx-super-select";
import { _getFocusedElementPierceShadowDom } from "@angular/cdk/platform";
import { AppToastComponent } from "../toast-simple/toast.component";
import { ToasterComponent } from "@coreui/angular";
@Component({
templateUrl: "user_tasks.component.html",
styleUrls: ["user_tasks.component.scss"]
})
export class UserTasksComponent implements OnInit {
public uid: number;
public uname: string;
public ispro: boolean = false;
@ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>;
toasterForm = {
autohide: true,
delay: 10000,
position: "fixed",
fade: true,
closeButton: true,
};
constructor(
private data_provider: dataProvider,
private router: Router,
@ -76,6 +88,55 @@ export class UserTasksComponent implements OnInit {
public firmwaretoinstallv6: string = "none";
public updateBehavior: string = "keep";
public firms_loaded: boolean = false;
public predefinedCrons: any[] = [
// High Frequency Monitoring
{ label: 'Every minute', value: '* * * * *', description: 'Critical monitoring - runs every minute' },
{ label: 'Every 2 minutes', value: '*/2 * * * *', description: 'High frequency monitoring' },
{ label: 'Every 5 minutes', value: '*/5 * * * *', description: 'Standard monitoring interval' },
{ label: 'Every 10 minutes', value: '*/10 * * * *', description: 'Regular monitoring checks' },
{ label: 'Every 15 minutes', value: '*/15 * * * *', description: 'Moderate monitoring frequency' },
{ label: 'Every 30 minutes', value: '*/30 * * * *', description: 'Low frequency monitoring' },
// Hourly Operations
{ label: 'Every hour', value: '0 * * * *', description: 'Hourly network checks' },
{ label: 'Every 2 hours', value: '0 */2 * * *', description: 'Bi-hourly operations' },
{ label: 'Every 4 hours', value: '0 */4 * * *', description: 'Quarterly daily checks' },
{ label: 'Every 6 hours', value: '0 */6 * * *', description: 'Four times daily' },
{ label: 'Every 8 hours', value: '0 */8 * * *', description: 'Three times daily' },
{ label: 'Every 12 hours', value: '0 */12 * * *', description: 'Twice daily operations' },
// Daily Maintenance
{ label: 'Daily at midnight', value: '0 0 * * *', description: 'Daily maintenance at 00:00' },
{ label: 'Daily at 1 AM', value: '0 1 * * *', description: 'Daily backup at 01:00' },
{ label: 'Daily at 2 AM', value: '0 2 * * *', description: 'Daily maintenance at 02:00' },
{ label: 'Daily at 3 AM', value: '0 3 * * *', description: 'Low traffic maintenance at 03:00' },
{ label: 'Daily at 6 AM', value: '0 6 * * *', description: 'Pre-business hours check' },
{ label: 'Daily at 6 PM', value: '0 18 * * *', description: 'End of business day backup' },
{ label: 'Daily at 10 PM', value: '0 22 * * *', description: 'Evening maintenance at 22:00' },
// Business Hours
{ label: 'Workdays at 8 AM', value: '0 8 * * 1-5', description: 'Start of business day - Mon to Fri' },
{ label: 'Workdays at 9 AM', value: '0 9 * * 1-5', description: 'Business hours start check' },
{ label: 'Workdays at 12 PM', value: '0 12 * * 1-5', description: 'Midday check - Mon to Fri' },
{ label: 'Workdays at 5 PM', value: '0 17 * * 1-5', description: 'End of business day - Mon to Fri' },
{ label: 'Workdays at 6 PM', value: '0 18 * * 1-5', description: 'After hours backup - Mon to Fri' },
// Weekly Operations
{ label: 'Weekly (Sunday midnight)', value: '0 0 * * 0', description: 'Weekly maintenance - Sunday 00:00' },
{ label: 'Weekly (Monday midnight)', value: '0 0 * * 1', description: 'Weekly start - Monday 00:00' },
{ label: 'Weekly (Friday 6 PM)', value: '0 18 * * 5', description: 'End of week backup - Friday 18:00' },
{ label: 'Weekly (Saturday 2 AM)', value: '0 2 * * 6', description: 'Weekend maintenance - Saturday 02:00' },
// Monthly Operations
{ label: 'Monthly (1st at midnight)', value: '0 0 1 * *', description: 'Monthly maintenance - 1st of month' },
{ label: 'Monthly (1st at 2 AM)', value: '0 2 1 * *', description: 'Monthly backup - 1st at 02:00' },
{ label: 'Monthly (15th at midnight)', value: '0 0 15 * *', description: 'Mid-month maintenance - 15th' },
{ label: 'Monthly (last day)', value: '0 0 28-31 * *', description: 'End of month operations' }
];
public showCronDropdown: boolean = false;
public cronSearch: string = '';
public selectedCronPreset: any = null;
public filteredCrons: any[] = [];
public sorting = {
enabled: true,
multiSorting: true,
@ -156,22 +217,43 @@ export class UserTasksComponent implements OnInit {
this.initGridTable();
}
show_toast(title: string, body: string, color: string) {
const { ...props } = { ...this.toasterForm, color, title, body };
const componentRef = this.viewChildren.first.addToast(
AppToastComponent,
props,
{}
);
componentRef.instance["closeButton"] = props.closeButton;
}
submit(action: string) {
var _self = this;
if (action == "add") {
this.data_provider
.Add_task(_self.SelectedTask, _self.SelectedTaskItems)
.then((res) => {
_self.initGridTable();
if (res && res.status === 'failed') {
_self.show_toast("Error", res.err, "danger");
} else {
_self.show_toast("Success", "Task created successfully", "success");
_self.initGridTable();
_self.EditTaskModalVisible = false;
}
});
} else {
this.data_provider
.Edit_task(_self.SelectedTask, _self.SelectedTaskItems)
.then((res) => {
_self.initGridTable();
if (res && res.status === 'failed') {
_self.show_toast("Error", res.err, "danger");
} else {
_self.show_toast("Success", "Task updated successfully", "success");
_self.initGridTable();
_self.EditTaskModalVisible = false;
}
});
}
this.EditTaskModalVisible = false;
}
onSelectedRowsNewMembers(rows: Array<GuiSelectedRow>): void {
@ -197,7 +279,7 @@ export class UserTasksComponent implements OnInit {
this.SelectedTask = {
id: 0,
action: "add",
taskcron: "* * * * *",
cron: "0 2 * * *",
desc_cron: "",
description: "",
members: "",
@ -206,7 +288,9 @@ export class UserTasksComponent implements OnInit {
snippetid: "",
task_type: "backup",
};
this.SelectedTask['data'] = { 'strategy': 'system', 'version_to_install': '', 'version_to_install_6': '' }
this.SelectedTask['data'] = { 'strategy': 'system', 'version_to_install': '', 'version_to_install_6': '' };
this.cronSearch = '';
this.selectedCronPreset = null;
this.SelectedMembers = [];
this.SelectedTaskItems = [];
this.EditTaskModalVisible = true;
@ -215,6 +299,12 @@ export class UserTasksComponent implements OnInit {
var _self = this;
this.SelectedTask = { ...item };
// Initialize cron search and preset tracking
this.cronSearch = '';
const currentCron = this.SelectedTask['cron'];
this.selectedCronPreset = this.predefinedCrons.find(cron => cron.value === currentCron) || null;
if (this.SelectedTask['task_type'] == 'firmware' && 'data' in this.SelectedTask && this.SelectedTask['data']) {
this.SelectedTask['data'] = JSON.parse(this.SelectedTask['data']);
if (this.SelectedTask['data']['strategy'] == 'defined') {
@ -243,7 +333,7 @@ export class UserTasksComponent implements OnInit {
}
}
_self.data_provider.get_snippets("", "", "", 0, 1000).then((res) => {
_self.data_provider.get_snippets("", "", "", 0, 1000,false).then((res) => {
_self.Snippets = res.map((x: any) => {
return { id: x.id, name: x.name };
});
@ -315,7 +405,7 @@ export class UserTasksComponent implements OnInit {
onSnippetsValueChanged(v: any) {
var _self = this;
if (v == "" || v.length < 3) return;
_self.data_provider.get_snippets(v, "", "", 0, 1000).then((res) => {
_self.data_provider.get_snippets(v, "", "", 0, 1000,false).then((res) => {
_self.Snippets = res.map((x: any) => {
return { id: String(x.id), name: x.name };
});
@ -333,7 +423,12 @@ export class UserTasksComponent implements OnInit {
} else {
var _self = this;
this.data_provider.Delete_task(_self.SelectedTask["id"]).then((res) => {
_self.initGridTable();
if (res && res.status === 'failed') {
_self.show_toast("Error", res.err, "danger");
} else {
_self.show_toast("Success", "Task deleted successfully", "success");
_self.initGridTable();
}
_self.DeleteConfirmModalVisible = false;
});
}
@ -363,4 +458,79 @@ export class UserTasksComponent implements OnInit {
_self.loading = false;
});
}
selectCron(cron: any): void {
this.SelectedTask['cron'] = cron.value;
this.selectedCronPreset = cron;
this.cronSearch = '';
this.showCronDropdown = false;
}
filterCrons(event: any): void {
const searchTerm = event.target.value.toLowerCase();
this.cronSearch = searchTerm;
this.selectedCronPreset = null;
if (searchTerm.length > 0) {
this.filteredCrons = this.predefinedCrons.filter(cron =>
cron.label.toLowerCase().includes(searchTerm) ||
cron.description.toLowerCase().includes(searchTerm) ||
cron.value.includes(searchTerm)
);
} else {
this.filteredCrons = this.predefinedCrons;
}
}
hideCronDropdown(): void {
setTimeout(() => {
this.showCronDropdown = false;
}, 200);
}
onCronInputFocus(): void {
this.filteredCrons = this.predefinedCrons;
this.showCronDropdown = true;
// If current cron matches a preset, highlight it
const currentCron = this.SelectedTask['cron'];
this.selectedCronPreset = this.predefinedCrons.find(cron => cron.value === currentCron) || null;
}
onCronInputChange(event: any): void {
this.SelectedTask['cron'] = event.target.value;
this.selectedCronPreset = null;
// Check if the entered value matches any preset
const enteredValue = event.target.value;
const matchingPreset = this.predefinedCrons.find(cron => cron.value === enteredValue);
if (matchingPreset) {
this.selectedCronPreset = matchingPreset;
}
}
getCronDescription(): string {
if (this.selectedCronPreset) {
return this.selectedCronPreset.description;
}
const currentCron = this.SelectedTask['cron'];
const matchingPreset = this.predefinedCrons.find(cron => cron.value === currentCron);
return matchingPreset ? matchingPreset.description : 'Custom cron expression';
}
onTaskTypeChange(): void {
if (this.SelectedTask['task_type'] === 'snippet') {
this.loadSnippets();
}
}
loadSnippets(): void {
var _self = this;
_self.data_provider.get_snippets("", "", "", 0, 10).then((res) => {
_self.Snippets = res.map((x: any) => {
return { id: x.id, name: x.name };
});
});
}
}

View file

@ -9,6 +9,9 @@ import {
GridModule,
ModalModule,
ButtonGroupModule,
BadgeModule,
AlertModule,
ToastModule,
} from "@coreui/angular";
import { UserTasksRoutingModule } from "./user_tasks-routing.module";
import { UserTasksComponent } from "./user_tasks.component";
@ -25,6 +28,9 @@ import { NgxSuperSelectModule} from "ngx-super-select";
FormModule,
ButtonModule,
ButtonGroupModule,
BadgeModule,
AlertModule,
ToastModule,
GuiGridModule,
ModalModule,
ReactiveFormsModule,