Manage scheduler and cob jobs

This commit is contained in:
Jose Alberto Hernandez
2022-09-13 08:45:22 -05:00
parent 414fe2a2d7
commit a09853cee8
44 changed files with 1107 additions and 193 deletions
@@ -0,0 +1,13 @@
<mat-card class="container-card">
<mat-card-content>
<mat-tab-group #tabGroup mat-align-tabs="center">
<mat-tab label="Scheduler Jobs">
<mifosx-manage-scheduler-jobs></mifosx-manage-scheduler-jobs>
</mat-tab>
<mat-tab label="Workflow Jobs">
<mifosx-workflow-jobs></mifosx-workflow-jobs>
</mat-tab>
</mat-tab-group>
</mat-card-content>
</mat-card>
@@ -0,0 +1,27 @@
.container-card {
margin: 0 auto;
max-width: 80rem;
width: 90%;
padding: 0;
padding-bottom: 50px;
min-height: 500px;
.header {
padding: 1%;
.header-title-group {
.client-card-title {
margin-top: 1.4rem;
color: white;
width: 90%;
p {
color: white;
}
}
}
}
.navigation-tabs {
overflow: auto;
}
i:hover {
cursor: pointer;
}
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ManageJobsComponent } from './manage-jobs.component';
describe('ManageJobsComponent', () => {
let component: ManageJobsComponent;
let fixture: ComponentFixture<ManageJobsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ManageJobsComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ManageJobsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'mifosx-manage-jobs',
templateUrl: './manage-jobs.component.html',
styleUrls: ['./manage-jobs.component.scss']
})
export class ManageJobsComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}
@@ -0,0 +1,44 @@
<div class="container">
<mat-card>
<form [formGroup]="jobForm" (ngSubmit)="submit()">
<mat-card-content>
<div fxLayout="column">
<mat-form-field>
<mat-label>Job Name</mat-label>
<input matInput required formControlName="displayName">
<mat-error *ngIf="jobForm.controls.displayName.hasError('required')">
Job Name is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Cron Expression</mat-label>
<input matInput required formControlName="cronExpression">
<mat-error *ngIf="jobForm.controls.cronExpression.hasError('required')">
Cron Expression is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-checkbox labelPosition="before" formControlName="active">
Is job active?
</mat-checkbox>
</div>
<mat-card-actions fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center" fxLayoutGap="5px">
<button type="button" mat-raised-button [routerLink]="['../']">Cancel</button>
<button mat-raised-button color="primary" [disabled]="!jobForm.valid" *mifosxHasPermission="'UPDATE_SCHEDULER'">Submit</button>
</mat-card-actions>
</mat-card-content>
</form>
</mat-card>
</div>
@@ -0,0 +1,3 @@
.container {
max-width: 37rem;
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { EditSchedulerJobComponent } from './edit-scheduler-job.component';
describe('EditSchedulerJobComponent', () => {
let component: EditSchedulerJobComponent;
let fixture: ComponentFixture<EditSchedulerJobComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ EditSchedulerJobComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(EditSchedulerJobComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,68 @@
/** Angular Imports */
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { FormBuilder, Validators } from '@angular/forms';
/** Custom Services */
import { SystemService } from 'app/system/system.service';
/**
* Edit scheduler job component.
*/
@Component({
selector: 'mifosx-edit-scheduler-job',
templateUrl: './edit-scheduler-job.component.html',
styleUrls: ['./edit-scheduler-job.component.scss']
})
export class EditSchedulerJobComponent implements OnInit {
/** Job Data. */
jobData: any;
/** Job Form. */
jobForm: any;
/**
* Retrieves the selected job data from `resolve`.
* @param {FormBuilder} formBuilder Form Builder.
* @param {SystemService} systemService System Service.
* @param {Router} router Router for navigation.
* @param {ActivatedRoute} route Activated Route.
*/
constructor(private route: ActivatedRoute,
private systemService: SystemService,
private router: Router,
private formBuilder: FormBuilder ) {
this.route.data.subscribe((data: { jobSelected: any }) => {
this.jobData = data.jobSelected;
});
}
/**
* Creates and sets job form.
*/
ngOnInit() {
this.createJobForm();
}
/**
* Creates and sets job form.
*/
createJobForm() {
this.jobForm = this.formBuilder.group({
'displayName': [this.jobData.displayName, Validators.required],
'cronExpression': [this.jobData.cronExpression, Validators.required],
'active': [this.jobData.active]
});
}
/**
* Submits the edit job form.
*/
submit() {
this.systemService.updateScheduler(this.jobData.jobId, this.jobForm.value)
.subscribe(() => {
this.router.navigate(['../'], { relativeTo: this.route });
});
}
}
@@ -0,0 +1,31 @@
/** Angular Imports */
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
/** rxjs Imports */
import { Observable} from 'rxjs';
/** Custom Services */
import { SystemService } from '../../system.service';
/**
* Edit Scheduler Job data resolver.
*/
@Injectable()
export class ManageSchedulerJobResolver implements Resolve<Object> {
/**
* @param {SystemService} systemService System service.
*/
constructor(private systemService: SystemService) {}
/**
* Returns the edit scheduler jobs data.
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const jobId = route.paramMap.get('id');
return this.systemService.getSelectedJob(jobId);
}
}
@@ -0,0 +1,149 @@
<div class="container m-b-20" fxLayout="row" fxLayoutGap="20px">
<div #schedulerStatus>
<h2 class="no-m">Scheduler Status: {{ schedulerActive ? 'Active' : 'Inactive' }}</h2>
</div>
<button mat-raised-button class="suspend">
<fa-icon icon="times-circle"></fa-icon>&nbsp;&nbsp;
Suspend
</button>
<button mat-raised-button class="activate" (click)="activateScheduler()" *ngIf="!schedulerActive">
<fa-icon icon="times-circle"></fa-icon>&nbsp;&nbsp;
Activate
</button>
</div>
<div class="container">
<div #filter fxLayout="row" fxLayoutGap="20px">
<mat-form-field fxFlex>
<mat-label>Filter</mat-label>
<input matInput (keyup)="applyFilter($event.target.value)">
</mat-form-field>
</div>
<div class="container space-top" fxLayout="row" fxLayoutGap="20px">
<button mat-raised-button color="primary" (click)="runSelectedJobs()">
<fa-icon icon="play"></fa-icon>&nbsp;&nbsp;
Run Selected Jobs
</button>
</div>
<div #jobsTable class="mat-elevation-z8 space-top">
<table mat-table [dataSource]="dataSource" matSort>
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef>
<mat-checkbox (change)="$event ? masterToggle() : null" [checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</th>
<td mat-cell *matCellDef="let row">
<mat-checkbox (click)="$event.stopPropagation()" (change)="$event ? selection.toggle(row) : null"
[checked]="selection.isSelected(row)">
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="displayName">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Name </th>
<td mat-cell *matCellDef="let job"> {{ job.displayName }} </td>
</ng-container>
<ng-container matColumnDef="nextRunTime">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Next Run </th>
<td mat-cell *matCellDef="let job"> {{ job.nextRunTime }} </td>
</ng-container>
<ng-container matColumnDef="previousRunTime">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Previous Run </th>
<td mat-cell *matCellDef="let job"><span *ngIf="job.lastRunHistory">{{ job.lastRunHistory.jobRunStartTime
}}</span></td>
</ng-container>
<ng-container matColumnDef="previousRunStatus">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Previous Run Status </th>
<td mat-cell *matCellDef="let job" class="center">
<div *ngIf="job.lastRunHistory">
<fa-icon *ngIf="job.lastRunHistory.status === 'success'" class="success" matTooltip="Successful"
matTooltipPosition="right" icon="check-circle" size="lg"></fa-icon>
<fa-icon *ngIf="!(job.lastRunHistory.status == 'success')" class="fail" matTooltip="Failed"
matTooltipPosition="right" icon="times-circle" size="lg"></fa-icon>
</div>
</td>
</ng-container>
<ng-container matColumnDef="currentlyRunning">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Currently Running </th>
<td mat-cell *matCellDef="let job" class="center">
<div [className]="job.currentlyRunning === true ? 'currently-running' : 'not-currently-running'">
<fa-icon matTooltip="{{ job.currentlyRunning === true ? 'Yes' : 'No' }}" matTooltipPosition="right"
icon="circle" size="lg"></fa-icon>
</div>
</td>
</ng-container>
<ng-container matColumnDef="errorLog">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Error Log </th>
<td mat-cell *matCellDef="let job" class="center">
<button mat-icon-button class="errorlog"
*ngIf="!(job.lastRunHistory && job.lastRunHistory.status == 'success')" matTooltip="Error Log"
matTooltipPosition="right">
<fa-icon icon="exclamation-circle" size="lg"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" [routerLink]="[row.jobId]" class="select-row"></tr>
</table>
<mat-paginator [pageSizeOptions]="[50, 100]" showFirstLastButtons></mat-paginator>
</div>
</div>
<div class="container space-top" fxLayout="row" fxLayoutGap="20px">
<button mat-raised-button color="primary" (click)="runSelectedJobs()">
<fa-icon icon="play"></fa-icon>&nbsp;&nbsp;
Run Selected Jobs<ng-template #templateSchedulerJobs let-data let-popover="popover">
<h2>Template</h2>
<p>This is the template I am trying to use. Is that fine.</p>
<button (click)="popover.close();nextStepSchedulerJobs()">Next</button>
</ng-template>
</button>
<button mat-raised-button color="primary" (click)="refresh()">
<fa-icon icon="sync"></fa-icon>&nbsp;&nbsp;
Refresh
</button>
</div>
<ng-template #templateSchedulerStatus let-data let-popover="popover">
<h4>Scheduler Status. Button will be used to change the status.</h4>
<div fxLayout="row" fxLayoutAlign="end" fxLayoutGap="2%" fxLayout.lt-md="column">
<button mat-raised-button color="warn" (click)="popover.close();configurationWizardService.closeConfigWizard()">Close</button>
<button mat-raised-button color="primary" (click)="popover.close();previousStep()">Back</button>
<button mat-raised-button color="primary" (click)="popover.close();showPopover(templateFilter, filter, 'bottom', true)">Next</button>
</div>
</ng-template>
<ng-template #templateFilter let-data let-popover="popover">
<h4>Search bar to filter jobs.</h4>
<div fxLayout="row" fxLayoutAlign="end" fxLayoutGap="2%" fxLayout.lt-md="column">
<button mat-raised-button color="warn" (click)="popover.close();configurationWizardService.closeConfigWizard()">Close</button>
<button mat-raised-button color="primary" (click)="popover.close();showPopover(templateSchedulerStatus,schedulerStatus, 'bottom', true)">Back</button>
<button mat-raised-button color="primary" (click)="popover.close();showPopover(templateJobsTable,jobsTable, 'top', true)">Next</button>
</div>
</ng-template>
<ng-template #templateJobsTable let-data let-popover="popover">
<h4>List of all scheduled batch jobs <a href="https://mifosforge.jira.com/wiki/spaces/docs/pages/67895356/Manage+Scheduler+Jobs" target="_blank">Schedular Jobs</a></h4>
<div fxLayout="row" fxLayoutAlign="end" fxLayoutGap="2%" fxLayout.lt-md="column">
<button mat-raised-button color="warn" (click)="popover.close();configurationWizardService.closeConfigWizard()">Close</button>
<button mat-raised-button color="primary" (click)="popover.close();showPopover(templateFilter, filter, 'bottom', true)">Back</button>
<button mat-raised-button color="primary" (click)="popover.close();nextStep()">Next</button>
</div>
</ng-template>
@@ -0,0 +1,41 @@
table {
width: 100%;
.select-row:hover {
cursor: pointer;
}
}
.success {
color: #32cd32;
}
.fail {
color: #f44366;
}
.currently-running {
color: #32cd32;
}
.not-currently-running {
color: #f44366;
}
.errorlog {
color: #ffa726;
}
.suspend {
background: #32cd32;
color: #ffffff;
}
.activate {
background: #f44366;
color: #ffffff;
}
.space-top {
margin-top: 30px;
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ManageSchedulerJobsComponent } from './manage-scheduler-jobs.component';
describe('ManageSchedulerJobsComponent', () => {
let component: ManageSchedulerJobsComponent;
let fixture: ComponentFixture<ManageSchedulerJobsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ManageSchedulerJobsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ManageSchedulerJobsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,237 @@
/** Angular Imports */
import { Component, OnInit, TemplateRef, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router';
import { SelectionModel } from '@angular/cdk/collections';
import { MatDialog } from '@angular/material/dialog';
/** rxjs Imports */
import { of } from 'rxjs';
import { SystemService } from '../../system.service';
/** Custom Services */
import { PopoverService } from '../../../configuration-wizard/popover/popover.service';
import { ConfigurationWizardService } from '../../../configuration-wizard/configuration-wizard.service';
/** Custom Dialog Component */
import { NextStepDialogComponent } from '../../../configuration-wizard/next-step-dialog/next-step-dialog.component';
/**
* Manage scheduler jobs component.
*/
@Component({
selector: 'mifosx-manage-scheduler-jobs',
templateUrl: './manage-scheduler-jobs.component.html',
styleUrls: ['./manage-scheduler-jobs.component.scss']
})
export class ManageSchedulerJobsComponent implements OnInit, AfterViewInit {
/** Jobs data. */
jobData: any;
/** Scheduler data */
schedulerData: any;
/** Columns to be displayed in manage scheduler jobs table. */
displayedColumns: string[] = ['select', 'displayName', 'nextRunTime', 'previousRunTime', 'previousRunStatus', 'currentlyRunning', 'errorLog'];
/** Data source for manage scheduler jobs table. */
dataSource: MatTableDataSource<any>;
/** Initialize Selection */
selection = new SelectionModel<any>(true, []);
/** Scheduler Status */
schedulerActive = false;
/** Jobs counter */
jobsCounter: any = 0;
/** Paginator for table. */
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
/** Sorter for manage scheduler jobs table. */
@ViewChild(MatSort, { static: true }) sort: MatSort;
/* Reference of scheduler status */
@ViewChild('schedulerStatus', { static: true }) schedulerStatus: ElementRef<any>;
/* Template for popover on scheduler status */
@ViewChild('templateSchedulerStatus', { static: true }) templateSchedulerStatus: TemplateRef<any>;
/* Reference of jobs table */
@ViewChild('jobsTable', { static: true }) jobsTable: ElementRef<any>;
/* Template for popover on jobs table */
@ViewChild('templateJobsTable') templateJobsTable: TemplateRef<any>;
/**
* Retrieves the scheduler jobs data from `resolve`.
* @param {ActivatedRoute} route Activated Route.
* @param {Matdialog} matdialog Matdialog.
* @param {Router} router Router for navigation.
* @param {ConfigurationWizardService} configurationWizardService ConfigurationWizard Service.
* @param {PopoverService} popoverService PopoverService.
*/
constructor(private route: ActivatedRoute,
private systemService: SystemService,
private router: Router,
private dialog: MatDialog,
private configurationWizardService: ConfigurationWizardService,
private popoverService: PopoverService) {
this.route.data.subscribe((data: { jobsScheduler: any }) => {
this.jobData = data.jobsScheduler[0];
this.schedulerData = data.jobsScheduler[1];
});
}
/**
* Filters data in manage scheduler jobs table based on passed value.
* @param {string} filterValue Value to filter data.
*/
applyFilter(filterValue: string) {
this.dataSource.filter = filterValue.trim().toLowerCase();
}
/**
* Whether the number of selected elements matches the total number of rows.
*/
isAllSelected() {
const numSelected = this.selection.selected.length;
return numSelected === this.jobsCounter;
}
/**
* Selects all rows if they are not all selected; otherwise clear selection.
*/
masterToggle() {
this.isAllSelected() ?
this.selection.clear() :
this.dataSource.data.forEach(row => this.selection.select(row));
}
/**
* Sets the manage scheduler jobs table.
*/
ngOnInit() {
this.setJobs();
this.getScheduler();
}
/**
* Initializes the data source, paginator and sorter for manage scheduler jobs table.
*/
setJobs() {
this.systemService.getJobs()
.subscribe((jobData: any) => {
this.dataSource = new MatTableDataSource(jobData);
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
this.jobsCounter = jobData.length;
this.selection.clear();
this.dataSource.sortingDataAccessor = (item, property) => {
switch (property) {
case 'previousRunStatus': return item.lastRunHistory.status;
case 'errorLog': return item.lastRunHistory.status;
case 'previousRunTime': return new Date(item.lastRunHistory.jobRunStartTime);
case 'nextRunTime': return new Date(item.nextRunTime);
default: return item[property];
}
};
});
}
getScheduler() {
this.systemService.getScheduler()
.subscribe((schedulerData: any) => {
this.schedulerData = schedulerData;
this.schedulerActive = this.schedulerData.active;
});
}
suspendScheduler(): void {
this.systemService.runCommandOnScheduler('stop')
.subscribe(() => {
this.getScheduler();
});
}
activateScheduler(): void {
this.systemService.runCommandOnScheduler('start')
.subscribe(() => {
this.getScheduler();
});
}
runSelectedJobs(): void {
this.selection.selected.forEach((job) => {
this.systemService.runSelectedJob(job.jobId);
});
}
refresh(): void {
this.setJobs();
}
/**
* Popover function
* @param template TemplateRef<any>.
* @param target HTMLElement | ElementRef<any>.
* @param position String.
* @param backdrop Boolean.
*/
showPopover(template: TemplateRef<any>, target: HTMLElement | ElementRef<any>, position: string, backdrop: boolean): void {
setTimeout(() => this.popoverService.open(template, target, position, backdrop, {}), 200);
}
/**
* To show popover.
*/
ngAfterViewInit() {
if (this.configurationWizardService.showSchedulerJobsPage === true) {
setTimeout(() => {
this.showPopover(this.templateSchedulerStatus, this.schedulerStatus.nativeElement, 'bottom', true);
});
}
if (this.configurationWizardService.showSchedulerJobsList === true) {
setTimeout(() => {
this.showPopover(this.templateJobsTable, this.jobsTable.nativeElement, 'top', true);
});
}
}
/**
* Opens Dialog for next step.
*/
nextStep() {
this.configurationWizardService.showSchedulerJobsPage = false;
this.configurationWizardService.showSchedulerJobsList = false;
this.openNextStepDialog();
}
/**
* Previous Step (Scheduler Jobs) Configuration Wizard.
*/
previousStep() {
this.configurationWizardService.showSchedulerJobsPage = false;
this.configurationWizardService.showSchedulerJobsList = false;
this.configurationWizardService.showSchedulerJobs = true;
this.router.navigate(['/system']);
}
/**
* Next Step (Accounting) Dialog Configuration Wizard.
*/
openNextStepDialog() {
const nextStepDialogRef = this.dialog.open( NextStepDialogComponent, {
data: {
nextStepName: 'Setup Accounting',
previousStepName: 'System',
stepPercentage: 60
},
});
nextStepDialogRef.afterClosed().subscribe((response: { nextStep: boolean }) => {
if (response.nextStep) {
this.configurationWizardService.showSchedulerJobsPage = false;
this.configurationWizardService.showSchedulerJobsList = false;
this.configurationWizardService.showChartofAccounts = true;
this.router.navigate(['/accounting']);
} else {
this.configurationWizardService.showSchedulerJobsPage = false;
this.configurationWizardService.showSchedulerJobsList = false;
this.router.navigate(['/home']);
}
});
}
}
@@ -0,0 +1,33 @@
/** Angular Imports */
import { Injectable } from '@angular/core';
import { Resolve } from '@angular/router';
/** rxjs Imports */
import { Observable, forkJoin } from 'rxjs';
/** Custom Services */
import { SystemService } from '../system.service';
/**
* Manage Scheduler Jobs data resolver.
*/
@Injectable()
export class ManageSchedulerJobsResolver implements Resolve<Object> {
/**
* @param {SystemService} systemService System service.
*/
constructor(private systemService: SystemService) {}
/**
* Returns the manage scheduler jobs data.
* @returns {Observable<any>}
*/
resolve() {
return forkJoin([
this.systemService.getJobs(),
this.systemService.getScheduler()
]);
}
}
@@ -0,0 +1,61 @@
<div class="container">
<div fxLayout="row" fxLayoutGap="20px">
<mat-form-field fxFlex>
<mat-label>Filter By Version</mat-label>
<input matInput (keyup)="applyFilter($event.target.value)">
</mat-form-field>
</div>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" matSort>
<ng-container matColumnDef="version" >
<th mat-header-cell *matHeaderCellDef mat-sort-header> Version </th>
<td mat-cell *matCellDef="let jobHistoryData"> {{ jobHistoryData.version }} </td>
</ng-container>
<ng-container matColumnDef="run_start_time">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Start time </th>
<td mat-cell *matCellDef="let jobHistoryData">{{ jobHistoryData.jobRunStartTime | datetimeFormat }}</td>
</ng-container>
<ng-container matColumnDef="run_end_time">
<th mat-header-cell *matHeaderCellDef mat-sort-header> End time </th>
<td mat-cell *matCellDef="let jobHistoryData">{{ jobHistoryData.jobRunEndTime | datetimeFormat }}</td>
</ng-container>
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Status </th>
<td mat-cell *matCellDef="let jobHistoryData">
<fa-icon *ngIf="jobHistoryData.status === 'success'" class="success" matTooltip="Successful" matTooltipPosition="right" icon="check-circle" size="lg"></fa-icon>
<fa-icon *ngIf="!(jobHistoryData.status == 'success')" class="fail" matTooltip="Failed" matTooltipPosition="right" icon="times-circle" size="lg"></fa-icon>
</td>
</ng-container>
<ng-container matColumnDef="run_type">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Run Type </th>
<td mat-cell *matCellDef="let jobHistoryData">{{ jobHistoryData.triggerType }}</td>
</ng-container>
<ng-container matColumnDef="error_log">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Error Log </th>
<td mat-cell *matCellDef="let jobHistoryData">
<button mat-icon-button class="errorlog" *ngIf="!(jobHistoryData.status == 'success')" matTooltip="Error Log" matTooltipPosition="right" (click)="openError(jobHistoryData.version)">
<fa-icon icon="exclamation-circle" size="lg"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" class="select-row"></tr>
</table>
<mat-paginator [pageSizeOptions]="[10, 25, 50, 100]" showFirstLastButtons></mat-paginator>
</div>
</div>
@@ -0,0 +1,32 @@
table {
width: 100%;
.select-row:hover {
cursor: pointer;
}
}
.success {
color: #32cd32;
}
.fail {
color: #f44366;
}
.currently-running {
color: #32cd32;
}
.not-currently-running {
color: #f44366;
}
.errorlog {
color: #ffa726;
}
.suspend {
background: #ffa726;
color: #ffffff;
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ViewHistorySchedulerJobComponent } from './view-history-scheduler-job.component';
describe('ViewHistorySchedulerJobComponent', () => {
let component: ViewHistorySchedulerJobComponent;
let fixture: ComponentFixture<ViewHistorySchedulerJobComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ViewHistorySchedulerJobComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ViewHistorySchedulerJobComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,95 @@
/** Angular Imports. */
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { MatSort } from '@angular/material/sort';
import { MatDialog } from '@angular/material/dialog';
import { ErrorDialogComponent } from 'app/shared/error-dialog/error-dialog.component';
import { ActivatedRoute, Router } from '@angular/router';
@Component({
selector: 'mifosx-view-history-scheduler-job',
templateUrl: './view-history-scheduler-job.component.html',
styleUrls: ['./view-history-scheduler-job.component.scss']
})
export class ViewHistorySchedulerJobComponent implements OnInit {
/** Job History data. */
jobHistoryData: any;
/** Columns to be displayed in Scheduler Job History. */
displayedColumns: string[] = ['version', 'run_start_time', 'run_end_time', 'status', 'run_type', 'error_log'];
/** Data source for Scheduler Job History table. */
dataSource: MatTableDataSource<any>;
/** Paginator for Scheduler Job History table. */
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
/** Sorter for Scheduler Job History table. */
@ViewChild(MatSort, { static: true }) sort: MatSort;
/**
* Retrieves the scheduler Job History data from `resolve`.
* @param {ActivatedRoute} route Activated Route.
*/
constructor(private route: ActivatedRoute,
private dialog: MatDialog,
private router: Router ) {
this.route.data.subscribe(( data: { jobsSchedulerHistory: any }) => {
this.jobHistoryData = data.jobsSchedulerHistory;
console.log(this.jobHistoryData);
});
}
/**
* Filters data in scheduler Job History table based on passed value.
* @param {string} filterValue Value to filter data.
*/
applyFilter(filterValue: string) {
const filterObject = [{
id: 'version',
value: filterValue
}];
this.dataSource.filter = JSON.stringify(filterObject);
}
ngOnInit() {
this.setJobHistory();
}
/**
* Initializes the data source, paginator and sorter for scheduler Job History table.
*/
setJobHistory() {
this.dataSource = new MatTableDataSource(this.jobHistoryData.pageItems);
this.dataSource.paginator = this.paginator;
/** Search By Version */
this.dataSource.filterPredicate =
(data: any, filtersJson: string) => {
const matchFilter: any[] = [];
const filters = JSON.parse(filtersJson);
filters.forEach((filter: any) => {
const val = data[filter.id] === null ? '' : data[filter.id];
if (filter.value !== '') {
matchFilter.push(val === parseInt(filter.value, 10));
} else if (filter.value === '') {
matchFilter.push(val.toString().toLowerCase().includes(filter.value.toLowerCase()));
}
});
return matchFilter.every(Boolean);
};
}
/**
* Open Error Dialog.
* @param version version Id.
*/
openError(version: any) {
const openErrorLogDialog = this.dialog.open(ErrorDialogComponent, {
width: '400px',
data: this.jobHistoryData.pageItems.filter( (data: any) => data.version === version )[0].jobRunErrorLog
});
openErrorLogDialog.afterClosed().subscribe((response: any) => {
this.router.navigate(['']);
});
}
}
@@ -0,0 +1,31 @@
/** Angular Imports */
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
/** rxjs Imports */
import { Observable, forkJoin } from 'rxjs';
/** Custom Services */
import { SystemService } from '../../../system.service';
/**
* View History Scheduler Jobs data resolver.
*/
@Injectable()
export class ViewHistorySchedulerJobsResolver implements Resolve<Object> {
/**
* @param {SystemService} systemService System service.
*/
constructor(private systemService: SystemService) {}
/**
* Returns the Scheduler Jobs History data.
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const jobId = route.paramMap.get('id');
return this.systemService.getHistoryScheduler(jobId);
}
}
@@ -0,0 +1,57 @@
<div class="container m-b-20" fxLayout="row" fxLayout.lt-md="column" fxLayoutAlign="end" fxLayoutGap="2%">
<button mat-raised-button color="primary" [routerLink]="['edit']">
<fa-icon icon="edit"></fa-icon>&nbsp;&nbsp;
Edit
</button>
<button mat-raised-button color="primary" [routerLink]="['viewhistory']">
View History
</button>
</div>
<div class="container">
<mat-card>
<mat-card-content>
<div fxLayout="row wrap" class="content">
<div fxFlex="50%" class="mat-body-strong">
Job Name
</div>
<div fxFlex="50%">
{{ jobData.displayName }}
</div>
<div fxFlex="50%" class="mat-body-strong">
Cron Expression
</div>
<div fxFlex="50%">
{{ jobData.cronExpression }}
</div>
<div fxFlex="50%" class="mat-body-strong">
Is Active Job?
</div>
<div fxFlex="50%">
{{ jobData.active }}
</div>
<div fxFlex="50%" class="mat-body-strong">
Follow
</div>
<div fxFlex="50%">
<a href="http://www.cronmaker.com/"> Click Here To Generate Cron Expression </a>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
@@ -0,0 +1,14 @@
a{
color: blue;
}
.container {
max-width: 37rem;
.content {
div {
margin: 1rem 0;
word-wrap: break-word;
}
}
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ViewSchedulerJobComponent } from './view-scheduler-job.component';
describe('ViewSchedulerJobComponent', () => {
let component: ViewSchedulerJobComponent;
let fixture: ComponentFixture<ViewSchedulerJobComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ViewSchedulerJobComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ViewSchedulerJobComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,28 @@
/** Angular Imports */
import { Component} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
/**
* View Scheduler Job component.
*/
@Component({
selector: 'mifosx-view-scheduler-job',
templateUrl: './view-scheduler-job.component.html',
styleUrls: ['./view-scheduler-job.component.scss']
})
export class ViewSchedulerJobComponent {
/** Job Data. */
jobData: any;
/**
* Retrieves the selected job data from `resolve`.
* @param {ActivatedRoute} route Activated Route.
*/
constructor(private route: ActivatedRoute) {
this.route.data.subscribe((data: { selectedJob: any }) => {
this.jobData = data.selectedJob;
});
}
}
@@ -0,0 +1,31 @@
/** Angular Imports */
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
/** rxjs Imports */
import { Observable } from 'rxjs';
/** Custom Services */
import { SystemService } from '../../../system.service';
/**
* Selected Scheduler Jobs data resolver.
*/
@Injectable()
export class ViewSchedulerJobResolver implements Resolve<Object> {
/**
* @param {SystemService} systemService System service.
*/
constructor(private systemService: SystemService) {}
/**
* Returns the selected scheduler job data.
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const jobId = route.paramMap.get('id');
return this.systemService.getSelectedJob(jobId);
}
}
@@ -0,0 +1,37 @@
<ngx-graph [view]="diagramSize" [links]="links" [nodes]="nodes" [curve]="curve" [layout]="layout"
[nodeHeight]="100" [layoutSettings]="layoutSettings" [enableZoom]="true" [center$]="center$">
<ng-template #defsTemplate>
<svg:marker id="arrow" viewBox="0 -5 10 10" refX="8" refY="0" markerWidth="4" markerHeight="4" orient="auto">
<svg:path d="M0,-5L10,0L0,5" class="arrow-head" />
</svg:marker>
</ng-template>
<ng-template #nodeTemplate let-node>
<svg:g class="node" xmlns="http://www.w3.org/2000/xhtml" width="150" height="100">
<svg:foreignObject width="150" height="100">
<xhtml:div class="card-container" xmlns="http://www.w3.org/1999/xhtml">
<label class="name">{{ node.data.name }}</label>
<label>{{ node.data.order }}</label>
</xhtml:div>
</svg:foreignObject>
</svg:g>
</ng-template>
<ng-template #linkTemplate let-link>
<svg:g class="edge">
<svg:path class="line" stroke-width="2" marker-end="url(#arrow)"></svg:path>
<svg:text class="edge-label" text-anchor="middle">
<textPath class="text-path" [attr.href]="'#' + link.id" [style.dominant-baseline]="link.dominantBaseline"
startOffset="50%">
{{ link.label }}
</textPath>
</svg:text>
</svg:g>
<svg:g class="link-midpoint" *ngIf="link.midPoint"
[attr.transform]="'translate(' + link.midPoint.x + ',' + link.midPoint.y + ')'">
<ellipse rx="30" ry="10" />
<svg:text alignment-baseline="central">{{ link.data.linkText }}</svg:text>
</svg:g>
</ng-template>
</ngx-graph>
@@ -0,0 +1,40 @@
:host ::ng-deep {
display: block;
height: inherit;
width: inherit;
.card-container {
height: 100;
min-width: 150;
display: block;
fill: #d7dada;
border: #000;
.name {
font-size: 12px;
}
label {
display: block;
text-align: center;
font-size: 20px;
margin-top: 4px;
margin-bottom: 8px;
}
}
.link-midpoint {
ellipse {
fill: white;
stroke: black;
stroke-width: 1;
}
text {
stroke: transparent;
fill: black;
text-anchor: middle;
font-size: 8px;
}
}
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { WorkflowDiagramComponent } from './workflow-diagram.component';
describe('WorkflowDiagramComponent', () => {
let component: WorkflowDiagramComponent;
let fixture: ComponentFixture<WorkflowDiagramComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ WorkflowDiagramComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(WorkflowDiagramComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,74 @@
import { Component, Input, OnInit } from '@angular/core';
import { DagreNodesOnlyLayout, Edge, Layout, Node } from '@swimlane/ngx-graph';
import * as shape from 'd3-shape';
import { Subject } from 'rxjs';
export class JobStep {
id: number;
stepName: string;
order: number;
}
@Component({
selector: 'mifosx-workflow-diagram',
templateUrl: './workflow-diagram.component.html',
styleUrls: ['./workflow-diagram.component.scss']
})
export class WorkflowDiagramComponent implements OnInit {
@Input() jobStepsData: JobStep[] = [];
diagramSize: [number, number] = [1024, 300];
public nodes: Node[] = [];
public links: Edge[] = [];
public layoutSettings = {
orientation: 'LR'
};
public curve: any = shape.curveLinear;
public layout: Layout = new DagreNodesOnlyLayout();
colorScheme = {
domain: ['#5AA454', '#A10A28', '#C7B42C']
};
center$ = new Subject<any>();
constructor() { }
ngOnInit(): void {
let nodeCounter = 0;
for (const jobStep of this.jobStepsData) {
const currentNode = `node_${jobStep.order}`;
const node: Node = {
id: currentNode,
label: jobStep.stepName,
data: {
name: jobStep.stepName,
order: jobStep.order
}
};
this.nodes.push(node);
if (nodeCounter > 0) {
const edge: Edge = {
id: `link_${(jobStep.id)}`,
source: `node_${(jobStep.order - 1)}`,
target: currentNode,
label: '',
data: {
linkText: 'Precedes of'
}
};
this.links.push(edge);
}
nodeCounter++;
}
// trigger center
this.center$.next();
}
public getStyles(node: Node): any {
return 'node_odd';
}
}
@@ -0,0 +1,62 @@
<div class="container" fxLayout="row wrap" fxLayoutGap="2%" fxLayout.lt-md="column">
<mat-form-field fxFlex="31%">
<mat-label>Job Name</mat-label>
<mat-select [formControl]="jobName" (selectionChange)="getWorkflowJobSteps($event.value)">
<mat-option *ngFor="let job of jobNameOptions" [value]="job">
{{ job }}
</mat-option>
</mat-select>
</mat-form-field>
<div class="action-button" fxLayoutGap="25px" *ngIf="jobStepsData.length > 0">
<button mat-raised-button color="primary">
<fa-icon icon="plus"></fa-icon>&nbsp;&nbsp;Link Job Step
</button>
</div>
<div class="mat-elevation-z8 container" *ngIf="jobStepsData.length > 0">
<table mat-table [dataSource]="jobStepsData" #table cdkDropList [cdkDropListData]="jobStepsData"
(cdkDropListDropped)="dropTable($event)">
<ng-container matColumnDef="stepName">
<th mat-header-cell *matHeaderCellDef> Step Name </th>
<td mat-cell *matCellDef="let jobStep"> {{ jobStep.stepName }} </td>
</ng-container>
<ng-container matColumnDef="stepOrder">
<th mat-header-cell *matHeaderCellDef> Order </th>
<td mat-cell *matCellDef="let jobStep"> {{ jobStep.order }} </td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> Actions </th>
<td mat-cell *matCellDef="let row; let rowIndex = index">
<button type="button" mat-icon-button color="warn" (click)="removeJobStep(rowIndex)" matTooltip="Delete"
matTooltipPosition="left">
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row cdkDrag [cdkDragData]="row" *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
<div *ngIf="stepOrderHasChanged" fxLayout="row" class="container margin-t space-top" fxLayout.xs="column"
fxLayoutAlign="center" fxLayoutGap="2%">
<button mat-raised-button [routerLink]="['../']">
Cancel
</button>
<button mat-raised-button color="primary" (click)="saveChanges()">
Apply Changes
</button>
</div>
<div class="container space-top" *ngIf="jobStepsData.length > 0 && !stepOrderHasChanged">
<mifosx-workflow-diagram [jobStepsData]="jobStepsData"></mifosx-workflow-diagram>
</div>
</div>
@@ -0,0 +1,35 @@
.container {
margin: auto !important;
}
.workflow-graph {
min-height: 400px;
}
.space-top {
margin-top: 20px !important;
}
.space-bottom {
margin-bottom: 20px !important;
}
.cdk-drag-preview {
box-sizing: border-box;
border-radius: 4px;
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
0 8px 10px 1px rgba(0, 0, 0, 0.14),
0 3px 14px 2px rgba(0, 0, 0, 0.12);
}
.cdk-drag-placeholder {
opacity: 0;
}
.cdk-drag-animating {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.cdk-drop-list-dragging .mat-row:not(.cdk-drag-placeholder) {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { WorkflowJobsComponent } from './workflow-job.component';
describe('WorkflowJobsComponent', () => {
let component: WorkflowJobsComponent;
let fixture: ComponentFixture<WorkflowJobsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ WorkflowJobsComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(WorkflowJobsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,97 @@
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { Component, OnInit, ViewChild } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { MatTable } from '@angular/material/table';
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
import { SystemService } from 'app/system/system.service';
import { JobStep } from './workflow-diagram/workflow-diagram.component';
@Component({
selector: 'mifosx-workflow-jobs',
templateUrl: './workflow-jobs.component.html',
styleUrls: ['./workflow-jobs.component.scss']
})
export class WorkflowJobsComponent implements OnInit {
stepOrderHasChanged = false;
jobNameOptions: any = [];
jobStepsData: any = [];
jobStepsDataBase: any = [];
jobStepName: String = null;
jobName = new FormControl('', Validators.required);
@ViewChild('table') table: MatTable<JobStep>;
/** Columns to be displayed in the table. */
displayedColumns: string[] = ['stepName', 'stepOrder', 'actions'];
constructor(private systemService: SystemService,
public dialog: MatDialog) {}
ngOnInit(): void {
this.systemService.getWorkflowJobNames().toPromise()
.then(jobNames => {
this.jobNameOptions = jobNames;
});
}
/**
* Get the Workflow Job steps data
* @param {string} jobName Value to Workflow Job name.
*/
getWorkflowJobSteps(jobName: string) {
this.systemService.getWorkflowJobSteps(jobName).toPromise()
.then(data => {
this.jobStepName = jobName;
this.jobStepsData = data.businessSteps.sort(function (a: JobStep, b: JobStep) {
return a.order - b.order;
});
this.jobStepsDataBase = this.jobStepsData;
});
}
dropTable(event: CdkDragDrop<JobStep[]>) {
const prevIndex = this.jobStepsData.findIndex((d: JobStep) => d === event.item.data);
moveItemInArray(this.jobStepsData, prevIndex, event.currentIndex);
this.jobStepsData = [...this.jobStepsData];
this.table.renderRows();
this.stepOrderHasChanged = true;
}
/**
* Delete particular Job Step from Workflow Job
*/
removeJobStep(index: number) {
const dialogRef = this.dialog.open(DeleteDialogComponent, {
data: { deleteContext: `this` }
});
dialogRef.afterClosed().subscribe((response: any) => {
if (response.delete) {
this.jobStepsData.splice(index, 1);
this.jobStepsData = this.jobStepsData.concat([]);
this.jobStepsData = [...this.jobStepsData];
this.stepOrderHasChanged = true;
}
});
}
saveChanges() {
// Set the new Order
let stepOrder = 1;
this.jobStepsData.forEach((jobStep: JobStep) => {
jobStep.order = stepOrder++;
});
const payload = {
'businessSteps': this.jobStepsData
};
this.systemService.putWorkflowJobSteps(this.jobStepName, payload).toPromise()
.then(data => {
this.stepOrderHasChanged = false;
});
}
}
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { WorkflowJobResolver } from './workflow-jobs.resolver';
describe('WorkflowJobResolver', () => {
let resolver: WorkflowJobResolver;
beforeEach(() => {
TestBed.configureTestingModule({});
resolver = TestBed.inject(WorkflowJobResolver);
});
it('should be created', () => {
expect(resolver).toBeTruthy();
});
});
@@ -0,0 +1,24 @@
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
import { SystemService } from 'app/system/system.service';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class WorkflowJobResolver implements Resolve<boolean> {
/**
* @param {SystemService} systemService System service.
*/
constructor(private systemService: SystemService) {}
/**
* Returns the Configuration data.
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
return this.systemService.getWorkflowJobNames();
}
}