Add "expires in" column for robot UI (#16227)

Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
孙世军 2022-01-14 15:42:02 +08:00 committed by GitHub
parent 67bf1638d6
commit 7ff0bf188a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 253 additions and 21 deletions

View File

@ -60,7 +60,7 @@
<clr-dg-column>{{'ROBOT_ACCOUNT.ENABLED_STATE' | translate}}</clr-dg-column>
<clr-dg-column>{{"SYSTEM_ROBOT.PROJECTS" | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="'creation_time'">{{'ROBOT_ACCOUNT.CREATETION' | translate}}</clr-dg-column>
<clr-dg-column>{{'SYSTEM_ROBOT.EXPIRES_AT' | translate}}</clr-dg-column>
<clr-dg-column>{{'SYSTEM_ROBOT.EXPIRES_IN' | translate}}</clr-dg-column>
<clr-dg-column>{{'ROBOT_ACCOUNT.DESCRIPTION' | translate}}</clr-dg-column>
<clr-dg-placeholder>{{
'SYSTEM_ROBOT.NOT_FOUND' | translate
@ -96,7 +96,7 @@
</span>
</clr-dg-cell>
<clr-dg-cell>{{r.creation_time | harborDatetime: 'short'}}</clr-dg-cell>
<clr-dg-cell>{{r.expires_at === -1?("ROBOT_ACCOUNT.NEVER_EXPIRED" | translate):(r.expires_at * 1000 | harborDatetime: 'short')}}</clr-dg-cell>
<clr-dg-cell><app-remaining-time [deadline]="r?.expires_at" [timeDiff]="deltaTime"></app-remaining-time></clr-dg-cell>
<clr-dg-cell>{{r.description}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>

View File

@ -12,7 +12,13 @@ import { ProjectService } from "../../../../../ng-swagger-gen/services/project.s
import { MessageHandlerService } from "../../../shared/services/message-handler.service";
import { OperationService } from "../../../shared/components/operation/operation.service";
import { ConfirmationDialogService } from "../../global-confirmation-dialog/confirmation-dialog.service";
import { SharedTestingModule } from "../../../shared/shared.module";
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { CommonModule } from '@angular/common';
import { ClarityModule } from '@clr/angular';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HarborDatetimePipe } from '../../../shared/pipes/harbor-datetime.pipe';
describe('SystemRobotAccountsComponent', () => {
let component: SystemRobotAccountsComponent;
@ -119,10 +125,16 @@ describe('SystemRobotAccountsComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
SharedTestingModule
TranslateModule.forRoot(),
CommonModule,
ClarityModule,
HttpClientTestingModule,
RouterTestingModule,
BrowserAnimationsModule,
],
declarations: [ SystemRobotAccountsComponent ],
declarations: [ SystemRobotAccountsComponent, HarborDatetimePipe],
providers: [
TranslateService,
{ provide: MessageHandlerService, useValue: fakedMessageHandlerService },
ConfirmationDialogService,
OperationService,

View File

@ -28,6 +28,7 @@ import { ConfirmationButtons, ConfirmationState, ConfirmationTargets } from "../
import { errorHandler } from "../../../shared/units/shared.utils";
import { ConfirmationMessage } from "../../global-confirmation-dialog/confirmation-message";
import { RobotPermission } from "../../../../../ng-swagger-gen/models/robot-permission";
import { SysteminfoService } from '../../../../../ng-swagger-gen/services/systeminfo.service';
const FIRST_PROJECTS_PAGE_SIZE: number = 100;
@Component({
@ -56,6 +57,7 @@ export class SystemRobotAccountsComponent implements OnInit, OnDestroy {
searchSub: Subscription;
searchKey: string;
subscription: Subscription;
deltaTime: number; // the different between server time and local time
constructor(private robotService: RobotService,
private projectService: ProjectService,
private msgHandler: MessageHandlerService,
@ -63,8 +65,10 @@ export class SystemRobotAccountsComponent implements OnInit, OnDestroy {
private operationService: OperationService,
private sanitizer: DomSanitizer,
private translate: TranslateService,
private systemInfoService: SysteminfoService
) {}
ngOnInit() {
this.getCurrenTime();
this.loadDataFromBackend();
if (!this.searchSub) {
this.searchSub = this.filterComponent.filterTerms.pipe(
@ -124,6 +128,15 @@ export class SystemRobotAccountsComponent implements OnInit, OnDestroy {
this.subscription = null;
}
}
getCurrenTime() {
this.systemInfoService.getSystemInfo().subscribe(
res => {
if (res?.current_time) {
this.deltaTime = new Date().getTime() - new Date(res?.current_time).getTime();
}
}
);
}
loadDataFromBackend() {
this.loadingData = true;
this.addBtnState = ClrLoadingState.LOADING;

View File

@ -172,3 +172,9 @@ export function onlyHasPushPermission(access: Access[]): boolean {
}
return false;
}
export enum RobotTimeRemainColor {
GREEN = 'green',
WARNING = 'yellow',
EXPIRED = 'red'
}

View File

@ -31,6 +31,7 @@ import { Registry } from "../../../../../../../../../ng-swagger-gen/models/regis
import { AppConfigService } from "../../../../../../../services/app-config.service";
import { ArtifactListPageService } from '../../artifact-list-page.service';
import { ClrLoadingState } from '@clr/angular';
import { Accessory } from "ng-swagger-gen/models/accessory";
describe("ArtifactListTabComponent (inline template)", () => {
@ -263,6 +264,16 @@ describe("ArtifactListTabComponent (inline template)", () => {
}
},
listAccessoriesResponse() {
const res: HttpResponse<Array<Accessory>> = new HttpResponse<Array<Accessory>>({
headers: new HttpHeaders({'x-total-count': '0'}),
body: []
});
return of(res).pipe(delay(0));
},
listAccessories() {
return of(null).pipe(delay(0));
},
listArtifactsResponse: () => {
if (filtereName === 'sha256:3e33e3e3') {
return of(

View File

@ -59,7 +59,7 @@
<clr-dg-column>{{'ROBOT_ACCOUNT.ENABLED_STATE' | translate}}</clr-dg-column>
<clr-dg-column>{{"SYSTEM_ROBOT.PERMISSION_COLUMN" | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="'creation_time'">{{'ROBOT_ACCOUNT.CREATETION' | translate}}</clr-dg-column>
<clr-dg-column>{{'SYSTEM_ROBOT.EXPIRES_AT' | translate}}</clr-dg-column>
<clr-dg-column>{{'SYSTEM_ROBOT.EXPIRES_IN' | translate}}</clr-dg-column>
<clr-dg-column>{{'ROBOT_ACCOUNT.DESCRIPTION' | translate}}</clr-dg-column>
<clr-dg-placeholder>{{
'SYSTEM_ROBOT.NOT_FOUND' | translate
@ -89,7 +89,7 @@
</div>
</clr-dg-cell>
<clr-dg-cell>{{r.creation_time | harborDatetime: 'short'}}</clr-dg-cell>
<clr-dg-cell>{{r.expires_at === -1?("ROBOT_ACCOUNT.NEVER_EXPIRED" | translate):(r.expires_at * 1000 | harborDatetime: 'short')}}</clr-dg-cell>
<clr-dg-cell><app-remaining-time [deadline]="r?.expires_at" [timeDiff]="deltaTime"></app-remaining-time></clr-dg-cell>
<clr-dg-cell>{{r.description}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>

View File

@ -1,7 +1,7 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { of, Subscription } from 'rxjs';
import { ActivatedRoute } from "@angular/router";
import { ActivatedRoute, RouterModule } from "@angular/router";
import { MessageHandlerService } from "../../../shared/services/message-handler.service";
import { RobotAccountComponent } from './robot-account.component';
import { UserPermissionService } from "../../../shared/services";
@ -12,7 +12,13 @@ import { Robot } from "../../../../../ng-swagger-gen/models/robot";
import { delay } from "rxjs/operators";
import { Action, PermissionsKinds, Resource } from "../../left-side-nav/system-robot-accounts/system-robot-util";
import { ConfirmationDialogService } from "../../global-confirmation-dialog/confirmation-dialog.service";
import { SharedTestingModule } from "../../../shared/shared.module";
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { CommonModule } from '@angular/common';
import { ClarityModule } from '@clr/angular';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HarborDatetimePipe } from '../../../shared/pipes/harbor-datetime.pipe';
describe('RobotAccountComponent', () => {
let component: RobotAccountComponent;
@ -106,9 +112,15 @@ describe('RobotAccountComponent', () => {
NO_ERRORS_SCHEMA
],
imports: [
SharedTestingModule,
TranslateModule.forRoot(),
CommonModule,
ClarityModule,
HttpClientTestingModule,
RouterTestingModule,
BrowserAnimationsModule,
],
providers: [
TranslateService,
{
provide: ActivatedRoute, useValue: {
snapshot: {
@ -127,7 +139,7 @@ describe('RobotAccountComponent', () => {
{ provide: UserPermissionService, useValue: mockUserPermissionService },
{ provide: RobotService, useValue: fakedRobotService},
],
declarations: [RobotAccountComponent]
declarations: [RobotAccountComponent, HarborDatetimePipe]
}).compileComponents();
}));

View File

@ -25,6 +25,7 @@ import { ConfirmationDialogService } from "../../global-confirmation-dialog/conf
import { ConfirmationButtons, ConfirmationState, ConfirmationTargets } from "../../../shared/entities/shared.const";
import { errorHandler } from "../../../shared/units/shared.utils";
import { ConfirmationMessage } from "../../global-confirmation-dialog/confirmation-message";
import { SysteminfoService } from '../../../../../ng-swagger-gen/services/systeminfo.service';
@Component({
selector: "app-robot-account",
@ -55,6 +56,7 @@ export class RobotAccountComponent implements OnInit, OnDestroy {
hasRobotReadPermission: boolean;
projectId: number;
projectName: string;
deltaTime: number; // the different between server time and local time
constructor(private robotService: RobotService,
private msgHandler: MessageHandlerService,
private operateDialogService: ConfirmationDialogService,
@ -63,8 +65,10 @@ export class RobotAccountComponent implements OnInit, OnDestroy {
private route: ActivatedRoute,
private translate: TranslateService,
private sanitizer: DomSanitizer,
private systemInfoService: SysteminfoService
) {}
ngOnInit() {
this.getCurrenTime();
this.projectId = +this.route.snapshot.parent.parent.params["id"];
let resolverData = this.route.snapshot.parent.parent.data;
if (resolverData) {
@ -121,6 +125,15 @@ export class RobotAccountComponent implements OnInit, OnDestroy {
);
}
}
getCurrenTime() {
this.systemInfoService.getSystemInfo().subscribe(
res => {
if (res?.current_time) {
this.deltaTime = new Date().getTime() - new Date(res?.current_time).getTime();
}
}
);
}
getPermissionsList(): void {
let permissionsList = [];
permissionsList.push(this.userPermissionService.getPermission(this.projectId,

View File

@ -0,0 +1 @@
<span [style.color]= 'color'>{{timeRemain | translate}}</span>

View File

@ -0,0 +1,62 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RemainingTimeComponent } from './remaining-time.component';
import { Component, ViewChild } from '@angular/core';
import { Project } from '../../../../../ng-swagger-gen/models/project';
import { RobotTimeRemainColor } from '../../../base/left-side-nav/system-robot-accounts/system-robot-util';
import { SharedTestingModule } from '../../shared.module';
describe('RemainingTimeComponent', () => {
let component: TestHostComponent;
let fixture: ComponentFixture<TestHostComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
SharedTestingModule
],
declarations: [TestHostComponent, RemainingTimeComponent],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TestHostComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should show green color', () => {
fixture.detectChanges();
expect(component?.remainingTimeComponent.color).toEqual(RobotTimeRemainColor.GREEN);
expect(component?.remainingTimeComponent.timeRemain).toEqual('ROBOT_ACCOUNT.NEVER_EXPIRED');
});
it('should show yellow color', () => {
component.deltaTime = 0;
component.expires_at = new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 29).getTime() / 1000;
fixture.detectChanges();
expect(component?.remainingTimeComponent.color).toEqual(RobotTimeRemainColor.WARNING);
expect(component?.remainingTimeComponent.timeRemain).toEqual('29d 0h 0m');
});
it('should show red color', () => {
component.deltaTime = 0;
component.expires_at = new Date(new Date().getTime() - 1000 * 60 * 60 * 24 * 31).getTime() / 1000;
fixture.detectChanges();
expect(component?.remainingTimeComponent.color).toEqual(RobotTimeRemainColor.EXPIRED);
expect(component?.remainingTimeComponent.timeRemain).toEqual('SYSTEM_ROBOT.EXPIRED');
});
});
// mock a TestHostComponent for ListProjectROComponent
@Component({
template: `
<app-remaining-time [deadline]="expires_at" [timeDiff]="deltaTime"></app-remaining-time>`
})
class TestHostComponent {
@ViewChild(RemainingTimeComponent)
remainingTimeComponent: RemainingTimeComponent;
expires_at: number = -1;
deltaTime: number = 100;
}

View File

@ -0,0 +1,83 @@
// 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,
Input,
OnInit,
OnDestroy, SimpleChanges, OnChanges,
} from '@angular/core';
import { RobotTimeRemainColor } from '../../../base/left-side-nav/system-robot-accounts/system-robot-util';
const SEC: number = 1000;
const MIN: number = 60 * 1000;
const DAY: number = 1000 * 60 * 60 * 24;
const HOUR: number = 1000 * 60 * 60;
const WARNING_DAYS = 30;
@Component({
selector: 'app-remaining-time',
templateUrl: 'remaining-time.component.html',
styleUrls: ['./remaining-time.component.scss']
})
export class RemainingTimeComponent implements OnInit, OnDestroy, OnChanges {
color: string;
timeRemain: string;
@Input()
timeDiff: number; // the different between server time and local time, unit millisecond, localTime - serverTime
@Input()
deadline: number; // unit second
intelVal: any;
constructor() { }
ngOnInit() {
if (!this.intelVal) {
this.intelVal = setInterval(() => {
this.refreshTimeRemain();
}, MIN);
}
}
ngOnDestroy() {
if (this.intelVal) {
clearInterval(this.intelVal);
this.intelVal = null;
}
}
ngOnChanges(changes: SimpleChanges): void {
if (changes && changes["timeDiff"] && changes["deadline"]) {
this.refreshTimeRemain();
}
}
refreshTimeRemain() {
if (this.timeDiff !== null && this.deadline !== null) {
if (this.deadline === -1) {
this.color = RobotTimeRemainColor.GREEN;
this.timeRemain = 'ROBOT_ACCOUNT.NEVER_EXPIRED';
return;
}
const time = new Date(this.deadline * SEC).getTime() - new Date(new Date().getTime() - this.timeDiff).getTime();
if (time > 0) {
const days = Math.floor(time / DAY);
const hours = Math.floor((time % DAY) / HOUR);
const minutes = Math.floor((time % HOUR) / MIN);
this.timeRemain = `${days}d ${hours}h ${minutes}m`;
if (days >= WARNING_DAYS) {
this.color = RobotTimeRemainColor.GREEN;
} else {
this.color = RobotTimeRemainColor.WARNING;
}
} else {
this.color = RobotTimeRemainColor.EXPIRED;
this.timeRemain = 'SYSTEM_ROBOT.EXPIRED';
}
}
}
}

View File

@ -78,6 +78,7 @@ import localeFr from '@angular/common/locales/fr';
import localePt from '@angular/common/locales/pt-PT';
import localeTr from '@angular/common/locales/tr';
import localeDe from '@angular/common/locales/de';
import { RemainingTimeComponent } from './components/remaining-time/remaining-time.component';
// add locale data for supported languages ['en-us', 'zh-cn', 'zh-tw', 'es-es', 'fr-fr', 'pt-br', 'tr-tr', 'de-de'];
// en-us defaulted supported
registerLocaleData(zh_cn, 'zh-cn');
@ -153,7 +154,8 @@ ClarityIcons.add({"robot-head": `
ListChartVersionRoComponent,
DatePickerComponent,
ImageNameInputComponent,
HarborDatetimePipe
HarborDatetimePipe,
RemainingTimeComponent
],
exports: [
TranslateModule,
@ -191,7 +193,8 @@ ClarityIcons.add({"robot-head": `
ListChartVersionRoComponent,
DatePickerComponent,
ImageNameInputComponent,
HarborDatetimePipe
HarborDatetimePipe,
RemainingTimeComponent
],
providers: [
{provide: EndpointService, useClass: EndpointDefaultService },

View File

@ -1704,7 +1704,9 @@
"STOP": "Stop",
"LIST": "List",
"REPOSITORY": "Repository",
"HELM_LABEL": "Helm Chart Label"
"HELM_LABEL": "Helm Chart Label",
"EXPIRES_IN": "Expires in",
"EXPIRED": "Expired"
},
"ACCESSORY": {
"DELETION_TITLE_ACCESSORY": "Confirm Accessory Deletion",

View File

@ -1704,7 +1704,9 @@
"STOP": "Stop",
"LIST": "List",
"REPOSITORY": "Repository",
"HELM_LABEL": "Helm Chart label"
"HELM_LABEL": "Helm Chart label",
"EXPIRES_IN": "Expires in",
"EXPIRED": "Expired"
},
"ACCESSORY": {
"DELETION_TITLE_ACCESSORY": "Confirm Accessory Deletion",

View File

@ -1703,7 +1703,9 @@
"STOP": "Stop",
"LIST": "List",
"REPOSITORY": "Repository",
"HELM_LABEL": "Helm Chart label"
"HELM_LABEL": "Helm Chart label",
"EXPIRES_IN": "Expires in",
"EXPIRED": "Expired"
},
"ACCESSORY": {
"DELETION_TITLE_ACCESSORY": "Confirm Accessory Deletion",

View File

@ -1672,7 +1672,9 @@
"STOP": "Stop",
"LIST": "List",
"REPOSITORY": "Repository",
"HELM_LABEL": "Helm Chart label"
"HELM_LABEL": "Helm Chart label",
"EXPIRES_IN": "Expires in",
"EXPIRED": "Expired"
},
"ACCESSORY": {
"DELETION_TITLE_ACCESSORY": "Confirm Accessory Deletion",

View File

@ -1700,7 +1700,9 @@
"STOP": "Parar",
"LIST": "Listar",
"REPOSITORY": "Repositório",
"HELM_LABEL": "Marcador do Helm Chart"
"HELM_LABEL": "Marcador do Helm Chart",
"EXPIRES_IN": "Expires in",
"EXPIRED": "Expired"
},
"ACCESSORY": {
"DELETION_TITLE_ACCESSORY": "Confirm Accessory Deletion",

View File

@ -1704,7 +1704,9 @@
"STOP": "Stop",
"LIST": "List",
"REPOSITORY": "Repository",
"HELM_LABEL": "Helm Chart label"
"HELM_LABEL": "Helm Chart label",
"EXPIRES_IN": "Expires in",
"EXPIRED": "Expired"
},
"ACCESSORY": {
"DELETION_TITLE_ACCESSORY": "Confirm Accessory Deletion",

View File

@ -1702,7 +1702,9 @@
"STOP": "停止",
"LIST": "查询",
"REPOSITORY": "仓库",
"HELM_LABEL": "Helm Chart 标签"
"HELM_LABEL": "Helm Chart 标签",
"EXPIRES_IN": "有效期剩余",
"EXPIRED": "已过期"
},
"ACCESSORY": {
"DELETION_TITLE_ACCESSORY": "删除附件确认",

View File

@ -1689,7 +1689,9 @@
"STOP": "Stop",
"LIST": "List",
"REPOSITORY": "Repository",
"HELM_LABEL": "Helm Chart label"
"HELM_LABEL": "Helm Chart label",
"EXPIRES_IN": "Expires in",
"EXPIRED": "Expired"
},
"ACCESSORY": {
"DELETION_TITLE_ACCESSORY": "Confirm Accessory Deletion",