diff --git a/src/portal/src/app/base/base.module.ts b/src/portal/src/app/base/base.module.ts index 5af49487e..e4a68b866 100644 --- a/src/portal/src/app/base/base.module.ts +++ b/src/portal/src/app/base/base.module.ts @@ -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], diff --git a/src/portal/src/app/base/harbor-shell/harbor-shell.component.html b/src/portal/src/app/base/harbor-shell/harbor-shell.component.html index 4d09e181a..17cc33f56 100644 --- a/src/portal/src/app/base/harbor-shell/harbor-shell.component.html +++ b/src/portal/src/app/base/harbor-shell/harbor-shell.component.html @@ -167,6 +167,19 @@ clrVerticalNavIcon> {{ 'CLEARANCES.CLEARANCES' | translate }} + + + {{ + 'JOB_SERVICE_DASHBOARD.JOB_SERVICE_DASHBOARD' + | translate + }} +
+ + {{ 'JOB_SERVICE_DASHBOARD.JOB_SERVICE_DASHBOARD' | translate }} + + +
+
+ +
+
+ +
+
+ +
+
+ + diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/job-service-dashboard.component.scss b/src/portal/src/app/base/left-side-nav/job-service-dashboard/job-service-dashboard.component.scss new file mode 100644 index 000000000..6d517ae92 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/job-service-dashboard.component.scss @@ -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; +} + + diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/job-service-dashboard.component.spec.ts b/src/portal/src/app/base/left-side-nav/job-service-dashboard/job-service-dashboard.component.spec.ts new file mode 100644 index 000000000..58f15c081 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/job-service-dashboard.component.spec.ts @@ -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; + + 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(); + }); +}); diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/job-service-dashboard.component.ts b/src/portal/src/app/base/left-side-nav/job-service-dashboard/job-service-dashboard.component.ts new file mode 100644 index 000000000..8846251bd --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/job-service-dashboard.component.ts @@ -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 {} diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/job-service-dashboard.interface.ts b/src/portal/src/app/base/left-side-nav/job-service-dashboard/job-service-dashboard.interface.ts new file mode 100644 index 000000000..bbde1da26 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/job-service-dashboard.interface.ts @@ -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', +} diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/job-service-dashboard.module.ts b/src/portal/src/app/base/left-side-nav/job-service-dashboard/job-service-dashboard.module.ts new file mode 100644 index 000000000..55be68337 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/job-service-dashboard.module.ts @@ -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 {} diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/pending-job-card/pending-job-card.component.html b/src/portal/src/app/base/left-side-nav/job-service-dashboard/pending-job-card/pending-job-card.component.html new file mode 100644 index 000000000..af01e01e0 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/pending-job-card/pending-job-card.component.html @@ -0,0 +1,49 @@ +
+
+ {{ 'JOB_SERVICE_DASHBOARD.PENDING_JOBS' | translate }} +
+
+
+ {{ 'REPLICATION.TOTAL' | translate }}: {{ total() }} +
+ +
+
+ +
+
{{ jobQueue[0]?.count || 0 }}
+
+
+
+ +
+
{{ jobQueue[1]?.count || 0 }}
+
+
+
+ +
+
{{ otherCount() }}
+
+
+
+ +
+
+ +
diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/pending-job-card/pending-job-card.component.scss b/src/portal/src/app/base/left-side-nav/job-service-dashboard/pending-job-card/pending-job-card.component.scss new file mode 100644 index 000000000..db0c4ea05 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/pending-job-card/pending-job-card.component.scss @@ -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; +} diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/pending-job-card/pending-job-card.component.spec.ts b/src/portal/src/app/base/left-side-nav/job-service-dashboard/pending-job-card/pending-job-card.component.spec.ts new file mode 100644 index 000000000..3b39ad913 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/pending-job-card/pending-job-card.component.spec.ts @@ -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; + + 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); + }); +}); diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/pending-job-card/pending-job-card.component.ts b/src/portal/src/app/base/left-side-nav/job-service-dashboard/pending-job-card/pending-job-card.component.ts new file mode 100644 index 000000000..88c0a2c96 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/pending-job-card/pending-job-card.component.ts @@ -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); + } +} diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/pending-job-list/pending-job-list.component.html b/src/portal/src/app/base/left-side-nav/job-service-dashboard/pending-job-list/pending-job-list.component.html new file mode 100644 index 000000000..6102bb1ec --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/pending-job-list/pending-job-list.component.html @@ -0,0 +1,102 @@ + + + +
+ + + + + + +
+ {{ + 'JOB_SERVICE_DASHBOARD.QUEUE_STOP_BTN_INFO' + | translate + }} +
+
+ {{ + 'JOB_SERVICE_DASHBOARD.QUEUE_PAUSE_BTN_INFO' + | translate + }} +
+
+ {{ + 'JOB_SERVICE_DASHBOARD.QUEUE_RESUME_BTN_INFO' + | translate + }} +
+
+
+
+ + + +
+
+ {{ + 'JOB_SERVICE_DASHBOARD.JOB_TYPE' | translate + }} + {{ + 'JOB_SERVICE_DASHBOARD.PENDING_COUNT' | translate + }} + {{ + 'JOB_SERVICE_DASHBOARD.LATENCY' | translate + }} + {{ + 'JOB_SERVICE_DASHBOARD.PAUSED' | translate + }} + {{ + 'JOB_SERVICE_DASHBOARD.NO_JOB_QUEUE' | translate + }} + + {{ j.job_type }} + {{ j.count || 0 }} + {{ getDuration(j?.latency) || 0 }} + {{ isPaused(j?.paused) | translate }} + + + + {{ + 'PAGINATION.PAGE_SIZE' | translate + }} + + {{ pagination.firstItem + 1 }} - + {{ pagination.lastItem + 1 }} + {{ 'GROUP.OF' | translate }} + + {{ jobQueue?.length }} {{ 'GROUP.ITEMS' | translate }} + + +
diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/pending-job-list/pending-job-list.component.scss b/src/portal/src/app/base/left-side-nav/job-service-dashboard/pending-job-list/pending-job-list.component.scss new file mode 100644 index 000000000..292e669e0 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/pending-job-list/pending-job-list.component.scss @@ -0,0 +1,9 @@ +.action-bar { + display: flex; + justify-content: space-between; + align-items: center; +} + +.refresh-btn { + margin-right: 1rem; +} diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/pending-job-list/pending-job-list.component.spec.ts b/src/portal/src/app/base/left-side-nav/job-service-dashboard/pending-job-list/pending-job-list.component.spec.ts new file mode 100644 index 000000000..287584c80 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/pending-job-list/pending-job-list.component.spec.ts @@ -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; + + 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); + }); +}); diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/pending-job-list/pending-job-list.component.ts b/src/portal/src/app/base/left-side-nav/job-service-dashboard/pending-job-list/pending-job-list.component.ts new file mode 100644 index 000000000..4afaa6210 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/pending-job-list/pending-job-list.component.ts @@ -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) + ); + }, + }); + } +} diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/schedule-card/schedule-card.component.html b/src/portal/src/app/base/left-side-nav/job-service-dashboard/schedule-card/schedule-card.component.html new file mode 100644 index 000000000..1dcfaa283 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/schedule-card/schedule-card.component.html @@ -0,0 +1,58 @@ +
+
+ {{ 'JOB_SERVICE_DASHBOARD.SCHEDULES' | translate }} +
+
+
+ {{ 'REPLICATION.TOTAL' | translate }}: {{ scheduleCount }} +
+
+
+ +
+
+ {{ + statusStr() | translate + }} + +
+
+
+ +
diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/schedule-card/schedule-card.component.scss b/src/portal/src/app/base/left-side-nav/job-service-dashboard/schedule-card/schedule-card.component.scss new file mode 100644 index 000000000..f068c348b --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/schedule-card/schedule-card.component.scss @@ -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; +} diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/schedule-card/schedule-card.component.spec.ts b/src/portal/src/app/base/left-side-nav/job-service-dashboard/schedule-card/schedule-card.component.spec.ts new file mode 100644 index 000000000..87938ae46 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/schedule-card/schedule-card.component.spec.ts @@ -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; + + const fakedJobserviceService = {}; + + const fakedScheduleService = { + getSchedulePaused() { + return of({}).pipe(delay(0)); + }, + listSchedulesResponse() { + const res: HttpResponse> = new HttpResponse< + Array + >({ + 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); + }); +}); diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/schedule-card/schedule-card.component.ts b/src/portal/src/app/base/left-side-nav/job-service-dashboard/schedule-card/schedule-card.component.ts new file mode 100644 index 000000000..cb946f484 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/schedule-card/schedule-card.component.ts @@ -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); + } +} diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/schedule-list/schedule-list.component.html b/src/portal/src/app/base/left-side-nav/job-service-dashboard/schedule-list/schedule-list.component.html new file mode 100644 index 000000000..26fd92208 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/schedule-list/schedule-list.component.html @@ -0,0 +1,66 @@ + + + + + + + {{ + 'TAG_RETENTION.SERIAL' | translate + }} + {{ + 'JOB_SERVICE_DASHBOARD.VENDOR_TYPE' | translate + }} + {{ + 'JOB_SERVICE_DASHBOARD.VENDOR_ID' | translate + }} + {{ + 'JOB_SERVICE_DASHBOARD.EXTRA_ATTR' | translate + }} + {{ + 'PROJECT.CREATION_TIME' | translate + }} + {{ + 'JOB_SERVICE_DASHBOARD.NO_SCHEDULE' | translate + }} + + {{ p.id }} + {{ p.vendor_type }} + {{ p.vendor_id }} + + +
{{ + p.extra_attrs + }} + +

+                
+ + + {{ p.creation_time | harborDatetime }} + + + + {{ + 'PAGINATION.PAGE_SIZE' | translate + }} + + {{ pagination.firstItem + 1 }} - + {{ pagination.lastItem + 1 }} + {{ 'GROUP.OF' | translate }} + + {{ total }} {{ 'GROUP.ITEMS' | translate }} + + + diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/schedule-list/schedule-list.component.scss b/src/portal/src/app/base/left-side-nav/job-service-dashboard/schedule-list/schedule-list.component.scss new file mode 100644 index 000000000..8a4ac96b3 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/schedule-list/schedule-list.component.scss @@ -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; +} diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/schedule-list/schedule-list.component.spec.ts b/src/portal/src/app/base/left-side-nav/job-service-dashboard/schedule-list/schedule-list.component.spec.ts new file mode 100644 index 000000000..375ffb6de --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/schedule-list/schedule-list.component.spec.ts @@ -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; + + const mockedSchedules: ScheduleTask[] = [ + { id: 1, vendor_type: 'test1' }, + { id: 2, vendor_type: 'test2' }, + ]; + + const fakedScheduleService = { + listSchedulesResponse() { + const res: HttpResponse> = new HttpResponse< + Array + >({ + 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); + }); +}); diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/schedule-list/schedule-list.component.ts b/src/portal/src/app/base/left-side-nav/job-service-dashboard/schedule-list/schedule-list.component.ts new file mode 100644 index 000000000..14ec84879 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/schedule-list/schedule-list.component.ts @@ -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; + } +} diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-card/donut-chart/donut-chart.component.html b/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-card/donut-chart/donut-chart.component.html new file mode 100644 index 000000000..6f9abc230 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-card/donut-chart/donut-chart.component.html @@ -0,0 +1,16 @@ +
+
+
+
+ {{ numerator }}/{{ denominator }} +
+
diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-card/donut-chart/donut-chart.component.scss b/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-card/donut-chart/donut-chart.component.scss new file mode 100644 index 000000000..ab0d38b5b --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-card/donut-chart/donut-chart.component.scss @@ -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; +} + + + + diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-card/donut-chart/donut-chart.component.spec.ts b/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-card/donut-chart/donut-chart.component.spec.ts new file mode 100644 index 000000000..f28a43bfb --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-card/donut-chart/donut-chart.component.spec.ts @@ -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; + + 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(); + }); +}); diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-card/donut-chart/donut-chart.component.ts b/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-card/donut-chart/donut-chart.component.ts new file mode 100644 index 000000000..677b684ba --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-card/donut-chart/donut-chart.component.ts @@ -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)`; + } +} diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-card/worker-card.component.html b/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-card/worker-card.component.html new file mode 100644 index 000000000..6eace60a4 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-card/worker-card.component.html @@ -0,0 +1,20 @@ +
+
+ {{ 'JOB_SERVICE_DASHBOARD.WORKERS' | translate }} +
+
+ +
+ +
diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-card/worker-card.component.scss b/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-card/worker-card.component.scss new file mode 100644 index 000000000..c93b7d463 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-card/worker-card.component.scss @@ -0,0 +1,9 @@ +.donut-chart { + display: flex; + align-items: center; + justify-content: center; +} + +.card-block { + height: 9rem; +} diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-card/worker-card.component.spec.ts b/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-card/worker-card.component.spec.ts new file mode 100644 index 000000000..bce8492f0 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-card/worker-card.component.spec.ts @@ -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; + + 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); + }); +}); diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-card/worker-card.component.ts b/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-card/worker-card.component.ts new file mode 100644 index 000000000..062955aa4 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-card/worker-card.component.ts @@ -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(); + } +} diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-list/worker-list.component.html b/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-list/worker-list.component.html new file mode 100644 index 000000000..1ae887757 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-list/worker-list.component.html @@ -0,0 +1,151 @@ +

{{ 'JOB_SERVICE_DASHBOARD.WORKER_POOL' | translate }}

+ + + + + + + {{ + 'JOB_SERVICE_DASHBOARD.WORKER_POOL_ID' | translate + }} + {{ + 'JOB_SERVICE_DASHBOARD.PID' | translate + }} + {{ + 'JOB_SERVICE_DASHBOARD.START_AT' | translate + }} + {{ + 'JOB_SERVICE_DASHBOARD.HEARTBEAT_AT' | translate + }} + {{ + 'JOB_SERVICE_DASHBOARD.CONCURRENCY' | translate + }} + {{ + 'JOB_SERVICE_DASHBOARD.NO_WORKER_POOL' | translate + }} + + {{ p.worker_pool_id }} + {{ p.pid }} + + {{ p.start_at | harborDatetime }} + {{ p.heartbeat_at | harborDatetime }} + {{ p.concurrency }} + + + + {{ + 'PAGINATION.PAGE_SIZE' | translate + }} + + {{ pagination.firstItem + 1 }} - + {{ pagination.lastItem + 1 }} + {{ 'GROUP.OF' | translate }} + + {{ pools?.length }} {{ 'GROUP.ITEMS' | translate }} + + + + +

{{ 'JOB_SERVICE_DASHBOARD.WORKERS' | translate }}

+ + +
+ + + + + + {{ + 'JOB_SERVICE_DASHBOARD.WORKER_FREE_BTN_INFO' + | translate + }} + + + +
+ + + +
+ {{ + 'JOB_SERVICE_DASHBOARD.WORKER_ID' | translate + }} + {{ + 'CVE_EXPORT.JOB_NAME' | translate + }} + {{ + 'JOB_SERVICE_DASHBOARD.JOB_ID' | translate + }} + {{ + 'JOB_SERVICE_DASHBOARD.JOB_PARAM' | translate + }} + {{ + 'JOB_SERVICE_DASHBOARD.START_AT' | translate + }} + {{ + 'JOB_SERVICE_DASHBOARD.CHECK_IN_AT' | translate + }} + {{ + 'JOB_SERVICE_DASHBOARD.NO_WORKER' | translate + }} + + {{ w.id }} + {{ w.job_name }} + {{ w.job_id }} + + + {{ + w.args + }} + +

+                    
+
+
+ {{ w.start_at | harborDatetime }} + {{ w.checkin_at | harborDatetime }} +
+ + + {{ + 'PAGINATION.PAGE_SIZE' | translate + }} + + {{ pagination.firstItem + 1 }} - + {{ pagination.lastItem + 1 }} + {{ 'GROUP.OF' | translate }} + + {{ workers?.length }} {{ 'GROUP.ITEMS' | translate }} + + +
+
diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-list/worker-list.component.scss b/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-list/worker-list.component.scss new file mode 100644 index 000000000..04b75b339 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-list/worker-list.component.scss @@ -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; +} diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-list/worker-list.component.spec.ts b/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-list/worker-list.component.spec.ts new file mode 100644 index 000000000..9f3612d5a --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-list/worker-list.component.spec.ts @@ -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; + + 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 + }); +}); diff --git a/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-list/worker-list.component.ts b/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-list/worker-list.component.ts new file mode 100644 index 000000000..04c1efcdd --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/job-service-dashboard/worker-list/worker-list.component.ts @@ -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; + } +} diff --git a/src/portal/src/app/base/left-side-nav/replication/replication/list-replication-rule/list-replication-rule.component.html b/src/portal/src/app/base/left-side-nav/replication/replication/list-replication-rule/list-replication-rule.component.html index f9f7808dc..8b3c23ebc 100644 --- a/src/portal/src/app/base/left-side-nav/replication/replication/list-replication-rule/list-replication-rule.component.html +++ b/src/portal/src/app/base/left-side-nav/replication/replication/list-replication-rule/list-replication-rule.component.html @@ -126,15 +126,15 @@ Deactivated + size="18"> + {{ 'WEBHOOK.DISABLED' | translate }}
- Enabled + {{ 'WEBHOOK.ENABLED' | translate }}
@@ -172,7 +172,14 @@ getFlattenLevelString(p.dest_namespace_replace_count) | translate }} - {{ p.trigger ? p.trigger.type : '' }} + + {{ getTriggerTypeI18n(p.trigger) | translate }} + + + {{ p?.trigger?.trigger_settings?.cron }} + + + {{ getBandwidthStr(p.speed) | translate }} diff --git a/src/portal/src/app/base/left-side-nav/replication/replication/list-replication-rule/list-replication-rule.component.scss b/src/portal/src/app/base/left-side-nav/replication/replication/list-replication-rule/list-replication-rule.component.scss index a4f930753..2210f1188 100644 --- a/src/portal/src/app/base/left-side-nav/replication/replication/list-replication-rule/list-replication-rule.component.scss +++ b/src/portal/src/app/base/left-side-nav/replication/replication/list-replication-rule/list-replication-rule.component.scss @@ -25,3 +25,7 @@ .margin-right-2px { margin-right: 2px; } + +.trigger { + width: 180px; +} diff --git a/src/portal/src/app/base/left-side-nav/replication/replication/list-replication-rule/list-replication-rule.component.ts b/src/portal/src/app/base/left-side-nav/replication/replication/list-replication-rule/list-replication-rule.component.ts index 3cf057d00..7ccb7577c 100644 --- a/src/portal/src/app/base/left-side-nav/replication/replication/list-replication-rule/list-replication-rule.component.ts +++ b/src/portal/src/app/base/left-side-nav/replication/replication/list-replication-rule/list-replication-rule.component.ts @@ -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); diff --git a/src/portal/src/app/base/project/p2p-provider/p2p-provider.service.ts b/src/portal/src/app/base/project/p2p-provider/p2p-provider.service.ts index 4aa7a488c..dd67752b3 100644 --- a/src/portal/src/app/base/project/p2p-provider/p2p-provider.service.ts +++ b/src/portal/src/app/base/project/p2p-provider/p2p-provider.service.ts @@ -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; diff --git a/src/portal/src/app/base/project/p2p-provider/policy/policy.component.ts b/src/portal/src/app/base/project/p2p-provider/policy/policy.component.ts index ac534384f..bbc81beb9 100644 --- a/src/portal/src/app/base/project/p2p-provider/policy/policy.component.ts +++ b/src/portal/src/app/base/project/p2p-provider/policy/policy.component.ts @@ -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]; diff --git a/src/portal/src/app/base/project/tag-feature-integration/tag-retention/tag-retention.component.html b/src/portal/src/app/base/project/tag-feature-integration/tag-retention/tag-retention.component.html index 770417a08..c4e56dc0d 100644 --- a/src/portal/src/app/base/project/tag-feature-integration/tag-retention/tag-retention.component.html +++ b/src/portal/src/app/base/project/tag-feature-integration/tag-retention/tag-retention.component.html @@ -168,6 +168,7 @@
+
+ + +
diff --git a/src/portal/src/app/shared/components/cron-schedule/cron-schedule.component.scss b/src/portal/src/app/shared/components/cron-schedule/cron-schedule.component.scss index a5e7395f2..2e88c425d 100644 --- a/src/portal/src/app/shared/components/cron-schedule/cron-schedule.component.scss +++ b/src/portal/src/app/shared/components/cron-schedule/cron-schedule.component.scss @@ -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; +} diff --git a/src/portal/src/app/shared/components/cron-schedule/cron-schedule.component.ts b/src/portal/src/app/shared/components/cron-schedule/cron-schedule.component.ts index dc2a1ff19..7ced460ac 100644 --- a/src/portal/src/app/shared/components/cron-schedule/cron-schedule.component.ts +++ b/src/portal/src/app/shared/components/cron-schedule/cron-schedule.component.ts @@ -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(); + 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) { diff --git a/src/portal/src/app/shared/entities/shared.const.ts b/src/portal/src/app/shared/entities/shared.const.ts index a5e434571..58fb6b90b 100644 --- a/src/portal/src/app/shared/entities/shared.const.ts +++ b/src/portal/src/app/shared/entities/shared.const.ts @@ -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 { diff --git a/src/portal/src/app/shared/units/utils.spec.ts b/src/portal/src/app/shared/units/utils.spec.ts index 5a5bba44f..d4a204879 100644 --- a/src/portal/src/app/shared/units/utils.spec.ts +++ b/src/portal/src/app/shared/units/utils.spec.ts @@ -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'); + }); }); diff --git a/src/portal/src/app/shared/units/utils.ts b/src/portal/src/app/shared/units/utils.ts index 58bbe013b..9fc9414ce 100644 --- a/src/portal/src/app/shared/units/utils.ts +++ b/src/portal/src/app/shared/units/utils.ts @@ -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', } diff --git a/src/portal/src/css/common.scss b/src/portal/src/css/common.scss index aaf53f87a..70ef6eaf8 100644 --- a/src/portal/src/css/common.scss +++ b/src/portal/src/css/common.scss @@ -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; +} diff --git a/src/portal/src/css/dark-theme.scss b/src/portal/src/css/dark-theme.scss index 7b2e792e4..9731297de 100644 --- a/src/portal/src/css/dark-theme.scss +++ b/src/portal/src/css/dark-theme.scss @@ -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"; diff --git a/src/portal/src/css/light-theme.scss b/src/portal/src/css/light-theme.scss index f337181c1..eecedc06a 100644 --- a/src/portal/src/css/light-theme.scss +++ b/src/portal/src/css/light-theme.scss @@ -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"; diff --git a/src/portal/src/i18n/lang/de-de-lang.json b/src/portal/src/i18n/lang/de-de-lang.json index 0fbc69cac..c154f7570 100644 --- a/src/portal/src/i18n/lang/de-de-lang.json +++ b/src/portal/src/i18n/lang/de-de-lang.json @@ -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" } } diff --git a/src/portal/src/i18n/lang/en-us-lang.json b/src/portal/src/i18n/lang/en-us-lang.json index 3cdd9660d..579b3d4c8 100644 --- a/src/portal/src/i18n/lang/en-us-lang.json +++ b/src/portal/src/i18n/lang/en-us-lang.json @@ -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" } } diff --git a/src/portal/src/i18n/lang/es-es-lang.json b/src/portal/src/i18n/lang/es-es-lang.json index ac9b7e228..bb4a9ff09 100644 --- a/src/portal/src/i18n/lang/es-es-lang.json +++ b/src/portal/src/i18n/lang/es-es-lang.json @@ -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" } } diff --git a/src/portal/src/i18n/lang/fr-fr-lang.json b/src/portal/src/i18n/lang/fr-fr-lang.json index 1567f1f22..9da2cc791 100644 --- a/src/portal/src/i18n/lang/fr-fr-lang.json +++ b/src/portal/src/i18n/lang/fr-fr-lang.json @@ -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" } } diff --git a/src/portal/src/i18n/lang/pt-br-lang.json b/src/portal/src/i18n/lang/pt-br-lang.json index 7128ed897..0d42b151b 100644 --- a/src/portal/src/i18n/lang/pt-br-lang.json +++ b/src/portal/src/i18n/lang/pt-br-lang.json @@ -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" } } diff --git a/src/portal/src/i18n/lang/tr-tr-lang.json b/src/portal/src/i18n/lang/tr-tr-lang.json index 37299e6a6..8975e13e0 100644 --- a/src/portal/src/i18n/lang/tr-tr-lang.json +++ b/src/portal/src/i18n/lang/tr-tr-lang.json @@ -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" } } diff --git a/src/portal/src/i18n/lang/zh-cn-lang.json b/src/portal/src/i18n/lang/zh-cn-lang.json index 1b6d2717f..21a060858 100644 --- a/src/portal/src/i18n/lang/zh-cn-lang.json +++ b/src/portal/src/i18n/lang/zh-cn-lang.json @@ -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": "停下选中的工作者当前正在执行的任务以便释放该工作者,被释放的工作会继续执行其他任务。" } } diff --git a/src/portal/src/i18n/lang/zh-tw-lang.json b/src/portal/src/i18n/lang/zh-tw-lang.json index aeade7107..eb23d360d 100644 --- a/src/portal/src/i18n/lang/zh-tw-lang.json +++ b/src/portal/src/i18n/lang/zh-tw-lang.json @@ -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" } }