Add Jobservice UI (#17722)

Signed-off-by: AllForNothing <sshijun@vmware.com>

Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
Shijun Sun 2022-11-18 14:39:26 +08:00 committed by GitHub
parent 321a9abfb3
commit ea812c9f16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 3294 additions and 9 deletions

View File

@ -123,6 +123,14 @@ const routes: Routes = [
'./left-side-nav/clearing-job/clearing-job.module'
).then(m => m.ClearingJobModule),
},
{
path: 'job-service-dashboard',
canActivate: [SystemAdminGuard],
loadChildren: () =>
import(
'./left-side-nav/job-service-dashboard/job-service-dashboard.module'
).then(m => m.JobServiceDashboardModule),
},
{
path: 'configs',
canActivate: [SystemAdminGuard],

View File

@ -167,6 +167,19 @@
clrVerticalNavIcon></clr-icon>
{{ 'CLEARANCES.CLEARANCES' | translate }}
</a>
<a
clrVerticalNavLink
*ngIf="hasAdminRole"
routerLink="/harbor/job-service-dashboard"
routerLinkActive="active">
<clr-icon
shape="dashboard"
clrVerticalNavIcon></clr-icon>
{{
'JOB_SERVICE_DASHBOARD.JOB_SERVICE_DASHBOARD'
| translate
}}
</a>
<a
clrVerticalNavLink
routerLinkActive="active"

View File

@ -44,6 +44,7 @@
</div>
<div class="cron-selection">
<cron-selection
class="w-100"
[externalValidation]="isValid()"
[labelCurrent]="getLabelCurrent"
[labelEdit]="getLabelCurrent"

View File

@ -44,6 +44,7 @@
</div>
<div class="cron-selection">
<cron-selection
class="w-100"
[labelCurrent]="getLabelCurrent"
[labelEdit]="getLabelCurrent"
[originCron]="originCron"

View File

@ -11,6 +11,7 @@
</div>
<div class="button-group">
<cron-selection
class="w-100"
[disabled]="!hasDefaultScanner"
[labelCurrent]="getLabelCurrent"
[labelEdit]="getLabelCurrent"

View File

@ -0,0 +1,49 @@
<h2 class="title">
<clr-icon class="dashboard" size="24" shape="dashboard"></clr-icon>
<span>{{ 'JOB_SERVICE_DASHBOARD.JOB_SERVICE_DASHBOARD' | translate }}</span>
</h2>
<div class="clr-row pr-10 mb-2">
<div class="clr-col">
<app-pending-job-card></app-pending-job-card>
</div>
<div class="clr-col">
<app-schedule-card></app-schedule-card>
</div>
<div class="clr-col">
<app-worker-card></app-worker-card>
</div>
</div>
<ul class="nav" role="tablist">
<li role="presentation" class="nav-item">
<button
id="pending-jobs"
class="btn btn-link nav-link"
type="button"
routerLink="pending-jobs"
routerLinkActive="active">
{{ 'JOB_SERVICE_DASHBOARD.JOB_QUEUE' | translate }}
</button>
</li>
<li role="presentation" class="nav-item">
<button
id="schedules"
class="btn btn-link nav-link"
type="button"
routerLink="schedules"
routerLinkActive="active">
{{ 'JOB_SERVICE_DASHBOARD.SCHEDULES' | translate }}
</button>
</li>
<li role="presentation" class="nav-item">
<button
id="workers"
class="btn btn-link nav-link"
type="button"
routerLink="workers"
routerLinkActive="active">
{{ 'JOB_SERVICE_DASHBOARD.WORKERS' | translate }}
</button>
</li>
</ul>
<router-outlet></router-outlet>

View File

@ -0,0 +1,25 @@
@mixin v-center {
display: flex;
align-items: center;
}
.title {
display: flex;
align-items: center;
margin-top: 0;
}
.dashboard {
margin-right: 0.5rem;
}
.pr-10 {
padding-right: 2rem;
}
.card-block {
height: 9rem;
}

View File

@ -0,0 +1,27 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SharedTestingModule } from 'src/app/shared/shared.module';
import { JobServiceDashboardComponent } from './job-service-dashboard.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
describe('JobServiceDashboardComponent', () => {
let component: JobServiceDashboardComponent;
let fixture: ComponentFixture<JobServiceDashboardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
schemas: [NO_ERRORS_SCHEMA],
imports: [SharedTestingModule],
declarations: [JobServiceDashboardComponent],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(JobServiceDashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,21 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
@Component({
selector: 'job-service-dashboard',
templateUrl: 'job-service-dashboard.component.html',
styleUrls: ['job-service-dashboard.component.scss'],
})
export class JobServiceDashboardComponent {}

View File

@ -0,0 +1,26 @@
export enum All {
ALL_WORKERS = 'all',
}
export enum PendingJobsActions {
PAUSE = 'pause',
RESUME = 'resume',
STOP = 'stop',
}
export const INTERVAL: number = 10000;
export enum ScheduleStatusString {
PAUSED = 'JOB_SERVICE_DASHBOARD.PAUSED',
RUNNING = 'JOB_SERVICE_DASHBOARD.RUNNING_STATUS',
}
export enum ScheduleExecuteBtnString {
RESUME_ALL = 'JOB_SERVICE_DASHBOARD.RESUME_ALL_BTN_TEXT',
PAUSE_ALL = 'JOB_SERVICE_DASHBOARD.PAUSE_ALL_BTN_TEXT',
}
export enum JobType {
SCHEDULER = 'scheduler',
ALL = 'all',
}

View File

@ -0,0 +1,51 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SharedModule } from 'src/app/shared/shared.module';
import { JobServiceDashboardComponent } from './job-service-dashboard.component';
import { PendingCardComponent } from './pending-job-card/pending-job-card.component';
import { PendingListComponent } from './pending-job-list/pending-job-list.component';
import { ScheduleCardComponent } from './schedule-card/schedule-card.component';
import { ScheduleListComponent } from './schedule-list/schedule-list.component';
import { DonutChartComponent } from './worker-card/donut-chart/donut-chart.component';
import { WorkerCardComponent } from './worker-card/worker-card.component';
import { WorkerListComponent } from './worker-list/worker-list.component';
const routes: Routes = [
{
path: '',
component: JobServiceDashboardComponent,
children: [
{
path: 'pending-jobs',
component: PendingListComponent,
},
{
path: 'schedules',
component: ScheduleListComponent,
},
{
path: 'workers',
component: WorkerListComponent,
},
{
path: '',
redirectTo: 'pending-jobs',
pathMatch: 'full',
},
],
},
];
@NgModule({
imports: [SharedModule, RouterModule.forChild(routes)],
declarations: [
JobServiceDashboardComponent,
DonutChartComponent,
PendingCardComponent,
ScheduleCardComponent,
WorkerCardComponent,
PendingListComponent,
ScheduleListComponent,
WorkerListComponent,
],
})
export class JobServiceDashboardModule {}

View File

@ -0,0 +1,49 @@
<div class="card">
<div class="card-header">
{{ 'JOB_SERVICE_DASHBOARD.PENDING_JOBS' | translate }}
</div>
<div class="card-block">
<div class="duration">
{{ 'REPLICATION.TOTAL' | translate }}: {{ total() }}
</div>
<ng-container *ngIf="!loading">
<div class="clr-row" *ngIf="jobQueue?.length">
<div class="clr-col-5 center">
<label class="clr-control-label">
{{ jobQueue[0]?.job_type }}
</label>
</div>
<div class="clr-col-7">{{ jobQueue[0]?.count || 0 }}</div>
</div>
<div class="clr-row" *ngIf="jobQueue?.length > 1">
<div class="clr-col-5 center">
<label class="clr-control-label">
{{ jobQueue[1]?.job_type }}
</label>
</div>
<div class="clr-col-7">{{ jobQueue[1]?.count || 0 }}</div>
</div>
<div class="clr-row" *ngIf="jobQueue?.length > 2">
<div class="clr-col-5 center">
<label class="clr-control-label">{{
'JOB_SERVICE_DASHBOARD.OTHERS' | translate
}}</label>
</div>
<div class="clr-col-7">{{ otherCount() }}</div>
</div>
</ng-container>
<div *ngIf="loading" class="loading">
<span class="spinner spinner-inline"></span>
</div>
</div>
<div class="card-footer">
<button
[disabled]="loadingStopAll || total() <= 0"
[clrLoading]="loadingStopAll"
(click)="stopAll()"
type="button"
class="btn btn-link">
{{ 'JOB_SERVICE_DASHBOARD.STOP_ALL' | translate }}
</button>
</div>
</div>

View File

@ -0,0 +1,23 @@
.duration {
width: 100%;
text-align: center;
font-size: 50px;
margin: 1.5rem 0 1rem;
}
.card-block {
height: 9rem;
}
.center {
display: flex;
align-items: center;
}
.loading {
display: flex;
height: 4rem;
align-items: center;
justify-content: center;
}

View File

@ -0,0 +1,62 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PendingCardComponent } from './pending-job-card.component';
import { SharedTestingModule } from '../../../../shared/shared.module';
import { JobQueue } from '../../../../../../ng-swagger-gen/models/job-queue';
import { of } from 'rxjs';
import { delay } from 'rxjs/operators';
describe('PendingCardComponent', () => {
let component: PendingCardComponent;
let fixture: ComponentFixture<PendingCardComponent>;
const mockedJobs: JobQueue[] = [
{
job_type: 'test1',
count: 1,
},
{
job_type: 'test2',
count: 2,
},
{
job_type: 'test3',
count: 3,
},
];
const fakedJobserviceService = {
listJobQueues() {
return of(mockedJobs).pipe(delay(0));
},
};
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [PendingCardComponent],
imports: [SharedTestingModule],
}).compileComponents();
fixture = TestBed.createComponent(PendingCardComponent);
component = fixture.componentInstance;
spyOn(component, 'loopGetPendingJobs').and.callFake(() => {
fakedJobserviceService.listJobQueues().subscribe(res => {
component.loading = false;
component.jobQueue = res.sort((a, b) => {
const ACount: number = a?.count | 0;
const BCount: number = b?.count | 0;
return BCount - ACount;
});
});
});
fixture.autoDetectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should render data', async () => {
await fixture.whenStable();
expect(component.total()).toEqual(6);
});
});

View File

@ -0,0 +1,185 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ConfirmationDialogService } from '../../../global-confirmation-dialog/confirmation-dialog.service';
import { JobserviceService } from '../../../../../../ng-swagger-gen/services/jobservice.service';
import { MessageHandlerService } from '../../../../shared/services/message-handler.service';
import { JobQueue } from '../../../../../../ng-swagger-gen/models/job-queue';
import { finalize } from 'rxjs/operators';
import {
INTERVAL,
JobType,
PendingJobsActions,
} from '../job-service-dashboard.interface';
import {
ConfirmationButtons,
ConfirmationState,
ConfirmationTargets,
} from '../../../../shared/entities/shared.const';
import { of, Subscription } from 'rxjs';
import {
EventService,
HarborEvent,
} from '../../../../services/event-service/event.service';
import {
operateChanges,
OperateInfo,
OperationState,
} from '../../../../shared/components/operation/operate';
import { errorHandler } from '../../../../shared/units/shared.utils';
import { OperationService } from '../../../../shared/components/operation/operation.service';
@Component({
selector: 'app-pending-job-card',
templateUrl: './pending-job-card.component.html',
styleUrls: ['./pending-job-card.component.scss'],
})
export class PendingCardComponent implements OnInit, OnDestroy {
loading: boolean = false;
jobQueue: JobQueue[] = [];
timeout: any;
loadingStopAll: boolean = false;
confirmSub: Subscription;
constructor(
private operateDialogService: ConfirmationDialogService,
private jobServiceService: JobserviceService,
private messageHandlerService: MessageHandlerService,
private eventService: EventService,
private operationService: OperationService
) {}
ngOnInit() {
this.loopGetPendingJobs(true);
this.initSub();
}
ngOnDestroy() {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
if (this.confirmSub) {
this.confirmSub.unsubscribe();
this.confirmSub = null;
}
}
initSub() {
if (!this.confirmSub) {
this.confirmSub =
this.operateDialogService.confirmationConfirm$.subscribe(
message => {
if (
message &&
message.state === ConfirmationState.CONFIRMED
) {
if (
message.source ===
ConfirmationTargets.STOP_ALL_PENDING_JOBS
) {
this.executeStopAll();
}
}
}
);
}
}
loopGetPendingJobs(withLoading?: boolean) {
if (withLoading) {
this.loading = true;
}
this.jobServiceService
.listJobQueues()
.pipe(finalize(() => (this.loading = false)))
.subscribe(res => {
if (res?.length) {
this.jobQueue = res.sort((a, b) => {
const ACount: number = a?.count | 0;
const BCount: number = b?.count | 0;
return BCount - ACount;
});
}
this.timeout = setTimeout(() => {
this.loopGetPendingJobs();
}, INTERVAL);
});
}
total(): number {
if (this.jobQueue?.length) {
let count: number = 0;
this.jobQueue.forEach((item, index) => {
count += item?.count | 0;
});
return count;
}
return 0;
}
otherCount(): number {
if (this.jobQueue?.length) {
let count: number = 0;
this.jobQueue.forEach((item, index) => {
if (index > 1) {
count += item?.count | 0;
}
});
return count;
}
return 0;
}
stopAll() {
this.operateDialogService.openComfirmDialog({
data: undefined,
param: null,
title: 'JOB_SERVICE_DASHBOARD.CONFIRM_STOP_ALL',
message: 'JOB_SERVICE_DASHBOARD.CONFIRM_STOP_ALL_CONTENT',
targetId: ConfirmationTargets.STOP_ALL_PENDING_JOBS,
buttons: ConfirmationButtons.CONFIRM_CANCEL,
});
}
executeStopAll() {
this.loadingStopAll = true;
const operationMessage = new OperateInfo();
operationMessage.name =
'JOB_SERVICE_DASHBOARD.OPERATION_STOP_ALL_QUEUES';
operationMessage.state = OperationState.progressing;
this.operationService.publishInfo(operationMessage);
this.jobServiceService
.actionPendingJobs({
jobType: JobType.ALL,
actionRequest: {
action: PendingJobsActions.STOP,
},
})
.pipe(finalize(() => (this.loadingStopAll = false)))
.subscribe({
next: res => {
this.messageHandlerService.info(
'JOB_SERVICE_DASHBOARD.STOP_ALL_SUCCESS'
);
this.refreshNow();
this.eventService.publish(
HarborEvent.REFRESH_JOB_SERVICE_DASHBOARD
);
operateChanges(operationMessage, OperationState.success);
},
error: err => {
this.messageHandlerService.error(err);
operateChanges(
operationMessage,
OperationState.failure,
errorHandler(err)
);
},
});
}
refreshNow() {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
this.loopGetPendingJobs(true);
}
}

View File

@ -0,0 +1,102 @@
<clr-datagrid
[(clrDgSelected)]="selectedRows"
[clrDgLoading]="loading"
(clrDgRefresh)="clrLoad($event)">
<clr-dg-action-bar>
<clr-dg-action-bar class="action-bar mt-0">
<div>
<button
[clrLoading]="loadingStop"
[disabled]="loadingStop || !selectedRows?.length"
type="button"
class="btn btn-secondary"
(click)="stop()">
{{ 'JOB_SERVICE_DASHBOARD.STOP_BTN' | translate }}
</button>
<button
(click)="pause()"
[disabled]="!canPause()"
type="button"
class="btn btn-secondary">
{{ 'JOB_SERVICE_DASHBOARD.PAUSE_BTN' | translate }}
</button>
<button
(click)="resume()"
[disabled]="!canResume()"
type="button"
class="btn btn-secondary">
{{ 'JOB_SERVICE_DASHBOARD.RESUME_BTN' | translate }}
</button>
<clr-tooltip>
<clr-icon
clrTooltipTrigger
shape="info-circle"
size="24"></clr-icon>
<clr-tooltip-content
clrPosition="top-right"
clrSize="lg"
*clrIfOpen>
<div>
{{
'JOB_SERVICE_DASHBOARD.QUEUE_STOP_BTN_INFO'
| translate
}}
</div>
<div>
{{
'JOB_SERVICE_DASHBOARD.QUEUE_PAUSE_BTN_INFO'
| translate
}}
</div>
<div>
{{
'JOB_SERVICE_DASHBOARD.QUEUE_RESUME_BTN_INFO'
| translate
}}
</div>
</clr-tooltip-content>
</clr-tooltip>
</div>
<span class="refresh-btn">
<clr-icon shape="refresh" (click)="getJobs()"></clr-icon>
</span>
</clr-dg-action-bar>
</clr-dg-action-bar>
<clr-dg-column [clrDgField]="'job_type'">{{
'JOB_SERVICE_DASHBOARD.JOB_TYPE' | translate
}}</clr-dg-column>
<clr-dg-column [clrDgField]="'count'" [clrDgColType]="'number'">{{
'JOB_SERVICE_DASHBOARD.PENDING_COUNT' | translate
}}</clr-dg-column>
<clr-dg-column [clrDgField]="'latency'" [clrDgColType]="'number'">{{
'JOB_SERVICE_DASHBOARD.LATENCY' | translate
}}</clr-dg-column>
<clr-dg-column>{{
'JOB_SERVICE_DASHBOARD.PAUSED' | translate
}}</clr-dg-column>
<clr-dg-placeholder>{{
'JOB_SERVICE_DASHBOARD.NO_JOB_QUEUE' | translate
}}</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let j of jobQueue" [clrDgItem]="j">
<clr-dg-cell>{{ j.job_type }}</clr-dg-cell>
<clr-dg-cell>{{ j.count || 0 }}</clr-dg-cell>
<clr-dg-cell>{{ getDuration(j?.latency) || 0 }}</clr-dg-cell>
<clr-dg-cell>{{ isPaused(j?.paused) | translate }}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<clr-dg-pagination
#pagination
[clrDgPageSize]="pageSize"
[clrDgTotalItems]="jobQueue?.length">
<clr-dg-page-size [clrPageSizeOptions]="[15, 25, 50]">{{
'PAGINATION.PAGE_SIZE' | translate
}}</clr-dg-page-size>
<span *ngIf="jobQueue?.length">
{{ pagination.firstItem + 1 }} -
{{ pagination.lastItem + 1 }}
{{ 'GROUP.OF' | translate }}
</span>
{{ jobQueue?.length }} {{ 'GROUP.ITEMS' | translate }}
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>

View File

@ -0,0 +1,9 @@
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
}
.refresh-btn {
margin-right: 1rem;
}

View File

@ -0,0 +1,63 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PendingListComponent } from './pending-job-list.component';
import { JobQueue } from '../../../../../../ng-swagger-gen/models/job-queue';
import { of } from 'rxjs';
import { delay, finalize } from 'rxjs/operators';
import { SharedTestingModule } from '../../../../shared/shared.module';
import { JobserviceService } from '../../../../../../ng-swagger-gen/services/jobservice.service';
describe('PendingListComponent', () => {
let component: PendingListComponent;
let fixture: ComponentFixture<PendingListComponent>;
const mockedJobs: JobQueue[] = [
{
job_type: 'test1',
count: 1,
},
{
job_type: 'test2',
count: 2,
},
{
job_type: 'test3',
count: 3,
},
];
const fakedJobserviceService = {
listJobQueues() {
return of(mockedJobs).pipe(delay(0));
},
};
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [PendingListComponent],
imports: [SharedTestingModule],
providers: [
{
provide: JobserviceService,
useValue: fakedJobserviceService,
},
],
}).compileComponents();
fixture = TestBed.createComponent(PendingListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should render list', async () => {
await fixture.whenStable();
component.loading = false;
fixture.detectChanges();
await fixture.whenStable();
const rows = fixture.nativeElement.querySelectorAll('clr-dg-row');
expect(rows.length).toEqual(3);
});
});

View File

@ -0,0 +1,332 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ClrDatagridStateInterface } from '@clr/angular/data/datagrid/interfaces/state.interface';
import {
durationStr,
getPageSizeFromLocalStorage,
PageSizeMapKeys,
setPageSizeToLocalStorage,
} from '../../../../shared/units/utils';
import { JobserviceService } from '../../../../../../ng-swagger-gen/services/jobservice.service';
import { MessageHandlerService } from '../../../../shared/services/message-handler.service';
import { finalize } from 'rxjs/operators';
import { JobQueue } from '../../../../../../ng-swagger-gen/models/job-queue';
import { NO, YES } from '../../clearing-job/clearing-job-interfact';
import { PendingJobsActions } from '../job-service-dashboard.interface';
import { forkJoin, Subscription } from 'rxjs';
import { ConfirmationDialogService } from '../../../global-confirmation-dialog/confirmation-dialog.service';
import {
EventService,
HarborEvent,
} from '../../../../services/event-service/event.service';
import {
ConfirmationButtons,
ConfirmationState,
ConfirmationTargets,
} from 'src/app/shared/entities/shared.const';
import {
operateChanges,
OperateInfo,
OperationState,
} from '../../../../shared/components/operation/operate';
import { OperationService } from '../../../../shared/components/operation/operation.service';
import { errorHandler } from '../../../../shared/units/shared.utils';
@Component({
selector: 'app-pending-job-list',
templateUrl: './pending-job-list.component.html',
styleUrls: ['./pending-job-list.component.scss'],
})
export class PendingListComponent implements OnInit, OnDestroy {
loading: boolean = false;
selectedRows: JobQueue[] = [];
pageSize: number = getPageSizeFromLocalStorage(
PageSizeMapKeys.PENDING_LIST_COMPONENT
);
jobQueue: JobQueue[] = [];
loadingStop: boolean = false;
loadingPause: boolean = false;
loadingResume: boolean = false;
confirmSub: Subscription;
eventSub: Subscription;
constructor(
private jobServiceService: JobserviceService,
private messageHandlerService: MessageHandlerService,
private operateDialogService: ConfirmationDialogService,
private eventService: EventService,
private operationService: OperationService
) {}
ngOnInit() {
this.getJobs();
this.initEventSub();
this.initSub();
}
ngOnDestroy() {
if (this.confirmSub) {
this.confirmSub.unsubscribe();
this.confirmSub = null;
}
if (this.eventSub) {
this.eventSub.unsubscribe();
this.eventSub = null;
}
}
initSub() {
if (!this.confirmSub) {
this.confirmSub =
this.operateDialogService.confirmationConfirm$.subscribe(
message => {
if (
message &&
message.state === ConfirmationState.CONFIRMED
) {
if (
message.source ===
ConfirmationTargets.STOPS_JOBS
) {
this.executeStop();
}
if (
message.source ===
ConfirmationTargets.PAUSE_JOBS
) {
this.executePause();
}
if (
message.source ===
ConfirmationTargets.RESUME_JOBS
) {
this.executeResume();
}
}
}
);
}
}
initEventSub() {
if (!this.eventSub) {
this.eventSub = this.eventService.subscribe(
HarborEvent.REFRESH_JOB_SERVICE_DASHBOARD,
() => {
this.getJobs();
}
);
}
}
getJobs() {
this.selectedRows = [];
this.loading = true;
this.jobServiceService
.listJobQueues()
.pipe(finalize(() => (this.loading = false)))
.subscribe({
next: res => {
this.jobQueue = res;
},
error: err => {
this.messageHandlerService.error(err);
},
});
}
clrLoad(state: ClrDatagridStateInterface): void {
if (state?.page?.size) {
this.pageSize = state.page.size;
setPageSizeToLocalStorage(
PageSizeMapKeys.PENDING_LIST_COMPONENT,
this.pageSize
);
}
}
isPaused(paused: boolean): string {
if (paused) {
return YES;
}
return NO;
}
getDuration(v: number): string {
if (v) {
return durationStr(v * 1000);
}
return null;
}
canPause(): boolean {
if (this.selectedRows?.length) {
return !this.selectedRows.some(item => item.paused);
}
return false;
}
canResume(): boolean {
if (this.selectedRows?.length) {
return !this.selectedRows.some(item => !item.paused);
}
return false;
}
stop() {
const jobs: string = this.selectedRows
.map(item => item.job_type)
.join(',');
this.operateDialogService.openComfirmDialog({
data: undefined,
param: jobs,
title: 'JOB_SERVICE_DASHBOARD.CONFIRM_STOPPING_JOBS',
message: 'JOB_SERVICE_DASHBOARD.CONFIRM_STOPPING_JOBS_CONTENT',
targetId: ConfirmationTargets.STOPS_JOBS,
buttons: ConfirmationButtons.CONFIRM_CANCEL,
});
}
pause() {
const jobs: string = this.selectedRows
.map(item => item.job_type)
.join(',');
this.operateDialogService.openComfirmDialog({
data: undefined,
param: jobs,
title: 'JOB_SERVICE_DASHBOARD.CONFIRM_PAUSING_JOBS',
message: 'JOB_SERVICE_DASHBOARD.CONFIRM_PAUSING_JOBS_CONTENT',
targetId: ConfirmationTargets.PAUSE_JOBS,
buttons: ConfirmationButtons.CONFIRM_CANCEL,
});
}
resume() {
const jobs: string = this.selectedRows
.map(item => item.job_type)
.join(',');
this.operateDialogService.openComfirmDialog({
data: undefined,
param: jobs,
title: 'JOB_SERVICE_DASHBOARD.CONFIRM_RESUMING_JOBS',
message: 'JOB_SERVICE_DASHBOARD.CONFIRM_RESUMING_JOBS_CONTENT',
targetId: ConfirmationTargets.RESUME_JOBS,
buttons: ConfirmationButtons.CONFIRM_CANCEL,
});
}
executeStop() {
this.loadingStop = true;
const operationMessage = new OperateInfo();
operationMessage.name =
'JOB_SERVICE_DASHBOARD.OPERATION_STOP_SPECIFIED_QUEUES';
operationMessage.state = OperationState.progressing;
operationMessage.data.name = this.selectedRows
.map(item => item.job_type)
.join(',');
this.operationService.publishInfo(operationMessage);
forkJoin(
this.selectedRows.map(item => {
return this.jobServiceService.actionPendingJobs({
jobType: item.job_type,
actionRequest: {
action: PendingJobsActions.STOP,
},
});
})
)
.pipe(finalize(() => (this.loadingStop = false)))
.subscribe({
next: res => {
this.messageHandlerService.info(
'JOB_SERVICE_DASHBOARD.STOP_SUCCESS'
);
this.getJobs();
operateChanges(operationMessage, OperationState.success);
},
error: err => {
this.messageHandlerService.error(err);
operateChanges(
operationMessage,
OperationState.failure,
errorHandler(err)
);
},
});
}
executePause() {
this.loadingPause = true;
const operationMessage = new OperateInfo();
operationMessage.name =
'JOB_SERVICE_DASHBOARD.OPERATION_PAUSE_SPECIFIED_QUEUES';
operationMessage.state = OperationState.progressing;
operationMessage.data.name = this.selectedRows
.map(item => item.job_type)
.join(',');
this.operationService.publishInfo(operationMessage);
forkJoin(
this.selectedRows.map(item => {
return this.jobServiceService.actionPendingJobs({
jobType: item.job_type,
actionRequest: {
action: PendingJobsActions.PAUSE,
},
});
})
)
.pipe(finalize(() => (this.loadingPause = false)))
.subscribe({
next: res => {
operateChanges(operationMessage, OperationState.success);
this.messageHandlerService.info(
'JOB_SERVICE_DASHBOARD.PAUSE_SUCCESS'
);
this.getJobs();
},
error: err => {
this.messageHandlerService.error(err);
operateChanges(
operationMessage,
OperationState.failure,
errorHandler(err)
);
},
});
}
executeResume() {
this.loadingResume = true;
const operationMessage = new OperateInfo();
operationMessage.name =
'JOB_SERVICE_DASHBOARD.OPERATION_RESUME_SPECIFIED_QUEUES';
operationMessage.state = OperationState.progressing;
operationMessage.data.name = this.selectedRows
.map(item => item.job_type)
.join(',');
this.operationService.publishInfo(operationMessage);
forkJoin(
this.selectedRows.map(item => {
return this.jobServiceService.actionPendingJobs({
jobType: item.job_type,
actionRequest: {
action: PendingJobsActions.RESUME,
},
});
})
)
.pipe(finalize(() => (this.loadingResume = false)))
.subscribe({
next: res => {
operateChanges(operationMessage, OperationState.success);
this.messageHandlerService.info(
'JOB_SERVICE_DASHBOARD.RESUME_SUCCESS'
);
this.getJobs();
},
error: err => {
this.messageHandlerService.error(err);
operateChanges(
operationMessage,
OperationState.failure,
errorHandler(err)
);
},
});
}
}

View File

@ -0,0 +1,58 @@
<div class="card">
<div class="card-header">
{{ 'JOB_SERVICE_DASHBOARD.SCHEDULES' | translate }}
</div>
<div class="card-block">
<div class="duration">
{{ 'REPLICATION.TOTAL' | translate }}: {{ scheduleCount }}
</div>
<div class="clr-row">
<div class="clr-col-5 center">
<label class="clr-control-label">{{
'WEBHOOK.STATUS' | translate
}}</label>
</div>
<div class="clr-col-7">
<span class="status" *ngIf="!loadingStatus">{{
statusStr() | translate
}}</span>
<span
*ngIf="loadingStatus"
class="spinner spinner-inline"></span>
</div>
</div>
</div>
<div class="card-footer">
<button
[disabled]="loadingStatus || clickOnGoing"
[clrLoading]="clickOnGoing"
(click)="pauseOrResume()"
type="button"
class="btn btn-link">
{{ btnText() | translate }}
</button>
<clr-tooltip>
<clr-icon
clrTooltipTrigger
shape="info-circle"
size="24"></clr-icon>
<clr-tooltip-content
clrPosition="top-right"
clrSize="lg"
*clrIfOpen>
<div>
{{
'JOB_SERVICE_DASHBOARD.SCHEDULE_PAUSE_BTN_INFO'
| translate
}}
</div>
<div>
{{
'JOB_SERVICE_DASHBOARD.SCHEDULE_RESUME_BTN_INFO'
| translate
}}
</div>
</clr-tooltip-content>
</clr-tooltip>
</div>
</div>

View File

@ -0,0 +1,15 @@
.duration {
width: 100%;
text-align: center;
font-size: 50px;
margin: 1.5rem 0 1rem;
}
.card-block {
height: 9rem;
}
.center {
display: flex;
align-items: center;
}

View File

@ -0,0 +1,68 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ScheduleCardComponent } from './schedule-card.component';
import { of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { SharedTestingModule } from '../../../../shared/shared.module';
import { JobserviceService } from '../../../../../../ng-swagger-gen/services/jobservice.service';
import { ScheduleStatusString } from '../job-service-dashboard.interface';
import { HttpHeaders, HttpResponse } from '@angular/common/http';
import { ScheduleTask } from '../../../../../../ng-swagger-gen/models/schedule-task';
import { ScheduleService } from '../../../../../../ng-swagger-gen/services/schedule.service';
describe('ScheduleCardComponent', () => {
let component: ScheduleCardComponent;
let fixture: ComponentFixture<ScheduleCardComponent>;
const fakedJobserviceService = {};
const fakedScheduleService = {
getSchedulePaused() {
return of({}).pipe(delay(0));
},
listSchedulesResponse() {
const res: HttpResponse<Array<ScheduleTask>> = new HttpResponse<
Array<ScheduleTask>
>({
headers: new HttpHeaders({ 'x-total-count': '0' }),
body: [],
});
return of(res).pipe(delay(0));
},
};
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ScheduleCardComponent],
imports: [SharedTestingModule],
providers: [
{
provide: JobserviceService,
useValue: fakedJobserviceService,
},
{
provide: ScheduleService,
useValue: fakedScheduleService,
},
],
}).compileComponents();
fixture = TestBed.createComponent(ScheduleCardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should show right status and right total count', () => {
component.loadingStatus = false;
fixture.detectChanges();
const totalDiv: HTMLDivElement =
fixture.nativeElement.querySelector('.duration');
expect(totalDiv.innerText).toContain('0');
const statusSpan: HTMLSpanElement =
fixture.nativeElement.querySelector('.status');
expect(statusSpan.innerText).toEqual(ScheduleStatusString.RUNNING);
});
});

View File

@ -0,0 +1,224 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ConfirmationDialogService } from '../../../global-confirmation-dialog/confirmation-dialog.service';
import { JobserviceService } from '../../../../../../ng-swagger-gen/services/jobservice.service';
import { MessageHandlerService } from '../../../../shared/services/message-handler.service';
import { finalize } from 'rxjs/operators';
import {
INTERVAL,
JobType,
PendingJobsActions,
ScheduleExecuteBtnString,
ScheduleStatusString,
} from '../job-service-dashboard.interface';
import {
ConfirmationButtons,
ConfirmationState,
ConfirmationTargets,
} from '../../../../shared/entities/shared.const';
import { Subscription } from 'rxjs';
import {
EventService,
HarborEvent,
} from '../../../../services/event-service/event.service';
import { OperationService } from '../../../../shared/components/operation/operation.service';
import {
operateChanges,
OperateInfo,
OperationState,
} from '../../../../shared/components/operation/operate';
import { errorHandler } from '../../../../shared/units/shared.utils';
import { ScheduleService } from '../../../../../../ng-swagger-gen/services/schedule.service';
@Component({
selector: 'app-schedule-card',
templateUrl: './schedule-card.component.html',
styleUrls: ['./schedule-card.component.scss'],
})
export class ScheduleCardComponent implements OnInit, OnDestroy {
scheduleCount: number = 0;
isPaused: boolean = false;
loadingStatus: boolean = false;
confirmSub: Subscription;
statusTimeout: any;
clickOnGoing: boolean = false;
constructor(
private operateDialogService: ConfirmationDialogService,
private jobServiceService: JobserviceService,
private messageHandlerService: MessageHandlerService,
private eventService: EventService,
private operationService: OperationService,
private scheduleService: ScheduleService
) {}
ngOnInit() {
this.initSub();
this.loopGetStatus(true);
this.getScheduleCount();
}
ngOnDestroy() {
if (this.statusTimeout) {
clearTimeout(this.statusTimeout);
this.statusTimeout = null;
}
if (this.confirmSub) {
this.confirmSub.unsubscribe();
this.confirmSub = null;
}
}
initSub() {
if (!this.confirmSub) {
this.confirmSub =
this.operateDialogService.confirmationConfirm$.subscribe(
message => {
if (
message &&
message.state === ConfirmationState.CONFIRMED
) {
if (
message.source ===
ConfirmationTargets.RESUME_ALL_SCHEDULES
) {
this.execute(PendingJobsActions.RESUME);
}
if (
message.source ===
ConfirmationTargets.PAUSE_ALL_SCHEDULES
) {
this.execute(PendingJobsActions.PAUSE);
}
}
}
);
}
}
loopGetStatus(withLoading?: boolean) {
if (withLoading) {
this.loadingStatus = true;
}
this.scheduleService
.getSchedulePaused({
jobType: JobType.ALL,
})
.pipe(finalize(() => (this.loadingStatus = false)))
.subscribe(res => {
this.isPaused = res?.paused;
this.statusTimeout = setTimeout(() => {
this.loopGetStatus();
this.getScheduleCount();
}, INTERVAL);
});
}
getScheduleCount() {
this.scheduleService
.listSchedulesResponse({
page: 1,
pageSize: 1,
})
.subscribe({
next: res => {
// Get total count
if (res.headers) {
let xHeader: string = res.headers.get('x-total-count');
if (xHeader) {
this.scheduleCount = Number.parseInt(xHeader, 10);
}
}
},
error: err => {
this.messageHandlerService.error(err);
},
});
}
btnText(): string {
if (this.isPaused) {
return ScheduleExecuteBtnString.RESUME_ALL;
}
return ScheduleExecuteBtnString.PAUSE_ALL;
}
statusStr(): string {
if (this.isPaused) {
return ScheduleStatusString.PAUSED;
}
return ScheduleStatusString.RUNNING;
}
pauseOrResume() {
if (this.isPaused) {
this.operateDialogService.openComfirmDialog({
data: undefined,
param: null,
title: 'JOB_SERVICE_DASHBOARD.CONFIRM_RESUMING_ALL',
message: 'JOB_SERVICE_DASHBOARD.CONFIRM_RESUMING_ALL_CONTENT',
targetId: ConfirmationTargets.RESUME_ALL_SCHEDULES,
buttons: ConfirmationButtons.CONFIRM_CANCEL,
});
} else {
this.operateDialogService.openComfirmDialog({
data: undefined,
param: null,
title: 'JOB_SERVICE_DASHBOARD.CONFIRM_PAUSING_ALL',
message: 'JOB_SERVICE_DASHBOARD.CONFIRM_PAUSING_ALL_CONTENT',
targetId: ConfirmationTargets.PAUSE_ALL_SCHEDULES,
buttons: ConfirmationButtons.CONFIRM_CANCEL,
});
}
}
execute(action: PendingJobsActions) {
this.clickOnGoing = true;
const operationMessage = new OperateInfo();
operationMessage.name = this.isPaused
? 'JOB_SERVICE_DASHBOARD.OPERATION_RESUME_SCHEDULE'
: 'JOB_SERVICE_DASHBOARD.OPERATION_PAUSE_SCHEDULE';
operationMessage.state = OperationState.progressing;
this.operationService.publishInfo(operationMessage);
this.jobServiceService
.actionPendingJobs({
jobType: JobType.SCHEDULER,
actionRequest: {
action: action,
},
})
.pipe(finalize(() => (this.clickOnGoing = false)))
.subscribe({
next: res => {
if (this.isPaused) {
this.messageHandlerService.info(
'JOB_SERVICE_DASHBOARD.RESUME_ALL_SUCCESS'
);
} else {
this.messageHandlerService.info(
'JOB_SERVICE_DASHBOARD.PAUSE_ALL_SUCCESS'
);
}
this.refreshNow();
this.eventService.publish(
HarborEvent.REFRESH_JOB_SERVICE_DASHBOARD
);
operateChanges(operationMessage, OperationState.success);
},
error: err => {
this.messageHandlerService.error(err);
operateChanges(
operationMessage,
OperationState.failure,
errorHandler(err)
);
},
});
}
refreshNow() {
if (this.statusTimeout) {
clearTimeout(this.statusTimeout);
this.statusTimeout = null;
}
this.loopGetStatus(true);
}
}

View File

@ -0,0 +1,66 @@
<clr-datagrid
[clrDgLoading]="loadingSchedules"
(clrDgRefresh)="clrLoad($event)">
<clr-dg-action-bar class="action-bar">
<span class="refresh-btn" (click)="clrLoad()">
<clr-icon shape="refresh"></clr-icon>
</span>
</clr-dg-action-bar>
<clr-dg-column [clrDgSortBy]="'id'">{{
'TAG_RETENTION.SERIAL' | translate
}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="'vendor_type'">{{
'JOB_SERVICE_DASHBOARD.VENDOR_TYPE' | translate
}}</clr-dg-column>
<clr-dg-column>{{
'JOB_SERVICE_DASHBOARD.VENDOR_ID' | translate
}}</clr-dg-column>
<clr-dg-column>{{
'JOB_SERVICE_DASHBOARD.EXTRA_ATTR' | translate
}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="'creation_time'">{{
'PROJECT.CREATION_TIME' | translate
}}</clr-dg-column>
<clr-dg-placeholder>{{
'JOB_SERVICE_DASHBOARD.NO_SCHEDULE' | translate
}}</clr-dg-placeholder>
<clr-dg-row *ngFor="let p of schedules" [clrDgItem]="p">
<clr-dg-cell>{{ p.id }}</clr-dg-cell>
<clr-dg-cell>{{ p.vendor_type }}</clr-dg-cell>
<clr-dg-cell>{{ p.vendor_id }}</clr-dg-cell>
<clr-dg-cell class="flex">
<clr-signpost>
<a class="btn btn-link link-normal" clrSignpostTrigger>{{
p.extra_attrs
}}</a>
<clr-signpost-content
class="pre"
[clrPosition]="'top-middle'"
*clrIfOpen>
<pre
[innerHTML]="
json(p.extra_attrs) | json | markdown
"></pre>
</clr-signpost-content>
</clr-signpost>
</clr-dg-cell>
<clr-dg-cell>{{ p.creation_time | harborDatetime }}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<clr-dg-pagination
#pagination
[clrDgPageSize]="pageSize"
[(clrDgPage)]="page"
[clrDgTotalItems]="total">
<clr-dg-page-size [clrPageSizeOptions]="[15, 25, 50]">{{
'PAGINATION.PAGE_SIZE' | translate
}}</clr-dg-page-size>
<span *ngIf="total">
{{ pagination.firstItem + 1 }} -
{{ pagination.lastItem + 1 }}
{{ 'GROUP.OF' | translate }}
</span>
{{ total }} {{ 'GROUP.ITEMS' | translate }}
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>

View File

@ -0,0 +1,24 @@
.action-bar {
display: flex;
justify-content: right;
}
.refresh-btn {
margin-right: 1rem;
}
.link-normal {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 10rem;
text-transform: unset;
}
.flex {
display: flex;
}
.pre {
min-width: 25rem;
}

View File

@ -0,0 +1,61 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ScheduleListComponent } from './schedule-list.component';
import { ScheduleTask } from '../../../../../../ng-swagger-gen/models/schedule-task';
import { of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { SharedTestingModule } from '../../../../shared/shared.module';
import { HttpHeaders, HttpResponse } from '@angular/common/http';
import { ScheduleService } from '../../../../../../ng-swagger-gen/services/schedule.service';
describe('ScheduleListComponent', () => {
let component: ScheduleListComponent;
let fixture: ComponentFixture<ScheduleListComponent>;
const mockedSchedules: ScheduleTask[] = [
{ id: 1, vendor_type: 'test1' },
{ id: 2, vendor_type: 'test2' },
];
const fakedScheduleService = {
listSchedulesResponse() {
const res: HttpResponse<Array<ScheduleTask>> = new HttpResponse<
Array<ScheduleTask>
>({
headers: new HttpHeaders({ 'x-total-count': '2' }),
body: mockedSchedules,
});
return of(res).pipe(delay(0));
},
};
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ScheduleListComponent],
imports: [SharedTestingModule],
providers: [
{
provide: ScheduleService,
useValue: fakedScheduleService,
},
],
}).compileComponents();
fixture = TestBed.createComponent(ScheduleListComponent);
component = fixture.componentInstance;
component.loadingSchedules = true;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should render list', async () => {
await fixture.whenStable();
component.loadingSchedules = false;
fixture.detectChanges();
await fixture.whenStable();
const rows = fixture.nativeElement.querySelectorAll('clr-dg-row');
expect(rows.length).toEqual(2);
});
});

View File

@ -0,0 +1,99 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ClrDatagridStateInterface } from '@clr/angular/data/datagrid/interfaces/state.interface';
import {
doSorting,
getPageSizeFromLocalStorage,
PageSizeMapKeys,
setPageSizeToLocalStorage,
} from '../../../../shared/units/utils';
import { MessageHandlerService } from '../../../../shared/services/message-handler.service';
import { finalize } from 'rxjs/operators';
import { ScheduleTask } from '../../../../../../ng-swagger-gen/models/schedule-task';
import {
EventService,
HarborEvent,
} from '../../../../services/event-service/event.service';
import { Subscription } from 'rxjs';
import { ScheduleService } from '../../../../../../ng-swagger-gen/services/schedule.service';
@Component({
selector: 'app-schedule-list',
templateUrl: './schedule-list.component.html',
styleUrls: ['./schedule-list.component.scss'],
})
export class ScheduleListComponent implements OnInit, OnDestroy {
loadingSchedules: boolean = false;
schedules: ScheduleTask[] = [];
total: number = 0;
page: number = 1;
pageSize: number = getPageSizeFromLocalStorage(
PageSizeMapKeys.SCHEDULE_LIST_COMPONENT
);
eventSub: Subscription;
constructor(
private messageHandlerService: MessageHandlerService,
private eventService: EventService,
private scheduleService: ScheduleService
) {}
ngOnInit() {
this.initEventSub();
}
ngOnDestroy() {
if (this.eventSub) {
this.eventSub.unsubscribe();
this.eventSub = null;
}
}
initEventSub() {
if (!this.eventSub) {
this.eventSub = this.eventService.subscribe(
HarborEvent.REFRESH_JOB_SERVICE_DASHBOARD,
() => {
this.clrLoad();
}
);
}
}
clrLoad(state?: ClrDatagridStateInterface): void {
if (state?.page?.size) {
this.pageSize = state.page.size;
setPageSizeToLocalStorage(
PageSizeMapKeys.SCHEDULE_LIST_COMPONENT,
this.pageSize
);
}
this.loadingSchedules = true;
this.scheduleService
.listSchedulesResponse({
page: this.page,
pageSize: this.pageSize,
})
.pipe(finalize(() => (this.loadingSchedules = false)))
.subscribe({
next: res => {
// Get total count
if (res.headers) {
let xHeader: string = res.headers.get('x-total-count');
if (xHeader) {
this.total = Number.parseInt(xHeader, 10);
}
}
this.schedules = doSorting(res.body, state);
},
error: err => {
this.messageHandlerService.error(err);
},
});
}
json(v: string): object {
if (v) {
return JSON.parse(v);
}
return null;
}
}

View File

@ -0,0 +1,16 @@
<div class="loading" [style.background-color]="getBigBGColor()">
<div
class="left"
[style.background-color]="getLeftBGColor()"
[style.transform]="getLeftRotate()"></div>
<div
class="right"
[style.background-color]="getRightBGColor()"
[style.transform]="getRightRotate()"></div>
<div
class="center"
[style.color]="getBigBGColor()"
[style.background-color]="getSmallBGColor()">
{{ numerator }}/{{ denominator }}
</div>
</div>

View File

@ -0,0 +1,44 @@
$big: 150px;
$small: 100px;
.loading {
display: flex;
width: $big;
height: $big;
position: relative;
border-radius: 50%;
}
.center {
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
width: $small;
height: $small;
position: absolute;
border-radius: 50%;
line-height: $small;
text-align: center;
font-size: 30px;
}
.left,
.right {
flex: 1;
}
.left {
border-radius: $big 0 0 $big;
transform-origin: right center;
}
.right {
border-radius: 0 $big $big 0;
transform-origin: left center;
}

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { DonutChartComponent } from './donut-chart.component';
describe('DonutChartComponent', () => {
let component: DonutChartComponent;
let fixture: ComponentFixture<DonutChartComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
schemas: [NO_ERRORS_SCHEMA],
declarations: [DonutChartComponent],
}).compileComponents();
fixture = TestBed.createComponent(DonutChartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,91 @@
import { Component, Input } from '@angular/core';
import { HAS_STYLE_MODE, StyleMode } from '../../../../../services/theme';
// this donut chart is created based on css border-radius and transform: rotate()
// as we have two themes, so need to set different colors for different theme
// for dark theme: .center bg-color #21333b; denominator #405C6B; numerator #49aeda
// for light theme: .center bg-color #fff; denominator #ccc; numerator #0072a3
enum DarkColors {
CENTER_BG_COLOR = '#21333b',
DEN_COLOR = '#405C6B',
NUM_COLOR = '#49aeda',
}
enum LightColors {
CENTER_BG_COLOR = '#fff',
DEN_COLOR = '#ccc',
NUM_COLOR = '#0072a3',
}
@Component({
selector: 'app-donut-chart',
templateUrl: './donut-chart.component.html',
styleUrls: ['./donut-chart.component.scss'],
})
export class DonutChartComponent {
@Input()
denominator: number;
@Input()
numerator: number;
isDarkTheme(): boolean {
return localStorage?.getItem(HAS_STYLE_MODE) === StyleMode.DARK;
}
getSmallBGColor(): string {
if (this.isDarkTheme()) {
return DarkColors.CENTER_BG_COLOR;
}
return LightColors.CENTER_BG_COLOR;
}
getBigBGColor(): string {
if (this.isDarkTheme()) {
return DarkColors.NUM_COLOR;
}
return LightColors.NUM_COLOR;
}
getLeftBGColor() {
if (this.isDarkTheme()) {
return DarkColors.DEN_COLOR;
}
return LightColors.DEN_COLOR;
}
getRightBGColor(): string {
if (this.getDegree() > 180) {
if (this.isDarkTheme()) {
return DarkColors.NUM_COLOR;
}
return LightColors.NUM_COLOR;
}
if (this.isDarkTheme()) {
return DarkColors.DEN_COLOR;
}
return LightColors.DEN_COLOR;
}
getDegree(): number {
if (this.numerator && this.denominator) {
return (this.numerator / this.denominator) * 360;
}
return 0;
}
getRightRotate() {
if (this.getDegree() > 0 && this.getDegree() <= 180) {
return `rotate(${this.getDegree()}deg)`;
}
return `rotate(0)`;
}
getLeftRotate() {
if (this.getDegree() > 180) {
return `rotate(${this.getDegree() - 180}deg)`;
}
return `rotate(0)`;
}
}

View File

@ -0,0 +1,20 @@
<div class="card">
<div class="card-header">
{{ 'JOB_SERVICE_DASHBOARD.WORKERS' | translate }}
</div>
<div class="card-block donut-chart">
<app-donut-chart
[denominator]="denominator"
[numerator]="busyWorkers?.length"></app-donut-chart>
</div>
<div class="card-footer">
<button
[disabled]="loadingFreeAll || busyWorkers?.length === 0"
[clrLoading]="loadingFreeAll"
(click)="freeAll()"
type="button"
class="btn btn-link">
{{ 'JOB_SERVICE_DASHBOARD.FREE_ALL' | translate }}
</button>
</div>
</div>

View File

@ -0,0 +1,9 @@
.donut-chart {
display: flex;
align-items: center;
justify-content: center;
}
.card-block {
height: 9rem;
}

View File

@ -0,0 +1,53 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SharedTestingModule } from '../../../../shared/shared.module';
import { JobserviceService } from '../../../../../../ng-swagger-gen/services/jobservice.service';
import { of } from 'rxjs';
import { WorkerCardComponent } from './worker-card.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { Worker } from '../../../../../../ng-swagger-gen/models/worker';
describe('WorkerCardComponent', () => {
let component: WorkerCardComponent;
let fixture: ComponentFixture<WorkerCardComponent>;
const mockedWorkers: Worker[] = [
{ id: '1', job_id: '1', job_name: 'test1' },
{ id: '2', job_id: '2', job_name: 'test2' },
];
const fakedJobserviceService = {
getWorkers() {
return of(mockedWorkers);
},
};
beforeEach(async () => {
await TestBed.configureTestingModule({
schemas: [NO_ERRORS_SCHEMA],
declarations: [WorkerCardComponent],
imports: [SharedTestingModule],
providers: [
{
provide: JobserviceService,
useValue: fakedJobserviceService,
},
],
}).compileComponents();
fixture = TestBed.createComponent(WorkerCardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should init timeout', () => {
expect(component.statusTimeout).toBeTruthy();
});
it('should get workers', () => {
expect(component.busyWorkers.length).toEqual(2);
});
});

View File

@ -0,0 +1,149 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Worker } from 'ng-swagger-gen/models';
import { JobserviceService } from 'ng-swagger-gen/services';
import { finalize, Subscription } from 'rxjs';
import { ConfirmationDialogService } from 'src/app/base/global-confirmation-dialog/confirmation-dialog.service';
import {
ConfirmationButtons,
ConfirmationState,
ConfirmationTargets,
} from 'src/app/shared/entities/shared.const';
import { MessageHandlerService } from 'src/app/shared/services/message-handler.service';
import { All, INTERVAL } from '../job-service-dashboard.interface';
import {
EventService,
HarborEvent,
} from '../../../../services/event-service/event.service';
import { OperationService } from '../../../../shared/components/operation/operation.service';
import {
operateChanges,
OperateInfo,
OperationState,
} from '../../../../shared/components/operation/operate';
import { errorHandler } from '../../../../shared/units/shared.utils';
@Component({
selector: 'app-worker-card',
templateUrl: './worker-card.component.html',
styleUrls: ['./worker-card.component.scss'],
})
export class WorkerCardComponent implements OnInit, OnDestroy {
denominator: number = 0;
statusTimeout: any;
loadingFreeAll: boolean = false;
confirmSub: Subscription;
busyWorkers: Worker[] = [];
constructor(
private operateDialogService: ConfirmationDialogService,
private jobServiceService: JobserviceService,
private messageHandlerService: MessageHandlerService,
private eventService: EventService,
private operationService: OperationService
) {}
ngOnInit(): void {
this.loopWorkerStatus();
this.initSub();
}
ngOnDestroy(): void {
if (this.statusTimeout) {
clearTimeout(this.statusTimeout);
this.statusTimeout = null;
}
if (this.confirmSub) {
this.confirmSub.unsubscribe();
this.confirmSub = null;
}
}
initSub() {
if (!this.confirmSub) {
this.confirmSub =
this.operateDialogService.confirmationConfirm$.subscribe(
message => {
if (
message &&
message.state === ConfirmationState.CONFIRMED
) {
if (
message.source ===
ConfirmationTargets.FREE_ALL_WORKERS
) {
this.executeFreeAll();
}
}
}
);
}
}
loopWorkerStatus() {
this.jobServiceService
.getWorkers({ poolId: All.ALL_WORKERS.toString() })
.subscribe(res => {
if (res) {
this.denominator = res.length;
this.busyWorkers = [];
res.forEach(item => {
if (item.job_id) {
this.busyWorkers.push(item);
}
});
this.statusTimeout = setTimeout(() => {
this.loopWorkerStatus();
}, INTERVAL);
}
});
}
freeAll() {
this.operateDialogService.openComfirmDialog({
data: undefined,
param: null,
title: 'JOB_SERVICE_DASHBOARD.CONFIRM_FREE_ALL',
message: 'JOB_SERVICE_DASHBOARD.CONFIRM_FREE_ALL_CONTENT',
targetId: ConfirmationTargets.FREE_ALL_WORKERS,
buttons: ConfirmationButtons.CONFIRM_CANCEL,
});
}
executeFreeAll() {
this.loadingFreeAll = true;
const operationMessage = new OperateInfo();
operationMessage.name = 'JOB_SERVICE_DASHBOARD.OPERATION_FREE_ALL';
operationMessage.state = OperationState.progressing;
this.operationService.publishInfo(operationMessage);
this.jobServiceService
.stopRunningJob({ jobId: All.ALL_WORKERS })
.pipe(finalize(() => (this.loadingFreeAll = false)))
.subscribe({
next: res => {
this.messageHandlerService.info(
'JOB_SERVICE_DASHBOARD.FREE_ALL_SUCCESS'
);
this.refreshNow();
this.eventService.publish(
HarborEvent.REFRESH_JOB_SERVICE_DASHBOARD
);
operateChanges(operationMessage, OperationState.success);
},
error: err => {
this.messageHandlerService.error(err);
operateChanges(
operationMessage,
OperationState.failure,
errorHandler(err)
);
},
});
}
refreshNow() {
if (this.statusTimeout) {
clearTimeout(this.statusTimeout);
this.statusTimeout = null;
}
this.loopWorkerStatus();
}
}

View File

@ -0,0 +1,151 @@
<h2>{{ 'JOB_SERVICE_DASHBOARD.WORKER_POOL' | translate }}</h2>
<clr-datagrid
[clrDgLoading]="loadingPools"
(clrDgRefresh)="clrLoadPool($event)"
(clrDgSelectedChange)="selectionChanged()"
[(clrDgSingleSelected)]="selectedPool">
<clr-dg-action-bar class="action-bar mt-0">
<span class="refresh-btn" (click)="getPools()">
<clr-icon shape="refresh"></clr-icon>
</span>
</clr-dg-action-bar>
<clr-dg-column [clrDgField]="'worker_pool_id'">{{
'JOB_SERVICE_DASHBOARD.WORKER_POOL_ID' | translate
}}</clr-dg-column>
<clr-dg-column [clrDgField]="'pid'">{{
'JOB_SERVICE_DASHBOARD.PID' | translate
}}</clr-dg-column>
<clr-dg-column [clrDgField]="'start_at'">{{
'JOB_SERVICE_DASHBOARD.START_AT' | translate
}}</clr-dg-column>
<clr-dg-column [clrDgField]="'heartbeat_at'">{{
'JOB_SERVICE_DASHBOARD.HEARTBEAT_AT' | translate
}}</clr-dg-column>
<clr-dg-column [clrDgField]="'concurrency'" [clrDgColType]="'number'">{{
'JOB_SERVICE_DASHBOARD.CONCURRENCY' | translate
}}</clr-dg-column>
<clr-dg-placeholder>{{
'JOB_SERVICE_DASHBOARD.NO_WORKER_POOL' | translate
}}</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let p of pools" [clrDgItem]="p">
<clr-dg-cell>{{ p.worker_pool_id }}</clr-dg-cell>
<clr-dg-cell>{{ p.pid }}</clr-dg-cell>
<clr-dg-cell>{{ p.start_at | harborDatetime }}</clr-dg-cell>
<clr-dg-cell>{{ p.heartbeat_at | harborDatetime }}</clr-dg-cell>
<clr-dg-cell>{{ p.concurrency }}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<clr-dg-pagination
#pagination
[clrDgPageSize]="5"
[clrDgTotalItems]="pools?.length">
<clr-dg-page-size [clrPageSizeOptions]="[5, 15, 30]">{{
'PAGINATION.PAGE_SIZE' | translate
}}</clr-dg-page-size>
<span *ngIf="pools?.length">
{{ pagination.firstItem + 1 }} -
{{ pagination.lastItem + 1 }}
{{ 'GROUP.OF' | translate }}
</span>
{{ pools?.length }} {{ 'GROUP.ITEMS' | translate }}
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
<ng-container *ngIf="selectedPool">
<h2>{{ 'JOB_SERVICE_DASHBOARD.WORKERS' | translate }}</h2>
<clr-datagrid
[clrDgLoading]="loadingWorkers"
(clrDgRefresh)="clrLoadWorker($event)"
[(clrDgSelected)]="selected">
<clr-dg-action-bar class="action-bar-worker">
<div>
<button
[clrLoading]="loadingFree"
[disabled]="loadingFree || !canFree()"
type="button"
class="btn btn-secondary"
(click)="freeWorker()">
{{ 'JOB_SERVICE_DASHBOARD.FREE' | translate }}
</button>
<clr-tooltip>
<clr-icon
clrTooltipTrigger
shape="info-circle"
size="24"></clr-icon>
<clr-tooltip-content
clrPosition="top-right"
clrSize="lg"
*clrIfOpen>
<span>
{{
'JOB_SERVICE_DASHBOARD.WORKER_FREE_BTN_INFO'
| translate
}}
</span>
</clr-tooltip-content>
</clr-tooltip>
</div>
<span class="refresh-btn" (click)="selectionChanged()">
<clr-icon shape="refresh"></clr-icon>
</span>
</clr-dg-action-bar>
<clr-dg-column [clrDgField]="'id'">{{
'JOB_SERVICE_DASHBOARD.WORKER_ID' | translate
}}</clr-dg-column>
<clr-dg-column [clrDgField]="'job_name'">{{
'CVE_EXPORT.JOB_NAME' | translate
}}</clr-dg-column>
<clr-dg-column [clrDgField]="'job_id'">{{
'JOB_SERVICE_DASHBOARD.JOB_ID' | translate
}}</clr-dg-column>
<clr-dg-column>{{
'JOB_SERVICE_DASHBOARD.JOB_PARAM' | translate
}}</clr-dg-column>
<clr-dg-column [clrDgField]="'start_at'">{{
'JOB_SERVICE_DASHBOARD.START_AT' | translate
}}</clr-dg-column>
<clr-dg-column [clrDgField]="'checkin_at'">{{
'JOB_SERVICE_DASHBOARD.CHECK_IN_AT' | translate
}}</clr-dg-column>
<clr-dg-placeholder>{{
'JOB_SERVICE_DASHBOARD.NO_WORKER' | translate
}}</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let w of workers" [clrDgItem]="w">
<clr-dg-cell>{{ w.id }}</clr-dg-cell>
<clr-dg-cell>{{ w.job_name }}</clr-dg-cell>
<clr-dg-cell>{{ w.job_id }}</clr-dg-cell>
<clr-dg-cell class="flex">
<clr-signpost>
<a class="btn btn-link link-normal" clrSignpostTrigger>{{
w.args
}}</a>
<clr-signpost-content
class="pre"
[clrPosition]="'top-middle'"
*clrIfOpen>
<pre [innerHTML]="json(w.args) | json | markdown"></pre>
</clr-signpost-content>
</clr-signpost>
</clr-dg-cell>
<clr-dg-cell>{{ w.start_at | harborDatetime }}</clr-dg-cell>
<clr-dg-cell>{{ w.checkin_at | harborDatetime }}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<clr-dg-pagination
#pagination
[clrDgPageSize]="workerPageSize"
[clrDgTotalItems]="workers?.length">
<clr-dg-page-size [clrPageSizeOptions]="[15, 25, 50]">{{
'PAGINATION.PAGE_SIZE' | translate
}}</clr-dg-page-size>
<span *ngIf="workers?.length">
{{ pagination.firstItem + 1 }} -
{{ pagination.lastItem + 1 }}
{{ 'GROUP.OF' | translate }}
</span>
{{ workers?.length }} {{ 'GROUP.ITEMS' | translate }}
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</ng-container>

View File

@ -0,0 +1,30 @@
.link-normal {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 10rem;
text-transform: unset;
}
.flex {
display: flex;
}
.action-bar {
display: flex;
justify-content: right;
}
.refresh-btn {
margin-right: 1rem;
}
.action-bar-worker {
display: flex;
justify-content: space-between;
align-items: center;
}
.pre {
min-width: 25rem;
}

View File

@ -0,0 +1,61 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { WorkerListComponent } from './worker-list.component';
import { of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { SharedTestingModule } from '../../../../shared/shared.module';
import { JobserviceService } from '../../../../../../ng-swagger-gen/services/jobservice.service';
import { Worker, WorkerPool } from 'ng-swagger-gen/models';
describe('WorkerListComponent', () => {
let component: WorkerListComponent;
let fixture: ComponentFixture<WorkerListComponent>;
const mockedWorkers: Worker[] = [
{ id: '1', job_id: '1', job_name: 'test1' },
{ id: '2', job_id: '2', job_name: 'test2' },
];
const mockedPools: WorkerPool[] = [
{ pid: 1, concurrency: 10, worker_pool_id: '1' },
];
const fakedJobserviceService = {
getWorkers() {
return of(mockedWorkers).pipe(delay(0));
},
getWorkerPools() {
return of(mockedPools).pipe(delay(0));
},
};
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [WorkerListComponent],
imports: [SharedTestingModule],
providers: [
{
provide: JobserviceService,
useValue: fakedJobserviceService,
},
],
}).compileComponents();
fixture = TestBed.createComponent(WorkerListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should render worker list', async () => {
component.selectionChanged();
await fixture.whenStable();
component.loadingPools = false;
component.loadingWorkers = false;
fixture.detectChanges();
await fixture.whenStable();
const rows = fixture.nativeElement.querySelectorAll('clr-dg-row');
expect(rows.length).toEqual(3); // 1 + 2
});
});

View File

@ -0,0 +1,242 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ClrDatagridStateInterface } from '@clr/angular/data/datagrid/interfaces/state.interface';
import { Worker } from 'ng-swagger-gen/models';
import { WorkerPool } from 'ng-swagger-gen/models/worker-pool';
import { JobserviceService } from 'ng-swagger-gen/services';
import { finalize, forkJoin, Subscription } from 'rxjs';
import { MessageHandlerService } from 'src/app/shared/services/message-handler.service';
import {
getPageSizeFromLocalStorage,
PageSizeMapKeys,
setPageSizeToLocalStorage,
} from 'src/app/shared/units/utils';
import { ConfirmationMessage } from '../../../global-confirmation-dialog/confirmation-message';
import {
ConfirmationButtons,
ConfirmationState,
ConfirmationTargets,
} from '../../../../shared/entities/shared.const';
import { ConfirmationDialogService } from '../../../global-confirmation-dialog/confirmation-dialog.service';
import {
EventService,
HarborEvent,
} from '../../../../services/event-service/event.service';
import { OperationService } from '../../../../shared/components/operation/operation.service';
import {
operateChanges,
OperateInfo,
OperationState,
} from '../../../../shared/components/operation/operate';
import { errorHandler } from '../../../../shared/units/shared.utils';
@Component({
selector: 'app-worker-list',
templateUrl: './worker-list.component.html',
styleUrls: ['./worker-list.component.scss'],
})
export class WorkerListComponent implements OnInit, OnDestroy {
loadingPools: boolean = false;
selectedPool: WorkerPool;
pools: WorkerPool[] = [];
loadingWorkers: boolean = false;
workers: Worker[] = [];
selected: Worker[] = [];
poolPageSize: number = getPageSizeFromLocalStorage(
PageSizeMapKeys.WORKER_LIST_COMPONENT_POOL,
5
);
workerPageSize: number = getPageSizeFromLocalStorage(
PageSizeMapKeys.WORKER_LIST_COMPONENT_WORKER
);
loadingFree: boolean = false;
confirmSub: Subscription;
eventSub: Subscription;
constructor(
private jobServiceService: JobserviceService,
private messageHandlerService: MessageHandlerService,
private operateDialogService: ConfirmationDialogService,
private eventService: EventService,
private operationService: OperationService
) {}
ngOnInit(): void {
this.getPools();
this.initSub();
this.initEventSub();
}
ngOnDestroy() {
if (this.confirmSub) {
this.confirmSub.unsubscribe();
this.confirmSub = null;
}
if (this.eventSub) {
this.eventSub.unsubscribe();
this.eventSub = null;
}
}
initSub() {
if (!this.confirmSub) {
this.confirmSub =
this.operateDialogService.confirmationConfirm$.subscribe(
message => {
if (
message &&
message.state === ConfirmationState.CONFIRMED
) {
if (
message.source ===
ConfirmationTargets.FREE_SPECIFIED_WORKERS
) {
this.executeFreeWorkers();
}
}
}
);
}
}
initEventSub() {
if (!this.eventSub) {
this.eventSub = this.eventService.subscribe(
HarborEvent.REFRESH_JOB_SERVICE_DASHBOARD,
() => {
this.selectionChanged();
}
);
}
}
getPools() {
this.loadingPools = true;
this.jobServiceService
.getWorkerPools()
.pipe(finalize(() => (this.loadingPools = false)))
.subscribe({
next: res => {
this.pools = res;
if (res?.length) {
this.selectedPool = res[0];
this.selectionChanged();
}
},
error: err => {
this.messageHandlerService.error(err);
},
});
}
selectionChanged() {
this.loadingWorkers = true;
this.jobServiceService
.getWorkers({ poolId: this.selectedPool?.worker_pool_id })
.pipe(finalize(() => (this.loadingWorkers = false)))
.subscribe({
next: res => {
this.workers = res;
},
error: err => {
this.messageHandlerService.error(err);
},
});
}
string(v: any) {
if (v) {
return JSON.stringify(v);
}
return null;
}
clrLoadPool(state: ClrDatagridStateInterface): void {
if (state?.page?.size) {
this.poolPageSize = state.page.size;
setPageSizeToLocalStorage(
PageSizeMapKeys.WORKER_LIST_COMPONENT_POOL,
this.poolPageSize
);
}
}
clrLoadWorker(state: ClrDatagridStateInterface): void {
if (state?.page?.size) {
this.workerPageSize = state.page.size;
setPageSizeToLocalStorage(
PageSizeMapKeys.WORKER_LIST_COMPONENT_POOL,
this.workerPageSize
);
}
}
canFree(): boolean {
if (this.selected?.length) {
let flag: boolean = true;
this.selected.forEach(item => {
if (!item.job_id) {
flag = false;
}
});
return flag;
}
return false;
}
freeWorker() {
const workers: string = this.selected.map(item => item.id).join(',');
const deletionMessage = new ConfirmationMessage(
'JOB_SERVICE_DASHBOARD.CONFIRM_FREE_WORKERS',
'JOB_SERVICE_DASHBOARD.CONFIRM_FREE_WORKERS_CONTENT',
workers,
this.selected,
ConfirmationTargets.FREE_SPECIFIED_WORKERS,
ConfirmationButtons.CONFIRM_CANCEL
);
this.operateDialogService.openComfirmDialog(deletionMessage);
}
executeFreeWorkers() {
this.loadingFree = true;
const operationMessage = new OperateInfo();
operationMessage.name =
'JOB_SERVICE_DASHBOARD.OPERATION_FREE_SPECIFIED_WORKERS';
operationMessage.state = OperationState.progressing;
operationMessage.data.name = this.selected
.map(item => item.id)
.join(',');
this.operationService.publishInfo(operationMessage);
forkJoin(
this.selected.map(item => {
return this.jobServiceService.stopRunningJob({
jobId: item.job_id,
});
})
)
.pipe(finalize(() => (this.loadingFree = false)))
.subscribe({
next: res => {
this.messageHandlerService.info(
'JOB_SERVICE_DASHBOARD.FREE_WORKER_SUCCESS'
);
this.selectionChanged();
operateChanges(operationMessage, OperationState.success);
},
error: err => {
this.messageHandlerService.error(err);
operateChanges(
operationMessage,
OperationState.failure,
errorHandler(err)
);
},
});
}
json(v: string): object {
if (v) {
return JSON.parse(v);
}
return null;
}
}

View File

@ -126,15 +126,15 @@
<clr-icon
shape="exclamation-triangle"
class="is-warning text-alignment"
size="18"></clr-icon
>Deactivated
size="18"></clr-icon>
{{ 'WEBHOOK.DISABLED' | translate }}
</div>
<div *ngIf="p?.enabled">
<clr-icon
shape="success-standard"
class="is-success text-alignment"
size="18"></clr-icon>
Enabled
{{ 'WEBHOOK.ENABLED' | translate }}
</div>
</div>
</clr-dg-cell>
@ -172,7 +172,14 @@
getFlattenLevelString(p.dest_namespace_replace_count)
| translate
}}</clr-dg-cell>
<clr-dg-cell>{{ p.trigger ? p.trigger.type : '' }}</clr-dg-cell>
<clr-dg-cell class="trigger">
{{ getTriggerTypeI18n(p.trigger) | translate }}
<clr-signpost *ngIf="p?.trigger?.trigger_settings?.cron">
<clr-signpost-content *clrIfOpen>
{{ p?.trigger?.trigger_settings?.cron }}
</clr-signpost-content>
</clr-signpost>
</clr-dg-cell>
<clr-dg-cell>{{
getBandwidthStr(p.speed) | translate
}}</clr-dg-cell>

View File

@ -25,3 +25,7 @@
.margin-right-2px {
margin-right: 2px;
}
.trigger {
width: 180px;
}

View File

@ -15,6 +15,7 @@ import {
Component,
EventEmitter,
Input,
OnInit,
Output,
ViewChild,
} from '@angular/core';
@ -51,13 +52,21 @@ import { BandwidthUnit, Flatten_I18n_MAP } from '../../replication';
import { KB_TO_MB } from '../create-edit-rule/create-edit-rule.component';
import { ReplicationService } from 'ng-swagger-gen/services/replication.service';
import { ReplicationPolicy } from '../../../../../../../ng-swagger-gen/models/replication-policy';
import { JobserviceService } from '../../../../../../../ng-swagger-gen/services/jobservice.service';
import { ReplicationTrigger } from '../../../../../../../ng-swagger-gen/models/replication-trigger';
import {
TRIGGER,
TRIGGER_I18N_MAP,
} from '../../../../project/p2p-provider/p2p-provider.service';
import { ScheduleService } from '../../../../../../../ng-swagger-gen/services/schedule.service';
import { JobType } from '../../../job-service-dashboard/job-service-dashboard.interface';
@Component({
selector: 'hbr-list-replication-rule',
templateUrl: './list-replication-rule.component.html',
styleUrls: ['./list-replication-rule.component.scss'],
})
export class ListReplicationRuleComponent {
export class ListReplicationRuleComponent implements OnInit {
@Input() selectedId: number | string;
@Input() withReplicationJob: boolean;
@Input() hasCreateReplicationPermission: boolean;
@ -86,13 +95,34 @@ export class ListReplicationRuleComponent {
totalCount: number = 0;
loading: boolean = true;
paused: boolean = false;
constructor(
private replicationService: ReplicationService,
private translateService: TranslateService,
private errorHandlerEntity: ErrorHandler,
private operationService: OperationService
private operationService: OperationService,
private scheduleService: ScheduleService
) {}
ngOnInit() {
this.scheduleService
.getSchedulePaused({ jobType: JobType.ALL })
.subscribe(res => {
this.paused = res?.paused;
});
}
getTriggerTypeI18n(t: ReplicationTrigger) {
if (t) {
if (this.paused && t?.type === TRIGGER.SCHEDULED) {
return TRIGGER_I18N_MAP[TRIGGER.SCHEDULED_PAUSED];
}
return TRIGGER_I18N_MAP[t?.type];
}
return null;
}
trancatedDescription(desc: string): string {
if (desc.length > 35) {
return desc.substr(0, 35);

View File

@ -66,6 +66,7 @@ export enum EXECUTION_STATUS {
export enum TRIGGER {
MANUAL = 'manual',
SCHEDULED = 'scheduled',
SCHEDULED_PAUSED = 'scheduled(paused)',
EVENT_BASED = 'event_based',
}
@ -73,6 +74,7 @@ export const TRIGGER_I18N_MAP = {
manual: 'P2P_PROVIDER.MANUAL',
scheduled: 'P2P_PROVIDER.SCHEDULED',
event_based: 'P2P_PROVIDER.EVENT_BASED',
'scheduled(paused)': 'JOB_SERVICE_DASHBOARD.SCHEDULE_PAUSED',
};
export const TIME_OUT: number = 7000;

View File

@ -56,6 +56,9 @@ import {
EventService,
HarborEvent,
} from '../../../../services/event-service/event.service';
import { JobserviceService } from '../../../../../../ng-swagger-gen/services/jobservice.service';
import { ScheduleService } from '../../../../../../ng-swagger-gen/services/schedule.service';
import { JobType } from '../../../left-side-nav/job-service-dashboard/job-service-dashboard.interface';
// The route path which will display this component
const URL_TO_DISPLAY: RegExp =
/\/harbor\/projects\/(\d+)\/p2p-provider\/policies/;
@ -109,6 +112,7 @@ export class PolicyComponent implements OnInit, OnDestroy {
policyPage: number = 1;
totalPolicy: number = 0;
state: ClrDatagridStateInterface;
paused: boolean = false;
constructor(
private route: ActivatedRoute,
private router: Router,
@ -117,10 +121,18 @@ export class PolicyComponent implements OnInit, OnDestroy {
private messageHandlerService: MessageHandlerService,
private userPermissionService: UserPermissionService,
private preheatService: PreheatService,
private event: EventService
private event: EventService,
private scheduleService: ScheduleService
) {}
ngOnInit() {
this.scheduleService
.getSchedulePaused({
jobType: JobType.ALL,
})
.subscribe(res => {
this.paused = res?.paused;
});
if (!this.scrollSub) {
this.scrollSub = this.event.subscribe(HarborEvent.SCROLL, v => {
if (v && URL_TO_DISPLAY.test(v.url)) {
@ -638,6 +650,9 @@ export class PolicyComponent implements OnInit, OnDestroy {
getTriggerTypeI18n(trigger: string): string {
if (JSON.parse(trigger).type) {
if (this.paused && JSON.parse(trigger).type === TRIGGER.SCHEDULED) {
return TRIGGER_I18N_MAP[TRIGGER.SCHEDULED_PAUSED];
}
return TRIGGER_I18N_MAP[JSON.parse(trigger).type];
}
return TRIGGER_I18N_MAP[TRIGGER.MANUAL];

View File

@ -168,6 +168,7 @@
</div>
<div class="cron-selection">
<cron-selection
class="w-100"
[labelWidth]="'150px'"
[disabled]="!(retention?.rules?.length > 0)"
#cronScheduleComponent

View File

@ -79,4 +79,5 @@ export enum HarborEvent {
STOP_SCAN_ARTIFACT = 'stopScanArtifact',
UPDATE_VULNERABILITY_INFO = 'UpdateVulnerabilityInfo',
REFRESH_EXPORT_JOBS = 'refreshExportJobs',
REFRESH_JOB_SERVICE_DASHBOARD = 'refreshJobServiceDashboard',
}

View File

@ -147,6 +147,27 @@
{{ 'BUTTON.CANCEL' | translate }}
</button>
</div>
<div *ngIf="paused && oriCron" class="clr-row mb-1">
<span class="font-style" [style.width]="labelWidth"></span>
<div class="alert alert-warning" role="alert">
<div class="alert-items p-0">
<div class="alert-item static">
<div class="alert-icon-wrapper">
<cds-icon
class="alert-icon"
shape="exclamation-triangle"></cds-icon>
</div>
<span class="alert-text">{{
'JOB_SERVICE_DASHBOARD.SCHEDULE_BEEN_PAUSED'
| translate
: {
param: labelCurrent
}
}}</span>
</div>
</div>
</div>
</div>
</ng-container>
<ng-container *ngIf="isInlineModel">
<div class="normal-wrapper-box height-1rem" *ngIf="!isEditMode">

View File

@ -32,3 +32,9 @@ span.required {
.mb-05 {
margin-bottom: 0.5rem;
}
.alert-warning {
flex-grow: 1;
margin-right: 0.6rem;
margin-left: 0.6rem;
}

View File

@ -6,11 +6,15 @@ import {
OnChanges,
SimpleChanges,
SimpleChange,
OnInit,
} from '@angular/core';
import { OriginCron } from '../../services/interface';
import { cronRegex } from '../../units/utils';
import { TranslateService } from '@ngx-translate/core';
import { ErrorHandler } from '../../units/error-handler/error-handler';
import { JobserviceService } from '../../../../../ng-swagger-gen/services/jobservice.service';
import { ScheduleService } from '../../../../../ng-swagger-gen/services/schedule.service';
import { JobType } from '../../../base/left-side-nav/job-service-dashboard/job-service-dashboard.interface';
const SCHEDULE_TYPE = {
NONE: 'None',
DAILY: 'Daily',
@ -24,7 +28,7 @@ const PREFIX: string = '0 ';
templateUrl: './cron-schedule.component.html',
styleUrls: ['./cron-schedule.component.scss'],
})
export class CronScheduleComponent implements OnChanges {
export class CronScheduleComponent implements OnChanges, OnInit {
@Input() externalValidation: boolean = true; //extra check
@Input() isInlineModel: boolean = false;
@Input() originCron: OriginCron;
@ -40,11 +44,26 @@ export class CronScheduleComponent implements OnChanges {
SCHEDULE_TYPE = SCHEDULE_TYPE;
scheduleType: string;
@Output() inputvalue = new EventEmitter<string>();
paused: boolean = false;
constructor(
private translate: TranslateService,
private errorHandler: ErrorHandler
private errorHandler: ErrorHandler,
private scheduleService: ScheduleService
) {}
ngOnInit() {
if (this.labelCurrent) {
this.translate
.get(this.labelCurrent)
.subscribe(res => (this.labelCurrent = res));
}
this.scheduleService
.getSchedulePaused({ jobType: JobType.ALL })
.subscribe(res => {
this.paused = res?.paused;
});
}
ngOnChanges(changes: SimpleChanges): void {
let cronChange: SimpleChange = changes['originCron'];
if (cronChange?.currentValue) {

View File

@ -56,6 +56,14 @@ export const enum ConfirmationTargets {
ALL_ACCESSORIES,
STOP_GC,
STOP_AUDIT_LOG_ROTATION,
FREE_ALL_WORKERS,
RESUME_ALL_SCHEDULES,
PAUSE_ALL_SCHEDULES,
STOP_ALL_PENDING_JOBS,
FREE_SPECIFIED_WORKERS,
STOPS_JOBS,
PAUSE_JOBS,
RESUME_JOBS,
}
export const enum ActionType {

View File

@ -1,6 +1,7 @@
import {
DEFAULT_PAGE_SIZE,
delUrlParam,
durationStr,
getPageSizeFromLocalStorage,
getQueryString,
getSizeNumber,
@ -119,4 +120,11 @@ describe('functions in utils.ts should work', () => {
setPageSizeToLocalStorage('test1', 10);
expect(getPageSizeFromLocalStorage('test1')).toEqual(10);
});
it('functions durationStr(distance: number) should work', () => {
expect(durationStr(11)).toEqual('0');
expect(durationStr(1111)).toEqual('1sec');
expect(durationStr(61111)).toEqual('1min 1sec');
expect(durationStr(3661111)).toEqual('1hrs 1min 1sec');
});
});

View File

@ -17,6 +17,8 @@ import {
import { AbstractControl } from '@angular/forms';
import { isValidCron } from 'cron-validator';
import { ClrDatagridStateInterface } from '@clr/angular';
import { ScheduleListComponent } from '../../base/left-side-nav/job-service-dashboard/schedule-list/schedule-list.component';
import { PendingListComponent } from '../../base/left-side-nav/job-service-dashboard/pending-job-list/pending-job-list.component';
/**
* Api levels
@ -928,6 +930,29 @@ export function setPageSizeToLocalStorage(key: string, pageSize: number) {
}
}
/**
* Convert seconds to xx hrs xx min xx sec
* @param distance in milliseconds
*/
export function durationStr(distance: number): string {
const hours = Math.floor(distance / 3600000);
distance -= hours * 3600000;
const minutes = Math.floor(distance / 60000);
distance -= minutes * 60000;
const seconds = Math.floor(distance / 1000);
let result: string = '';
if (seconds) {
result = `${seconds}sec`;
}
if (minutes) {
result = `${minutes}min ${seconds}sec`;
}
if (hours) {
result = `${hours}hrs ${minutes}min ${seconds}sec`;
}
return result ? result : '0';
}
export enum PageSizeMapKeys {
LIST_PROJECT_COMPONENT = 'ListProjectComponent',
REPOSITORY_GRIDVIEW_COMPONENT = 'RepositoryGridviewComponent',
@ -957,4 +982,8 @@ export enum PageSizeMapKeys {
SYSTEM_SCANNER_COMPONENT = 'ConfigurationScannerComponent',
GC_HISTORY_COMPONENT = 'GcHistoryComponent',
SYSTEM_GROUP_COMPONENT = 'SystemGroupComponent',
WORKER_LIST_COMPONENT_POOL = 'WorkerListComponentPool',
WORKER_LIST_COMPONENT_WORKER = 'WorkerListComponentWorker',
SCHEDULE_LIST_COMPONENT = 'ScheduleListComponent',
PENDING_LIST_COMPONENT = 'PendingListComponent',
}

View File

@ -347,3 +347,13 @@ app-artifact-filter {
color: $normal-border-color !important;;
}
}
job-service-dashboard {
.duration {
color: $text-color-job-service-dashboard;
}
}
.datagrid-numeric-filter-input {
background-color: $datagrid-numeric-filter-input-bg-color;
}

View File

@ -45,4 +45,6 @@ $pull-command-icon-color: #4aaed9;
$pull-command-icon-hover-color: #007CBB;
$select-all-for-dropdown-color: #4aaed9;
$normal-border-color: #acbac3;
$text-color-job-service-dashboard: #49aeda;
$datagrid-numeric-filter-input-bg-color: #21333b;
@import "./common.scss";

View File

@ -46,4 +46,6 @@ $pull-command-icon-color: #007CBB;
$pull-command-icon-hover-color: #4aaed9;
$select-all-for-dropdown-color: #0072a3;
$normal-border-color: #6a7a81;
$text-color-job-service-dashboard: #0072a3;
$datagrid-numeric-filter-input-bg-color: unset;
@import "./common.scss";

View File

@ -1765,5 +1765,75 @@
"JOB_NAME_REQUIRED": "Job name is required",
"JOB_NAME_EXISTING": "Job name already exists",
"TRIGGER_EXPORT_SUCCESS": "Trigger exporting CVEs successfully!"
},
"JOB_SERVICE_DASHBOARD": {
"SCHEDULE_PAUSED": "Scheduled(Paused)",
"SCHEDULE_BEEN_PAUSED": "{{param}} has been paused",
"PENDING_JOBS": "Pending Jobs In Queues",
"OTHERS": "Others",
"STOP_ALL": "STOP ALL",
"CONFIRM_STOP_ALL": "Confirm Stopping All",
"CONFIRM_STOP_ALL_CONTENT": "Do you want to stop all the job queues?",
"STOP_ALL_SUCCESS": "Stopped all the job queues successfully",
"STOP_BTN": "STOP",
"PAUSE_BTN": "PAUSE",
"RESUME_BTN": "RESUME",
"JOB_TYPE": "Job Type",
"PENDING_COUNT": "Pending Count",
"LATENCY": "Latency",
"PAUSED": "Paused",
"NO_JOB_QUEUE": "We could not find any job queue",
"CONFIRM_STOPPING_JOBS": "Confirm Stopping Jobs",
"CONFIRM_STOPPING_JOBS_CONTENT": "Do you want to stop the jobs {{param}}?",
"CONFIRM_PAUSING_JOBS": "Confirm Pausing Jobs",
"CONFIRM_PAUSING_JOBS_CONTENT": "Do you want to pause the jobs {{param}}?",
"CONFIRM_RESUMING_JOBS": "Confirm Resuming Jobs",
"CONFIRM_RESUMING_JOBS_CONTENT": "Do you want to resume the jobs {{param}}?",
"STOP_SUCCESS": "Stopped jobs Successfully",
"PAUSE_SUCCESS": "Paused jobs Successfully",
"RESUME_SUCCESS": "Resumed jobs Successfully",
"SCHEDULES": "Schedules",
"RUNNING_STATUS": "Running",
"RESUME_ALL_BTN_TEXT": "RESUME ALL",
"PAUSE_ALL_BTN_TEXT": "PAUSE ALL",
"CONFIRM_PAUSING_ALL": "Confirm Pausing All",
"CONFIRM_PAUSING_ALL_CONTENT": "Do you want to pause all the jobs schedules?",
"CONFIRM_RESUMING_ALL": "Confirm Resuming All",
"CONFIRM_RESUMING_ALL_CONTENT": "Do you want to resume all the jobs schedules?",
"PAUSE_ALL_SUCCESS": "Paused all the schedules Successfully",
"RESUME_ALL_SUCCESS": "Resumed all the schedules Successfully",
"VENDOR_TYPE": "Vendor Type",
"VENDOR_ID": "Vendor ID",
"EXTRA_ATTR": "Extra Attribute",
"NO_SCHEDULE": "We could not find any schedule",
"WORKERS": "Workers",
"FREE_ALL": "Free all",
"CONFIRM_FREE_ALL": "Confirm Freeing All",
"CONFIRM_FREE_ALL_CONTENT": "Do you want to free all the workers?",
"CONFIRM_FREE_WORKERS": "Confirm Freeing Workers",
"CONFIRM_FREE_WORKERS_CONTENT": "Do you want to free the workers {{param}}?",
"FREE_WORKER_SUCCESS": "Freed workers successfully",
"FREE_ALL_SUCCESS": "Freed all the workers successfully",
"WORKER_POOL": "Worker Pools",
"WORKER_POOL_ID": "Worker Pool ID",
"PID": "Pid",
"START_AT": "Started At",
"HEARTBEAT_AT": "Heartbeat At",
"CONCURRENCY": "Concurrency",
"NO_WORKER_POOL": "We could not find any worker pool",
"FREE": "Free",
"WORKER_ID": "Worker ID",
"JOB_ID": "Job ID",
"JOB_PARAM": "Job Parameter",
"CHECK_IN_AT": "Checked In At",
"NO_WORKER": "We could not find any worker",
"JOB_QUEUE": "Job Queues",
"JOB_SERVICE_DASHBOARD": "Job Service Dashboard",
"QUEUE_STOP_BTN_INFO": "STOP — Stop all jobs in the queue and remove them from the queue.",
"QUEUE_PAUSE_BTN_INFO": "PAUSE — Pause to execute jobs in this type of job queue, jobs can be enqueued when the queue is paused.",
"QUEUE_RESUME_BTN_INFO": "RESUME — Resume to execute jobs in this type of job queue.",
"SCHEDULE_PAUSE_BTN_INFO": "PAUSE — Pause all schedules to execute.",
"SCHEDULE_RESUME_BTN_INFO": "RESUME — Resume all schedules to execute.",
"WORKER_FREE_BTN_INFO": "Stop the current running job to free the worker"
}
}

View File

@ -1765,5 +1765,83 @@
"JOB_NAME_REQUIRED": "Job name is required",
"JOB_NAME_EXISTING": "Job name already exists",
"TRIGGER_EXPORT_SUCCESS": "Trigger exporting CVEs successfully!"
},
"JOB_SERVICE_DASHBOARD": {
"SCHEDULE_PAUSED": "Scheduled(Paused)",
"SCHEDULE_BEEN_PAUSED": "{{param}} has been paused",
"PENDING_JOBS": "Pending Jobs In Queues",
"OTHERS": "Others",
"STOP_ALL": "STOP ALL",
"CONFIRM_STOP_ALL": "Confirm Stopping All",
"CONFIRM_STOP_ALL_CONTENT": "Do you want to stop all the job queues?",
"STOP_ALL_SUCCESS": "Stopped all the job queues successfully",
"STOP_BTN": "STOP",
"PAUSE_BTN": "PAUSE",
"RESUME_BTN": "RESUME",
"JOB_TYPE": "Job Type",
"PENDING_COUNT": "Pending Count",
"LATENCY": "Latency",
"PAUSED": "Paused",
"NO_JOB_QUEUE": "We could not find any job queue",
"CONFIRM_STOPPING_JOBS": "Confirm Stopping Jobs",
"CONFIRM_STOPPING_JOBS_CONTENT": "Do you want to stop the jobs {{param}}?",
"CONFIRM_PAUSING_JOBS": "Confirm Pausing Jobs",
"CONFIRM_PAUSING_JOBS_CONTENT": "Do you want to pause the jobs {{param}}?",
"CONFIRM_RESUMING_JOBS": "Confirm Resuming Jobs",
"CONFIRM_RESUMING_JOBS_CONTENT": "Do you want to resume the jobs {{param}}?",
"STOP_SUCCESS": "Stopped jobs successfully",
"PAUSE_SUCCESS": "Paused jobs successfully",
"RESUME_SUCCESS": "Resumed jobs successfully",
"SCHEDULES": "Schedules",
"RUNNING_STATUS": "Running",
"RESUME_ALL_BTN_TEXT": "RESUME ALL",
"PAUSE_ALL_BTN_TEXT": "PAUSE ALL",
"CONFIRM_PAUSING_ALL": "Confirm Pausing All",
"CONFIRM_PAUSING_ALL_CONTENT": "Do you want to pause all the jobs schedules?",
"CONFIRM_RESUMING_ALL": "Confirm Resuming All",
"CONFIRM_RESUMING_ALL_CONTENT": "Do you want to resume all the jobs schedules?",
"PAUSE_ALL_SUCCESS": "Paused all the schedules successfully",
"RESUME_ALL_SUCCESS": "Resumed all the schedules successfully",
"VENDOR_TYPE": "Vendor Type",
"VENDOR_ID": "Vendor ID",
"EXTRA_ATTR": "Extra Attribute",
"NO_SCHEDULE": "We could not find any schedule",
"WORKERS": "Workers",
"FREE_ALL": "Free all",
"CONFIRM_FREE_ALL": "Confirm Freeing All",
"CONFIRM_FREE_ALL_CONTENT": "Do you want to free all the workers?",
"CONFIRM_FREE_WORKERS": "Confirm Freeing Workers",
"CONFIRM_FREE_WORKERS_CONTENT": "Do you want to free the workers {{param}}?",
"FREE_WORKER_SUCCESS": "Freed workers successfully",
"FREE_ALL_SUCCESS": "Freed all the workers successfully",
"WORKER_POOL": "Worker Pools",
"WORKER_POOL_ID": "Worker Pool ID",
"PID": "Pid",
"START_AT": "Started At",
"HEARTBEAT_AT": "Heartbeat At",
"CONCURRENCY": "Concurrency",
"NO_WORKER_POOL": "We could not find any worker pool",
"FREE": "Free",
"WORKER_ID": "Worker ID",
"JOB_ID": "Job ID",
"JOB_PARAM": "Job Parameter",
"CHECK_IN_AT": "Checked In At",
"NO_WORKER": "We could not find any worker",
"JOB_QUEUE": "Job Queues",
"JOB_SERVICE_DASHBOARD": "Job Service Dashboard",
"OPERATION_STOP_ALL_QUEUES": "Stop all job queues",
"OPERATION_STOP_SPECIFIED_QUEUES": "Stop specified job queues",
"OPERATION_PAUSE_SPECIFIED_QUEUES": "Pause specified job queues",
"OPERATION_RESUME_SPECIFIED_QUEUES": "Resume specified job queues",
"OPERATION_PAUSE_SCHEDULE": "Pause all schedules",
"OPERATION_RESUME_SCHEDULE": "Resume all schedules",
"OPERATION_FREE_ALL": "Free all workers",
"OPERATION_FREE_SPECIFIED_WORKERS": "Free specified workers",
"QUEUE_STOP_BTN_INFO": "STOP — Stop all jobs in the queue and remove them from the queue.",
"QUEUE_PAUSE_BTN_INFO": "PAUSE — Pause to execute jobs in this type of job queue, jobs can be enqueued when the queue is paused.",
"QUEUE_RESUME_BTN_INFO": "RESUME — Resume to execute jobs in this type of job queue.",
"SCHEDULE_PAUSE_BTN_INFO": "PAUSE — Pause all schedules to execute.",
"SCHEDULE_RESUME_BTN_INFO": "RESUME — Resume all schedules to execute.",
"WORKER_FREE_BTN_INFO": "Stop the current running job to free the worker"
}
}

View File

@ -1764,5 +1764,75 @@
"JOB_NAME_REQUIRED": "Job name is required",
"JOB_NAME_EXISTING": "Job name already exists",
"TRIGGER_EXPORT_SUCCESS": "Trigger exporting CVEs successfully!"
},
"JOB_SERVICE_DASHBOARD": {
"SCHEDULE_PAUSED": "Scheduled(Paused)",
"SCHEDULE_BEEN_PAUSED": "{{param}} has been paused",
"PENDING_JOBS": "Pending Jobs In Queues",
"OTHERS": "Others",
"STOP_ALL": "STOP ALL",
"CONFIRM_STOP_ALL": "Confirm Stopping All",
"CONFIRM_STOP_ALL_CONTENT": "Do you want to stop all the job queues?",
"STOP_ALL_SUCCESS": "Stopped all the job queues successfully",
"STOP_BTN": "STOP",
"PAUSE_BTN": "PAUSE",
"RESUME_BTN": "RESUME",
"JOB_TYPE": "Job Type",
"PENDING_COUNT": "Pending Count",
"LATENCY": "Latency",
"PAUSED": "Paused",
"NO_JOB_QUEUE": "We could not find any job queue",
"CONFIRM_STOPPING_JOBS": "Confirm Stopping Jobs",
"CONFIRM_STOPPING_JOBS_CONTENT": "Do you want to stop the jobs {{param}}?",
"CONFIRM_PAUSING_JOBS": "Confirm Pausing Jobs",
"CONFIRM_PAUSING_JOBS_CONTENT": "Do you want to pause the jobs {{param}}?",
"CONFIRM_RESUMING_JOBS": "Confirm Resuming Jobs",
"CONFIRM_RESUMING_JOBS_CONTENT": "Do you want to resume the jobs {{param}}?",
"STOP_SUCCESS": "Stopped jobs Successfully",
"PAUSE_SUCCESS": "Paused jobs Successfully",
"RESUME_SUCCESS": "Resumed jobs Successfully",
"SCHEDULES": "Schedules",
"RUNNING_STATUS": "Running",
"RESUME_ALL_BTN_TEXT": "RESUME ALL",
"PAUSE_ALL_BTN_TEXT": "PAUSE ALL",
"CONFIRM_PAUSING_ALL": "Confirm Pausing All",
"CONFIRM_PAUSING_ALL_CONTENT": "Do you want to pause all the jobs schedules?",
"CONFIRM_RESUMING_ALL": "Confirm Resuming All",
"CONFIRM_RESUMING_ALL_CONTENT": "Do you want to resume all the jobs schedules?",
"PAUSE_ALL_SUCCESS": "Paused all the schedules Successfully",
"RESUME_ALL_SUCCESS": "Resumed all the schedules Successfully",
"VENDOR_TYPE": "Vendor Type",
"VENDOR_ID": "Vendor ID",
"EXTRA_ATTR": "Extra Attribute",
"NO_SCHEDULE": "We could not find any schedule",
"WORKERS": "Workers",
"FREE_ALL": "Free all",
"CONFIRM_FREE_ALL": "Confirm Freeing All",
"CONFIRM_FREE_ALL_CONTENT": "Do you want to free all the workers?",
"CONFIRM_FREE_WORKERS": "Confirm Freeing Workers",
"CONFIRM_FREE_WORKERS_CONTENT": "Do you want to free the workers {{param}}?",
"FREE_WORKER_SUCCESS": "Freed workers successfully",
"FREE_ALL_SUCCESS": "Freed all the workers successfully",
"WORKER_POOL": "Worker Pools",
"WORKER_POOL_ID": "Worker Pool ID",
"PID": "Pid",
"START_AT": "Started At",
"HEARTBEAT_AT": "Heartbeat At",
"CONCURRENCY": "Concurrency",
"NO_WORKER_POOL": "We could not find any worker pool",
"FREE": "Free",
"WORKER_ID": "Worker ID",
"JOB_ID": "Job ID",
"JOB_PARAM": "Job Parameter",
"CHECK_IN_AT": "Checked In At",
"NO_WORKER": "We could not find any worker",
"JOB_QUEUE": "Job Queues",
"JOB_SERVICE_DASHBOARD": "Job Service Dashboard",
"QUEUE_STOP_BTN_INFO": "STOP — Stop all jobs in the queue and remove them from the queue.",
"QUEUE_PAUSE_BTN_INFO": "PAUSE — Pause to execute jobs in this type of job queue, jobs can be enqueued when the queue is paused.",
"QUEUE_RESUME_BTN_INFO": "RESUME — Resume to execute jobs in this type of job queue.",
"SCHEDULE_PAUSE_BTN_INFO": "PAUSE — Pause all schedules to execute.",
"SCHEDULE_RESUME_BTN_INFO": "RESUME — Resume all schedules to execute.",
"WORKER_FREE_BTN_INFO": "Stop the current running job to free the worker"
}
}

View File

@ -1734,5 +1734,75 @@
"JOB_NAME_REQUIRED": "Job name is required",
"JOB_NAME_EXISTING": "Job name already exists",
"TRIGGER_EXPORT_SUCCESS": "Trigger exporting CVEs successfully!"
},
"JOB_SERVICE_DASHBOARD": {
"SCHEDULE_PAUSED": "Scheduled(Paused)",
"SCHEDULE_BEEN_PAUSED": "{{param}} has been paused",
"PENDING_JOBS": "Pending Jobs In Queues",
"OTHERS": "Other",
"STOP_ALL": "STOP ALL",
"CONFIRM_STOP_ALL": "Confirm Stopping All",
"CONFIRM_STOP_ALL_CONTENT": "Do you want to stop all the job queues?",
"STOP_ALL_SUCCESS": "Stopped all the job queues successfully",
"STOP_BTN": "STOP",
"PAUSE_BTN": "PAUSE",
"RESUME_BTN": "RESUME",
"JOB_TYPE": "Job Type",
"PENDING_COUNT": "Pending Count",
"LATENCY": "Latency",
"PAUSED": "Paused",
"NO_JOB_QUEUE": "We could not find any job queue",
"CONFIRM_STOPPING_JOBS": "Confirm Stopping Jobs",
"CONFIRM_STOPPING_JOBS_CONTENT": "Do you want to stop the jobs {{param}}?",
"CONFIRM_PAUSING_JOBS": "Confirm Pausing Jobs",
"CONFIRM_PAUSING_JOBS_CONTENT": "Do you want to pause the jobs {{param}}?",
"CONFIRM_RESUMING_JOBS": "Confirm Resuming Jobs",
"CONFIRM_RESUMING_JOBS_CONTENT": "Do you want to resume the jobs {{param}}?",
"STOP_SUCCESS": "Stopped jobs Successfully",
"PAUSE_SUCCESS": "Paused jobs Successfully",
"RESUME_SUCCESS": "Resumed jobs Successfully",
"SCHEDULES": "Schedules",
"RUNNING_STATUS": "Running",
"RESUME_ALL_BTN_TEXT": "RESUME ALL",
"PAUSE_ALL_BTN_TEXT": "PAUSE ALL",
"CONFIRM_PAUSING_ALL": "Confirm Pausing All",
"CONFIRM_PAUSING_ALL_CONTENT": "Do you want to pause all the jobs schedules?",
"CONFIRM_RESUMING_ALL": "Confirm Resuming All",
"CONFIRM_RESUMING_ALL_CONTENT": "Do you want to resume all the jobs schedules?",
"PAUSE_ALL_SUCCESS": "Paused all the schedules Successfully",
"RESUME_ALL_SUCCESS": "Resumed all the schedules Successfully",
"VENDOR_TYPE": "Vendor Type",
"VENDOR_ID": "Vendor ID",
"EXTRA_ATTR": "Extra Attribute",
"NO_SCHEDULE": "We could not find any schedule",
"WORKERS": "Workers",
"FREE_ALL": "Free all",
"CONFIRM_FREE_ALL": "Confirm Freeing All",
"CONFIRM_FREE_ALL_CONTENT": "Do you want to free all the workers?",
"CONFIRM_FREE_WORKERS": "Confirm Freeing Workers",
"CONFIRM_FREE_WORKERS_CONTENT": "Do you want to free the workers {{param}}?",
"FREE_WORKER_SUCCESS": "Freed workers successfully",
"FREE_ALL_SUCCESS": "Freed all the workers successfully",
"WORKER_POOL": "Worker Pools",
"WORKER_POOL_ID": "Worker Pool ID",
"PID": "Pid",
"START_AT": "Started At",
"HEARTBEAT_AT": "Heartbeat At",
"CONCURRENCY": "Concurrency",
"NO_WORKER_POOL": "We could not find any worker pool",
"FREE": "Free",
"WORKER_ID": "Worker ID",
"JOB_ID": "Job ID",
"JOB_PARAM": "Job Parameter",
"CHECK_IN_AT": "Checked In At",
"NO_WORKER": "We could not find any worker",
"JOB_QUEUE": "Job Queues",
"JOB_SERVICE_DASHBOARD": "Job Service Dashboard",
"QUEUE_STOP_BTN_INFO": "STOP — Stop all jobs in the queue and remove them from the queue.",
"QUEUE_PAUSE_BTN_INFO": "PAUSE — Pause to execute jobs in this type of job queue, jobs can be enqueued when the queue is paused.",
"QUEUE_RESUME_BTN_INFO": "RESUME — Resume to execute jobs in this type of job queue.",
"SCHEDULE_PAUSE_BTN_INFO": "PAUSE — Pause all schedules to execute.",
"SCHEDULE_RESUME_BTN_INFO": "RESUME — Resume all schedule to execute.",
"WORKER_FREE_BTN_INFO": "Stop the current running job to free the worker"
}
}

View File

@ -1761,5 +1761,75 @@
"JOB_NAME_REQUIRED": "Job name is required",
"JOB_NAME_EXISTING": "Job name already exists",
"TRIGGER_EXPORT_SUCCESS": "Trigger exporting CVEs successfully!"
},
"JOB_SERVICE_DASHBOARD": {
"SCHEDULE_PAUSED": "Scheduled(Paused)",
"SCHEDULE_BEEN_PAUSED": "{{param}} has been paused",
"PENDING_JOBS": "Pending Jobs In Queues",
"OTHERS": "Others",
"STOP_ALL": "STOP ALL",
"CONFIRM_STOP_ALL": "Confirm Stopping All",
"CONFIRM_STOP_ALL_CONTENT": "Do you want to stop all the job queues?",
"STOP_ALL_SUCCESS": "Stopped all the job queues successfully",
"STOP_BTN": "STOP",
"PAUSE_BTN": "PAUSE",
"RESUME_BTN": "RESUME",
"JOB_TYPE": "Job Type",
"PENDING_COUNT": "Pending Count",
"LATENCY": "Latency",
"PAUSED": "Paused",
"NO_JOB_QUEUE": "We could not find any job queue",
"CONFIRM_STOPPING_JOBS": "Confirm Stopping Jobs",
"CONFIRM_STOPPING_JOBS_CONTENT": "Do you want to stop the jobs {{param}}?",
"CONFIRM_PAUSING_JOBS": "Confirm Pausing Jobs",
"CONFIRM_PAUSING_JOBS_CONTENT": "Do you want to pause the jobs {{param}}?",
"CONFIRM_RESUMING_JOBS": "Confirm Resuming Jobs",
"CONFIRM_RESUMING_JOBS_CONTENT": "Do you want to resume the jobs {{param}}?",
"STOP_SUCCESS": "Stopped jobs Successfully",
"PAUSE_SUCCESS": "Paused jobs Successfully",
"RESUME_SUCCESS": "Resumed jobs Successfully",
"SCHEDULES": "Schedules",
"RUNNING_STATUS": "Running",
"RESUME_ALL_BTN_TEXT": "RESUME ALL",
"PAUSE_ALL_BTN_TEXT": "PAUSE ALL",
"CONFIRM_PAUSING_ALL": "Confirm Pausing All",
"CONFIRM_PAUSING_ALL_CONTENT": "Do you want to pause all the jobs schedules?",
"CONFIRM_RESUMING_ALL": "Confirm Resuming All",
"CONFIRM_RESUMING_ALL_CONTENT": "Do you want to resume all the jobs schedules?",
"PAUSE_ALL_SUCCESS": "Paused all the schedules Successfully",
"RESUME_ALL_SUCCESS": "Resumed all the schedules Successfully",
"VENDOR_TYPE": "Vendor Type",
"VENDOR_ID": "Vendor ID",
"EXTRA_ATTR": "Extra Attribute",
"NO_SCHEDULE": "We could not find any schedule",
"WORKERS": "Workers",
"FREE_ALL": "Free all",
"CONFIRM_FREE_ALL": "Confirm Freeing All",
"CONFIRM_FREE_ALL_CONTENT": "Do you want to free all the workers?",
"CONFIRM_FREE_WORKERS": "Confirm Freeing Workers",
"CONFIRM_FREE_WORKERS_CONTENT": "Do you want to free the workers {{param}}?",
"FREE_WORKER_SUCCESS": "Freed workers successfully",
"FREE_ALL_SUCCESS": "Freed all the workers successfully",
"WORKER_POOL": "Worker Pools",
"WORKER_POOL_ID": "Worker Pool ID",
"PID": "Pid",
"START_AT": "Started At",
"HEARTBEAT_AT": "Heartbeat At",
"CONCURRENCY": "Concurrency",
"NO_WORKER_POOL": "We could not find any worker pool",
"FREE": "Free",
"WORKER_ID": "Worker ID",
"JOB_ID": "Job ID",
"JOB_PARAM": "Job Parameter",
"CHECK_IN_AT": "Checked In At",
"NO_WORKER": "We could not find any worker",
"JOB_QUEUE": "Job Queues",
"JOB_SERVICE_DASHBOARD": "Job Service Dashboard",
"QUEUE_STOP_BTN_INFO": "STOP — Stop all jobs in the queue and remove them from the queue.",
"QUEUE_PAUSE_BTN_INFO": "PAUSE — Pause to execute jobs in this type of job queue, jobs can be enqueued when the queue is paused.",
"QUEUE_RESUME_BTN_INFO": "RESUME — Resume to execute jobs in this type of job queue.",
"SCHEDULE_PAUSE_BTN_INFO": "PAUSE — Pause all schedules to execute.",
"SCHEDULE_RESUME_BTN_INFO": "RESUME — Resume all schedule to execute.",
"WORKER_FREE_BTN_INFO": "Stop the current running job to free the worker"
}
}

View File

@ -1765,5 +1765,75 @@
"JOB_NAME_REQUIRED": "Job name is required",
"JOB_NAME_EXISTING": "Job name already exists",
"TRIGGER_EXPORT_SUCCESS": "Trigger exporting CVEs successfully!"
},
"JOB_SERVICE_DASHBOARD": {
"SCHEDULE_PAUSED": "Scheduled(Paused)",
"SCHEDULE_BEEN_PAUSED": "{{param}} has been paused",
"PENDING_JOBS": "Pending Jobs In Queues",
"OTHERS": "Others",
"STOP_ALL": "STOP ALL",
"CONFIRM_STOP_ALL": "Confirm Stopping All",
"CONFIRM_STOP_ALL_CONTENT": "Do you want to stop all the job queues?",
"STOP_ALL_SUCCESS": "Stopped all the job queues successfully",
"STOP_BTN": "STOP",
"PAUSE_BTN": "PAUSE",
"RESUME_BTN": "RESUME",
"JOB_TYPE": "Job Type",
"PENDING_COUNT": "Pending Count",
"LATENCY": "Latency",
"PAUSED": "Paused",
"NO_JOB_QUEUE": "We could not find any job queue",
"CONFIRM_STOPPING_JOBS": "Confirm Stopping Jobs",
"CONFIRM_STOPPING_JOBS_CONTENT": "Do you want to stop the jobs {{param}}?",
"CONFIRM_PAUSING_JOBS": "Confirm Pausing Jobs",
"CONFIRM_PAUSING_JOBS_CONTENT": "Do you want to pause the jobs {{param}}?",
"CONFIRM_RESUMING_JOBS": "Confirm Resuming Jobs",
"CONFIRM_RESUMING_JOBS_CONTENT": "Do you want to resume the jobs {{param}}?",
"STOP_SUCCESS": "Stopped jobs Successfully",
"PAUSE_SUCCESS": "Paused jobs Successfully",
"RESUME_SUCCESS": "Resumed jobs Successfully",
"SCHEDULES": "Schedules",
"RUNNING_STATUS": "Running",
"RESUME_ALL_BTN_TEXT": "RESUME ALL",
"PAUSE_ALL_BTN_TEXT": "PAUSE ALL",
"CONFIRM_PAUSING_ALL": "Confirm Pausing All",
"CONFIRM_PAUSING_ALL_CONTENT": "Do you want to pause all the jobs schedules?",
"CONFIRM_RESUMING_ALL": "Confirm Resuming All",
"CONFIRM_RESUMING_ALL_CONTENT": "Do you want to resume all the jobs schedules?",
"PAUSE_ALL_SUCCESS": "Paused all the schedules Successfully",
"RESUME_ALL_SUCCESS": "Resumed all the schedules Successfully",
"VENDOR_TYPE": "Vendor Type",
"VENDOR_ID": "Vendor ID",
"EXTRA_ATTR": "Extra Attribute",
"NO_SCHEDULE": "We could not find any schedule",
"WORKERS": "Workers",
"FREE_ALL": "Free all",
"CONFIRM_FREE_ALL": "Confirm Freeing All",
"CONFIRM_FREE_ALL_CONTENT": "Do you want to free all the workers?",
"CONFIRM_FREE_WORKERS": "Confirm Freeing Workers",
"CONFIRM_FREE_WORKERS_CONTENT": "Do you want to free the workers {{param}}?",
"FREE_WORKER_SUCCESS": "Freed workers successfully",
"FREE_ALL_SUCCESS": "Freed all the workers successfully",
"WORKER_POOL": "Worker Pools",
"WORKER_POOL_ID": "Worker Pool ID",
"PID": "Pid",
"START_AT": "Started At",
"HEARTBEAT_AT": "Heartbeat At",
"CONCURRENCY": "Concurrency",
"NO_WORKER_POOL": "We could not find any worker pool",
"FREE": "Free",
"WORKER_ID": "Worker ID",
"JOB_ID": "Job ID",
"JOB_PARAM": "Job Parameter",
"CHECK_IN_AT": "Checked In At",
"NO_WORKER": "We could not find any worker",
"JOB_QUEUE": "Job Queues",
"JOB_SERVICE_DASHBOARD": "Job Service Dashboard",
"QUEUE_STOP_BTN_INFO": "STOP — Stop all jobs in the queue and remove them from the queue.",
"QUEUE_PAUSE_BTN_INFO": "PAUSE — Pause to execute jobs in this type of job queue, jobs can be enqueued when the queue is paused.",
"QUEUE_RESUME_BTN_INFO": "RESUME — Resume to execute jobs in this type of job queue.",
"SCHEDULE_PAUSE_BTN_INFO": "PAUSE — Pause all schedules to execute.",
"SCHEDULE_RESUME_BTN_INFO": "RESUME — Resume all schedule to execute.",
"WORKER_FREE_BTN_INFO": "Stop the current running job to free the worker"
}
}

View File

@ -1763,5 +1763,75 @@
"JOB_NAME_REQUIRED": "任务名称为必填项",
"JOB_NAME_EXISTING": "任务名称已存在",
"TRIGGER_EXPORT_SUCCESS": "触发导出 CVEs 任务成功!"
},
"JOB_SERVICE_DASHBOARD": {
"SCHEDULE_PAUSED": "定时(已暂停)",
"SCHEDULE_BEEN_PAUSED": "{{param}} 已被暂停",
"PENDING_JOBS": "待执行任务",
"OTHERS": "其他",
"STOP_ALL": "停止全部",
"CONFIRM_STOP_ALL": "确认停止全部",
"CONFIRM_STOP_ALL_CONTENT": "您想停止全部任务栈吗?",
"STOP_ALL_SUCCESS": "停止全部任务栈成功",
"STOP_BTN": "停止",
"PAUSE_BTN": "暂停",
"RESUME_BTN": "重启",
"JOB_TYPE": "任务类型",
"PENDING_COUNT": "待执行数",
"LATENCY": "等待时间",
"PAUSED": "已暂停",
"NO_JOB_QUEUE": "未发现任何任务栈",
"CONFIRM_STOPPING_JOBS": "确认停止任务栈",
"CONFIRM_STOPPING_JOBS_CONTENT": "您想停止这些任务栈吗 {{param}}?",
"CONFIRM_PAUSING_JOBS": "确认暂停任务栈",
"CONFIRM_PAUSING_JOBS_CONTENT": "您想暂停这些任务栈吗 {{param}}?",
"CONFIRM_RESUMING_JOBS": "确认重启任务栈",
"CONFIRM_RESUMING_JOBS_CONTENT": "您想重启这些任务栈吗 {{param}}?",
"STOP_SUCCESS": "停止任务栈成功",
"PAUSE_SUCCESS": "暂停任务栈成功",
"RESUME_SUCCESS": "R重启任务栈成功",
"SCHEDULES": "定时任务",
"RUNNING_STATUS": "运行中",
"RESUME_ALL_BTN_TEXT": "重启全部",
"PAUSE_ALL_BTN_TEXT": "暂停全部",
"CONFIRM_PAUSING_ALL": "确认暂停全部",
"CONFIRM_PAUSING_ALL_CONTENT": "您想暂停所有定时任务吗?",
"CONFIRM_RESUMING_ALL": "确认重启全部",
"CONFIRM_RESUMING_ALL_CONTENT": "您想重启所有定时任务吗?",
"PAUSE_ALL_SUCCESS": "暂停所有定时任务成功",
"RESUME_ALL_SUCCESS": "重启所有定时任务成功",
"VENDOR_TYPE": "供应商类型",
"VENDOR_ID": "供应商 ID",
"EXTRA_ATTR": "其他属性",
"NO_SCHEDULE": "未发现任何定时任务",
"WORKERS": "工作者",
"FREE_ALL": "停下全部",
"CONFIRM_FREE_ALL": "确认停下全部",
"CONFIRM_FREE_ALL_CONTENT": "您想停下全部工作者吗?",
"CONFIRM_FREE_WORKERS": "确认停下工作者",
"CONFIRM_FREE_WORKERS_CONTENT": "您想停下这些工作者吗 {{param}}?",
"FREE_WORKER_SUCCESS": "停下全部工作者成功",
"FREE_ALL_SUCCESS": "停下全部工作者成功",
"WORKER_POOL": "工作者池",
"WORKER_POOL_ID": "工作者池 ID",
"PID": "Pid",
"START_AT": "开始时间",
"HEARTBEAT_AT": "上次心跳检测",
"CONCURRENCY": "并行数",
"NO_WORKER_POOL": "未发现任何工作者池",
"FREE": "停下",
"WORKER_ID": "工作者 ID",
"JOB_ID": "任务 ID",
"JOB_PARAM": "任务参数",
"CHECK_IN_AT": "检查时间",
"NO_WORKER": "未发现任何工作者",
"JOB_QUEUE": "任务栈",
"JOB_SERVICE_DASHBOARD": "任务中心",
"QUEUE_STOP_BTN_INFO": "停止 — 停止选中的任务栈中所有正在执行的任务,并清空任务栈。",
"QUEUE_PAUSE_BTN_INFO": "暂停 — 暂停执行选中的任务栈中的任务,当任务栈处于暂停状态时,进入该栈的任务会进入待执行状态。",
"QUEUE_RESUME_BTN_INFO": "重启 — 重启选中的任务栈并开始执行栈中的任务。",
"SCHEDULE_PAUSE_BTN_INFO": "暂停 — 暂停所有定时任务,暂停中的定时任务将不会被执行。",
"SCHEDULE_RESUME_BTN_INFO": "重启 — 重启所有定时任务,定时任务在触发时会正常执行。",
"WORKER_FREE_BTN_INFO": "停下选中的工作者当前正在执行的任务以便释放该工作者,被释放的工作会继续执行其他任务。"
}
}

View File

@ -1756,5 +1756,75 @@
"JOB_NAME_REQUIRED": "Job name is required",
"JOB_NAME_EXISTING": "Job name already exists",
"TRIGGER_EXPORT_SUCCESS": "Trigger exporting CVEs successfully!"
},
"JOB_SERVICE_DASHBOARD": {
"SCHEDULE_PAUSED": "Scheduled(Paused)",
"SCHEDULE_BEEN_PAUSED": "{{param}} has been paused",
"PENDING_JOBS": "Pending Jobs In Queues",
"OTHERS": "Others",
"STOP_ALL": "STOP ALL",
"CONFIRM_STOP_ALL": "Confirm Stopping All",
"CONFIRM_STOP_ALL_CONTENT": "Do you want to stop all the job queues?",
"STOP_ALL_SUCCESS": "Stopped all the job queues successfully",
"STOP_BTN": "STOP",
"PAUSE_BTN": "PAUSE",
"RESUME_BTN": "RESUME",
"JOB_TYPE": "Job Type",
"PENDING_COUNT": "Pending Count",
"LATENCY": "Latency",
"PAUSED": "Paused",
"NO_JOB_QUEUE": "We could not find any job queue",
"CONFIRM_STOPPING_JOBS": "Confirm Stopping Jobs",
"CONFIRM_STOPPING_JOBS_CONTENT": "Do you want to stop the jobs {{param}}?",
"CONFIRM_PAUSING_JOBS": "Confirm Pausing Jobs",
"CONFIRM_PAUSING_JOBS_CONTENT": "Do you want to pause the jobs {{param}}?",
"CONFIRM_RESUMING_JOBS": "Confirm Resuming Jobs",
"CONFIRM_RESUMING_JOBS_CONTENT": "Do you want to resume the jobs {{param}}?",
"STOP_SUCCESS": "Stopped jobs Successfully",
"PAUSE_SUCCESS": "Paused jobs Successfully",
"RESUME_SUCCESS": "Resumed jobs Successfully",
"SCHEDULES": "Schedules",
"RUNNING_STATUS": "Running",
"RESUME_ALL_BTN_TEXT": "RESUME ALL",
"PAUSE_ALL_BTN_TEXT": "PAUSE ALL",
"CONFIRM_PAUSING_ALL": "Confirm Pausing All",
"CONFIRM_PAUSING_ALL_CONTENT": "Do you want to pause all the jobs schedules?",
"CONFIRM_RESUMING_ALL": "Confirm Resuming All",
"CONFIRM_RESUMING_ALL_CONTENT": "Do you want to resume all the jobs schedules?",
"PAUSE_ALL_SUCCESS": "Paused all the schedules Successfully",
"RESUME_ALL_SUCCESS": "Resumed all the schedules Successfully",
"VENDOR_TYPE": "Vendor Type",
"VENDOR_ID": "Vendor ID",
"EXTRA_ATTR": "Extra Attribute",
"NO_SCHEDULE": "We could not find any schedule",
"WORKERS": "Workers",
"FREE_ALL": "Free all",
"CONFIRM_FREE_ALL": "Confirm Freeing All",
"CONFIRM_FREE_ALL_CONTENT": "Do you want to free all the workers?",
"CONFIRM_FREE_WORKERS": "Confirm Freeing Workers",
"CONFIRM_FREE_WORKERS_CONTENT": "Do you want to free the workers {{param}}?",
"FREE_WORKER_SUCCESS": "Freed workers successfully",
"FREE_ALL_SUCCESS": "Freed all the workers successfully",
"WORKER_POOL": "Worker Pools",
"WORKER_POOL_ID": "Worker Pool ID",
"PID": "Pid",
"START_AT": "Started At",
"HEARTBEAT_AT": "Heartbeat At",
"CONCURRENCY": "Concurrency",
"NO_WORKER_POOL": "We could not find any worker pool",
"FREE": "Free",
"WORKER_ID": "Worker ID",
"JOB_ID": "Job ID",
"JOB_PARAM": "Job Parameter",
"CHECK_IN_AT": "Checked In At",
"NO_WORKER": "We could not find any worker",
"JOB_QUEUE": "Job Queues",
"JOB_SERVICE_DASHBOARD": "Job Service Dashboard",
"QUEUE_STOP_BTN_INFO": "STOP — Stop all jobs in the queue and remove them from the queue.",
"QUEUE_PAUSE_BTN_INFO": "PAUSE — Pause to execute jobs in this type of job queue, jobs can be enqueued when the queue is paused.",
"QUEUE_RESUME_BTN_INFO": "RESUME — Resume to execute jobs in this type of job queue.",
"SCHEDULE_PAUSE_BTN_INFO": "PAUSE — Pause all schedules to execute.",
"SCHEDULE_RESUME_BTN_INFO": "RESUME — Resume all schedule to execute.",
"WORKER_FREE_BTN_INFO": "Stop the current running job to free the worker"
}
}