Add new app level warning message (#18449)

1. Show a app level warning if there is a stuck job
2. Chang `Replication finished` to `Replication status changed`

Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
Shijun Sun 2023-03-31 13:08:57 +08:00 committed by GitHub
parent a95808c120
commit 95972ba693
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 592 additions and 311 deletions

View File

@ -1,5 +1,4 @@
<clr-main-container>
<global-message [isAppLevel]="true"></global-message>
<navigator (showDialogModalAction)="openModal($event)"></navigator>
<search-result></search-result>
<div

View File

@ -11,7 +11,7 @@
// 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';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { AppConfigService } from './services/app-config.service';
@ -31,20 +31,28 @@ import {
} from './shared/entities/shared.const';
import { SkinableConfig } from './services/skinable-config.service';
import { isSupportedLanguage } from './shared/units/shared.utils';
import {
CHECK_HEALTH_INTERVAL,
JobServiceDashboardHealthCheckService,
} from './base/left-side-nav/job-service-dashboard/job-service-dashboard-health-check.service';
import { SessionService } from './shared/services/session.service';
@Component({
selector: 'harbor-app',
templateUrl: 'app.component.html',
})
export class AppComponent {
export class AppComponent implements OnInit, OnDestroy {
themeArray: ThemeInterface[] = clone(THEME_ARRAY);
styleMode: string = this.themeArray[0].showStyle;
interval: any;
constructor(
private translate: TranslateService,
private appConfigService: AppConfigService,
private titleService: Title,
public theme: ThemeService,
private skinableConfig: SkinableConfig
private skinableConfig: SkinableConfig,
private jobServiceDashboardHealthCheckService: JobServiceDashboardHealthCheckService,
private sessionService: SessionService
) {
// init language
this.initLanguage();
@ -66,6 +74,26 @@ export class AppComponent {
});
this.setTheme();
}
ngOnDestroy(): void {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
}
ngOnInit(): void {
if (!this.interval) {
this.interval = setInterval(() => {
if (this.sessionService.getCurrentUser()?.has_admin_role) {
this.jobServiceDashboardHealthCheckService.checkHealth();
} else {
this.jobServiceDashboardHealthCheckService.setHealthy(true);
}
}, CHECK_HEALTH_INTERVAL);
}
}
setTheme() {
let styleMode = this.themeArray[0].showStyle;
const localHasStyle =

View File

@ -22,6 +22,7 @@ import { PasswordSettingComponent } from './password-setting/password-setting.co
import { AccountSettingsModalComponent } from './account-settings/account-settings-modal.component';
import { ForgotPasswordComponent } from './password-setting/forgot-password/forgot-password.component';
import { GlobalConfirmationDialogComponent } from './global-confirmation-dialog/global-confirmation-dialog.component';
import { AppLevelAlertsComponent } from './harbor-shell/app-level-alerts/app-level-alerts.component';
const routes: Routes = [
{
@ -172,6 +173,7 @@ const routes: Routes = [
AccountSettingsModalComponent,
ForgotPasswordComponent,
GlobalConfirmationDialogComponent,
AppLevelAlertsComponent,
],
})
export class BaseModule {}

View File

@ -0,0 +1,81 @@
<clr-alerts>
<clr-alert
*ngIf="showReadOnly && isLogin()"
[clrAlertType]="message?.type"
[clrAlertAppLevel]="true"
[clrAlertClosable]="false">
<clr-alert-item>
<span class="alert-text">{{ message?.message | translate }}</span>
</clr-alert-item>
</clr-alert>
<clr-alert
*ngIf="showLogin"
[clrAlertType]="message?.type"
[clrAlertAppLevel]="true">
<clr-alert-item>
<span class="alert-text">{{ message?.message | translate }}</span>
<div class="alert-actions">
<button
class="btn alert-action no-underline"
(click)="signIn()">
{{ 'BUTTON.LOG_IN' | translate }}
</button>
</div>
</clr-alert-item>
</clr-alert>
<clr-alert
(clrAlertClosedChange)="closeHealthWarning()"
*ngIf="showJobServiceDashboardHealthCheck() && isLogin()"
[clrAlertType]="'warning'"
[clrAlertAppLevel]="true">
<clr-alert-item>
<span class="alert-text">
{{ 'JOB_SERVICE_DASHBOARD.WAITING_TOO_LONG_1' | translate }}
<a
class="alert-action"
href="#"
routerLink="/harbor/job-service-dashboard/pending-jobs">
{{ 'JOB_SERVICE_DASHBOARD.WAITING_TOO_LONG_2' | translate }}
</a>
</span>
<div class="alert-actions">
{{ 'JOB_SERVICE_DASHBOARD.WAITING_TOO_LONG_3' | translate }}
<a
class="alert-action"
rel="noopener noreferrer"
target="_blank"
href="https://github.com/goharbor/harbor/wiki/Reduce-job-queue-latency(wait-time)"
>{{
'JOB_SERVICE_DASHBOARD.WAITING_TOO_LONG_4' | translate
}}</a
>
</div>
</clr-alert-item>
</clr-alert>
<clr-alert
[clrAlertType]="'info'"
[clrAlertAppLevel]="true"
(clrAlertClosedChange)="closeInfo()"
*ngIf="shouldShowScannerInfo() && isLogin()">
<clr-alert-item>
<span class="alert-text">
{{ 'SCANNER.HELP_INFO_1' | translate }}
<a
class="alert-action"
rel="noopener noreferrer"
target="_blank"
href="{{ scannerDocUrl }}"
>{{ 'SCANNER.HELP_INFO_2' | translate }}</a
></span
>
<div class="alert-actions last">
<a
class="alert-action"
href="#"
routerLink="/harbor/interrogation-services/scanners"
>{{ 'SCANNER.ALL_SCANNERS' | translate }}</a
>
</div>
</clr-alert-item>
</clr-alert>
</clr-alerts>

View File

@ -0,0 +1,16 @@
.alert-text {
flex: 0 0 auto !important;
}
.alert-action {
text-decoration: underline
}
.last {
position: absolute;
right: 3rem;
}
.no-underline {
text-decoration: none;
}

View File

@ -0,0 +1,76 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AppLevelAlertsComponent } from './app-level-alerts.component';
import { SharedTestingModule } from '../../../shared/shared.module';
import { HttpHeaders, HttpResponse } from '@angular/common/http';
import { of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { Scanner } from '../../left-side-nav/interrogation-services/scanner/scanner';
import { ScannerService } from 'ng-swagger-gen/services/scanner.service';
import { SessionService } from 'src/app/shared/services/session.service';
describe('AppLevelAlertsComponent', () => {
let component: AppLevelAlertsComponent;
let fixture: ComponentFixture<AppLevelAlertsComponent>;
const fakeScannerService = {
listScannersResponse() {
const response: HttpResponse<Array<Scanner>> = new HttpResponse<
Array<Scanner>
>({
headers: new HttpHeaders({
'x-total-count': '1',
}),
body: [
{
name: 'test',
is_default: true,
},
],
});
return of(response).pipe(delay(0));
},
listScanners() {
return of([
{
name: 'test',
is_default: true,
},
]).pipe(delay(0));
},
};
const fakeSessionService = {
getCurrentUser: function () {
return { has_admin_role: true };
},
};
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SharedTestingModule],
declarations: [AppLevelAlertsComponent],
providers: [
{
provide: ScannerService,
useValue: fakeScannerService,
},
{ provide: SessionService, useValue: fakeSessionService },
],
}).compileComponents();
fixture = TestBed.createComponent(AppLevelAlertsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should show scanner alert', async () => {
await fixture.whenStable();
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.alerts.alert-info')).toBeTruthy();
});
});

View File

@ -0,0 +1,191 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { SCANNERS_DOC } from '../../left-side-nav/interrogation-services/scanner/scanner';
import { SessionService } from '../../../shared/services/session.service';
import { DEFAULT_PAGE_SIZE, delUrlParam } from '../../../shared/units/utils';
import { forkJoin, Observable, Subscription } from 'rxjs';
import { Project } from '../../../../../ng-swagger-gen/models/project';
import { ScannerService } from '../../../../../ng-swagger-gen/services/scanner.service';
import { UN_LOGGED_PARAM } from '../../../account/sign-in/sign-in.service';
import {
CommonRoutes,
httpStatusCode,
} from '../../../shared/entities/shared.const';
import { ActivatedRoute, Router } from '@angular/router';
import { MessageService } from '../../../shared/components/global-message/message.service';
import { Message } from '../../../shared/components/global-message/message';
import { JobServiceDashboardHealthCheckService } from '../../left-side-nav/job-service-dashboard/job-service-dashboard-health-check.service';
import { AppLevelMessage } from '../../../shared/services/message-handler.service';
const HAS_SHOWED_SCANNER_INFO: string = 'hasShowScannerInfo';
const YES: string = 'yes';
@Component({
selector: 'app-app-level-alerts',
templateUrl: './app-level-alerts.component.html',
styleUrls: ['./app-level-alerts.component.scss'],
})
export class AppLevelAlertsComponent implements OnInit, OnDestroy {
scannerDocUrl: string = SCANNERS_DOC;
showScannerInfo: boolean = false;
message: Message;
appLevelMsgSub: Subscription;
clearSub: Subscription;
showLogin: boolean = false;
showReadOnly: boolean = false;
constructor(
private session: SessionService,
private scannerService: ScannerService,
private router: Router,
private messageService: MessageService,
private route: ActivatedRoute,
private jobServiceDashboardHealthCheckService: JobServiceDashboardHealthCheckService
) {}
ngOnInit() {
if (
!(
localStorage &&
localStorage.getItem(HAS_SHOWED_SCANNER_INFO) === YES
)
) {
if (this.session.getCurrentUser()?.has_admin_role) {
this.getDefaultScanner();
}
}
if (!this.appLevelMsgSub) {
this.appLevelMsgSub =
this.messageService.appLevelAnnounced$.subscribe(message => {
this.message = message;
this.showReadOnly =
message.statusCode === httpStatusCode.AppLevelWarning &&
message.message === AppLevelMessage.REPO_READ_ONLY;
if (message.statusCode === httpStatusCode.Unauthorized) {
this.showLogin = true;
// User session timed out, then redirect to sign-in page
if (
this.session.getCurrentUser() &&
!this.isSignInUrl() &&
this.route.snapshot.queryParams[UN_LOGGED_PARAM] !==
YES
) {
const url = delUrlParam(
this.router.url,
UN_LOGGED_PARAM
);
this.session.clear(); // because of SignInGuard, must clear user session before navigating to sign-in page
this.router.navigate(
[CommonRoutes.EMBEDDED_SIGN_IN],
{
queryParams: { redirect_url: url },
}
);
}
} else {
this.showLogin = false;
}
});
}
if (!this.clearSub) {
this.clearSub = this.messageService.clearChan$.subscribe(clear => {
this.showLogin = false;
this.showReadOnly = false;
});
}
}
ngOnDestroy() {
if (this.appLevelMsgSub) {
this.appLevelMsgSub.unsubscribe();
this.appLevelMsgSub = null;
}
}
shouldShowScannerInfo(): boolean {
return (
this.session.getCurrentUser()?.has_admin_role &&
this.showScannerInfo
);
}
getDefaultScanner() {
this.scannerService
.listScannersResponse({
pageSize: DEFAULT_PAGE_SIZE,
page: 1,
})
.subscribe(res => {
if (res.headers) {
const xHeader: string = res.headers.get('X-Total-Count');
const totalCount = parseInt(xHeader, 0);
let arr = res.body || [];
if (totalCount <= DEFAULT_PAGE_SIZE) {
// already gotten all scanners
if (arr && arr.length) {
this.showScannerInfo = arr.some(
scanner => scanner.is_default
);
}
} else {
// get all the scanners in specified times
const times: number = Math.ceil(
totalCount / DEFAULT_PAGE_SIZE
);
const observableList: Observable<Project[]>[] = [];
for (let i = 2; i <= times; i++) {
observableList.push(
this.scannerService.listScanners({
page: i,
pageSize: DEFAULT_PAGE_SIZE,
})
);
}
forkJoin(observableList).subscribe(response => {
if (response && response.length) {
response.forEach(item => {
arr = arr.concat(item);
});
this.showScannerInfo = arr.some(
scanner => scanner.is_default
);
}
});
}
}
});
}
closeInfo() {
if (localStorage) {
localStorage.setItem(HAS_SHOWED_SCANNER_INFO, YES);
}
this.showScannerInfo = false;
}
signIn(): void {
// remove queryParam UN_LOGGED_PARAM of redirect url
const url = delUrlParam(this.router.url, UN_LOGGED_PARAM);
this.router.navigate([CommonRoutes.EMBEDDED_SIGN_IN], {
queryParams: { redirect_url: url },
});
}
isSignInUrl(): boolean {
const url: string =
this.router.url?.indexOf('?') === -1
? this.router.url
: this.router.url?.split('?')[0];
return url === CommonRoutes.EMBEDDED_SIGN_IN;
}
showJobServiceDashboardHealthCheck(): boolean {
return (
this.jobServiceDashboardHealthCheckService.hasUnhealthyQueue() &&
!this.jobServiceDashboardHealthCheckService.hasManuallyClosed()
);
}
closeHealthWarning() {
this.jobServiceDashboardHealthCheckService.setManuallyClosed(true);
}
isLogin(): boolean {
return this.session.getCurrentUser()?.has_admin_role;
}
}

View File

@ -1,5 +1,5 @@
<clr-main-container>
<div class="clr-row scanner-info" *ngIf="showScannerInfo && isSystemAdmin">
<!--<div class="clr-row scanner-info" *ngIf="showScannerInfo && isSystemAdmin">
<div class="clr-col-2"></div>
<div class="clr-col text-center">
<clr-icon shape="info-standard" size="20"></clr-icon>
@ -26,8 +26,8 @@
shape="times"
size="24"></clr-icon>
</div>
</div>
<global-message [isAppLevel]="true"></global-message>
</div>-->
<app-app-level-alerts></app-app-level-alerts>
<navigator
(showAccountSettingsModal)="openModal($event)"
(showDialogModalAction)="openModal($event)"></navigator>
@ -39,7 +39,7 @@
[class.content-area-override]="!shouldOverrideContent"
[class.start-content-padding]="shouldOverrideContent"
(scroll)="publishScrollEvent()">
<global-message [isAppLevel]="false"></global-message>
<global-message></global-message>
<!-- Only appear when searching -->
<search-result></search-result>
<router-outlet></router-outlet>

View File

@ -38,43 +38,3 @@ clr-vertical-nav {
.font-size-13 {
font-size: 13px;
}
.scanner-info {
margin-right: 0;
margin-left: 0;
background-color: #0079b8;
min-height: 2rem;
color: #fff;
align-items: center;
font-size: 0.5rem;
a {
cursor: pointer;
text-decoration: #bababa underline;
font-weight: 600;
}
a:hover, a:visited, a:link {
color: #fff;
}
}
.all-scanners {
margin-right: 1.5rem;
}
.close-icon {
margin-right: 1rem;
}
.ml-05 {
margin-left: 0.5rem;
}
.right {
text-align: right;
}
.pointer {
cursor: pointer;
}

View File

@ -20,9 +20,6 @@ import { ErrorHandler } from '../../shared/units/error-handler';
import { AccountSettingsModalComponent } from '../account-settings/account-settings-modal.component';
import { InlineAlertComponent } from '../../shared/components/inline-alert/inline-alert.component';
import { ScannerService } from '../../../../ng-swagger-gen/services/scanner.service';
import { HttpHeaders, HttpResponse } from '@angular/common/http';
import { Registry } from '../../../../ng-swagger-gen/models/registry';
import { delay } from 'rxjs/operators';
import { UserService } from '../../../../ng-swagger-gen/services/user.service';
describe('HarborShellComponent', () => {
@ -72,22 +69,6 @@ describe('HarborShellComponent', () => {
};
},
};
let fakeScannerService = {
listScannersResponse() {
const response: HttpResponse<Array<Registry>> = new HttpResponse<
Array<Registry>
>({
headers: new HttpHeaders({
'x-total-count': [].length.toString(),
}),
body: [],
});
return of(response).pipe(delay(0));
},
listScanners() {
return of([]).pipe(delay(0));
},
};
const fakedUserService = {
getCurrentUserInfo() {
return of({});
@ -120,7 +101,6 @@ describe('HarborShellComponent', () => {
useValue: fakeSearchTriggerService,
},
{ provide: AppConfigService, useValue: fakeAppConfigService },
{ provide: ScannerService, useValue: fakeScannerService },
{
provide: MessageHandlerService,
useValue: mockMessageHandlerService,
@ -143,7 +123,6 @@ describe('HarborShellComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(HarborShellComponent);
component = fixture.componentInstance;
component.showScannerInfo = true;
component.accountSettingsModal = TestBed.createComponent(
AccountSettingsModalComponent
).componentInstance;

View File

@ -20,7 +20,7 @@ import {
ChangeDetectorRef,
} from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { forkJoin, Observable, Subscription } from 'rxjs';
import { Subscription } from 'rxjs';
import { AppConfigService } from '../../services/app-config.service';
import { ModalEvent } from '../modal-event';
import { modalEvents } from '../modal-events.const';
@ -34,19 +34,14 @@ import {
CONFIG_AUTH_MODE,
} from '../../shared/entities/shared.const';
import { THEME_ARRAY, ThemeInterface } from '../../services/theme';
import { clone, DEFAULT_PAGE_SIZE } from '../../shared/units/utils';
import { clone } from '../../shared/units/utils';
import { ThemeService } from '../../services/theme.service';
import { AccountSettingsModalComponent } from '../account-settings/account-settings-modal.component';
import {
EventService,
HarborEvent,
} from '../../services/event-service/event.service';
import { SCANNERS_DOC } from '../left-side-nav/interrogation-services/scanner/scanner';
import { ScannerService } from '../../../../ng-swagger-gen/services/scanner.service';
import { Project } from '../../../../ng-swagger-gen/models/project';
const HAS_SHOWED_SCANNER_INFO: string = 'hasShowScannerInfo';
const YES: string = 'yes';
const HAS_STYLE_MODE: string = 'styleModeLocal';
@Component({
@ -73,10 +68,7 @@ export class HarborShellComponent implements OnInit, OnDestroy {
searchSub: Subscription;
searchCloseSub: Subscription;
showScannerInfo: boolean = false;
scannerDocUrl: string = SCANNERS_DOC;
themeArray: ThemeInterface[] = clone(THEME_ARRAY);
styleMode = this.themeArray[0].showStyle;
@ViewChild('scrollDiv') scrollDiv: ElementRef;
scrollToPositionSub: Subscription;
@ -86,7 +78,6 @@ export class HarborShellComponent implements OnInit, OnDestroy {
private session: SessionService,
private searchTrigger: SearchTriggerService,
private appConfigService: AppConfigService,
private scannerService: ScannerService,
public theme: ThemeService,
private event: EventService,
private cd: ChangeDetectorRef
@ -117,16 +108,6 @@ export class HarborShellComponent implements OnInit, OnDestroy {
this.isSearchResultsOpened = false;
}
);
if (
!(
localStorage &&
localStorage.getItem(HAS_SHOWED_SCANNER_INFO) === YES
)
) {
if (this.isSystemAdmin) {
this.getDefaultScanner();
}
}
// set local in app
if (localStorage) {
this.styleMode = localStorage.getItem(HAS_STYLE_MODE);
@ -150,59 +131,6 @@ export class HarborShellComponent implements OnInit, OnDestroy {
});
}
}
closeInfo() {
if (localStorage) {
localStorage.setItem(HAS_SHOWED_SCANNER_INFO, YES);
}
this.showScannerInfo = false;
}
getDefaultScanner() {
this.scannerService
.listScannersResponse({
pageSize: DEFAULT_PAGE_SIZE,
page: 1,
})
.subscribe(res => {
if (res.headers) {
const xHeader: string = res.headers.get('X-Total-Count');
const totalCount = parseInt(xHeader, 0);
let arr = res.body || [];
if (totalCount <= DEFAULT_PAGE_SIZE) {
// already gotten all scanners
if (arr && arr.length) {
this.showScannerInfo = arr.some(
scanner => scanner.is_default
);
}
} else {
// get all the scanners in specified times
const times: number = Math.ceil(
totalCount / DEFAULT_PAGE_SIZE
);
const observableList: Observable<Project[]>[] = [];
for (let i = 2; i <= times; i++) {
observableList.push(
this.scannerService.listScanners({
page: i,
pageSize: DEFAULT_PAGE_SIZE,
})
);
}
forkJoin(observableList).subscribe(response => {
if (response && response.length) {
response.forEach(item => {
arr = arr.concat(item);
});
this.showScannerInfo = arr.some(
scanner => scanner.is_default
);
}
});
}
}
});
}
ngOnDestroy(): void {
if (this.searchSub) {
this.searchSub.unsubscribe();

View File

@ -0,0 +1,42 @@
import { TestBed } from '@angular/core/testing';
import { JobServiceDashboardHealthCheckService } from './job-service-dashboard-health-check.service';
import { JobserviceService } from '../../../../../ng-swagger-gen/services/jobservice.service';
import { of } from 'rxjs';
describe('JobServiceDashboardHealthCheckService', () => {
let service: JobServiceDashboardHealthCheckService;
const fakedJobserviceService = {
listJobQueues() {
return of({});
},
};
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{
provide: JobserviceService,
useValue: fakedJobserviceService,
},
],
});
service = TestBed.inject(JobServiceDashboardHealthCheckService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should return false when hasUnhealthyQueue is called', () => {
expect(service.hasUnhealthyQueue()).toBeFalsy();
});
it('should return false when hasManuallyClosed is called', () => {
expect(service.hasManuallyClosed()).toBeFalsy();
});
it('should return false when hasUnhealthyQueue is called', () => {
expect(service.hasUnhealthyQueue()).toBeFalsy();
});
});

View File

@ -0,0 +1,38 @@
import { Injectable } from '@angular/core';
import { JobserviceService } from '../../../../../ng-swagger-gen/services/jobservice.service';
export const HEALTHY_TIME: number = 24; // unit hours
export const CHECK_HEALTH_INTERVAL: number = 15 * 60 * 1000; //15 minutes, unit ms
@Injectable({
providedIn: 'root',
})
export class JobServiceDashboardHealthCheckService {
private _hasUnhealthyQueue: boolean = false;
private _hasManuallyClosed: boolean = false;
constructor(private jobServiceService: JobserviceService) {}
hasUnhealthyQueue(): boolean {
return this._hasUnhealthyQueue;
}
hasManuallyClosed(): boolean {
return this._hasManuallyClosed;
}
setHealthy(value: boolean): void {
this._hasUnhealthyQueue = value;
}
setManuallyClosed(value: boolean): void {
this._hasManuallyClosed = value;
}
checkHealth(): void {
this.jobServiceService.listJobQueues().subscribe(res => {
this._hasUnhealthyQueue = res?.some(
item => item.latency >= HEALTHY_TIME * 60 * 60
);
});
}
}

View File

@ -80,7 +80,16 @@
<clr-dg-row *clrDgItems="let j of jobQueue" [clrDgItem]="j">
<clr-dg-cell>{{ j.job_type }}</clr-dg-cell>
<clr-dg-cell>{{ j.count || 0 }}</clr-dg-cell>
<clr-dg-cell>{{ getDuration(j?.latency) || 0 }}</clr-dg-cell>
<clr-dg-cell>
<span class="container">
<cds-icon
*ngIf="showWarning(j?.latency)"
size="20"
class="warning"
shape="exclamation-triangle"></cds-icon>
<span class="ml-5px">{{ getDuration(j?.latency) || 0 }}</span>
</span>
</clr-dg-cell>
<clr-dg-cell>{{ isPaused(j?.paused) | translate }}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>

View File

@ -7,3 +7,16 @@
.refresh-btn {
margin-right: 1rem;
}
.warning {
color: #a36500;
}
.container {
display: flex;
align-items: center;
}
.ml-5px {
margin-left: 5px;
}

View File

@ -27,6 +27,7 @@ import {
import { OperationService } from '../../../../shared/components/operation/operation.service';
import { errorHandler } from '../../../../shared/units/shared.utils';
import { JobServiceDashboardSharedDataService } from '../job-service-dashboard-shared-data.service';
import { HEALTHY_TIME } from '../job-service-dashboard-health-check.service';
@Component({
selector: 'app-pending-job-list',
@ -310,4 +311,7 @@ export class PendingListComponent implements OnInit, OnDestroy {
},
});
}
showWarning(latency: number): boolean {
return latency && latency >= HEALTHY_TIME * 60 * 60;
}
}

View File

@ -1,4 +1,4 @@
:host::ng-deep.modal-dialog {
width: 30rem;
width: 31rem;
height: 35rem;
}

View File

@ -20,7 +20,7 @@ describe('WebhookService', () => {
expect(service).toBeTruthy();
const eventType: string = 'REPLICATION';
expect(service.eventTypeToText(eventType)).toEqual(
'Replication finished'
'Replication status changed'
);
const mockedEventType: string = 'TEST';
expect(service.eventTypeToText(mockedEventType)).toEqual('TEST');

View File

@ -15,7 +15,7 @@ import { Injectable } from '@angular/core';
import { MarkdownPipe } from 'ngx-markdown/src/markdown.pipe';
const EVENT_TYPES_TEXT_MAP = {
REPLICATION: 'Replication finished',
REPLICATION: 'Replication status changed',
PUSH_ARTIFACT: 'Artifact pushed',
PULL_ARTIFACT: 'Artifact pulled',
DELETE_ARTIFACT: 'Artifact deleted',

View File

@ -1,20 +1,11 @@
<div [class.alert-app-level]="!isAppLevel" *ngIf="globalMessageOpened">
<div *ngIf="globalMessageOpened">
<clr-alert
[clrAlertType]="globalMessage.type"
[clrAlertClosable]="!needAuth"
[clrAlertAppLevel]="isAppLevel"
[clrAlertAppLevel]="false"
[(clrAlertClosed)]="!globalMessageOpened"
(clrAlertClosedChange)="onClose()">
<div class="alert-item">
<clr-alert-item class="flex-center">
<span class="alert-text">{{ message }}</span>
<div class="alert-actions alert-style" *ngIf="needAuth">
<button class="btn alert-action" (click)="signIn()">
{{ 'BUTTON.LOG_IN' | translate }}
</button>
</div>
</div>
</clr-alert-item>
</clr-alert>
</div>
<div
*ngIf="globalMessageOpened && needAuth && !isFromGlobalSearch()"
class="mask-layer"></div>

View File

@ -2,13 +2,10 @@
display: inline;
}
.mask-layer {
position: absolute;
width: 100%;
height: 100%;
z-index: 1000;
}
.alert-text {
flex: 0 1 auto !important;
}
}
.flex-center {
justify-content: center;
}

View File

@ -27,32 +27,4 @@ describe('MessageComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
it('should open mask layer when unauthorized', async () => {
component.globalMessageOpened = true;
component.globalMessage = Message.newMessage(
401,
'unauthorized',
AlertType.DANGER
);
fixture.detectChanges();
await fixture.whenStable();
const ele: HTMLDivElement =
fixture.nativeElement.querySelector('.mask-layer');
expect(ele).toBeTruthy();
});
it("should not open mask layer when it's not unauthorized", async () => {
component.globalMessageOpened = true;
component.globalMessage = Message.newMessage(
403,
'forbidden',
AlertType.WARNING
);
fixture.detectChanges();
await fixture.whenStable();
const ele: HTMLDivElement =
fixture.nativeElement.querySelector('.mask-layer');
expect(ele).toBeFalsy();
});
});

View File

@ -17,14 +17,7 @@ import { Subscription } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { Message } from './message';
import { MessageService } from './message.service';
import {
CommonRoutes,
dismissInterval,
httpStatusCode,
} from '../../entities/shared.const';
import { delUrlParam } from '../../units/utils';
import { UN_LOGGED_PARAM, YES } from '../../../account/sign-in/sign-in.service';
import { SessionService } from '../../services/session.service';
import { dismissInterval } from '../../entities/shared.const';
@Component({
selector: 'global-message',
@ -32,48 +25,27 @@ import { SessionService } from '../../services/session.service';
styleUrls: ['message.component.scss'],
})
export class MessageComponent implements OnInit, OnDestroy {
@Input() isAppLevel: boolean;
globalMessage: Message = new Message();
globalMessageOpened: boolean = false;
messageText: string = '';
timer: any = null;
appLevelMsgSub: Subscription;
msgSub: Subscription;
clearSub: Subscription;
constructor(
private elementRef: ElementRef,
private messageService: MessageService,
private router: Router,
private route: ActivatedRoute,
private translate: TranslateService,
private session: SessionService
private translate: TranslateService
) {}
ngOnInit(): void {
// Only subscribe application level message
if (this.isAppLevel) {
this.appLevelMsgSub =
this.messageService.appLevelAnnounced$.subscribe(message => {
this.globalMessageOpened = true;
this.globalMessage = message;
this.checkLoginStatus();
this.messageText = message.message;
this.translateMessage(message);
});
} else {
// Only subscribe general messages
if (!this.msgSub) {
this.msgSub = this.messageService.messageAnnounced$.subscribe(
message => {
this.globalMessageOpened = true;
this.globalMessage = message;
this.checkLoginStatus();
this.messageText = message.message;
this.translateMessage(message);
// Make the message alert bar dismiss after several intervals.
// Only for this case
if (this.timer) {
@ -84,38 +56,15 @@ export class MessageComponent implements OnInit, OnDestroy {
() => this.onClose(),
dismissInterval
);
// Hack the Clarity Alert style with native dom
setTimeout(() => {
let nativeDom: any = this.elementRef.nativeElement;
let queryDoms: any[] =
nativeDom.getElementsByClassName('alert');
if (queryDoms && queryDoms.length > 0) {
let hackDom: any = queryDoms[0];
hackDom.className +=
' alert-global alert-global-align';
}
}, 0);
}
);
}
this.clearSub = this.messageService.clearChan$.subscribe(clear => {
this.onClose();
});
}
ngOnDestroy() {
if (this.appLevelMsgSub) {
this.appLevelMsgSub.unsubscribe();
}
if (this.msgSub) {
this.msgSub.unsubscribe();
}
if (this.clearSub) {
this.clearSub.unsubscribe();
this.msgSub = null;
}
}
@ -137,57 +86,14 @@ export class MessageComponent implements OnInit, OnDestroy {
.get(key, { param: param })
.subscribe((res: string) => (this.messageText = res));
}
public get needAuth(): boolean {
return this.globalMessage
? this.globalMessage.statusCode === httpStatusCode.Unauthorized
: false;
}
// Show message text
public get message(): string {
return this.messageText;
}
signIn(): void {
// remove queryParam UN_LOGGED_PARAM of redirect url
const url = delUrlParam(this.router.url, UN_LOGGED_PARAM);
this.router.navigate([CommonRoutes.EMBEDDED_SIGN_IN], {
queryParams: { redirect_url: url },
});
}
onClose() {
if (this.timer) {
clearTimeout(this.timer);
}
this.globalMessageOpened = false;
}
// if navigate from global search(un-logged users visit public project)
isFromGlobalSearch(): boolean {
return this.route.snapshot.queryParams[UN_LOGGED_PARAM] === YES;
}
checkLoginStatus() {
if (this.globalMessage.statusCode === httpStatusCode.Unauthorized) {
// User session timed out, then redirect to sign-in page
if (
this.session.getCurrentUser() &&
!this.isSignInUrl() &&
this.route.snapshot.queryParams[UN_LOGGED_PARAM] !== YES
) {
const url = delUrlParam(this.router.url, UN_LOGGED_PARAM);
this.session.clear(); // because of SignInGuard, must clear user session before navigating to sign-in page
this.router.navigate([CommonRoutes.EMBEDDED_SIGN_IN], {
queryParams: { redirect_url: url },
});
}
}
}
isSignInUrl(): boolean {
const url: string =
this.router.url?.indexOf('?') === -1
? this.router.url
: this.router.url?.split('?')[0];
return url === CommonRoutes.EMBEDDED_SIGN_IN;
}
}

View File

@ -33,6 +33,7 @@ export const dismissInterval = 10 * 1000;
export const httpStatusCode = {
Unauthorized: 401,
Forbidden: 403,
AppLevelWarning: 503,
};
export const enum ConfirmationTargets {
EMPTY,

View File

@ -74,8 +74,8 @@ export class MessageHandlerService implements ErrorHandler {
public handleReadOnly(): void {
this.msgService.announceAppLevelMessage(
503,
'REPO_READ_ONLY',
httpStatusCode.AppLevelWarning,
AppLevelMessage.REPO_READ_ONLY,
AlertType.WARNING
);
}
@ -131,3 +131,7 @@ export class MessageHandlerService implements ErrorHandler {
this.showInfo(log);
}
}
export enum AppLevelMessage {
REPO_READ_ONLY = 'REPO_READ_ONLY',
}

View File

@ -27,6 +27,7 @@ import { FlushAll } from '../units/cache-util';
import { SignInCredential } from '../../account/sign-in/sign-in-credential';
import { ProjectMemberEntity } from '../../../../ng-swagger-gen/models/project-member-entity';
import { DeFaultLang } from '../entities/shared.const';
import { JobServiceDashboardHealthCheckService } from '../../base/left-side-nav/job-service-dashboard/job-service-dashboard-health-check.service';
const signInUrl = '/c/login';
const currentUserEndpoint = CURRENT_BASE_HREF + '/users/current';
@ -54,7 +55,8 @@ export class SessionService {
projectMembers: ProjectMemberEntity[];
constructor(
private http: HttpClient,
public sessionViewmodel: SessionViewmodelFactory
public sessionViewmodel: SessionViewmodelFactory,
private jobServiceDashboardHealthCheckService: JobServiceDashboardHealthCheckService
) {}
// Handle the related exceptions
@ -94,12 +96,12 @@ export class SessionService {
*/
retrieveUser(): Observable<SessionUserBackend> {
return this.http.get(currentUserEndpoint, HTTP_GET_OPTIONS).pipe(
map(
(response: SessionUserBackend) =>
(this.currentUser = this.sessionViewmodel.getCurrentUser(
response
) as SessionUser)
),
map((response: SessionUserBackend) => {
this.currentUser = this.sessionViewmodel.getCurrentUser(
response
) as SessionUser;
this.jobServiceDashboardHealthCheckService.checkHealth();
}),
catchError(error => this.handleError(error))
);
}

View File

@ -366,3 +366,11 @@ job-service-dashboard {
.my-action-item:hover {
background-color: $label-hover-bg-color !important;
}
.alert.alert-warning {
padding: 0 !important;
}
.alerts.alert-info {
background-color: $alert-info-bg-color;
}

View File

@ -47,4 +47,5 @@ $select-all-for-dropdown-color: #4aaed9;
$normal-border-color: #acbac3;
$text-color-job-service-dashboard: #49aeda;
$datagrid-numeric-filter-input-bg-color: #21333b;
$alert-info-bg-color: none;
@import "./common.scss";

View File

@ -48,4 +48,5 @@ $select-all-for-dropdown-color: #0072a3;
$normal-border-color: #6a7a81;
$text-color-job-service-dashboard: #0072a3;
$datagrid-numeric-filter-input-bg-color: unset;
$alert-info-bg-color: #107eb0;
@import "./common.scss";

View File

@ -1786,7 +1786,11 @@
"SCHEDULE_PAUSE_BTN_INFO": "PAUSIEREN — Pausiert die Ausführung aller Pläne.",
"SCHEDULE_RESUME_BTN_INFO": "FORTSETZEN — Setzt die Ausführung aller Pläne fort.",
"WORKER_FREE_BTN_INFO": "Halte den aktuell laufenden Job an um den Arbeiter zu befreien.",
"CRON": "Cron"
"CRON": "Cron",
"WAITING_TOO_LONG_1": "Certain jobs have been pending for execution for over 24 hours. Please check the job service ",
"WAITING_TOO_LONG_2": "dashboard.",
"WAITING_TOO_LONG_3": "For more details, please refer to the ",
"WAITING_TOO_LONG_4": "Wiki."
},
"CLARITY": {
"OPEN": "Open",

View File

@ -1786,7 +1786,11 @@
"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",
"CRON": "Cron"
"CRON": "Cron",
"WAITING_TOO_LONG_1": "Certain jobs have been pending for execution for over 24 hours. Please check the job service ",
"WAITING_TOO_LONG_2": "dashboard.",
"WAITING_TOO_LONG_3": "For more details, please refer to the ",
"WAITING_TOO_LONG_4": "Wiki."
},
"CLARITY": {
"OPEN": "Open",

View File

@ -1783,7 +1783,11 @@
"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",
"CRON": "Cron"
"CRON": "Cron",
"WAITING_TOO_LONG_1": "Certain jobs have been pending for execution for over 24 hours. Please check the job service ",
"WAITING_TOO_LONG_2": "dashboard.",
"WAITING_TOO_LONG_3": "For more details, please refer to the ",
"WAITING_TOO_LONG_4": "Wiki."
},
"CLARITY": {
"OPEN": "Open",

View File

@ -1753,7 +1753,11 @@
"SCHEDULE_PAUSE_BTN_INFO": "PAUSE — Met en pause toutes les programmations de tâches.",
"SCHEDULE_RESUME_BTN_INFO": "REPRENDRE — Reprend les files d'attente de tâches à exécuter.",
"WORKER_FREE_BTN_INFO": "Arrête les tâches en cours pour libérer le worker",
"CRON": "Cron"
"CRON": "Cron",
"WAITING_TOO_LONG_1": "Certain jobs have been pending for execution for over 24 hours. Please check the job service ",
"WAITING_TOO_LONG_2": "dashboard.",
"WAITING_TOO_LONG_3": "For more details, please refer to the ",
"WAITING_TOO_LONG_4": "Wiki."
},
"CLARITY": {
"OPEN": "Ouvrir",

View File

@ -1783,7 +1783,11 @@
"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",
"CRON": "Cron"
"CRON": "Cron",
"WAITING_TOO_LONG_1": "Certain jobs have been pending for execution for over 24 hours. Please check the job service ",
"WAITING_TOO_LONG_2": "dashboard.",
"WAITING_TOO_LONG_3": "For more details, please refer to the ",
"WAITING_TOO_LONG_4": "Wiki."
},
"CLARITY": {
"OPEN": "Open",

View File

@ -1786,7 +1786,11 @@
"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",
"CRON": "Cron"
"CRON": "Cron",
"WAITING_TOO_LONG_1": "Certain jobs have been pending for execution for over 24 hours. Please check the job service ",
"WAITING_TOO_LONG_2": "dashboard.",
"WAITING_TOO_LONG_3": "For more details, please refer to the ",
"WAITING_TOO_LONG_4": "Wiki."
},
"CLARITY": {
"OPEN": "Open",

View File

@ -1783,7 +1783,11 @@
"SCHEDULE_PAUSE_BTN_INFO": "暂停 — 暂停所有定时任务,暂停中的定时任务将不会被执行。",
"SCHEDULE_RESUME_BTN_INFO": "重启 — 重启所有定时任务,定时任务在触发时会正常执行。",
"WORKER_FREE_BTN_INFO": "停下选中的工作者当前正在执行的任务以便释放该工作者,被释放的工作者会继续执行其他任务。",
"CRON": "Cron"
"CRON": "Cron",
"WAITING_TOO_LONG_1": "检测到有任务已等待执行超过 24 小时。 请检查任务中心的",
"WAITING_TOO_LONG_2": "仪表盘。",
"WAITING_TOO_LONG_3": "更多详情,请参考 ",
"WAITING_TOO_LONG_4": "Wiki。"
},
"CLARITY": {
"OPEN": "打开",

View File

@ -1776,7 +1776,11 @@
"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",
"CRON": "Cron"
"CRON": "Cron",
"WAITING_TOO_LONG_1": "Certain jobs have been pending for execution for over 24 hours. Please check the job service ",
"WAITING_TOO_LONG_2": "dashboard.",
"WAITING_TOO_LONG_3": "For more details, please refer to the ",
"WAITING_TOO_LONG_4": "Wiki."
},
"CLARITY": {
"OPEN": "Open",