diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list-page.component.html b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list-page.component.html index 046a9c16d..b994abfc7 100644 --- a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list-page.component.html +++ b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list-page.component.html @@ -1,5 +1,5 @@
- diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list-page.component.spec.ts b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list-page.component.spec.ts index 175086d35..4d7311124 100644 --- a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list-page.component.spec.ts +++ b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list-page.component.spec.ts @@ -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; - 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); + }); }); diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list-page.component.ts b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list-page.component.ts index 70d4dc941..081ef1d55 100644 --- a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list-page.component.ts +++ b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list-page.component.ts @@ -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 = (resolverData['projectResolver']).name; - this.hasProjectAdminRole = (resolverData['projectResolver']).has_project_admin_role; - this.isGuest = (resolverData['projectResolver']).current_user_role_id === 3; - this.projectMemberRoleId = (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]); } diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list-page.service.spec.ts b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list-page.service.spec.ts new file mode 100644 index 000000000..64591f23a --- /dev/null +++ b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list-page.service.spec.ts @@ -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(); + })); +}); diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list-page.service.ts b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list-page.service.ts new file mode 100644 index 000000000..2d9d74ba8 --- /dev/null +++ b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list-page.service.ts @@ -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[] = []; + 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[] = []; + 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[], 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) => { + 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)); + } +} diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-info/artifact-info.component.html b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-info/artifact-info.component.html new file mode 100644 index 000000000..88397378f --- /dev/null +++ b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-info/artifact-info.component.html @@ -0,0 +1,35 @@ +
+
+
+ + + + + {{ 'REPOSITORY.MARKDOWN' | translate }} +
+
+
+ +
+ +
+

{{'REPOSITORY.NO_INFO' | translate }}

+

+
+
+
+
+
+
+ +
+
+ + +
+ +
+
diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list.component.scss b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-info/artifact-info.component.scss similarity index 52% rename from src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list.component.scss rename to src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-info/artifact-info.component.scss index f7e1e5930..05340dd42 100644 --- a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list.component.scss +++ b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-info/artifact-info.component.scss @@ -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; -} \ No newline at end of file diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-info/artifact-info.component.spec.ts b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-info/artifact-info.component.spec.ts new file mode 100644 index 000000000..5e0d51267 --- /dev/null +++ b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-info/artifact-info.component.spec.ts @@ -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; + 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(); + }); +}); diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-info/artifact-info.component.ts b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-info/artifact-info.component.ts new file mode 100644 index 000000000..349769964 --- /dev/null +++ b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-info/artifact-info.component.ts @@ -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 = (resolverData['projectResolver']).name; + this.hasProjectAdminRole = (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(); + } + } +} diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/artifact-list-tab.component.html b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/artifact-list-tab.component.html index fa1db74e8..e7459f489 100644 --- a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/artifact-list-tab.component.html +++ b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/artifact-list-tab.component.html @@ -32,7 +32,7 @@
+ [style.left.px]='110'>
@@ -59,19 +59,19 @@
-
+
× -
-
{{'LABEL.NO_LABELS' | translate }} +
{{'LABEL.NO_LABELS' | translate }}
-
- @@ -87,7 +87,7 @@
-
- + + + + + + +
+ {{a.type}} +
+
+ +
+ {{size(a.size)}} +
+
+ +
+ {{a.creation_time | harborDatetime: 'short'}} +
+
+ + + + {{pagination.firstItem + 1}} + - + {{pagination.lastItem +1 }} {{'ROBOT_ACCOUNT.OF' | + translate}} + {{total}} {{'ROBOT_ACCOUNT.ITEMS' | translate}} + + + diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/sub-accessories/sub-accessories.component.scss b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/sub-accessories/sub-accessories.component.scss new file mode 100644 index 000000000..a89d05dde --- /dev/null +++ b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/sub-accessories/sub-accessories.component.scss @@ -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; +} diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/sub-accessories/sub-accessories.component.spec.ts b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/sub-accessories/sub-accessories.component.spec.ts new file mode 100644 index 000000000..7776fbdef --- /dev/null +++ b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/sub-accessories/sub-accessories.component.spec.ts @@ -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; + 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); + }); + +}); diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/sub-accessories/sub-accessories.component.ts b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/sub-accessories/sub-accessories.component.ts new file mode 100644 index 000000000..fecceddc0 --- /dev/null +++ b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list-tab/sub-accessories/sub-accessories.component.ts @@ -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 = new EventEmitter(); + 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); + } + ); + } +} diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list.component.html b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list.component.html deleted file mode 100644 index dcc33942b..000000000 --- a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list.component.html +++ /dev/null @@ -1,64 +0,0 @@ -
-
-
- -
-
-

{{showCurrentTitle}}

-

{{artifactDigest | slice:0:15}}

-
-
-
- -
-
- - -
-
-
- - - - - {{ 'REPOSITORY.MARKDOWN' | translate }} -
-
-
-

{{'REPOSITORY.NO_INFO' | translate }}

-

-
-
-
-
-
- -
-
- - -
- -
-
-
-
- -
-
-
-
diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list.component.spec.ts b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list.component.spec.ts deleted file mode 100644 index baf22e12c..000000000 --- a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list.component.spec.ts +++ /dev/null @@ -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; - 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(); - }); -}); diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list.component.ts b/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list.component.ts deleted file mode 100644 index 73b87b2f1..000000000 --- a/src/portal/src/app/base/project/repository/artifact/artifact-list-page/artifact-list/artifact-list.component.ts +++ /dev/null @@ -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(); - @Output() backEvt: EventEmitter = new EventEmitter(); - @Output() putArtifactReferenceArr: EventEmitter = 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 = 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(); - } - } -} diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-summary.component.html b/src/portal/src/app/base/project/repository/artifact/artifact-summary.component.html index 510e5971c..578c5a581 100644 --- a/src/portal/src/app/base/project/repository/artifact/artifact-summary.component.html +++ b/src/portal/src/app/base/project/repository/artifact/artifact-summary.component.html @@ -1,4 +1,4 @@ -
+
< {{'SIDE_NAV.PROJECTS'| translate}} < @@ -11,9 +11,6 @@
-
- -

diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-summary.component.ts b/src/portal/src/app/base/project/repository/artifact/artifact-summary.component.ts index 212402975..60435f9e1 100644 --- a/src/portal/src/app/base/project/repository/artifact/artifact-summary.component.ts +++ b/src/portal/src/app/base/project/repository/artifact/artifact-summary.component.ts @@ -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]); } diff --git a/src/portal/src/app/base/project/repository/artifact/artifact-tag/artifact-tag.component.html b/src/portal/src/app/base/project/repository/artifact/artifact-tag/artifact-tag.component.html index 15e6fb4e1..5cdb3970b 100644 --- a/src/portal/src/app/base/project/repository/artifact/artifact-tag/artifact-tag.component.html +++ b/src/portal/src/app/base/project/repository/artifact/artifact-tag/artifact-tag.component.html @@ -42,7 +42,7 @@ {{'TAG.NAME' | translate}} {{'REPOSITORY.PULL_COMMAND' | translate}} - {{'REPOSITORY.SIGNED' | translate}} + {{'ACCESSORY.NOTARY_SIGNED' | translate}} {{'TAG.PULL_TIME' | translate}} {{'TAG.PUSH_TIME' | translate}} diff --git a/src/portal/src/app/base/project/repository/artifact/artifact.module.ts b/src/portal/src/app/base/project/repository/artifact/artifact.module.ts index d08eafc1c..53479f253 100644 --- a/src/portal/src/app/base/project/repository/artifact/artifact.module.ts +++ b/src/portal/src/app/base/project/repository/artifact/artifact.module.ts @@ -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 { } diff --git a/src/portal/src/app/base/project/repository/artifact/artifact.service.ts b/src/portal/src/app/base/project/repository/artifact/artifact.service.ts index b4fc1c98d..5357cf424 100644 --- a/src/portal/src/app/base/project/repository/artifact/artifact.service.ts +++ b/src/portal/src/app/base/project/repository/artifact/artifact.service.ts @@ -17,7 +17,6 @@ import { Icon } from "ng-swagger-gen/models/icon"; export abstract class ArtifactService { reference: string[]; triggerUploadArtifact = new Subject(); - 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(); - TriggerArtifactChan$ = this.triggerUploadArtifact.asObservable(); private _iconMap: {[key: string]: SafeUrl} = {}; private _sharedIconObservableMap: {[key: string]: Observable} = {}; constructor(private iconService: IconService, diff --git a/src/portal/src/app/base/project/repository/artifact/artifact.ts b/src/portal/src/app/base/project/repository/artifact/artifact.ts index 78f4b6bfa..b7e7c8908 100644 --- a/src/portal/src/app/base/project/repository/artifact/artifact.ts +++ b/src/portal/src/app/base/project/repository/artifact/artifact.ts @@ -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"; + + + diff --git a/src/portal/src/app/shared/entities/shared.const.ts b/src/portal/src/app/shared/entities/shared.const.ts index ebfae035a..86825fa22 100644 --- a/src/portal/src/app/shared/entities/shared.const.ts +++ b/src/portal/src/app/shared/entities/shared.const.ts @@ -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 { diff --git a/src/portal/src/i18n/lang/de-de-lang.json b/src/portal/src/i18n/lang/de-de-lang.json index 507a199d1..f61d46fd2 100644 --- a/src/portal/src/i18n/lang/de-de-lang.json +++ b/src/portal/src/i18n/lang/de-de-lang.json @@ -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!" } } diff --git a/src/portal/src/i18n/lang/en-us-lang.json b/src/portal/src/i18n/lang/en-us-lang.json index a00c3fb62..9ccfcb488 100644 --- a/src/portal/src/i18n/lang/en-us-lang.json +++ b/src/portal/src/i18n/lang/en-us-lang.json @@ -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!" } } diff --git a/src/portal/src/i18n/lang/es-es-lang.json b/src/portal/src/i18n/lang/es-es-lang.json index f35eae274..028341de6 100644 --- a/src/portal/src/i18n/lang/es-es-lang.json +++ b/src/portal/src/i18n/lang/es-es-lang.json @@ -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!" } } diff --git a/src/portal/src/i18n/lang/fr-fr-lang.json b/src/portal/src/i18n/lang/fr-fr-lang.json index 5d9c4021d..9969a5398 100644 --- a/src/portal/src/i18n/lang/fr-fr-lang.json +++ b/src/portal/src/i18n/lang/fr-fr-lang.json @@ -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!" } } diff --git a/src/portal/src/i18n/lang/pt-br-lang.json b/src/portal/src/i18n/lang/pt-br-lang.json index 8d31450cb..873f17bd0 100644 --- a/src/portal/src/i18n/lang/pt-br-lang.json +++ b/src/portal/src/i18n/lang/pt-br-lang.json @@ -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!" } } diff --git a/src/portal/src/i18n/lang/tr-tr-lang.json b/src/portal/src/i18n/lang/tr-tr-lang.json index 72f996393..89e3e9d47 100644 --- a/src/portal/src/i18n/lang/tr-tr-lang.json +++ b/src/portal/src/i18n/lang/tr-tr-lang.json @@ -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!" } } diff --git a/src/portal/src/i18n/lang/zh-cn-lang.json b/src/portal/src/i18n/lang/zh-cn-lang.json index 2e171df76..e6b6e2495 100644 --- a/src/portal/src/i18n/lang/zh-cn-lang.json +++ b/src/portal/src/i18n/lang/zh-cn-lang.json @@ -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": "未发现任何附件!" } } diff --git a/src/portal/src/i18n/lang/zh-tw-lang.json b/src/portal/src/i18n/lang/zh-tw-lang.json index 3a568f5b8..387326880 100644 --- a/src/portal/src/i18n/lang/zh-tw-lang.json +++ b/src/portal/src/i18n/lang/zh-tw-lang.json @@ -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!" } }