Add co-sign UI (#16155)

Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
孙世军 2022-01-05 13:41:51 +08:00 committed by GitHub
parent b417e877b5
commit 2eda360d9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 2273 additions and 1754 deletions

View File

@ -1,5 +1,5 @@
<div>
<div class="breadcrumb" *ngIf="!withAdmiral">
<div class="breadcrumb">
<span class="back-icon"><</span>
<a (click)="goProBack()">{{'SIDE_NAV.PROJECTS'| translate}}</a>
<span class="back-icon"><</span>
@ -10,6 +10,29 @@
&lt;<a (click)="jumpDigest(i)" >{{digest | slice:0:15}}</a></span>
</span>
</div>
<artifact-list [repoName]="repoName" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole"
[projectId]="projectId" [memberRoleID]="projectMemberRoleId" [isGuest]="isGuest"></artifact-list>
<section class="overview-section">
<div class="title-wrapper">
<div class="title-block">
<h2 sub-header-title class="custom-h2" *ngIf="!artifactDigest">{{repoName}}</h2>
<h2 sub-header-title class="custom-h2" *ngIf="artifactDigest">{{artifactDigest | slice:0:15}}</h2>
</div>
</div>
</section>
<section class="detail-section">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<ul id="configTabs" class="nav" role="tablist">
<li role="presentation" class="nav-item" *ngIf="!artifactDigest">
<button id="repo-info" class="btn btn-link nav-link" aria-controls="info"
type="button" routerLinkActive="active" routerLink="./info-tab">{{'REPOSITORY.INFO' | translate}}</button>
</li>
<li role="presentation" class="nav-item">
<button id="repo-image" class="btn btn-link nav-link" aria-controls="image"
type="button" routerLinkActive="active" routerLink="./artifacts-tab">{{'REPOSITORY.ARTIFACTS' | translate}}</button>
</li>
</ul>
<router-outlet></router-outlet>
</div>
</section>
</div>

View File

@ -1,39 +1,13 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ArtifactListPageComponent } from './artifact-list-page.component';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { of } from 'rxjs';
import { ActivatedRoute, Router } from '@angular/router';
import { SessionService } from "../../../../../shared/services/session.service";
import { AppConfigService } from "../../../../../services/app-config.service";
import { ArtifactService } from "../artifact.service";
import { ActivatedRoute } from '@angular/router';
import { SharedTestingModule } from "../../../../../shared/shared.module";
import { ArtifactListPageService } from './artifact-list-page.service';
describe('ArtifactListPageComponent', () => {
let component: ArtifactListPageComponent;
let fixture: ComponentFixture<ArtifactListPageComponent>;
const mockSessionService = {
getCurrentUser: () => { }
};
const mockAppConfigService = {
getConfig: () => {
return {
project_creation_restriction: "",
with_chartmuseum: "",
with_notary: "",
with_trivy: "",
with_admiral: "",
registry_url: "",
};
}
};
const mockRouter = {
navigate: () => { }
};
const mockArtifactService = {
triggerUploadArtifact: {
next: () => {}
}
};
const mockActivatedRoute = {
RouterparamMap: of({ get: (key) => 'value' }),
snapshot: {
@ -65,19 +39,13 @@ describe('ArtifactListPageComponent', () => {
};
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
schemas: [
CUSTOM_ELEMENTS_SCHEMA
],
imports: [
SharedTestingModule
],
declarations: [ArtifactListPageComponent],
providers: [
{ provide: SessionService, useValue: mockSessionService },
{ provide: AppConfigService, useValue: mockAppConfigService },
{ provide: Router, useValue: mockRouter },
ArtifactListPageService,
{ provide: ActivatedRoute, useValue: mockActivatedRoute },
{ provide: ArtifactService, useValue: mockArtifactService },
]
})
.compileComponents();
@ -92,4 +60,10 @@ describe('ArtifactListPageComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have two tabs', async () => {
await fixture.whenStable();
const tabs = fixture.nativeElement.querySelectorAll('.nav-item');
expect(tabs.length).toEqual(2);
});
});

View File

@ -11,13 +11,10 @@
// 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, OnInit, ViewChild } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ArtifactListComponent } from "./artifact-list/artifact-list.component";
import { ArtifactService } from "../artifact.service";
import { AppConfigService } from "../../../../../services/app-config.service";
import { SessionService } from "../../../../../shared/services/session.service";
import { Project } from "../../../project";
import { ArtifactListPageService } from "./artifact-list-page.service";
@Component({
selector: 'artifact-list-page',
@ -26,29 +23,25 @@ import { Project } from "../../../project";
})
export class ArtifactListPageComponent implements OnInit {
projectId: number;
projectId: string;
projectName: string;
projectMemberRoleId: number;
repoName: string;
referArtifactNameArray: string[] = [];
hasProjectAdminRole: boolean = false;
isGuest: boolean;
registryUrl: string;
@ViewChild(ArtifactListComponent)
repositoryComponent: ArtifactListComponent;
depth: string;
artifactDigest: string;
constructor(
private route: ActivatedRoute,
private router: Router,
private artifactService: ArtifactService,
private appConfigService: AppConfigService,
private session: SessionService) {
private artifactListPageService: ArtifactListPageService) {
this.route.params.subscribe(params => {
this.depth = this.route.snapshot.params['depth'];
if (this.depth) {
const arr: string[] = this.depth.split('-');
this.referArtifactNameArray = arr.slice(0, arr.length - 1);
this.artifactDigest = this.depth.split('-')[arr.length - 1];
} else {
this.referArtifactNameArray = [];
this.artifactDigest = null;
}
});
}
@ -58,28 +51,11 @@ export class ArtifactListPageComponent implements OnInit {
let resolverData = this.route.snapshot.parent.data;
if (resolverData) {
this.projectName = (<Project>resolverData['projectResolver']).name;
this.hasProjectAdminRole = (<Project>resolverData['projectResolver']).has_project_admin_role;
this.isGuest = (<Project>resolverData['projectResolver']).current_user_role_id === 3;
this.projectMemberRoleId = (<Project>resolverData['projectResolver']).current_user_role_id;
}
this.repoName = this.route.snapshot.params['repo'];
this.registryUrl = this.appConfigService.getConfig().registry_url;
this.artifactListPageService.init(+this.projectId);
}
get withNotary(): boolean {
return this.appConfigService.getConfig().with_notary;
}
get withAdmiral(): boolean {
return this.appConfigService.getConfig().with_admiral;
}
get hasSignedIn(): boolean {
return this.session.getCurrentUser() !== null;
}
hasChanges(): boolean {
return this.repositoryComponent.hasChanges();
}
watchGoBackEvt(projectId: string| number): void {
this.router.navigate(["harbor", "projects", projectId, "repositories"]);
}
@ -92,7 +68,7 @@ export class ArtifactListPageComponent implements OnInit {
jumpDigest(index: number) {
const arr: string[] = this.referArtifactNameArray.slice(0, index + 1 );
if ( arr && arr.length) {
this.router.navigate(["harbor", "projects", this.projectId, "repositories", this.repoName, "depth", arr.join('-')]);
this.router.navigate(["harbor", "projects", this.projectId, "repositories", this.repoName, "artifacts-tab", "depth", arr.join('-')]);
} else {
this.router.navigate(["harbor", "projects", this.projectId, "repositories", this.repoName]);
}

View File

@ -0,0 +1,21 @@
import { inject, TestBed } from '@angular/core/testing';
import { ArtifactListPageService } from './artifact-list-page.service';
import { SharedTestingModule } from '../../../../../shared/shared.module';
describe('ArtifactListPageService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
SharedTestingModule,
],
providers: [
ArtifactListPageService
]
});
});
it('should be initialized', inject([ArtifactListPageService], (service: ArtifactListPageService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -0,0 +1,184 @@
import { Injectable } from "@angular/core";
import { ClrLoadingState } from "@clr/angular";
import { ScanningResultService, UserPermissionService, USERSTATICPERMISSION } from "../../../../../shared/services";
import { LabelState } from "./artifact-list/artifact-list-tab/artifact-list-tab.component";
import { forkJoin, Observable } from "rxjs";
import { LabelService } from "ng-swagger-gen/services/label.service";
import { Label } from "ng-swagger-gen/models/label";
import { ErrorHandler } from "../../../../../shared/units/error-handler";
import { clone } from "../../../../../shared/units/utils";
const PAGE_SIZE: number = 100;
@Injectable()
export class ArtifactListPageService {
private _scanBtnState: ClrLoadingState;
private _allLabels: LabelState[] = [];
imageStickLabels: LabelState[] = [];
imageFilterLabels: LabelState[] = [];
private _hasEnabledScanner: boolean = false;
private _hasAddLabelImagePermission: boolean = false;
private _hasRetagImagePermission: boolean = false;
private _hasDeleteImagePermission: boolean = false;
private _hasScanImagePermission: boolean = false;
constructor(private scanningService: ScanningResultService,
private labelService: LabelService,
private userPermissionService: UserPermissionService,
private errorHandlerService: ErrorHandler) {
}
resetClonedLabels() {
this.imageStickLabels = clone(this._allLabels);
this.imageFilterLabels = clone(this._allLabels);
}
getScanBtnState(): ClrLoadingState {
return this._scanBtnState;
}
hasEnabledScanner(): boolean {
return this._hasEnabledScanner;
}
hasAddLabelImagePermission(): boolean {
return this._hasAddLabelImagePermission;
}
hasRetagImagePermission(): boolean {
return this._hasRetagImagePermission;
}
hasDeleteImagePermission(): boolean {
return this._hasDeleteImagePermission;
}
hasScanImagePermission(): boolean {
return this._hasScanImagePermission;
}
init(projectId: number) {
this._getProjectScanner(projectId);
this._getPermissionRule(projectId);
}
private _getProjectScanner(projectId: number): void {
this._hasEnabledScanner = false;
this._scanBtnState = ClrLoadingState.LOADING;
this.scanningService.getProjectScanner(projectId)
.subscribe(response => {
if (response && "{}" !== JSON.stringify(response) && !response.disabled
&& response.health === "healthy") {
this._scanBtnState = ClrLoadingState.SUCCESS;
this._hasEnabledScanner = true;
} else {
this._scanBtnState = ClrLoadingState.ERROR;
}
}, error => {
this._scanBtnState = ClrLoadingState.ERROR;
});
}
private _getAllLabels(projectId: number): void {
// get all project labels
this.labelService.ListLabelsResponse({
pageSize: PAGE_SIZE,
page: 1,
scope: 'p',
projectId: projectId
}).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 <= PAGE_SIZE) { // already gotten all project labels
if (arr && arr.length) {
arr.forEach(data => {
this._allLabels.push({'iconsShow': false, 'label': data, 'show': true});
});
this.resetClonedLabels();
}
} else { // get all the project labels in specified times
const times: number = Math.ceil(totalCount / PAGE_SIZE);
const observableList: Observable<Label[]>[] = [];
for (let i = 2; i <= times; i++) {
observableList.push(this.labelService.ListLabels({
page: i,
pageSize: PAGE_SIZE,
scope: 'p',
projectId: projectId
}));
}
this._handleLabelRes(observableList, arr);
}
}
});
// get all global labels
this.labelService.ListLabelsResponse({
pageSize: PAGE_SIZE,
page: 1,
scope: 'g',
}).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 <= PAGE_SIZE) { // already gotten all global labels
if (arr && arr.length) {
arr.forEach(data => {
this._allLabels.push({'iconsShow': false, 'label': data, 'show': true});
});
this.resetClonedLabels();
}
} else { // get all the global labels in specified times
const times: number = Math.ceil(totalCount / PAGE_SIZE);
const observableList: Observable<Label[]>[] = [];
for (let i = 2; i <= times; i++) {
observableList.push(this.labelService.ListLabels({
page: i,
pageSize: PAGE_SIZE,
scope: 'g',
}));
}
this._handleLabelRes(observableList, arr);
}
}
});
}
private _handleLabelRes(observableList: Observable<Label[]>[], arr: Label[]) {
forkJoin(observableList).subscribe(response => {
if (response && response.length) {
response.forEach(item => {
arr = arr.concat(item);
});
arr.forEach(data => {
this._allLabels.push({'iconsShow': false, 'label': data, 'show': true});
});
this.resetClonedLabels();
}
});
}
private _getPermissionRule(projectId: number): void {
const permissions = [
{
resource: USERSTATICPERMISSION.REPOSITORY_ARTIFACT_LABEL.KEY,
action: USERSTATICPERMISSION.REPOSITORY_ARTIFACT_LABEL.VALUE.CREATE
},
{resource: USERSTATICPERMISSION.REPOSITORY.KEY, action: USERSTATICPERMISSION.REPOSITORY.VALUE.PULL},
{resource: USERSTATICPERMISSION.ARTIFACT.KEY, action: USERSTATICPERMISSION.ARTIFACT.VALUE.DELETE},
{resource: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE},
];
this.userPermissionService.hasProjectPermissions(projectId, permissions).subscribe((results: Array<boolean>) => {
this._hasAddLabelImagePermission = results[0];
this._hasRetagImagePermission = results[1];
this._hasDeleteImagePermission = results[2];
this._hasScanImagePermission = results[3];
// only has label permission
if (this._hasAddLabelImagePermission) {
this._getAllLabels(projectId);
}
}, error => this.errorHandlerService.error(error));
}
}

View File

@ -0,0 +1,35 @@
<section id="info" role="tabpanel" aria-labelledby="repo-info">
<form #repoInfoForm="ngForm">
<div id="info-edit-button">
<button class="btn " [disabled]="editing || !hasProjectAdminRole || onSaving " (click)="editInfo()">
<clr-icon shape="pencil" size="16"></clr-icon>&nbsp;{{'BUTTON.EDIT' | translate}}
</button>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 1024" preserveAspectRatio="xMinYMin" class="markdown">
<path d="M950.154 192H73.846C33.127 192 0 225.12699999999995 0 265.846v492.308C0 798.875 33.127 832 73.846 832h876.308c40.721 0 73.846-33.125 73.846-73.846V265.846C1024 225.12699999999995 990.875 192 950.154 192zM576 703.875L448 704V512l-96 123.077L256 512v192H128V320h128l96 128 96-128 128-0.125V703.875zM767.091 735.875L608 512h96V320h128v192h96L767.091 735.875z" />
</svg>
<span>{{ 'REPOSITORY.MARKDOWN' | translate }}</span>
</div>
<div id="no-editing" *ngIf="!editing">
<div class="loading" *ngIf="loading">
<span class="spinner spinner-inline"></span>
</div>
<ng-container *ngIf="!loading">
<div *ngIf="!imageInfo" class="no-info-div">
<p>{{'REPOSITORY.NO_INFO' | translate }}<p>
</div>
<div *ngIf="imageInfo" class="info-div">
<div class="info-pre" [innerHTML]="imageInfo | markdown"></div>
</div>
</ng-container>
</div>
<div *ngIf="editing">
<textarea id="info-edit-textarea" class="clr-textarea w-100" rows="5" name="info-edit-textarea"
[(ngModel)]="imageInfo"></textarea>
</div>
<div class="" *ngIf="editing">
<button id="edit-save" class="btn btn-primary" [disabled]="!hasChanges()" (click)="saveInfo()">{{'BUTTON.SAVE' | translate}}</button>
<button id="edit-cancel" class="btn" (click)="cancelInfo()">{{'BUTTON.CANCEL' | translate}}</button>
</div>
<confirmation-dialog #confirmationDialog (confirmAction)="confirmCancel($event)"></confirmation-dialog>
</form>
</section>

View File

@ -1,28 +1,3 @@
.option-right {
padding-right: 16px;
margin-bottom: 12px;
}
.arrow-back {
cursor: pointer;
}
.arrow-block {
border-right: 2px solid #cccccc;
margin-right: 6px;
display: inline-flex;
padding: 6px 6px 6px 12px;
}
.title-block {
display: inline-block;
}
.tag-name {
font-weight: 300;
font-size: 32px;
}
.no-info-div {
background: white;
border: 1px;
@ -35,7 +10,17 @@
border: 1px;
border-style: solid;
border-color: #CCCCCC;
padding: 0px 12px 24px 12px;
padding: 0 12px 24px 12px;
}
.loading {
height: 3rem;
border: 1px;
border-style: solid;
border-color: #CCCCCC;
display: flex;
align-items: center;
justify-content: center;
}
.info-pre {
@ -57,12 +42,3 @@
fill: gray;
}
}
#images-container {
margin-top: 12px;
}
harbor-tag {
position: relative;
top: 24px;
}

View File

@ -0,0 +1,37 @@
import { ComponentFixture, TestBed, waitForAsync, } from '@angular/core/testing';
import { of } from "rxjs";
import { ArtifactInfoComponent } from './artifact-info.component';
import { SharedTestingModule } from 'src/app/shared/shared.module';
import { RepositoryService } from 'ng-swagger-gen/services/repository.service';
describe('ArtifactInfoComponent', () => {
let compRepo: ArtifactInfoComponent;
let fixture: ComponentFixture<ArtifactInfoComponent>;
let FakedRepositoryService = {
updateRepository: () => of(null),
getRepository: () => of({description: ''})
};
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
SharedTestingModule,
],
declarations: [
ArtifactInfoComponent
],
providers: [
{ provide: RepositoryService, useValue: FakedRepositoryService},
]
});
}));
beforeEach(() => {
fixture = TestBed.createComponent(ArtifactInfoComponent);
compRepo = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(compRepo).toBeTruthy();
});
});

View File

@ -0,0 +1,134 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// 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, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { RepositoryService } from 'ng-swagger-gen/services/repository.service';
import { ConfirmationMessage } from 'src/app/base/global-confirmation-dialog/confirmation-message';
import { ConfirmationAcknowledgement } from 'src/app/base/global-confirmation-dialog/confirmation-state-message';
import { Project } from 'src/app/base/project/project';
import { ConfirmationDialogComponent } from 'src/app/shared/components/confirmation-dialog/confirmation-dialog.component';
import { ConfirmationState, ConfirmationTargets } from 'src/app/shared/entities/shared.const';
import { ErrorHandler } from 'src/app/shared/units/error-handler/error-handler';
import { dbEncodeURIComponent } from 'src/app/shared/units/utils';
import { finalize } from "rxjs/operators";
@Component({
selector: 'artifact-info',
templateUrl: './artifact-info.component.html',
styleUrls: ['./artifact-info.component.scss']
})
export class ArtifactInfoComponent implements OnInit {
projectName: string;
repoName: string;
hasProjectAdminRole: boolean = false;
onSaving: boolean = false;
loading: boolean = false;
editing: boolean = false;
imageInfo: string;
orgImageInfo: string;
@ViewChild('confirmationDialog')
confirmationDlg: ConfirmationDialogComponent;
constructor(
private errorHandler: ErrorHandler,
private repositoryService: RepositoryService,
private translate: TranslateService,
private activatedRoute: ActivatedRoute,
) {
}
ngOnInit(): void {
this.repoName = this.activatedRoute.snapshot?.parent?.params['repo'];
let resolverData = this.activatedRoute.snapshot?.parent?.parent?.data;
if (resolverData) {
this.projectName = (<Project>resolverData['projectResolver']).name;
this.hasProjectAdminRole = (<Project>resolverData['projectResolver']).has_project_admin_role;
}
this.retrieve();
}
retrieve() {
let params: RepositoryService.GetRepositoryParams = {
projectName: this.projectName,
repositoryName: dbEncodeURIComponent(this.repoName),
};
this.loading = true;
this.repositoryService.getRepository(params)
.pipe(finalize(() => this.loading = false))
.subscribe(response => {
this.orgImageInfo = response.description;
this.imageInfo = response.description;
}, error => this.errorHandler.error(error));
}
refresh() {
this.retrieve();
}
hasChanges() {
return this.imageInfo !== this.orgImageInfo;
}
reset(): void {
this.imageInfo = this.orgImageInfo;
}
editInfo() {
this.editing = true;
}
saveInfo() {
if (!this.hasChanges()) {
return;
}
this.onSaving = true;
let params: RepositoryService.UpdateRepositoryParams = {
repositoryName: dbEncodeURIComponent(this.repoName),
repository: {description: this.imageInfo},
projectName: this.projectName,
};
this.repositoryService.updateRepository(params)
.subscribe(() => {
this.onSaving = false;
this.translate.get('CONFIG.SAVE_SUCCESS').subscribe((res: string) => {
this.errorHandler.info(res);
});
this.editing = false;
this.refresh();
}, error => {
this.onSaving = false;
this.errorHandler.error(error);
});
}
cancelInfo() {
let msg = new ConfirmationMessage(
'CONFIG.CONFIRM_TITLE',
'CONFIG.CONFIRM_SUMMARY',
'',
{},
ConfirmationTargets.CONFIG
);
this.confirmationDlg.open(msg);
}
confirmCancel(ack: ConfirmationAcknowledgement): void {
this.editing = false;
if (ack && ack.source === ConfirmationTargets.CONFIG &&
ack.state === ConfirmationState.CONFIRMED) {
this.reset();
}
}
}

View File

@ -32,7 +32,7 @@
<div class="row flex-items-xs-right rightPos">
<div id="filterArea" *ngIf="!depth">
<div class='filterLabelPiece' *ngIf="(openLabelFilterPiece &&filterByType ==='labels')"
[style.left.px]='96'>
[style.left.px]='110'>
<hbr-label-piece *ngIf="showlabel" [hidden]='!filterOneLabel' [label]="filterOneLabel"
[labelWidth]="130"></hbr-label-piece>
</div>
@ -59,19 +59,19 @@
</div>
</div>
<div class="label-filter-panel" *ngIf="!withAdmiral" [hidden]="!(openLabelFilterPanel&&filterByType==='labels')">
<div class="label-filter-panel" [hidden]="!(openLabelFilterPanel&&filterByType==='labels')">
<a class="filterClose" (click)="closeFilter()">&times;</a>
<label
class="filterLabelHeader filter-dark">{{'REPOSITORY.FILTER_ARTIFACT_BY_LABEL' | translate}}</label>
<div class="form-group"><input clrInput type="text" placeholder="Filter labels"
<div class="form-group mb-05"><input clrInput type="text" placeholder="Filter labels"
[(ngModel)]="filterName" (keyup)="handleInputFilter()"></div>
<div [hidden]='imageFilterLabels.length' class="no-labels">{{'LABEL.NO_LABELS' | translate }}
<div [hidden]='artifactListPageService?.imageFilterLabels.length' class="no-labels">{{'LABEL.NO_LABELS' | translate }}
</div>
<div [hidden]='!imageFilterLabels.length' class="has-label">
<button type="button" class="labelBtn" *ngFor='let label of imageFilterLabels'
<div [hidden]='!artifactListPageService?.imageFilterLabels.length' class="has-label">
<button type="button" class="labelBtn" *ngFor='let label of artifactListPageService?.imageFilterLabels'
[hidden]="!label.show" (click)="rightFilterLabel(label)">
<clr-icon shape="check" class='pull-left' [hidden]='!label.iconsShow'></clr-icon>
<div class='labelDiv'>
<div class='labelDiv top-3-px'>
<hbr-label-piece [label]="label.label" [labelWidth]="160"></hbr-label-piece>
</div>
</button>
@ -87,7 +87,7 @@
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid [clrDgLoading]="loading" (clrDgRefresh)="clrDgRefresh($event)" class="datagrid-top" [class.embeded-datagrid]="isEmbedded"
<clr-datagrid [clrDgLoading]="loading" (clrDgRefresh)="clrDgRefresh($event)" class="datagrid-top"
[(clrDgSelected)]="selectedRow">
<clr-dg-action-bar>
<button id="scan-btn" [clrLoading]="scanBtnState" type="button" class="btn btn-secondary scan-btn"
@ -111,7 +111,7 @@
<div class="action-dropdown-item no-border" aria-label="copy digest" clrDropdownItem
[clrDisabled]="!(selectedRow.length==1&& !depth)" (click)="showDigestId()">
{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</div>
<clr-dropdown *ngIf="!withAdmiral">
<clr-dropdown>
<button class="action-dropdown-item" clrDropdownTrigger
[disabled]="!canAddLabel()||!hasAddLabelImagePermission ||depth || inprogress"
(click)="addLabels()">
@ -124,11 +124,11 @@
<div class="form-group filter-label-input"><input clrInput type="text"
placeholder="Filter labels" [(ngModel)]="stickName"
(keyup)="handleStickInputFilter()"></div>
<div [hidden]='imageStickLabels.length' class="no-labels">
<div [hidden]='artifactListPageService?.imageStickLabels.length' class="no-labels">
{{'LABEL.NO_LABELS' | translate }}</div>
<div [hidden]='!imageStickLabels.length' class="has-label">
<div [hidden]='!artifactListPageService?.imageStickLabels.length' class="has-label">
<button type="button" class="dropdown-item" clrDropdownItem
*ngFor='let label of imageStickLabels' [hidden]='!label.show'
*ngFor='let label of artifactListPageService?.imageStickLabels' [hidden]='!label.show'
(click)="stickLabel(label)">
<clr-icon shape="check" class='pull-left' [hidden]='!label.iconsShow'>
</clr-icon>
@ -142,7 +142,7 @@
</clr-dropdown-menu>
</clr-dropdown>
<div class="action-dropdown-item" aria-label="retag" *ngIf="!withAdmiral"
<div class="action-dropdown-item" aria-label="retag"
[clrDisabled]="!(selectedRow.length===1)|| !hasRetagImagePermission||depth" (click)="retag()"
clrDropdownItem>{{'REPOSITORY.RETAG' | translate}}</div>
<div class="action-dropdown-item" clrDropdownItem *ngIf="hasDeleteImagePermission"
@ -155,24 +155,25 @@
<clr-dg-column class="flex-max-width" [clrDgSortBy]="'digest'">{{'REPOSITORY.ARTIFACTS_COUNT' | translate}}
</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
<clr-dg-column class="pull-command-column">{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="depth">{{'REPOSITORY.PLATFORM' | translate}}</clr-dg-column>
<clr-dg-column class="w-rem-4">{{'REPOSITORY.TAGS' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.TAGS' | translate}}</clr-dg-column>
<clr-dg-column class="co-signed-column">{{'ACCESSORY.CO_SIGNED' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="'size'">{{'REPOSITORY.SIZE' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.VULNERABILITY' | translate}}</clr-dg-column>
<clr-dg-column>{{'ARTIFACT.ANNOTATION' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="!withAdmiral">{{'REPOSITORY.LABELS' | translate}}</clr-dg-column>
<clr-dg-column class="vul-column">{{'REPOSITORY.VULNERABILITY' | translate}}</clr-dg-column>
<clr-dg-column class="annotations-column">{{'ARTIFACT.ANNOTATION' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.LABELS' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="pushComparator">{{'REPOSITORY.PUSH_TIME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="pullComparator">{{'REPOSITORY.PULL_TIME' | translate}}</clr-dg-column>
<clr-dg-placeholder>{{'ARTIFACT.PLACEHOLDER' | translate }}</clr-dg-placeholder>
<clr-dg-row *ngFor="let artifact of artifactList" [clrDgItem]="artifact" >
<clr-dg-cell class="truncated flex-max-width">
<clr-dg-cell class="flex-max-width truncated">
<div class="cell white-normal">
<div class="artifact-icon clr-display-inline-block" *ngIf="artifact.icon">
<img *ngIf="getIcon(artifact.icon)" class="artifact-icon" [title]="artifact.type"
[src]="getIcon(artifact.icon)" (error)="showDefaultIcon($event)" />
</div>
<a href="javascript:void(0)" class="max-width-100 margin-left-5" (click)="goIntoArtifactSummaryPage(artifact)"
<a href="javascript:void(0)" class="digest margin-left-5" (click)="goIntoArtifactSummaryPage(artifact)"
title="{{artifact.digest}}">
{{ artifact.digest | slice:0:15}}</a>
<clr-tooltip *ngIf="artifact?.references && artifact?.references?.length">
@ -189,7 +190,7 @@
</clr-tooltip>
</div>
</clr-dg-cell>
<clr-dg-cell class="truncated" title="{{artifact.pullCommand}}">
<clr-dg-cell title="{{artifact.pullCommand}}">
<hbr-copy-input *ngIf="artifact.pullCommand" #copyInput (onCopyError)="onCpError($event)" iconMode="true" defaultValue="{{artifact.pullCommand}}"></hbr-copy-input>
</clr-dg-cell>
<clr-dg-cell *ngIf="depth">
@ -197,20 +198,15 @@
{{artifact.platform?.os}}/{{artifact.platform?.architecture}}{{artifact.platform?.variant?'/'+artifact.platform?.variant: ''}}
</div>
</clr-dg-cell>
<clr-dg-cell class="w-rem-4">
<clr-dg-cell class="center">
<div *ngIf="artifact.tags" class="truncated width-p-100">
<clr-tooltip class="width-p-100">
<div clrTooltipTrigger class="level-border">
<div>
<div class="inner truncated ">
<span>
{{artifact?.tags[0]?.name}}
</span>
<span class="eslip"
*ngIf="artifact?.tags?.length>1">...</span>
<span *ngIf="artifact?.tags?.length>1" > ({{artifact?.tagNumber}})</span>
</div>
<div clrTooltipTrigger class="center">
<div class="center">
<span #tagName class="truncated">{{artifact?.tags[0]?.name}}</span>
<span class="eslip"
*ngIf="artifact?.tags?.length>1 && isOverflow()">...</span>
<span *ngIf="artifact?.tags?.length>1 || isOverflow()">({{artifact?.tagNumber}})</span>
</div>
</div>
<clr-tooltip-content [clrPosition]="'top-right'" class="lg" [clrSize]="'lg'" *clrIfOpen>
@ -219,8 +215,7 @@
<tr>
<th class="left tag-header-color">
{{'REPOSITORY.TAGS' | translate | uppercase}}</th>
<th *ngIf="withNotary" class="left tag-header-color">
{{'REPOSITORY.SIGNED' | translate | uppercase}}</th>
<th *ngIf="withNotary" class="left tag-header-color">{{'ACCESSORY.NOTARY_SIGNED' | translate | uppercase}}</th>
<th class="left tag-header-color">
{{'REPOSITORY.PULL_TIME' | translate | uppercase}}</th>
<th class="left tag-header-color">
@ -253,6 +248,11 @@
</div>
</clr-dg-cell>
<clr-dg-cell>
<span *ngIf="artifact.coSigned === 'checking'" class="spinner spinner-inline ml-2"></span>
<clr-icon shape="check-circle" *ngIf="artifact.coSigned === 'true'" size="20" class="signed"></clr-icon>
<clr-icon shape="times-circle" *ngIf="artifact.coSigned === 'false'" size="16" class="color-red"></clr-icon>
</clr-dg-cell>
<clr-dg-cell>
<div class="cell">
{{artifact.size?sizeTransform(artifact.size+''): ""}}
@ -290,7 +290,7 @@
</div>
</div>
</clr-dg-cell>
<clr-dg-cell *ngIf="!withAdmiral">
<clr-dg-cell>
<div class="cell">
<hbr-label-piece *ngIf="artifact.labels?.length" [label]="artifact.labels[0]" [labelWidth]="90">
</hbr-label-piece>
@ -316,6 +316,7 @@
<div class="cell">
{{artifact.pull_time === availableTime ? "" : (artifact.pull_time| harborDatetime: 'short')}}</div>
</clr-dg-cell>
<sub-accessories (deleteAccessory)="deleteAccessory($event)" [projectName]="projectName" [repositoryName]="repoName" *ngIf="artifact?.accessories?.length" [total]="artifact?.accessoryNumber" [accessories]="artifact?.accessories" [clrIfExpanded]="false" ngProjectAs="clr-dg-row-detail"></sub-accessories>
</clr-dg-row>
<clr-dg-footer>
<clr-dg-pagination #pagination [clrDgTotalItems]="totalCount" [(clrDgPage)]="currentPage" [clrDgPageSize]="pageSize">

View File

@ -73,6 +73,9 @@
}
.truncated {
width: 100px;
line-height: 20px;
height: 20px;
display: inline-block;
overflow: hidden;
white-space: nowrap;
@ -104,8 +107,8 @@
position: relative;
padding-left: 0.5rem;
padding-right: 0.5rem;
line-height: 1;
height: 1.2rem;
line-height: 1.3rem;
height: 1.3rem;
}
.dropdown-menu input {
@ -142,7 +145,7 @@
.labelDiv {
position: absolute;
left: 34px;
top: 5px;
top: 0;
}
.trigger-item hbr-label-piece {
@ -306,11 +309,10 @@ clr-datagrid {
.white-normal {
white-space: normal;
}
.max-width-100 {
.digest {
width: 128px;
// overflow: hidden;
// white-space: nowrap;
// text-overflow: ellipsis;
flex-grow:0;
flex-shrink:0;
}
.max-width-38 {
max-width: 38px !important;
@ -324,11 +326,12 @@ clr-datagrid {
.artifact-icon {
width: 0.8rem;
height: 0.8rem;
vertical-align: top;
}
.width-p-100 {
width: 100%;
.lg {
width: 450px;
width: 550px;
}
}
.w-rem-4 {
@ -339,6 +342,7 @@ clr-datagrid {
}
.tag-body-color {
color: #ccc;
max-width: 120px;
}
.table {
.tag-tr {
@ -414,3 +418,28 @@ clr-datagrid {
.margin-left-5 {
margin-left: 5px;
}
.pull-command-column {
width: 6rem !important;
}
.co-signed-column {
width: 6rem !important;
}
.vul-column {
width: 11rem !important;
}
.annotations-column {
width: 5rem !important;
}
.signed {
color: #00d40f;
}
.mb-05 {
margin-bottom: 0.5rem;
}
.top-3-px {
top: 3px;
}
.center {
display: flex;
align-items: center;
}

View File

@ -28,6 +28,9 @@ import { Tag } from "../../../../../../../../../ng-swagger-gen/models/tag";
import { SharedTestingModule } from "../../../../../../../shared/shared.module";
import { LabelService } from "../../../../../../../../../ng-swagger-gen/services/label.service";
import { Registry } from "../../../../../../../../../ng-swagger-gen/models/registry";
import { AppConfigService } from "../../../../../../../services/app-config.service";
import { ArtifactListPageService } from '../../artifact-list-page.service';
import { ClrLoadingState } from '@clr/angular';
describe("ArtifactListTabComponent (inline template)", () => {
@ -290,6 +293,38 @@ describe("ArtifactListTabComponent (inline template)", () => {
return of(res).pipe(delay(0));
}
};
const mockedAppConfigService = {
getConfig() {
return {};
}
};
const mockedArtifactListPageService = {
imageStickLabels: [],
imageFilterLabels: [],
resetClonedLabels() {
},
getScanBtnState(): ClrLoadingState {
return ClrLoadingState.DEFAULT;
},
hasEnabledScanner(): boolean {
return true;
},
hasAddLabelImagePermission(): boolean {
return true;
},
hasRetagImagePermission(): boolean {
return true;
},
hasDeleteImagePermission(): boolean {
return true;
},
hasScanImagePermission(): boolean {
return true;
},
init() {
}
};
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
@ -306,7 +341,9 @@ describe("ArtifactListTabComponent (inline template)", () => {
CopyInputComponent
],
providers: [
ArtifactDefaultService,
{ provide: ArtifactListPageService, useValue: mockedArtifactListPageService },
{ provide: ArtifactService, useClass: ArtifactDefaultService },
{ provide: AppConfigService, useValue: mockedAppConfigService },
{ provide: Router, useValue: mockRouter },
{ provide: ArtifactService, useValue: mockNewArtifactService },
{ provide: ProjectService, useClass: ProjectDefaultService },
@ -325,12 +362,7 @@ describe("ArtifactListTabComponent (inline template)", () => {
comp = fixture.componentInstance;
comp.projectId = 1;
comp.repoName = "library/nginx";
comp.hasDeleteImagePermission = true;
comp.hasScanImagePermission = true;
comp.hasSignedIn = true;
comp.registryUrl = "http://registry.testing.com";
comp.withNotary = false;
comp.withAdmiral = false;
let labelService: LabelService;
userPermissionService = fixture.debugElement.injector.get(UserPermissionService);
let http: HttpClient;
@ -359,7 +391,7 @@ describe("ArtifactListTabComponent (inline template)", () => {
comp.artifactList = mockArtifacts;
fixture.detectChanges();
await fixture.whenStable();
const el: HTMLAnchorElement = fixture.nativeElement.querySelector('a.max-width-100');
const el: HTMLAnchorElement = fixture.nativeElement.querySelector('.digest');
expect(el).toBeTruthy();
expect(el.textContent).toBeTruthy();
expect(el.textContent.trim()).toEqual("sha256:4875cda3");
@ -372,7 +404,7 @@ describe("ArtifactListTabComponent (inline template)", () => {
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
const el: HTMLAnchorElement = fixture.nativeElement.querySelector('a.max-width-100');
const el: HTMLAnchorElement = fixture.nativeElement.querySelector('.digest');
expect(el).toBeTruthy();
expect(el.textContent).toBeTruthy();
expect(el.textContent.trim()).toEqual('sha256:3e33e3e3');
@ -386,7 +418,7 @@ describe("ArtifactListTabComponent (inline template)", () => {
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
const el: HTMLAnchorElement = fixture.nativeElement.querySelector('a.max-width-100');
const el: HTMLAnchorElement = fixture.nativeElement.querySelector('.digest');
expect(el).toBeTruthy();
expect(el.textContent).toBeTruthy();
expect(el.textContent.trim()).toEqual('sha256:3e33e3e3');

View File

@ -0,0 +1,44 @@
<clr-datagrid (clrDgRefresh)="clrLoad()" [clrDgLoading]="loading">
<clr-dg-column>{{'ACCESSORY.ACCESSORY' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.TYPE' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.SIZE' | translate}}</clr-dg-column>
<clr-dg-column>{{'ROBOT_ACCOUNT.CREATETION' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let a of displayedAccessories" [clrDgItem]="a">
<clr-dg-action-overflow>
<button class="action-item" (click)="delete(a)">{{'REPOSITORY.DELETE' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell>
<div class="cell">
<div title="{{a?.digest}}" class="artifact-icon clr-display-inline-block">
<img *ngIf="getIcon(a.icon)" class="artifact-icon" [title]="a.type"
[src]="getIcon(a.icon)" (error)="showDefaultIcon($event)" />
</div>
<a href="javascript:void(0)" (click)="goIntoArtifactSummaryPage(a)" class="margin-left-5">{{a?.digest?.slice(0,15)}}</a>
</div>
</clr-dg-cell>
<clr-dg-cell>
<div class="cell">
{{a.type}}
</div>
</clr-dg-cell>
<clr-dg-cell>
<div class="cell">
{{size(a.size)}}
</div>
</clr-dg-cell>
<clr-dg-cell>
<div class="cell">
{{a.creation_time | harborDatetime: 'short'}}
</div>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer *ngIf="total > pageSize">
<clr-dg-pagination #pagination [clrDgPageSize]="pageSize" [(clrDgPage)]="currentPage" [clrDgTotalItems]="total">
<span *ngIf="total">{{pagination.firstItem + 1}}
-
{{pagination.lastItem +1 }} {{'ROBOT_ACCOUNT.OF' |
translate}} </span>
{{total}} {{'ROBOT_ACCOUNT.ITEMS' | translate}}
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>

View File

@ -0,0 +1,13 @@
.artifact-icon {
width: 0.8rem;
height: 0.8rem;
}
.cell {
display: flex;
align-items: center;
width: 100%;
height: 100%;
}
.margin-left-5 {
margin-left: 5px;
}

View File

@ -0,0 +1,108 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SharedTestingModule } from 'src/app/shared/shared.module';
import { SubAccessoriesComponent } from './sub-accessories.component';
import { Accessory } from "../../../../../../../../../../ng-swagger-gen/models/accessory";
import { AccessoryType } from "../../../../artifact";
import { ArtifactService as NewArtifactService } from "../../../../../../../../../../ng-swagger-gen/services/artifact.service";
import { of } from "rxjs";
import { ArtifactDefaultService, ArtifactService } from '../../../../artifact.service';
describe('SubAccessoriesComponent', () => {
const mockedAccessories: Accessory[] = [
{
id: 1,
artifact_id: 1,
digest: 'sha256:test',
type: AccessoryType.COSIGN,
size: 1024
},
{
id: 2,
artifact_id: 2,
digest: 'sha256:test2',
type: AccessoryType.COSIGN,
size: 1024
},
{
id: 3,
artifact_id: 3,
digest: 'sha256:test3',
type: AccessoryType.COSIGN,
size: 1024
},
{
id: 4,
artifact_id: 4,
digest: 'sha256:test4',
type: AccessoryType.COSIGN,
size: 1024
},
{
id: 5,
artifact_id: 5,
digest: 'sha256:test5',
type: AccessoryType.COSIGN,
size: 1024
},
];
const page2: Accessory[] = [
{
id: 6,
artifact_id: 6,
digest: 'sha256:test6',
type: AccessoryType.COSIGN,
size: 1024
},
];
const mockedArtifactService = {
listAccessories() {
return of(page2);
}
};
let component: SubAccessoriesComponent;
let fixture: ComponentFixture<SubAccessoriesComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
SharedTestingModule,
],
declarations: [
SubAccessoriesComponent
],
providers: [
{ provide: NewArtifactService, useValue: mockedArtifactService },
{ provide: ArtifactService, useClass: ArtifactDefaultService },
]
});
});
beforeEach(() => {
fixture = TestBed.createComponent(SubAccessoriesComponent);
component = fixture.componentInstance;
component.accessories = mockedAccessories;
component.total = 6;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should render rows', async () => {
await fixture.whenStable();
const rows = fixture.nativeElement.querySelectorAll('clr-dg-row');
expect(rows.length).toEqual(5);
});
it('should render next page', async () => {
await fixture.whenStable();
const nextPageButton: HTMLButtonElement = fixture.nativeElement.querySelector('.pagination-next');
nextPageButton.click();
fixture.detectChanges();
await fixture.whenStable();
const rows = fixture.nativeElement.querySelectorAll('clr-dg-row');
expect(rows.length).toEqual(1);
});
});

View File

@ -0,0 +1,101 @@
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { clone, dbEncodeURIComponent, formatSize } from "../../../../../../../../shared/units/utils";
import { UN_LOGGED_PARAM, YES } from "../../../../../../../../account/sign-in/sign-in.service";
import { ActivatedRoute, Router } from "@angular/router";
import { Accessory } from "ng-swagger-gen/models/accessory";
import { ArtifactService as NewArtifactService } from "ng-swagger-gen/services/artifact.service";
import { ErrorHandler } from "../../../../../../../../shared/units/error-handler";
import { finalize } from "rxjs/operators";
import { SafeUrl } from '@angular/platform-browser';
import { ArtifactService } from "../../../../artifact.service";
import { artifactDefault } from '../../../../artifact';
export const ACCESSORY_PAGE_SIZE: number = 5;
@Component({
selector: 'sub-accessories',
templateUrl: 'sub-accessories.component.html',
styleUrls: ['./sub-accessories.component.scss']
})
export class SubAccessoriesComponent implements OnInit {
@Input()
projectName: string;
@Input()
repositoryName: string;
@Input()
artifactDigest: string;
@Input()
accessories: Accessory[] = [];
@Output()
deleteAccessory: EventEmitter<Accessory> = new EventEmitter<Accessory>();
currentPage: number = 1;
@Input()
total: number = 0;
pageSize: number = ACCESSORY_PAGE_SIZE;
page: number = 1;
displayedAccessories: Accessory[] = [];
loading: boolean = false;
constructor(private activatedRoute: ActivatedRoute,
private router: Router,
private newArtifactService: NewArtifactService,
private artifactService: ArtifactService,
private errorHandlerService: ErrorHandler) {
}
ngOnInit(): void {
this.displayedAccessories = clone(this.accessories);
}
size(size: number) {
return formatSize(size.toString());
}
getIcon(icon: string): SafeUrl {
return this.artifactService.getIcon(icon);
}
showDefaultIcon(event: any) {
if (event && event.target) {
event.target.src = artifactDefault;
}
}
goIntoArtifactSummaryPage(accessory: Accessory): void {
const relativeRouterLink: string[] = ['artifacts', accessory.digest];
if (this.activatedRoute.snapshot.queryParams[UN_LOGGED_PARAM] === YES) {
this.router.navigate(relativeRouterLink, {relativeTo: this.activatedRoute, queryParams: {[UN_LOGGED_PARAM]: YES}});
} else {
this.router.navigate(relativeRouterLink, {relativeTo: this.activatedRoute});
}
}
delete(a: Accessory) {
this.deleteAccessory.emit(a);
}
clrLoad() {
if (this.currentPage === 1) {
this.displayedAccessories = clone(this.accessories);
return;
}
this.loading = true;
const listTagParams: NewArtifactService.ListAccessoriesParams = {
projectName: this.projectName,
repositoryName: dbEncodeURIComponent(this.repositoryName),
reference: this.artifactDigest,
page: 1,
pageSize: ACCESSORY_PAGE_SIZE
};
this.newArtifactService.listAccessories(listTagParams)
.pipe(finalize(() => this.loading = false))
.subscribe(
res => {
this.displayedAccessories = res;
},
error => {
this.errorHandlerService.error(error);
}
);
}
}

View File

@ -1,64 +0,0 @@
<section class="overview-section">
<div class="title-wrapper">
<div class="title-block arrow-block" *ngIf="withAdmiral">
<clr-icon class="rotate-90 arrow-back" shape="arrow" size="36" (click)="goBack()"></clr-icon>
</div>
<div class="title-block">
<h2 sub-header-title class="custom-h2" *ngIf="!artifactDigest">{{showCurrentTitle}}</h2>
<h2 sub-header-title class="custom-h2" *ngIf="artifactDigest">{{artifactDigest | slice:0:15}}</h2>
</div>
</div>
</section>
<section class="detail-section">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
<ul id="configTabs" class="nav" role="tablist">
<li role="presentation" class="nav-item" *ngIf="!artifactDigest">
<button id="repo-info" class="btn btn-link nav-link" aria-controls="info" [class.active]='isCurrentTabLink("repo-info")'
type="button" (click)='tabLinkClick("repo-info")'>{{'REPOSITORY.INFO' | translate}}</button>
</li>
<li role="presentation" class="nav-item">
<button id="repo-image" class="btn btn-link nav-link active" aria-controls="image" [class.active]='isCurrentTabLink("repo-image")'
type="button" (click)='tabLinkClick("repo-image")'>{{'REPOSITORY.ARTIFACTS' | translate}}</button>
</li>
</ul>
<section id="info" role="tabpanel" aria-labelledby="repo-info" [hidden]='!isCurrentTabContent("info")'>
<form #repoInfoForm="ngForm">
<div id="info-edit-button">
<button class="btn " [disabled]="editing || !hasProjectAdminRole " (click)="editInfo()">
<clr-icon shape="pencil" size="16"></clr-icon>&nbsp;{{'BUTTON.EDIT' | translate}}
</button>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 1024" preserveAspectRatio="xMinYMin" class="markdown">
<path d="M950.154 192H73.846C33.127 192 0 225.12699999999995 0 265.846v492.308C0 798.875 33.127 832 73.846 832h876.308c40.721 0 73.846-33.125 73.846-73.846V265.846C1024 225.12699999999995 990.875 192 950.154 192zM576 703.875L448 704V512l-96 123.077L256 512v192H128V320h128l96 128 96-128 128-0.125V703.875zM767.091 735.875L608 512h96V320h128v192h96L767.091 735.875z" />
</svg>
<span>{{ 'REPOSITORY.MARKDOWN' | translate }}</span>
</div>
<div id="no-editing" *ngIf="!editing">
<div *ngIf="!hasInfo()" class="no-info-div">
<p>{{'REPOSITORY.NO_INFO' | translate }}<p>
</div>
<div *ngIf="hasInfo()" class="info-div">
<div class="info-pre" [innerHTML]="imageInfo | markdown"></div>
</div>
</div>
<div *ngIf="editing">
<textarea id="info-edit-textarea" class="clr-textarea w-100" rows="5" name="info-edit-textarea"
[(ngModel)]="imageInfo"></textarea>
</div>
<div class="" *ngIf="editing">
<button id="edit-save" class="btn btn-primary" [disabled]="!hasChanges()" (click)="saveInfo()">{{'BUTTON.SAVE' | translate}}</button>
<button id="edit-cancel" class="btn" (click)="cancelInfo()">{{'BUTTON.CANCEL' | translate}}</button>
</div>
<confirmation-dialog #confirmationDialog (confirmAction)="confirmCancel($event)"></confirmation-dialog>
</form>
</section>
<section id="image" role="tabpanel" aria-labelledby="repo-image" [hidden]='!isCurrentTabContent("image")'>
<div id="images-container">
<artifact-list-tab ngProjectAs="clr-dg-row-detail"
class="sub-grid-custom" [repoName]="repoName" artifact [registryUrl]="registryUrl" [withNotary]="withNotary" [withAdmiral]="withAdmiral" [hasSignedIn]="hasSignedIn"
[isGuest]="isGuest" [projectId]="projectId" [memberRoleID]="memberRoleID"></artifact-list-tab>
</div>
</section>
</div>
</section>

View File

@ -1,101 +0,0 @@
import { ComponentFixture, TestBed, waitForAsync, } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ArtifactListComponent } from './artifact-list.component';
import { of } from "rxjs";
import { delay } from 'rxjs/operators';
import { ActivatedRoute } from '@angular/router';
import { SystemInfo, SystemInfoDefaultService, SystemInfoService, } from "../../../../../../shared/services";
import { ArtifactDefaultService, ArtifactService } from "../../artifact.service";
import { ErrorHandler } from "../../../../../../shared/units/error-handler";
import { RepositoryService as NewRepositoryService } from "../../../../../../../../ng-swagger-gen/services/repository.service";
import { SharedTestingModule } from "../../../../../../shared/shared.module";
describe('ArtifactListComponent (inline template)', () => {
let compRepo: ArtifactListComponent;
let fixture: ComponentFixture<ArtifactListComponent>;
let systemInfoService: SystemInfoService;
let artifactService: ArtifactService;
let spyRepos: jasmine.Spy;
let spySystemInfo: jasmine.Spy;
let mockActivatedRoute = {
data: of(
{
projectResolver: {
name: 'library'
}
}
),
params: {
subscribe: () => {
return of(null);
}
},
snapshot: { data: null }
};
let mockSystemInfo: SystemInfo = {
'with_notary': true,
'with_admiral': false,
'admiral_endpoint': 'NA',
'auth_mode': 'db_auth',
'registry_url': '10.112.122.56',
'project_creation_restriction': 'everyone',
'self_registration': true,
'has_ca_root': false,
'harbor_version': 'v1.1.1-rc1-160-g565110d'
};
let newRepositoryService = {
updateRepository: () => of(null),
getRepository: () => of({description: ''})
};
const fakedErrorHandler = {
error: () => {}
};
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
SharedTestingModule,
],
schemas: [
NO_ERRORS_SCHEMA
],
declarations: [
ArtifactListComponent
],
providers: [
{ provide: ErrorHandler, useValue: fakedErrorHandler },
{ provide: SystemInfoService, useClass: SystemInfoDefaultService },
{ provide: ArtifactService, useClass: ArtifactDefaultService },
{ provide: ActivatedRoute, useValue: mockActivatedRoute },
{ provide: NewRepositoryService, useValue: newRepositoryService},
]
});
}));
beforeEach(() => {
fixture = TestBed.createComponent(ArtifactListComponent);
compRepo = fixture.componentInstance;
compRepo.projectId = 1;
compRepo.hasProjectAdminRole = true;
compRepo.repoName = 'library/nginx';
systemInfoService = fixture.debugElement.injector.get(SystemInfoService);
artifactService = fixture.debugElement.injector.get(ArtifactService);
spySystemInfo = spyOn(systemInfoService, 'getSystemInfo').and.returnValues(of(mockSystemInfo).pipe(delay(0)));
fixture.detectChanges();
});
let originalTimeout;
beforeEach(function () {
originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100000;
});
afterEach(function () {
jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout;
});
it('should create', () => {
expect(compRepo).toBeTruthy();
});
});

View File

@ -1,205 +0,0 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// 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, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { ArtifactClickEvent, State, SystemInfo, SystemInfoService } from "../../../../../../shared/services";
import { ConfirmationDialogComponent, } from "../../../../../../shared/components/confirmation-dialog";
import { ErrorHandler } from "../../../../../../shared/units/error-handler";
import { ArtifactService } from "../../artifact.service";
import { ConfirmationState, ConfirmationTargets } from "../../../../../../shared/entities/shared.const";
import { ActivatedRoute } from "@angular/router";
import { Project } from '../../../../project';
import { RepositoryService as NewRepositoryService } from "../../../../../../../../ng-swagger-gen/services/repository.service";
import { dbEncodeURIComponent } from '../../../../../../shared/units/utils';
import { ConfirmationMessage } from "../../../../../global-confirmation-dialog/confirmation-message";
import { ConfirmationAcknowledgement } from "../../../../../global-confirmation-dialog/confirmation-state-message";
const TabLinkContentMap: { [index: string]: string } = {
'repo-info': 'info',
'repo-image': 'image'
};
@Component({
selector: 'artifact-list',
templateUrl: './artifact-list.component.html',
styleUrls: ['./artifact-list.component.scss']
})
export class ArtifactListComponent implements OnInit {
signedCon: { [key: string]: any | string[] } = {};
@Input() projectId: number;
@Input() memberRoleID: number;
@Input() repoName: string;
@Input() hasSignedIn: boolean;
@Input() hasProjectAdminRole: boolean;
@Input() isGuest: boolean;
@Output() tagClickEvent = new EventEmitter<ArtifactClickEvent>();
@Output() backEvt: EventEmitter<any> = new EventEmitter<any>();
@Output() putArtifactReferenceArr: EventEmitter<string[]> = new EventEmitter<[]>();
onGoing = false;
editing = false;
inProgress = true;
currentTabID = 'repo-image';
systemInfo: SystemInfo;
imageInfo: string;
orgImageInfo: string;
timerHandler: any;
projectName: string = '';
@ViewChild('confirmationDialog')
confirmationDlg: ConfirmationDialogComponent;
showCurrentTitle: string;
artifactDigest: string;
constructor(
private errorHandler: ErrorHandler,
private systemInfoService: SystemInfoService,
private artifactService: ArtifactService,
private newRepositoryService: NewRepositoryService,
private translate: TranslateService,
private activatedRoute: ActivatedRoute,
) {
this.activatedRoute.params.subscribe(params => {
let depth = this.activatedRoute.snapshot.params['depth'];
if (depth) {
const arr: string[] = depth.split('-');
this.artifactDigest = depth.split('-')[arr.length - 1];
}
});
}
public get registryUrl(): string {
return this.systemInfo ? this.systemInfo.registry_url : '';
}
public get withNotary(): boolean {
return this.systemInfo ? this.systemInfo.with_notary : false;
}
public get withAdmiral(): boolean {
return this.systemInfo ? this.systemInfo.with_admiral : false;
}
ngOnInit(): void {
if (!this.projectId) {
this.errorHandler.error('Project ID cannot be unset.');
return;
}
const resolverData = this.activatedRoute.snapshot.data;
if (resolverData) {
const pro: Project = <Project>resolverData['projectResolver'];
this.projectName = pro.name;
}
this.showCurrentTitle = this.repoName || 'null';
this.retrieve();
this.inProgress = false;
this.artifactService.TriggerArtifactChan$.subscribe(res => {
if (res === 'repoName') {
this.showCurrentTitle = this.repoName;
} else {
this.showCurrentTitle = res[res.length - 1];
}
});
}
retrieve(state?: State) {
let params: NewRepositoryService.GetRepositoryParams = {
projectName: this.projectName,
repositoryName: dbEncodeURIComponent(this.repoName),
};
this.newRepositoryService.getRepository(params)
.subscribe(response => {
this.orgImageInfo = response.description;
this.imageInfo = response.description;
}, error => this.errorHandler.error(error));
this.systemInfoService.getSystemInfo()
.subscribe(systemInfo => this.systemInfo = systemInfo, error => this.errorHandler.error(error));
}
refresh() {
this.retrieve();
}
isCurrentTabLink(tabID: string): boolean {
return this.currentTabID === tabID;
}
isCurrentTabContent(ContentID: string): boolean {
return TabLinkContentMap[this.currentTabID] === ContentID;
}
tabLinkClick(tabID: string) {
this.currentTabID = tabID;
}
goBack(): void {
this.backEvt.emit(this.projectId);
}
hasChanges() {
return this.imageInfo !== this.orgImageInfo;
}
reset(): void {
this.imageInfo = this.orgImageInfo;
}
hasInfo() {
return this.imageInfo && this.imageInfo.length > 0;
}
editInfo() {
this.editing = true;
}
saveInfo() {
if (!this.hasChanges()) {
return;
}
this.onGoing = true;
let params: NewRepositoryService.UpdateRepositoryParams = {
repositoryName: dbEncodeURIComponent(this.repoName),
repository: {description: this.imageInfo},
projectName: this.projectName,
};
this.newRepositoryService.updateRepository(params)
.subscribe(() => {
this.onGoing = false;
this.translate.get('CONFIG.SAVE_SUCCESS').subscribe((res: string) => {
this.errorHandler.info(res);
});
this.editing = false;
this.refresh();
}, error => {
this.onGoing = false;
this.errorHandler.error(error);
});
}
cancelInfo() {
let msg = new ConfirmationMessage(
'CONFIG.CONFIRM_TITLE',
'CONFIG.CONFIRM_SUMMARY',
'',
{},
ConfirmationTargets.CONFIG
);
this.confirmationDlg.open(msg);
}
confirmCancel(ack: ConfirmationAcknowledgement): void {
this.editing = false;
if (ack && ack.source === ConfirmationTargets.CONFIG &&
ack.state === ConfirmationState.CONFIRMED) {
this.reset();
}
}
}

View File

@ -1,4 +1,4 @@
<div class="arrow-block" *ngIf="!withAdmiral">
<div class="arrow-block">
<span class="back-icon margin-right-5px"><</span>
<a class="pl-0" (click)="goBackPro()">{{'SIDE_NAV.PROJECTS'| translate}}</a>
<span class="back-icon"><</span>
@ -11,9 +11,6 @@
</div>
<div class="title-wrapper">
<div class="title-block arrow-block" *ngIf="withAdmiral">
<clr-icon class="rotate-90 arrow-back" shape="arrow" size="36" (click)="onBack()"></clr-icon>
</div>
<div class="title-block">
<h2 class="custom-h2 center-align-items">
<div class="artifact-icon clr-display-inline-block" *ngIf="artifact.icon">

View File

@ -43,10 +43,6 @@ export class ArtifactSummaryComponent implements OnInit {
) {
}
get withAdmiral(): boolean {
return this.appConfigService.getConfig().with_admiral;
}
goBack(): void {
this.router.navigate(["harbor", "projects", this.projectId, "repositories", this.repositoryName]);
}
@ -61,7 +57,7 @@ export class ArtifactSummaryComponent implements OnInit {
jumpDigest(index: number) {
const arr: string[] = this.referArtifactNameArray.slice(0, index + 1 );
if ( arr && arr.length) {
this.router.navigate(["harbor", "projects", this.projectId, "repositories", this.repositoryName, "depth", arr.join('-')]);
this.router.navigate(["harbor", "projects", this.projectId, "repositories", this.repositoryName, "artifacts-tab", "depth", arr.join('-')]);
} else {
this.router.navigate(["harbor", "projects", this.projectId, "repositories", this.repositoryName]);
}

View File

@ -42,7 +42,7 @@
</clr-dg-action-bar>
<clr-dg-column [clrDgField]="'name'">{{'TAG.NAME' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="hasPullCommand()">{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="withNotary">{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="withNotary">{{'ACCESSORY.NOTARY_SIGNED' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="'pull_time'">{{'TAG.PULL_TIME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="'push_time'">{{'TAG.PUSH_TIME' | translate}}</clr-dg-column>

View File

@ -2,7 +2,6 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from "@angular/router";
import { SharedModule } from "../../../../shared/shared.module";
import { ArtifactListPageComponent } from "./artifact-list-page/artifact-list-page.component";
import { ArtifactListComponent } from "./artifact-list-page/artifact-list/artifact-list.component";
import { ArtifactListTabComponent } from "./artifact-list-page/artifact-list/artifact-list-tab/artifact-list-tab.component";
import { ArtifactSummaryComponent } from "./artifact-summary.component";
import { ArtifactTagComponent } from "./artifact-tag/artifact-tag.component";
@ -19,25 +18,45 @@ import { ResultTipComponent } from "./vulnerability-scanning/result-tip.componen
import { ResultBarChartComponent } from "./vulnerability-scanning/result-bar-chart.component";
import { ResultTipHistogramComponent } from "./vulnerability-scanning/result-tip-histogram/result-tip-histogram.component";
import { HistogramChartComponent } from "./vulnerability-scanning/histogram-chart/histogram-chart.component";
import { ArtifactInfoComponent } from "./artifact-list-page/artifact-list/artifact-info/artifact-info.component";
import { SubAccessoriesComponent } from "./artifact-list-page/artifact-list/artifact-list-tab/sub-accessories/sub-accessories.component";
import { ArtifactListPageService } from "./artifact-list-page/artifact-list-page.service";
const routes: Routes = [
{
path: ':repo',
component: ArtifactListPageComponent,
children: [
{
path: 'info-tab',
component: ArtifactInfoComponent,
},
{
path: 'artifacts-tab',
component: ArtifactListTabComponent
},
{ path: '', redirectTo: 'artifacts-tab', pathMatch: 'full' },
]
},
{
path: ':repo/depth/:depth',
path: ':repo',
component: ArtifactListPageComponent,
children: [
{
path: 'artifacts-tab/depth/:depth',
component: ArtifactListTabComponent
}
]
},
{
path: ':repo/artifacts/:digest',
path: ':repo/artifacts-tab/artifacts/:digest',
component: ArtifactSummaryComponent,
resolve: {
artifactResolver: ArtifactDetailRoutingResolverService
}
},
{
path: ':repo/depth/:depth/artifacts/:digest',
path: ':repo/artifacts-tab/depth/:depth/artifacts/:digest',
component: ArtifactSummaryComponent,
resolve: {
artifactResolver: ArtifactDetailRoutingResolverService
@ -47,7 +66,6 @@ const routes: Routes = [
@NgModule({
declarations: [
ArtifactListPageComponent,
ArtifactListComponent,
ArtifactListTabComponent,
ArtifactSummaryComponent,
ArtifactTagComponent,
@ -61,14 +79,17 @@ const routes: Routes = [
ResultTipComponent,
ResultBarChartComponent,
ResultTipHistogramComponent,
HistogramChartComponent
HistogramChartComponent,
ArtifactInfoComponent,
SubAccessoriesComponent
],
imports: [
RouterModule.forChild(routes),
SharedModule
],
providers: [
{provide: ArtifactService, useClass: ArtifactDefaultService }
ArtifactListPageService,
{provide: ArtifactService, useClass: ArtifactDefaultService },
]
})
export class ArtifactModule { }

View File

@ -17,7 +17,6 @@ import { Icon } from "ng-swagger-gen/models/icon";
export abstract class ArtifactService {
reference: string[];
triggerUploadArtifact = new Subject<string>();
TriggerArtifactChan$ = this.triggerUploadArtifact.asObservable();
abstract getIcon(digest: string): SafeUrl;
abstract setIcon(digest: string, url: SafeUrl);
abstract getIconsFromBackEnd(artifactList: Artifact[]);
@ -26,7 +25,6 @@ export abstract class ArtifactService {
export class ArtifactDefaultService extends ArtifactService {
triggerUploadArtifact = new Subject<string>();
TriggerArtifactChan$ = this.triggerUploadArtifact.asObservable();
private _iconMap: {[key: string]: SafeUrl} = {};
private _sharedIconObservableMap: {[key: string]: Observable<Icon>} = {};
constructor(private iconService: IconService,

View File

@ -1,72 +1,93 @@
import { Accessory } from "ng-swagger-gen/models/accessory";
import { Artifact } from "../../../../../../ng-swagger-gen/models/artifact";
import { Platform } from "../../../../../../ng-swagger-gen/models/platform";
export interface ArtifactFront extends Artifact {
platform?: Platform;
showImage?: string;
pullCommand?: string;
annotationsArray?: Array<{[key: string]: any}>;
tagNumber?: number;
platform?: Platform;
showImage?: string;
pullCommand?: string;
annotationsArray?: Array<{ [key: string]: any }>;
tagNumber?: number;
coSigned?: string;
accessoryNumber?: number;
}
export interface AccessoryFront extends Accessory {
pullCommand?: string;
tagNumber?: number;
scan_overview?: any;
}
export const mutipleFilter = [
{
filterBy: 'type',
filterByShowText: 'Type',
listItem: [
{
filterText: 'IMAGE',
showItem: 'ARTIFACT.IMAGE',
},
{
filterText: 'CHART',
showItem: 'ARTIFACT.CHART',
},
{
filterText: 'CNAB',
showItem: 'ARTIFACT.CNAB',
}
]
},
{
filterBy: 'tags',
filterByShowText: 'Tags',
listItem: [
{
filterText: '*',
showItem: 'ARTIFACT.TAGGED',
},
{
filterText: 'nil',
showItem: 'ARTIFACT.UNTAGGED',
},
{
filterText: '',
showItem: 'ARTIFACT.ALL',
}
]
},
{
filterBy: 'labels',
filterByShowText: 'Label',
listItem: []
},
];
export const artifactImages = [
'IMAGE', 'CHART', 'CNAB', 'OPENPOLICYAGENT'
];
export const artifactPullCommands = [
{
type: artifactImages[0],
pullCommand: 'docker pull'
},
{
type: artifactImages[1],
pullCommand: 'helm chart pull'
},
{
type: artifactImages[2],
pullCommand: 'cnab-to-oci pull'
}
];
export const artifactDefault = "images/artifact-default.svg";
{
filterBy: 'type',
filterByShowText: 'Type',
listItem: [
{
filterText: 'IMAGE',
showItem: 'ARTIFACT.IMAGE',
},
{
filterText: 'CHART',
showItem: 'ARTIFACT.CHART',
},
{
filterText: 'CNAB',
showItem: 'ARTIFACT.CNAB',
}
]
},
{
filterBy: 'tags',
filterByShowText: 'Tags',
listItem: [
{
filterText: '*',
showItem: 'ARTIFACT.TAGGED',
},
{
filterText: 'nil',
showItem: 'ARTIFACT.UNTAGGED',
},
{
filterText: '',
showItem: 'ARTIFACT.ALL',
}
]
},
{
filterBy: 'labels',
filterByShowText: 'Label',
listItem: []
},
];
export enum AccessoryType {
COSIGN = 'signature.cosign'
}
export const artifactImages = [
'IMAGE', 'CHART', 'CNAB', 'OPENPOLICYAGENT'
];
export const artifactPullCommands = [
{
type: artifactImages[0],
pullCommand: 'docker pull'
},
{
type: AccessoryType.COSIGN,
pullCommand: 'docker pull'
},
{
type: artifactImages[1],
pullCommand: 'helm chart pull'
},
{
type: artifactImages[2],
pullCommand: 'cnab-to-oci pull'
}
];
export const artifactDefault = "images/artifact-default.svg";

View File

@ -48,7 +48,9 @@ export const enum ConfirmationTargets {
P2P_PROVIDER_DELETE,
PROJECT_ROBOT_ACCOUNT,
PROJECT_ROBOT_ACCOUNT_ENABLE_OR_DISABLE,
WEBHOOK
WEBHOOK,
ACCESSORY,
ALL_ACCESSORIES
}
export const enum ActionType {

View File

@ -1705,5 +1705,21 @@
"LIST": "List",
"REPOSITORY": "Repository",
"HELM_LABEL": "Helm Chart Label"
},
"ACCESSORY": {
"DELETION_TITLE_ACCESSORY": "Confirm Accessory Deletion",
"DELETION_SUMMARY_ACCESSORY": "Do you want to delete all the accessories of the artifact {{param}}?",
"DELETION_SUMMARY_ONE_ACCESSORY": "Do you want to delete the accessory(s) {{param}}?",
"DELETE_ACCESSORY": "Delete Accessory",
"DELETED_SUCCESS": "Accessory deleted successfully",
"DELETED_FAILED": "Deleting accessory failed",
"CO_SIGNED": "Co-signed",
"NOTARY_SIGNED": "Notary signed",
"ACCESSORY": "Accessory",
"ACCESSORIES": "Accessories",
"SUBJECT_ARTIFACT": "Subject Artifact",
"CO_SIGN": "Co-sign",
"NOTARY": "Notary",
"PLACEHOLDER": "We couldn't find any accessories!"
}
}

View File

@ -1705,5 +1705,21 @@
"LIST": "List",
"REPOSITORY": "Repository",
"HELM_LABEL": "Helm Chart label"
},
"ACCESSORY": {
"DELETION_TITLE_ACCESSORY": "Confirm Accessory Deletion",
"DELETION_SUMMARY_ACCESSORY": "Do you want to delete all the accessories of the artifact {{param}}?",
"DELETION_SUMMARY_ONE_ACCESSORY": "Do you want to delete the accessory(s) {{param}}?",
"DELETE_ACCESSORY": "Delete Accessory",
"DELETED_SUCCESS": "Accessory deleted successfully",
"DELETED_FAILED": "Deleting accessory failed",
"CO_SIGNED": "Co-signed",
"NOTARY_SIGNED": "Notary signed",
"ACCESSORY": "Accessory",
"ACCESSORIES": "Accessories",
"SUBJECT_ARTIFACT": "Subject Artifact",
"CO_SIGN": "Co-sign",
"NOTARY": "Notary",
"PLACEHOLDER": "We couldn't find any accessories!"
}
}

View File

@ -1704,5 +1704,21 @@
"LIST": "List",
"REPOSITORY": "Repository",
"HELM_LABEL": "Helm Chart label"
},
"ACCESSORY": {
"DELETION_TITLE_ACCESSORY": "Confirm Accessory Deletion",
"DELETION_SUMMARY_ACCESSORY": "Do you want to delete all the accessories of the artifact {{param}}?",
"DELETION_SUMMARY_ONE_ACCESSORY": "Do you want to delete the accessory(s) {{param}}?",
"DELETE_ACCESSORY": "Delete Accessory",
"DELETED_SUCCESS": "Accessory deleted successfully",
"DELETED_FAILED": "Deleting accessory failed",
"CO_SIGNED": "Co-signed",
"NOTARY_SIGNED": "Notary signed",
"ACCESSORY": "Accessory",
"ACCESSORIES": "Accessories",
"SUBJECT_ARTIFACT": "Subject Artifact",
"CO_SIGN": "Co-sign",
"NOTARY": "Notary",
"PLACEHOLDER": "We couldn't find any accessories!"
}
}

View File

@ -1673,5 +1673,21 @@
"LIST": "List",
"REPOSITORY": "Repository",
"HELM_LABEL": "Helm Chart label"
},
"ACCESSORY": {
"DELETION_TITLE_ACCESSORY": "Confirm Accessory Deletion",
"DELETION_SUMMARY_ACCESSORY": "Do you want to delete all the accessories of the artifact {{param}}?",
"DELETION_SUMMARY_ONE_ACCESSORY": "Do you want to delete the accessory(s) {{param}}?",
"DELETE_ACCESSORY": "Delete Accessory",
"DELETED_SUCCESS": "Accessory deleted successfully",
"DELETED_FAILED": "Deleting accessory failed",
"CO_SIGNED": "Co-signed",
"NOTARY_SIGNED": "Notary signed",
"ACCESSORY": "Accessory",
"ACCESSORIES": "Accessories",
"SUBJECT_ARTIFACT": "Subject Artifact",
"CO_SIGN": "Co-sign",
"NOTARY": "Notary",
"PLACEHOLDER": "We couldn't find any accessories!"
}
}

View File

@ -1701,5 +1701,21 @@
"LIST": "Listar",
"REPOSITORY": "Repositório",
"HELM_LABEL": "Marcador do Helm Chart"
},
"ACCESSORY": {
"DELETION_TITLE_ACCESSORY": "Confirm Accessory Deletion",
"DELETION_SUMMARY_ACCESSORY": "Do you want to delete all the accessories of the artifact {{param}}?",
"DELETION_SUMMARY_ONE_ACCESSORY": "Do you want to delete the accessory(s) {{param}}?",
"DELETE_ACCESSORY": "Delete Accessory",
"DELETED_SUCCESS": "Accessory deleted successfully",
"DELETED_FAILED": "Deleting accessory failed",
"CO_SIGNED": "Co-signed",
"NOTARY_SIGNED": "Notary signed",
"ACCESSORY": "Accessory",
"ACCESSORIES": "Accessories",
"SUBJECT_ARTIFACT": "Subject Artifact",
"CO_SIGN": "Co-sign",
"NOTARY": "Notary",
"PLACEHOLDER": "We couldn't find any accessories!"
}
}

View File

@ -1705,5 +1705,21 @@
"LIST": "List",
"REPOSITORY": "Repository",
"HELM_LABEL": "Helm Chart label"
},
"ACCESSORY": {
"DELETION_TITLE_ACCESSORY": "Confirm Accessory Deletion",
"DELETION_SUMMARY_ACCESSORY": "Do you want to delete all the accessories of the artifact {{param}}?",
"DELETION_SUMMARY_ONE_ACCESSORY": "Do you want to delete the accessory(s) {{param}}?",
"DELETE_ACCESSORY": "Delete Accessory",
"DELETED_SUCCESS": "Accessory deleted successfully",
"DELETED_FAILED": "Deleting accessory failed",
"CO_SIGNED": "Co-signed",
"NOTARY_SIGNED": "Notary signed",
"ACCESSORY": "Accessory",
"ACCESSORIES": "Accessories",
"SUBJECT_ARTIFACT": "Subject Artifact",
"CO_SIGN": "Co-sign",
"NOTARY": "Notary",
"PLACEHOLDER": "We couldn't find any accessories!"
}
}

View File

@ -129,13 +129,13 @@
"ADMIN_RENAME_TIP": "单击将用户名改为 \"admin@harbor.local\", 注意这个操作是无法撤销的",
"RENAME_SUCCESS": "用户名更改成功!",
"ADMIN_RENAME_BUTTON": "更改用户名",
"RENAME_CONFIRM_INFO": "更改用户名为admin@harbor.local是无法撤销的, 确定更改吗?",
"RENAME_CONFIRM_INFO": "更改用户名为admin@harbor.local是无法撤销的, 确定更改吗?",
"CLI_PASSWORD": "CLI密码",
"CLI_PASSWORD_TIP": "使用docker/helm cli访问Harbor时可以使用此cli密码作为密码。",
"COPY_SUCCESS": "复制成功",
"COPY_ERROR": "复制失败",
"ADMIN_CLI_SECRET_BUTTON": "生成新的CLI密码",
"ADMIN_CLI_SECRET_RESET_BUTTON": "输入自己的CLI密码",
"ADMIN_CLI_SECRET_RESET_BUTTON": "输入自己的CLI密码",
"NEW_SECRET": "密码",
"CONFIRM_SECRET": "重复出入密码",
"GENERATE_SUCCESS": "成功设置新的CLI密码",
@ -200,7 +200,7 @@
"ADD_USER_TITLE": "创建用户",
"SAVE_SUCCESS": "成功创建用户。",
"DELETION_TITLE": "删除用户确认",
"DELETION_SUMMARY": "确认删除用户 {{param}}?",
"DELETION_SUMMARY": "确认删除用户 {{param}}?",
"DELETE_SUCCESS": "成功删除用户。",
"OF": "共计",
"ITEMS": "条记录",
@ -236,7 +236,7 @@
"OF": "共计",
"ITEMS": "条记录",
"DELETION_TITLE": "移除项目成员确认",
"DELETION_SUMMARY": "确认删除项目 {{param}}",
"DELETION_SUMMARY": "确认删除项目 {{param}}",
"FILTER_PLACEHOLDER": "过滤项目",
"REPLICATION_RULE": "复制规则",
"CREATED_SUCCESS": "成功创建项目。",
@ -248,7 +248,7 @@
"STORAGE_QUOTA": "存储容量",
"COUNT_QUOTA_TIP": "请输入一个'1' ~ '100000000'之间的整数, '-1'表示不设置上限。",
"STORAGE_QUOTA_TIP": "存储配额的上限仅采用整数值上限为1024TB。输入“-1”作为无限制配额。",
"QUOTA_UNLIMIT_TIP": "如果想要对存储不设置上限,请输入-1。",
"QUOTA_UNLIMIT_TIP": "如果想要对存储不设置上限,请输入-1。",
"TYPE": "类型",
"PROXY_CACHE": "镜像代理",
"PROXY_CACHE_TOOLTIP": "开启此项,以使得该项目成为目标仓库的镜像代理.仅支持 DockerHub, Docker Registry, Harbor, Aws ECR, Azure ACR, Quay 和 Google GCR 类型的仓库",
@ -325,13 +325,13 @@
"UNKNOWN_ERROR": "添加成员时发生未知错误",
"FILTER_PLACEHOLDER": "过滤成员",
"DELETION_TITLE": "删除项目成员确认",
"DELETION_SUMMARY": "确认删除项目成员 {{param}}?",
"DELETION_SUMMARY": "确认删除项目成员 {{param}}?",
"ADDED_SUCCESS": "成功新增成员",
"DELETED_SUCCESS": "成功删除成员",
"SWITCHED_SUCCESS": "切换角色成功",
"OF": "共计",
"SWITCH_TITLE": "切换项目成员确认",
"SWITCH_SUMMARY": "确认切换项目成员 {{param}}??",
"SWITCH_SUMMARY": "确认切换项目成员 {{param}}??",
"SET_ROLE": "设置角色",
"REMOVE": "移除成员",
"GROUP_NAME_REQUIRED": "组名称为必填项",
@ -368,7 +368,7 @@
"CREATED_SUCCESS": "创建账户 '{{param}}' 成功。",
"COPY_SUCCESS": "成功复制 '{{param}}' 的令牌",
"DELETION_TITLE": "删除账户确认",
"DELETION_SUMMARY": "确认删除机器人账户 {{param}}?",
"DELETION_SUMMARY": "确认删除机器人账户 {{param}}?",
"PULL_IS_MUST": "拉取权限默认选中且不可修改。",
"EXPORT_TO_FILE": "导出到文件中",
"EXPIRES_AT": "到期日",
@ -1219,7 +1219,7 @@
"UNKNOWN_ERROR": "发生未知错误,请稍后再试。",
"UNAUTHORIZED_ERROR": "会话无效或者已经过期, 请重新登录以继续。",
"REPO_READ_ONLY": "Harbor 被设置为只读模式在此模式下不能删除仓库、artifact、 Tag 及推送镜像。",
"FORBIDDEN_ERROR": "当前操作被禁止,请确认有合法的权限。",
"FORBIDDEN_ERROR": "当前操作被禁止,请确认有合法的权限。",
"GENERAL_ERROR": "调用后台服务时出现错误: {{param}}。",
"BAD_REQUEST_ERROR": "错误请求, 操作无法完成。",
"NOT_FOUND_ERROR": "对象不存在, 请求无法完成。",
@ -1504,11 +1504,11 @@
"SETUP_TIMESTAMP": "创建时间",
"PROVIDER": "供应商",
"DELETION_TITLE": "删除实例",
"DELETION_SUMMARY": "确认删除实例 {{param}}?",
"DELETION_SUMMARY": "确认删除实例 {{param}}?",
"ENABLE_TITLE": "启用实例",
"ENABLE_SUMMARY": "确认启用实例 {{param}}?",
"ENABLE_SUMMARY": "确认启用实例 {{param}}?",
"DISABLE_TITLE": "禁用实例",
"DISABLE_SUMMARY": "确认禁用实例 {{param}}?",
"DISABLE_SUMMARY": "确认禁用实例 {{param}}?",
"IMAGE": "镜像",
"START_TIME": "开始时间",
"FINISH_TIME": "完成时间",
@ -1645,7 +1645,7 @@
"ENABLE_TITLE": "启用机器人",
"ENABLE_SUMMARY": "您想启用机器人 {{param}}?",
"DISABLE_TITLE": "禁用机器人",
"DISABLE_SUMMARY": "想禁用机器人 {{param}}?",
"DISABLE_SUMMARY": "想禁用机器人 {{param}}?",
"ENABLE_ROBOT_SUCCESSFULLY": "启用机器人成功",
"DISABLE_ROBOT_SUCCESSFULLY": "禁用机器人成功",
"ROBOT_ACCOUNT": "机器人账户",
@ -1703,5 +1703,21 @@
"LIST": "查询",
"REPOSITORY": "仓库",
"HELM_LABEL": "Helm Chart 标签"
},
"ACCESSORY": {
"DELETION_TITLE_ACCESSORY": "删除附件确认",
"DELETION_SUMMARY_ACCESSORY": "您确定要删除 Artifact {{param}} 的所有附件吗?",
"DELETION_SUMMARY_ONE_ACCESSORY": "您确定要删除附件 {{param}} ",
"DELETE_ACCESSORY": "删除附件",
"DELETED_SUCCESS": "删除附件成功",
"DELETED_FAILED": "删除附件失败",
"CO_SIGNED": "Co-sign 签名",
"NOTARY_SIGNED": "Notary 签名",
"ACCESSORY": "附件",
"ACCESSORIES": "附件",
"SUBJECT_ARTIFACT": "主体 Artifact",
"CO_SIGN": "Co-sign",
"NOTARY": "Notary",
"PLACEHOLDER": "未发现任何附件!"
}
}

View File

@ -1690,5 +1690,21 @@
"LIST": "List",
"REPOSITORY": "Repository",
"HELM_LABEL": "Helm Chart label"
},
"ACCESSORY": {
"DELETION_TITLE_ACCESSORY": "Confirm Accessory Deletion",
"DELETION_SUMMARY_ACCESSORY": "Do you want to delete all the accessories of the artifact {{param}}?",
"DELETION_SUMMARY_ONE_ACCESSORY": "Do you want to delete the accessory(s) {{param}}?",
"DELETE_ACCESSORY": "Delete Accessory",
"DELETED_SUCCESS": "Accessory deleted successfully",
"DELETED_FAILED": "Deleting accessory failed",
"CO_SIGNED": "Co-signed",
"NOTARY_SIGNED": "Notary signed",
"ACCESSORY": "Accessory",
"ACCESSORIES": "Accessories",
"SUBJECT_ARTIFACT": "Subject Artifact",
"CO_SIGN": "Co-sign",
"NOTARY": "Notary",
"PLACEHOLDER": "We couldn't find any accessories!"
}
}