mirror of
https://github.com/MikroWizard/mikrofront.git
synced 2025-12-06 10:09:29 +00:00
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:
parent
b20a3d7826
commit
2f68c49936
4 changed files with 884 additions and 184 deletions
|
|
@ -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>
|
||||
|
||||
<div [cFormFloating]="true" class="mb-3">
|
||||
<input cFormControl id="floatingInput" placeholder="SelectedTask['description']"
|
||||
[(ngModel)]="SelectedTask['description']" />
|
||||
<label cLabel for="floatingInput">Description</label>
|
||||
<!-- 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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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 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">
|
||||
{{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>
|
||||
<!-- 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">
|
||||
<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">
|
||||
{{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>
|
||||
|
||||
|
|
@ -340,4 +491,6 @@
|
|||
Close
|
||||
</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
</c-modal>
|
||||
|
||||
<c-toaster position="fixed" placement="top-end"></c-toaster>
|
||||
371
src/app/views/user_tasks/user_tasks.component.scss
Normal file
371
src/app/views/user_tasks/user_tasks.component.scss
Normal 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; }
|
||||
|
|
@ -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 };
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue