Oci ui include artifact list and artifact summary

Signed-off-by: Yogi_Wang <yawang@vmware.com>

Signed-off-by: AllForNothing <sshijun@vmware.com>
Signed-off-by: Yogi_Wang <yawang@vmware.com>
This commit is contained in:
Yogi_Wang 2020-02-13 15:39:29 +08:00
parent e03e3e3354
commit 2553ee3831
104 changed files with 3930 additions and 1671 deletions

View File

@ -18,6 +18,7 @@ import { SystemAdminGuard } from './shared/route/system-admin-activate.service';
import { AuthCheckGuard } from './shared/route/auth-user-activate.service';
import { SignInGuard } from './shared/route/sign-in-guard-activate.service';
import { MemberGuard } from './shared/route/member-guard-activate.service';
import { ArtifactGuard } from './shared/route/artifact-guard-activate.service';
import { MemberPermissionGuard } from './shared/route/member-permission-guard-activate.service';
import { OidcGuard } from './shared/route/oidc-guard-active.service';
@ -42,8 +43,8 @@ import { AuditLogComponent } from './log/audit-log.component';
import { LogPageComponent } from './log/log-page.component';
import { RepositoryPageComponent } from './repository/repository-page.component';
import { TagRepositoryComponent } from './repository/tag-repository/tag-repository.component';
import { TagDetailPageComponent } from './repository/tag-detail/tag-detail-page.component';
import { ArtifactListPageComponent } from './repository/artifact-list-page/artifact-list-page.component';
import { ArtifactSummaryPageComponent } from './repository/artifact-summary-page/artifact-summary-page.component';
import { LeavingRepositoryRouteDeactivate } from './shared/route/leaving-repository-deactivate.service';
import { ProjectComponent } from './project/project.component';
@ -71,6 +72,7 @@ import { LabelsComponent } from "./labels/labels.component";
import { ProjectQuotasComponent } from "./project-quotas/project-quotas.component";
import { VulnerabilityConfigComponent } from "../lib/components/config/vulnerability/vulnerability-config.component";
import { USERSTATICPERMISSION } from "../lib/services";
import { LeavingArtifactSummaryRouteDeactivate } from './shared/route/leaving-artifact-summary-deactivate.service';
const harborRoutes: Routes = [
@ -169,7 +171,7 @@ const harborRoutes: Routes = [
},
{
path: 'tags/:id/:repo',
component: TagRepositoryComponent,
component: ArtifactListPageComponent,
canActivate: [MemberGuard],
resolve: {
projectResolver: ProjectRoutingResolver
@ -177,17 +179,27 @@ const harborRoutes: Routes = [
},
{
path: 'projects/:id/repositories/:repo',
component: TagRepositoryComponent,
component: ArtifactListPageComponent,
canActivate: [MemberGuard],
canDeactivate: [LeavingRepositoryRouteDeactivate],
resolve: {
projectResolver: ProjectRoutingResolver
}
},
},
{
path: 'projects/:id/repositories/:repo/tags/:tag',
component: TagDetailPageComponent,
path: 'projects/:id/repositories/:repo/depth/:depth',
component: ArtifactListPageComponent,
canActivate: [MemberGuard],
canDeactivate: [LeavingRepositoryRouteDeactivate],
resolve: {
projectResolver: ProjectRoutingResolver
},
},
{
path: 'projects/:id/repositories/:repo/artifacts/:digest',
component: ArtifactSummaryPageComponent,
canActivate: [MemberGuard, ArtifactGuard],
canDeactivate: [LeavingArtifactSummaryRouteDeactivate],
resolve: {
projectResolver: ProjectRoutingResolver
}
@ -258,7 +270,7 @@ const harborRoutes: Routes = [
action: USERSTATICPERMISSION.REPOSITORY.VALUE.LIST
}
},
component: TagRepositoryComponent
component: ArtifactListPageComponent
},
{
path: 'members',

View File

@ -5,6 +5,7 @@ import { debounceTime } from 'rxjs/operators';
import { HelmChartVersion } from '../helm-chart.interface.service';
import { ResourceType } from '../../../shared/shared.const';
import { Label, Tag } from "../../../../lib/services";
import { Artifact } from '../../../../lib/components/artifact/artifact';
@Component({
selector: "hbr-chart-version-label-filter",
@ -46,7 +47,7 @@ export class LabelFilterComponent implements ClrDatagridFilterInterface<any>, On
if (this.resourceType === ResourceType.CHART_VERSION) {
return (cv as HelmChartVersion).labels.some(label => this.selectedLabels.get(label.id));
} else if (this.resourceType === ResourceType.REPOSITORY_TAG) {
return (cv as Tag).labels.some(label => this.selectedLabels.get(label.id));
return (cv as Artifact).labels.some(label => this.selectedLabels.get(label.id));
} else {
return true;
}

View File

@ -0,0 +1,15 @@
<div>
<div class="breadcrumb" *ngIf="!withAdmiral">
<a (click)="goProBack()">{{'SIDE_NAV.PROJECTS'| translate}}</a>
<span class="back-icon"><</span>
<a (click)="watchGoBackEvt(projectId)">{{'REPOSITORY.REPOSITORIES'| translate}}</a>
<span *ngIf="referArtifactNameArray.length===1">&lt;<a (click)="backInitRepo()">{{repoName}}</a></span>
<span *ngIf="referArtifactNameArray.length>=2" >
<span *ngFor="let digest of referArtifactNameArray;let i = index">
&lt;<a (click)="jumpDigest(referArtifactNameArray,i)" >{{digest | slice:0:15}}</a></span>
</span>
</div>
<artifact-list [repoName]="repoName" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole"
[projectId]="projectId" [memberRoleID]="projectMemberRoleId" [isGuest]="isGuest"
(tagClickEvent)="watchTagClickEvt($event)" (backEvt)="watchGoBackEvt($event)" (putArtifactReferenceArr)="putArtifactReferenceArr($event)"></artifact-list>
</div>

View File

@ -1,6 +1,6 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { TagRepositoryComponent } from './tag-repository.component';
import { ArtifactListPageComponent } from './artifact-list-page.component';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ClarityModule } from '@clr/angular';
@ -12,9 +12,10 @@ import { ActivatedRoute, Router } from '@angular/router';
import { AppConfigService } from '../../app-config.service';
import { SessionService } from '../../shared/session.service';
describe('TagRepositoryComponent', () => {
let component: TagRepositoryComponent;
let fixture: ComponentFixture<TagRepositoryComponent>;
import { ArtifactService } from '../../../lib/services';
describe('ArtifactListPageComponent', () => {
let component: ArtifactListPageComponent;
let fixture: ComponentFixture<ArtifactListPageComponent>;
const mockSessionService = {
getCurrentUser: () => { }
};
@ -33,6 +34,11 @@ describe('TagRepositoryComponent', () => {
const mockRouter = {
navigate: () => { }
};
const mockArtifactService = {
triggerUploadArtifact: {
next: () => {}
}
};
const mockActivatedRoute = {
RouterparamMap: of({ get: (key) => 'value' }),
snapshot: {
@ -69,20 +75,21 @@ describe('TagRepositoryComponent', () => {
NoopAnimationsModule,
HttpClientTestingModule
],
declarations: [TagRepositoryComponent],
declarations: [ArtifactListPageComponent],
providers: [
TranslateService,
{ provide: SessionService, useValue: mockSessionService },
{ provide: AppConfigService, useValue: mockAppConfigService },
{ provide: Router, useValue: mockRouter },
{ provide: ActivatedRoute, useValue: mockActivatedRoute },
{ provide: ArtifactService, useValue: mockArtifactService },
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TagRepositoryComponent);
fixture = TestBed.createComponent(ArtifactListPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -16,29 +16,32 @@ import { ActivatedRoute, Router } from '@angular/router';
import { AppConfigService } from '../../app-config.service';
import { SessionService } from '../../shared/session.service';
import { Project } from '../../project/project';
import { RepositoryComponent } from "../../../lib/components/repository/repository.component";
import { TagClickEvent } from "../../../lib/services";
import { ArtifactListComponent } from "../../../lib/components/artifact-list/artifact-list.component";
import { ArtifactClickEvent, ArtifactService } from "../../../lib/services";
import { clone } from '../../../lib/utils/utils';
@Component({
selector: 'tag-repository',
templateUrl: 'tag-repository.component.html',
styleUrls: ['./tag-repository.component.scss']
selector: 'artifact-list-page',
templateUrl: 'artifact-list-page.component.html',
styleUrls: ['./artifact-list-page.component.scss']
})
export class TagRepositoryComponent implements OnInit {
export class ArtifactListPageComponent implements OnInit {
projectId: number;
projectMemberRoleId: number;
repoName: string;
referArtifactNameArray: string[] = [];
hasProjectAdminRole: boolean = false;
isGuest: boolean;
registryUrl: string;
@ViewChild(RepositoryComponent, {static: false})
repositoryComponent: RepositoryComponent;
@ViewChild(ArtifactListComponent, {static: false})
repositoryComponent: ArtifactListComponent;
constructor(
private route: ActivatedRoute,
private router: Router,
private artifactService: ArtifactService,
private appConfigService: AppConfigService,
private session: SessionService) {
}
@ -57,8 +60,12 @@ export class TagRepositoryComponent implements OnInit {
this.projectMemberRoleId = (<Project>resolverData['projectResolver']).current_user_role_id;
}
this.repoName = this.route.snapshot.params['repo'];
this.registryUrl = this.appConfigService.getConfig().registry_url;
let refer = JSON.parse(sessionStorage.getItem('reference'));
if (refer && refer.projectId === this.projectId && refer.repo === this.repoName) {
this.referArtifactNameArray = refer.referArray;
}
}
get withNotary(): boolean {
@ -76,8 +83,13 @@ export class TagRepositoryComponent implements OnInit {
return this.repositoryComponent.hasChanges();
}
watchTagClickEvt(tagEvt: TagClickEvent): void {
let linkUrl = ['harbor', 'projects', tagEvt.project_id, 'repositories', tagEvt.repository_name, 'tags', tagEvt.tag_name];
watchTagClickEvt(artifactEvt: ArtifactClickEvent): void {
//
sessionStorage.setItem('referenceSummary', JSON.stringify({ projectId: this.projectId, repo: this.repoName,
"digest": artifactEvt.digest, referArray: this.referArtifactNameArray}));
let linkUrl = ['harbor', 'projects', artifactEvt.project_id, 'repositories'
, artifactEvt.repository_name, 'artifacts', artifactEvt.digest];
this.router.navigate(linkUrl);
}
@ -87,4 +99,26 @@ export class TagRepositoryComponent implements OnInit {
goProBack(): void {
this.router.navigate(["harbor", "projects"]);
}
backInitRepo() {
this.referArtifactNameArray = [];
sessionStorage.removeItem('reference');
this.updateArtifactList('repoName');
}
jumpDigest(referArtifactNameArray: string[], index: number) {
this.referArtifactNameArray = referArtifactNameArray.slice(index);
this.referArtifactNameArray.pop();
this.referArtifactNameArray = referArtifactNameArray.slice(index);
sessionStorage.setItem('reference', JSON.stringify({ projectId: this.projectId, repo: this.repoName,
referArray: referArtifactNameArray.slice(index)}));
this.updateArtifactList(referArtifactNameArray.slice(index));
}
updateArtifactList(res): void {
this.artifactService.triggerUploadArtifact.next(res);
}
putArtifactReferenceArr(digestArray) {
this.referArtifactNameArray = digestArray;
}
}

View File

@ -0,0 +1,17 @@
<div>
<div class="arrow-block" *ngIf="!withAdmiral">
<a (click)="goBackPro()">{{'SIDE_NAV.PROJECTS'| translate}}</a>
<span class="back-icon"><</span>
<a (click)="goBackRep()">{{'REPOSITORY.REPOSITORIES'| translate}}</a>
<span class="back-icon"><</span>
<a (click)="goBack(repositoryName)">{{'REPOSITORY.ARTIFACTS'| translate}}</a>
<span *ngFor="let digest of referArtifactNameArray;let i = index">
&lt;<a (click)="jumpDigest(referArtifactNameArray,i)" >{{digest | slice:0:15}}</a></span>
</div>
<artifact-summary (backEvt)="goBack($event)"
[artifactDigest]="artifactDigest"
[withAdmiral]="withAdmiral"
[projectId]="projectId"
[repositoryName]="repositoryName"></artifact-summary>
</div>

View File

@ -1,6 +1,6 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { TagDetailPageComponent } from './tag-detail-page.component';
import { ArtifactSummaryPageComponent } from './artifact-summary-page.component';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ClarityModule } from '@clr/angular';
@ -12,9 +12,9 @@ import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ActivatedRoute, Router } from '@angular/router';
import {AppConfigService} from "../../app-config.service";
import { SessionService } from '../../shared/session.service';
describe('TagDetailPageComponent', () => {
let component: TagDetailPageComponent;
let fixture: ComponentFixture<TagDetailPageComponent>;
describe('ArtifactSummaryPageComponent', () => {
let component: ArtifactSummaryPageComponent;
let fixture: ComponentFixture<ArtifactSummaryPageComponent>;
const mockSessionService = {
getCurrentUser: () => { }
};
@ -68,7 +68,7 @@ describe('TagDetailPageComponent', () => {
NoopAnimationsModule,
HttpClientTestingModule
],
declarations: [TagDetailPageComponent],
declarations: [ArtifactSummaryPageComponent],
providers: [
TranslateService,
{ provide: SessionService, useValue: mockSessionService },
@ -81,7 +81,7 @@ describe('TagDetailPageComponent', () => {
}));
beforeEach(() => {
fixture = TestBed.createComponent(TagDetailPageComponent);
fixture = TestBed.createComponent(ArtifactSummaryPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -0,0 +1,72 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import {AppConfigService} from "../../app-config.service";
@Component({
selector: 'artifact-summary-page',
templateUrl: 'artifact-summary-page.component.html',
styleUrls: ["artifact-summary-page.component.scss"]
})
export class ArtifactSummaryPageComponent implements OnInit, OnDestroy {
tagId: string;
artifactDigest: string;
repositoryName: string;
projectId: string | number;
referArtifactNameArray: string[] = [];
constructor(
private route: ActivatedRoute,
private appConfigService: AppConfigService,
private router: Router
) {
}
ngOnInit(): void {
this.repositoryName = this.route.snapshot.params["repo"];
this.artifactDigest = this.route.snapshot.params["digest"];
this.projectId = this.route.snapshot.params["id"];
let refer = JSON.parse(sessionStorage.getItem('referenceSummary'));
if (refer && refer.projectId === this.projectId && refer.repo === this.repositoryName && refer.digest === this.artifactDigest) {
this.referArtifactNameArray = refer.referArray;
}
}
get withAdmiral(): boolean {
return this.appConfigService.getConfig().with_admiral;
}
goBack(repositoryName: string): void {
this.router.navigate(["harbor", "projects", this.projectId, "repositories", repositoryName]);
}
goBackRep(): void {
this.router.navigate(["harbor", "projects", this.projectId, "repositories"]);
}
goBackPro(): void {
this.router.navigate(["harbor", "projects"]);
}
ngOnDestroy(): void {
sessionStorage.removeItem('referenceSummary');
}
jumpDigest(referArtifactNameArray: string[], index: number) {
sessionStorage.removeItem('referenceSummary');
sessionStorage.setItem('reference', JSON.stringify({ projectId: this.projectId, repo: this.repositoryName,
referArray: referArtifactNameArray.slice(index)}));
this.router.navigate(["harbor", "projects", this.projectId, "repositories", this.repositoryName]);
}
}

View File

@ -46,7 +46,7 @@ export class RepositoryPageComponent implements OnInit {
}
watchRepoClickEvent(repoEvt: RepositoryItem): void {
let linkUrl = ['harbor', 'projects', repoEvt.project_id, 'repositories', repoEvt.name];
let linkUrl = ['harbor', 'projects', repoEvt.project_id, 'repositories', repoEvt.name.split('/')[1]];
this.router.navigate(linkUrl);
}
}

View File

@ -17,9 +17,9 @@ import { RouterModule } from '@angular/router';
import { SharedModule } from '../shared/shared.module';
import { RepositoryPageComponent } from './repository-page.component';
import { TagRepositoryComponent } from './tag-repository/tag-repository.component';
import { ArtifactListPageComponent } from './artifact-list-page/artifact-list-page.component';
import { TopRepoComponent } from './top-repo/top-repo.component';
import { TagDetailPageComponent } from './tag-detail/tag-detail-page.component';
import { ArtifactSummaryPageComponent } from './artifact-summary-page/artifact-summary-page.component';
@NgModule({
imports: [
@ -28,14 +28,14 @@ import { TagDetailPageComponent } from './tag-detail/tag-detail-page.component';
],
declarations: [
RepositoryPageComponent,
TagRepositoryComponent,
ArtifactListPageComponent,
TopRepoComponent,
TagDetailPageComponent
ArtifactSummaryPageComponent
],
exports: [
RepositoryPageComponent,
TopRepoComponent,
TagDetailPageComponent
ArtifactSummaryPageComponent
],
providers: []
})

View File

@ -1,14 +0,0 @@
<div>
<div class="arrow-block" *ngIf="!withAdmiral">
<a (click)="goBackPro()">{{'SIDE_NAV.PROJECTS'| translate}}</a>
<span class="back-icon"><</span>
<a (click)="goBackRep()">{{'REPOSITORY.REPOSITORIES'| translate}}</a>
<span class="back-icon"><</span>
<a (click)="goBack(repositoryId)">{{repositoryId}}</a>
</div>
<hbr-tag-detail (backEvt)="goBack($event)"
[tagId]="tagId"
[withAdmiral]="withAdmiral"
[projectId]="projectId"
[repositoryId]="repositoryId"></hbr-tag-detail>
</div>

View File

@ -1,56 +0,0 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import {AppConfigService} from "../../app-config.service";
import { SessionService } from '../../shared/session.service';
@Component({
selector: 'repository',
templateUrl: 'tag-detail-page.component.html',
styleUrls: ["tag-detail-page.component.scss"]
})
export class TagDetailPageComponent implements OnInit {
tagId: string;
repositoryId: string;
projectId: string | number;
constructor(
private route: ActivatedRoute,
private appConfigService: AppConfigService,
private router: Router,
private session: SessionService
) {
}
ngOnInit(): void {
this.repositoryId = this.route.snapshot.params["repo"];
this.tagId = this.route.snapshot.params["tag"];
this.projectId = this.route.snapshot.params["id"];
}
get withAdmiral(): boolean {
return this.appConfigService.getConfig().with_admiral;
}
goBack(tag: string): void {
this.router.navigate(["harbor", "projects", this.projectId, "repositories", tag]);
}
goBackRep(): void {
this.router.navigate(["harbor", "projects", this.projectId, "repositories"]);
}
goBackPro(): void {
this.router.navigate(["harbor", "projects"]);
}
}

View File

@ -1,10 +0,0 @@
<div>
<div class="breadcrumb" *ngIf="!withAdmiral">
<a (click)="goProBack()">{{'SIDE_NAV.PROJECTS'| translate}}</a>
<span class="back-icon"><</span>
<a (click)="watchGoBackEvt(projectId)">{{'REPOSITORY.REPOSITORIES'| translate}}</a>
</div>
<hbr-repository [repoName]="repoName" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole"
[projectId]="projectId" [memberRoleID]="projectMemberRoleId" [isGuest]="isGuest"
(tagClickEvent)="watchTagClickEvt($event)" (backEvt)="watchGoBackEvt($event)"></hbr-repository>
</div>

View File

@ -1,6 +1,6 @@
<clr-datagrid (clrDgRefresh)="refresh($event)">
<clr-dg-column>{{'REPOSITORY.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.TAGS_COUNT' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.ARTIFACTS_COUNT' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.PULL_COUNT' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let r of repositories" [clrDgItem]='r'>
<clr-dg-cell><a href="javascript:void(0)" (click)="gotoLink(projectId || r.project_id, r.name || r.repository_name)">{{r.name || r.repository_name}}</a></clr-dg-cell>

View File

@ -0,0 +1,12 @@
import { TestBed } from '@angular/core/testing';
import { ArtifactGuardActivateService } from './artifact-guard-activate.service';
describe('ArtifactGuardActivateService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: ArtifactGuardActivateService = TestBed.get(ArtifactGuardActivateService);
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,74 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class ArtifactGuardActivateService {
constructor() { }
}
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {
CanActivate, Router,
ActivatedRouteSnapshot,
RouterStateSnapshot,
CanActivateChild
} from '@angular/router';
import { SessionService } from '../../shared/session.service';
import { Observable, of } from 'rxjs';
import { map, catchError, switchMap } from 'rxjs/operators';
import { ProjectService, ArtifactService } from "../../../lib/services";
import { CommonRoutes } from "../../../lib/entities/shared.const";
@Injectable()
export class ArtifactGuard implements CanActivate, CanActivateChild {
constructor(
private sessionService: SessionService,
private artifactService: ArtifactService,
private projectService: ProjectService,
private router: Router) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
const projectId = route.params['id'];
const repoName = route.params['repo'];
const digest = route.params['digest'];
return this.projectService.getProject(projectId).pipe(
switchMap((project) => {
return this.hasArtifactPerm(project.name, repoName, digest);
}),
catchError(err => {
this.router.navigate([CommonRoutes.HARBOR_DEFAULT]);
return of(false);
})
);
}
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
return this.canActivate(route, state);
}
hasArtifactPerm(projectName: string, repoName: string, digest): Observable<boolean> {
// Note: current user will have the permission to visit the project when the user can get response from GET /projects/:id API.
return this.artifactService.getArtifactFromDigest(projectName, repoName, digest).pipe(
() => {
return of(true);
},
catchError(err => {
this.router.navigate([CommonRoutes.HARBOR_DEFAULT]);
return of(false);
})
);
}
}

View File

@ -0,0 +1,29 @@
import { TestBed, inject } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { LeavingArtifactSummaryRouteDeactivate } from './leaving-artifact-summary-deactivate.service';
import { ConfirmationDialogService } from '../confirmation-dialog/confirmation-dialog.service';
import { of } from 'rxjs';
describe('LeavingArtifactSummaryRouteDeactivate', () => {
let fakeConfirmationDialogService = {
confirmationConfirm$: of({
state: 1,
source: 2
})
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
providers: [
LeavingArtifactSummaryRouteDeactivate,
{ provide: ConfirmationDialogService, useValue: fakeConfirmationDialogService }
]
});
});
it('should be created', inject([LeavingArtifactSummaryRouteDeactivate], (service: LeavingArtifactSummaryRouteDeactivate) => {
expect(service).toBeTruthy();
}));
});

View File

@ -0,0 +1,45 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import {
CanDeactivate, Router,
ActivatedRouteSnapshot,
RouterStateSnapshot
} from '@angular/router';
import { ConfirmationDialogService } from '../confirmation-dialog/confirmation-dialog.service';
import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message';
import { ConfirmationState, ConfirmationTargets } from '../shared.const';
import { ArtifactListPageComponent } from '../../repository/artifact-list-page/artifact-list-page.component';
import { Observable } from 'rxjs';
@Injectable()
export class LeavingArtifactSummaryRouteDeactivate implements CanDeactivate<ArtifactListPageComponent> {
constructor(
private router: Router,
private confirmation: ConfirmationDialogService) { }
canDeactivate(
tagRepo: ArtifactListPageComponent,
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> | boolean {
// Confirmation before leaving config route
return new Observable((observer) => {
sessionStorage.removeItem('referenceSummary');
return observer.next(true);
});
}
}

View File

@ -22,21 +22,22 @@ import { ConfirmationDialogService } from '../confirmation-dialog/confirmation-d
import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message';
import { ConfirmationState, ConfirmationTargets } from '../shared.const';
import { TagRepositoryComponent } from '../../repository/tag-repository/tag-repository.component';
import { ArtifactListPageComponent } from '../../repository/artifact-list-page/artifact-list-page.component';
import { Observable } from 'rxjs';
@Injectable()
export class LeavingRepositoryRouteDeactivate implements CanDeactivate<TagRepositoryComponent> {
export class LeavingRepositoryRouteDeactivate implements CanDeactivate<ArtifactListPageComponent> {
constructor(
private router: Router,
private confirmation: ConfirmationDialogService) { }
canDeactivate(
tagRepo: TagRepositoryComponent,
tagRepo: ArtifactListPageComponent,
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> | boolean {
// Confirmation before leaving config route
return new Observable((observer) => {
// if (state)
if (tagRepo && tagRepo.hasChanges()) {
let msg: ConfirmationMessage = new ConfirmationMessage(
"CONFIG.LEAVING_CONFIRMATION_TITLE",
@ -49,15 +50,24 @@ export class LeavingRepositoryRouteDeactivate implements CanDeactivate<TagReposi
return this.confirmation.confirmationConfirm$.subscribe(confirmMsg => {
if (confirmMsg && confirmMsg.source === ConfirmationTargets.REPOSITORY) {
if (confirmMsg.state === ConfirmationState.CONFIRMED) {
//
sessionStorage.removeItem('reference');
return observer.next(true);
} else {
return observer.next(false); // Prevent leading route
}
} else {
//
sessionStorage.removeItem('reference');
return observer.next(true); // Should go on
}
});
} else {
//
sessionStorage.removeItem('reference');
return observer.next(true);
}
});

View File

@ -26,9 +26,11 @@ import { AuthCheckGuard } from "./route/auth-user-activate.service";
import { SignInGuard } from "./route/sign-in-guard-activate.service";
import { SystemAdminGuard } from "./route/system-admin-activate.service";
import { MemberGuard } from "./route/member-guard-activate.service";
import { ArtifactGuard } from "./route/artifact-guard-activate.service";
import { MemberPermissionGuard } from "./route/member-permission-guard-activate.service";
import { OidcGuard } from "./route/oidc-guard-active.service";
import { LeavingRepositoryRouteDeactivate } from "./route/leaving-repository-deactivate.service";
import { LeavingArtifactSummaryRouteDeactivate } from "./route/leaving-artifact-summary-deactivate.service";
import { PortValidatorDirective } from "./port.directive";
import { MaxLengthExtValidatorDirective } from "./max-length-ext.directive";
@ -137,7 +139,9 @@ const uiLibConfig: IServiceConfig = {
AuthCheckGuard,
SignInGuard,
LeavingRepositoryRouteDeactivate,
LeavingArtifactSummaryRouteDeactivate,
MemberGuard,
ArtifactGuard,
MemberPermissionGuard,
OidcGuard,
MessageHandlerService,

View File

@ -121,3 +121,66 @@ hbr-tag {
color: $light-color-green !important;
}
}
// style of hbr-artifact-summary component
@mixin align-text-mixin($values...) {
@each $var in $values {
&[align="$var"] {
text-align: $var;
}
}
}
%code-block {
background: $color-ddd;
border-radius: 2px;
padding: 2px 4px;
}
.md-div {
code:not([class*="language-"]) {
@extend %code-block;
color: $color-657b83
}
pre:not([class*="language-"]) {
background: $color-fdf6e3;
code:not([class*="language-"]) {
@extend %code-block;
background: transparent;
}
}
table {
display: block;
width: 100%;
overflow: auto;
padding: 0;
border-spacing: 0;
border-collapse: collapse;
margin-bottom: 16px;
td,
th {
padding: 6px 13px;
border: 1px solid $color-ddd;
@include align-text-mixin(left, right, center);
}
tr {
&:nth-child(2n) {
background-color: $color-f2;
}
}
}
}
.table-tag {
.tag-thead {
.tag-header-color {
color: $mode-background-color3;
}
};
.tag-tbody {
.tag-tr {
.tag-body-color {
color: $mode-background-color2;
}
}
}
}

View File

@ -20,4 +20,9 @@ $fill-color1: #ccc;
$right-status-fill-color: white;
$light-color-green: #4cd400;
$color-ddd: #21333b;
$color-f2: none;
$color-657b83: none;
$color-fdf6e3: none;
@import "./common.scss";

View File

@ -21,4 +21,10 @@ $select-back-color: $mode-background-color;
$label-form-color: $mode-background-color3;
$right-status-fill-color: #1d5100;
$light-color-green: $right-status-fill-color;
$color-ddd: #ddd;
$color-f2: #f2f2f2;
$color-657b83: #657b83;
$color-fdf6e3: #fdf6e3;
@import "./common.scss";

View File

@ -447,3 +447,9 @@ clr-datagrid {
button:focus {
outline: none !important;
}
.cursor-pointer {
cursor: pointer
}
.text-align-r {
text-align: right !important;
}

View File

@ -616,6 +616,9 @@
"DELETE": "Delete",
"NAME": "Name",
"TAGS_COUNT": "Tags",
"PLATFORM": "OS/ARCH",
"ARTIFACT_TOOTIP": "Click this icon to enter the Artifact list of refer",
"ARTIFACTS_COUNT": "Artifacts",
"PULL_COUNT": "Pulls",
"PULL_COMMAND": "Pull Command",
"PULL_TIME": "Pull Time",
@ -628,12 +631,14 @@
"DELETION_SUMMARY_REPO_SIGNED": "Repository '{{repoName}}' cannot be deleted because the following signed images existing.\n{{signedImages}} \nYou should unsign all the signed images before deleting the repository!",
"DELETION_SUMMARY_REPO": "Do you want to delete repository {{repoName}}?",
"DELETION_TITLE_TAG": "Confirm Tag Deletion",
"DELETION_SUMMARY_TAG": "Do you want to delete tag {{param}}? If you delete this tag, all other tags referenced by the same digest will also be deleted",
"DELETION_SUMMARY_TAG": "Do you want to delete tag {{param}}? If you delete this tag, all other tags referenced by the same digest will also be deleted.",
"DELETION_TITLE_TAG_DENIED": "Signed tag cannot be deleted",
"DELETION_SUMMARY_TAG_DENIED": "The tag must be removed from the Notary before it can be deleted.\nDelete from Notary via this command:\n",
"TAGS_NO_DELETE": "Delete is prohibited in read only mode.",
"FILTER_FOR_REPOSITORIES": "Filter Repositories",
"TAG": "Tag",
"ARTIFACT": "Aarifact",
"ARTIFACTS": "Artifacts",
"SIZE": "Size",
"VULNERABILITY": "Vulnerabilities",
"BUILD_HISTORY": "Build History",
@ -660,8 +665,9 @@
"LABELS": "Labels",
"ADD_LABEL_TO_IMAGE": "Add labels to this image",
"FILTER_BY_LABEL": "Filter images by label",
"FILTER_ARTIFACT_BY_LABEL": "Filter actifact by label",
"ADD_LABELS": "Add labels",
"RETAG": "Retag",
"RETAG": "Copy",
"ACTION": "ACTION",
"DEPLOY": "DEPLOY",
"ADDITIONAL_INFO": "Add Additional Info",
@ -987,6 +993,12 @@
"PUSH_COMMAND": "Push an image to this project:",
"COPY_ERROR": "Copy failed, please try to manually copy the command references."
},
"ARTIFACT": {
"FILTER_FOR_ARTIFACTS": "Filter Artifacts",
"ADDITIONS": "Additions",
"COMMON_PROPERTIES": "Common Properties",
"COMMON_ALL": "Common properties across all digest"
},
"TAG": {
"CREATION_TIME_PREFIX": "Create on",
"CREATOR_PREFIX": "by",
@ -1007,7 +1019,14 @@
"AUTHOR": "Author",
"LABELS": "Labels",
"CREATION": "Create on",
"COMMAND": "Commands"
"COMMAND": "Commands",
"UPLOADTIME": "Upload Time",
"NAME": "Name",
"PULL_TIME": "Pull Time",
"PUSH_TIME": "Push Time",
"OF": "of",
"ADD_TAG": "ADD TAG",
"REMOVE_TAG": "REMOVE TAG"
},
"LABEL": {
"LABEL": "Label",

View File

@ -617,6 +617,9 @@
"DELETE": "Eliminar",
"NAME": "Nombre",
"TAGS_COUNT": "Etiquetas",
"PLATFORM": "OS/ARCH",
"ARTIFACT_TOOTIP": "Click this icon to enter the Artifact list of refer",
"ARTIFACTS_COUNT": "Artifacts",
"PULL_COUNT": "Pulls",
"PULL_COMMAND": "Comando Pull",
"PULL_TIME": "Pull Time",
@ -629,12 +632,14 @@
"DELETION_SUMMARY_REPO_SIGNED": "Repository '{{repoName}}' cannot be deleted because the following signed images existing.\n{{signedImages}} \nYou should unsign all the signed images before deleting the repository!",
"DELETION_SUMMARY_REPO": "¿Quiere eliminar el repositorio {{repoName}}?",
"DELETION_TITLE_TAG": "Confirmación de Eliminación de Etiqueta",
"DELETION_SUMMARY_TAG": "¿Quiere eliminar la etiqueta {{param}}? If you delete this tag, all other tags referenced by the same digest will also be deleted",
"DELETION_SUMMARY_TAG": "¿Quiere eliminar la etiqueta {{param}}? If you delete this tag, all other tags referenced by the same digest will also be deleted.",
"DELETION_TITLE_TAG_DENIED": "La etiqueta firmada no puede ser eliminada",
"DELETION_SUMMARY_TAG_DENIED": "La etiqueta debe ser eliminada de la Notaría antes de eliminarla.\nEliminarla de la Notaría con este comando:\n",
"TAGS_NO_DELETE": "Delete is prohibited in read only mode.",
"FILTER_FOR_REPOSITORIES": "Filtrar Repositorios",
"TAG": "Etiqueta",
"ARTIFACT": "Aarifact",
"ARTIFACTS": "Artifacts",
"SIZE": "Size",
"VULNERABILITY": "Vulnerabilities",
"BUILD_HISTORY": "Construir Historia",
@ -661,8 +666,9 @@
"LABELS": "Labels",
"ADD_LABEL_TO_IMAGE": "Add labels to this image",
"FILTER_BY_LABEL": "Filter images by label",
"FILTER_ARTIFACT_BY_LABEL": "Filter actifact by label",
"ADD_LABELS": "Add labels",
"RETAG": "Retag",
"RETAG": "Copy",
"ACTION": "ACTION",
"DEPLOY": "DEPLOY",
"ADDITIONAL_INFO": "Add Additional Info",
@ -986,6 +992,12 @@
"PUSH_COMMAND": "Push an image to this project:",
"COPY_ERROR": "Copy failed, please try to manually copy the command references."
},
"ARTIFACT": {
"FILTER_FOR_ARTIFACTS": "Filter Artifacts",
"ADDITIONS": "Additions",
"COMMON_PROPERTIES": "Common Properties",
"COMMON_ALL": "Common properties across all digest"
},
"TAG": {
"CREATION_TIME_PREFIX": "Create on",
"CREATOR_PREFIX": "by",
@ -1006,7 +1018,14 @@
"AUTHOR": "Author",
"LABELS": "Labels",
"CREATION": "Tiempo de creación",
"COMMAND": "Mando"
"COMMAND": "Mando",
"UPLOADTIME": "Upload Time",
"NAME": "Name",
"PULL_TIME": "Pull Time",
"PUSH_TIME": "Push Time",
"OF": "of",
"ADD_TAG": "ADD TAG",
"REMOVE_TAG": "REMOVE TAG"
},
"LABEL": {

View File

@ -332,7 +332,6 @@
"PERMISSIONS_HELMCHART": "Helm Chart",
"PUSH": "Push",
"PULL": "Pull",
"FILTER_PLACEHOLDER": "Filter Robot Accounts",
"ROBOT_NAME": "ne peut pas contenir de caractères spéciaux(~#$%) et la longueur maximale devrait être de 255 caractères.",
"ACCOUNT_EXISTING": "le robot est existe déjà.",
@ -606,6 +605,9 @@
"DELETE": "Supprimer",
"NAME": "Nom",
"TAGS_COUNT": "Tags",
"PLATFORM": "OS/ARCH",
"ARTIFACT_TOOTIP": "Click this icon to enter the Artifact list of refer",
"ARTIFACTS_COUNT": "Artifacts",
"PULL_COUNT": "Pulls",
"PULL_COMMAND": "Commande de Pull",
"PULL_TIME": "Pull Time",
@ -618,12 +620,14 @@
"DELETION_SUMMARY_REPO_SIGNED": "Le Dépôt '{{repoName}}' ne peut pas être supprimé parce que les images suivantes signées existent. \n{{signedImages}} \nVous devez retirer la signature de toutes les images signées avant de supprimer le dépôt !",
"DELETION_SUMMARY_REPO": "Voulez-vous supprimer le dépôt {{repoName}} ?",
"DELETION_TITLE_TAG": "Confirmer la suppression du Tag",
"DELETION_SUMMARY_TAG": "Voulez-vous supprimer le tag {{param}}, If you delete this tag, all other tags referenced by the same digest will also be deleted?",
"DELETION_SUMMARY_TAG": "Voulez-vous supprimer le tag {{param}}? If you delete this tag, all other tags referenced by the same digest will also be deleted.",
"DELETION_TITLE_TAG_DENIED": "Un tag signé ne peut être supprimé",
"DELETION_SUMMARY_TAG_DENIED": "La balise doit être supprimée du Résumé avant qu'elle ne puisse être supprimée. \nSupprimer du Résumé via cette commande: \n",
"TAGS_NO_DELETE": "Upload/Delete is prohibited in read only mode.",
"FILTER_FOR_REPOSITORIES": "Filtrer les Dépôts",
"TAG": "Tag",
"ARTIFACT": "Aarifact",
"ARTIFACTS": "Artifacts",
"SIZE": "Taille",
"VULNERABILITY": "Vulnérabilitée",
"BUILD_HISTORY": "Construire l'histoire",
@ -648,8 +652,9 @@
"LABELS": "Labels",
"ADD_LABEL_TO_IMAGE": "Add labels to this image",
"FILTER_BY_LABEL": "Filter images by label",
"FILTER_ARTIFACT_BY_LABEL": "Filter actifact by label",
"ADD_LABELS": "Add labels",
"RETAG": "Retag",
"RETAG": "Copy",
"ACTION": "ACTION",
"DEPLOY": "DEPLOY",
"ADDITIONAL_INFO": "Add Additional Info",
@ -960,6 +965,12 @@
"PUSH_COMMAND": "Pousser une image dans ce projet :",
"COPY_ERROR": "Copie échouée, veuillez essayer de copier manuellement les commandes de référence."
},
"ARTIFACT": {
"FILTER_FOR_ARTIFACTS": "Filter Artifacts",
"ADDITIONS": "Additions",
"COMMON_PROPERTIES": "Common Properties",
"COMMON_ALL": "Common properties across all digest"
},
"TAG": {
"CREATION_TIME_PREFIX": "Créer le",
"CREATOR_PREFIX": "par",
@ -979,7 +990,14 @@
"AUTHOR": "Author",
"LABELS": "Labels",
"CREATION": "Créer sur",
"COMMAND": "Les commandes"
"COMMAND": "Les commandes",
"UPLOADTIME": "Upload Time",
"NAME": "Name",
"PULL_TIME": "Pull Time",
"PUSH_TIME": "Push Time",
"OF": "of",
"ADD_TAG": "ADD TAG",
"REMOVE_TAG": "REMOVE TAG"
},
"LABEL": {
"LABEL": "Label",
@ -1086,7 +1104,6 @@
"ON": "on",
"AT": "at",
"NOSCHEDULE": "An error occurred in Get schedule"
},
"GC": {
"CURRENT_SCHEDULE": "Current Schedule",
@ -1314,4 +1331,4 @@
"HELP_INFO_1": "The default scanner has been installed. To install other scanners refer to the ",
"HELP_INFO_2": "documentation."
}
}
}

View File

@ -616,6 +616,9 @@
"DELETE": "Remover",
"NAME": "Nome",
"TAGS_COUNT": "Tags",
"PLATFORM": "OS/ARCH",
"ARTIFACT_TOOTIP": "Click this icon to enter the Artifact list of refer",
"ARTIFACTS_COUNT": "Artifacts",
"PULL_COUNT": "Pulls",
"PULL_COMMAND": "Comando de Pull",
"PULL_TIME": "Pull Time",
@ -628,12 +631,14 @@
"DELETION_SUMMARY_REPO_SIGNED": "Repositório '{{repoName}}' não pode ser removido pois existem as seguintes imagens assinadas.\n{{signedImages}} \nVocê deve remover a assinatura de todas as imagens assinadas antes de remover o repositório!",
"DELETION_SUMMARY_REPO": "Você deseja remover o repositório {{repoName}}?",
"DELETION_TITLE_TAG": "Confirmar remoção de Tag",
"DELETION_SUMMARY_TAG": "Você quer remover a Tag {{param}}? If you delete this tag, all other tags referenced by the same digest will also be deleted",
"DELETION_SUMMARY_TAG": "Você quer remover a Tag {{param}}? If you delete this tag, all other tags referenced by the same digest will also be deleted.",
"DELETION_TITLE_TAG_DENIED": "Tags assinadas não podem ser removidas",
"DELETION_SUMMARY_TAG_DENIED": "A tag deve ser removida do Notary antes de ser apagada.\nRemova do Notary com o seguinte comando:\n",
"TAGS_NO_DELETE": "Remover é proibido em modo somente leitura.",
"FILTER_FOR_REPOSITORIES": "Filtrar repositórios",
"TAG": "Tag",
"ARTIFACT": "Aarifact",
"ARTIFACTS": "Artifacts",
"SIZE": "Tamanho",
"VULNERABILITY": "Vulnerabilidade",
"SIGNED": "Assinada",
@ -660,7 +665,9 @@
"LABELS": "Labels",
"ADD_LABEL_TO_IMAGE": "Adicionar labels a essa imagem",
"FILTER_BY_LABEL": "Filtrar imagens por label",
"FILTER_ARTIFACT_BY_LABEL": "Filter actifact by label",
"ADD_LABELS": "Adicionar labels",
"RETAG": "Copy",
"ACTION": "AÇÃO",
"DEPLOY": "DEPLOY",
"ADDITIONAL_INFO": "Adicionar informação adicional",
@ -981,6 +988,12 @@
"PUSH_COMMAND": "Envia uma imagem para esse projeto:",
"COPY_ERROR": "Copia falhou, por favor tente copiar o comando de referência manualmente."
},
"ARTIFACT": {
"FILTER_FOR_ARTIFACTS": "Filter Artifacts",
"ADDITIONS": "Additions",
"COMMON_PROPERTIES": "Common Properties",
"COMMON_ALL": "Common properties across all digest"
},
"TAG": {
"CREATION_TIME_PREFIX": "Criado em",
"CREATOR_PREFIX": "por",
@ -998,7 +1011,14 @@
"COPY_ERROR": "Cópia falhou, por favor tente copiar manualmente.",
"FILTER_FOR_TAGS": "Filtrar Tags",
"AUTHOR": "Autor",
"LABELS": "Labels"
"LABELS": "Labels",
"UPLOADTIME": "Upload Time",
"NAME": "Name",
"PULL_TIME": "Pull Time",
"PUSH_TIME": "Push Time",
"OF": "of",
"ADD_TAG": "ADD TAG",
"REMOVE_TAG": "REMOVE TAG"
},
"LABEL": {
"LABEL": "Label",

View File

@ -615,6 +615,9 @@
"DELETE": "Sil",
"NAME": "İsim",
"TAGS_COUNT": "Etiketler",
"PLATFORM": "OS/ARCH",
"ARTIFACT_TOOTIP": "Click this icon to enter the Artifact list of refer",
"ARTIFACTS_COUNT": "Artifacts",
"PULL_COUNT": "İndirmeler",
"PULL_COMMAND": "İndirme Komutu",
"PULL_TIME": "İndirme Zamanı",
@ -627,12 +630,14 @@
"DELETION_SUMMARY_REPO_SIGNED": "Depo '{{repoName}}' aşağıdaki imzalanmış görüntüler mevcut olduğu için silinemez. \n {{signedImages}} \nBir depoyu silmeden önce imzalı tüm imajları imzalamanız gerekir!",
"DELETION_SUMMARY_REPO": "Depoyu silmek istiyor musunuz?{{repoName}}?",
"DELETION_TITLE_TAG": "Etiket Silme İşlemini Onayla",
"DELETION_SUMMARY_TAG": "Etiketi silmek ister misiniz {{param}}? If you delete this tag, all other tags referenced by the same digest will also be deleted",
"DELETION_SUMMARY_TAG": "Etiketi silmek ister misiniz {{param}}? If you delete this tag, all other tags referenced by the same digest will also be deleted.",
"DELETION_TITLE_TAG_DENIED": "İmzalı etiket silinemez",
"DELETION_SUMMARY_TAG_DENIED": "Etiket silinmeden önce Harbordan kaldırılmalıdır. \nBu komutla Harbor'den silin: \n",
"TAGS_NO_DELETE": "Salt okunur modda silmek yasaktır.",
"FILTER_FOR_REPOSITORIES": "Depoları Filtrele",
"TAG": "Etiket",
"ARTIFACT": "Aarifact",
"ARTIFACTS": "Artifacts",
"SIZE": "Boyut",
"VULNERABILITY": "Güvenlik Açığı",
"BUILD_HISTORY": "Geçmişi Oluştur",
@ -659,8 +664,9 @@
"LABELS": "Etiketler",
"ADD_LABEL_TO_IMAGE": "Bu imaja etiketler ekle",
"FILTER_BY_LABEL": "İmajları etikete göre filtrele",
"FILTER_ARTIFACT_BY_LABEL": "Filter actifact by label",
"ADD_LABELS": "Etiketler Ekle",
"RETAG": "Yeniden etiketleme",
"RETAG": "Copy",
"ACTION": "AKSİYON",
"DEPLOY": "YÜKLE",
"ADDITIONAL_INFO": "Ek Bilgi Ekle",
@ -986,6 +992,12 @@
"PUSH_COMMAND": "Bu projeye bir imaj gönder:",
"COPY_ERROR": "Kopyalama başarısız oldu, lütfen komut referanslarını el ile kopyalamayı deneyin."
},
"ARTIFACT": {
"FILTER_FOR_ARTIFACTS": "Filter Artifacts",
"ADDITIONS": "Additions",
"COMMON_PROPERTIES": "Common Properties",
"COMMON_ALL": "Common properties across all digest"
},
"TAG": {
"CREATION_TIME_PREFIX": "Oluştur",
"CREATOR_PREFIX": "tarafından",
@ -1006,7 +1018,14 @@
"AUTHOR": "Yazar",
"LABELS": "Etiketler",
"CREATION": "Oluşturma",
"COMMAND": "Komutlar"
"COMMAND": "Komutlar",
"UPLOADTIME": "Upload Time",
"NAME": "Name",
"PULL_TIME": "Pull Time",
"PUSH_TIME": "Push Time",
"OF": "of",
"ADD_TAG": "ADD TAG",
"REMOVE_TAG": "REMOVE TAG"
},
"LABEL": {
"LABEL": "Etiket",

View File

@ -617,6 +617,9 @@
"DELETE": "删除",
"NAME": "名称",
"TAGS_COUNT": "Tag 数量",
"PLATFORM": "OS/ARCH",
"ARTIFACT_TOOTIP": "Click this icon to enter the Artifact list of refer",
"ARTIFACTS_COUNT": "Artifacts",
"PULL_COUNT": "下载数",
"PULL_COMMAND": "Pull命令",
"PULL_TIME": "拉取时间",
@ -629,12 +632,14 @@
"DELETION_SUMMARY_REPO_SIGNED": "镜像仓库 '{{repoName}}' 不能被删除,因为存在以下签名镜像.\n{{signedImages}} \n在删除镜像仓库前需先删除所有的签名镜像",
"DELETION_SUMMARY_REPO": "确认删除镜像仓库 {{repoName}}?",
"DELETION_TITLE_TAG": "删除镜像 Tag 确认",
"DELETION_SUMMARY_TAG": "确认删除镜像 Tag {{param}}? 如果您删除此 Tag则这个 Tag 引用的同一个 digest 的所有其他 Tag 也将被删除",
"DELETION_SUMMARY_TAG": "确认删除镜像 Tag {{param}}? 如果您删除此 Tag则这个 Tag 引用的同一个 digest 的所有其他 Tag 也将被删除",
"DELETION_TITLE_TAG_DENIED": "已签名的镜像不能被删除",
"DELETION_SUMMARY_TAG_DENIED": "要删除此镜像 Tag 必须首先从 Notary 中删除。\n请执行如下 Notary 命令删除:\n",
"TAGS_NO_DELETE": "在只读模式下删除是被禁止的",
"FILTER_FOR_REPOSITORIES": "过滤镜像仓库",
"TAG": "Tag",
"ARTIFACT": "Aarifact",
"ARTIFACTS": "Artifacts",
"SIZE": "大小",
"VULNERABILITY": "漏洞",
"BUILD_HISTORY": "构建历史",
@ -661,8 +666,9 @@
"LABELS": "标签",
"ADD_LABEL_TO_IMAGE": "添加标签到此镜像",
"ADD_LABELS": "添加标签",
"RETAG": "Tag 拷贝",
"RETAG": "拷贝",
"FILTER_BY_LABEL": "过滤标签",
"FILTER_ARTIFACT_BY_LABEL": "通过标签过滤Artifact",
"ACTION": "操作",
"DEPLOY": "部署",
"ADDITIONAL_INFO": "添加信息",
@ -986,6 +992,12 @@
"PUSH_COMMAND": "推送镜像到当前项目:",
"COPY_ERROR": "拷贝失败,请尝试手动拷贝参考命令。"
},
"ARTIFACT": {
"FILTER_FOR_ARTIFACTS": "Filter Artifacts",
"ADDITIONS": "其他",
"COMMON_PROPERTIES": "属性",
"COMMON_ALL": "公共属性"
},
"TAG": {
"CREATION_TIME_PREFIX": "创建时间:",
"CREATOR_PREFIX": "创建者:",
@ -1006,7 +1018,15 @@
"AUTHOR": "作者",
"LABELS": "标签",
"CREATION": "创建时间",
"COMMAND": "命令"
"COMMAND": "命令",
"UPLOADTIME": "上传时间",
"NAME": "名称",
"PULL_TIME": "拉取时间",
"PUSH_TIME": "推送时间",
"OF": "共计",
"ITEMS": "条记录",
"ADD_TAG": "添加 TAG",
"REMOVE_TAG": "删除 TAG"
},
"LABEL": {
"LABEL": "标签",

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0.78 -3.04 64 64" width="64" height="64"><g transform="matrix(.114654 0 0 .114654 -.882488 -4.335594)"><path d="M332.5 446.3v-79.9H214.6V214.3h117.9v-79.9H134.6v311.9z" fill="#808184"/><path d="M372.5 134.4h79.9v79.9h-79.9zm0 119.9h79.9v192h-79.9zm-358-243v558.2h558.2V11.3zm477.9 475.1H94.6V94.3h397.8z" fill="#262261"/></g></svg>

After

Width:  |  Height:  |  Size: 380 B

View File

@ -0,0 +1,74 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.21411 17.0806H10.931V13.7016H7.21411V17.0806Z" fill="#46A4E0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.82153 17.0806H6.53844V13.7016H2.82153L2.82153 17.0806Z" fill="#46A4E0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.6069 17.0806H15.3236V13.7016H11.6069V17.0806Z" fill="#46A4E0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.9995 17.0806H19.7163V13.7016H15.9995V17.0806Z" fill="#46A4E0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.3921 17.0806H24.109V13.7016H20.3921V17.0806Z" fill="#46A4E0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.21411 13.0259H10.931V9.6468H7.21411V13.0259Z" fill="#46A4E0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.6069 13.0259H15.3236V9.6468H11.6069V13.0259Z" fill="#46A4E0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.9995 13.0259H19.7163V9.6468H15.9995V13.0259Z" fill="#46A4E0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.9995 8.97119H19.7163V5.59221H15.9995V8.97119Z" fill="#46A4E0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M35.1797 14.7449C34.4168 14.2315 32.4125 14.0122 30.9553 14.4047C30.8771 12.9531 30.1284 11.7298 28.759 10.6628L28.2523 10.3227L27.9141 10.833C27.2503 11.8408 26.9706 13.1837 27.0697 14.4047C27.1477 15.1568 27.4094 16.0024 27.9141 16.6157C27.5275 16.9163 26.325 17.4972 24.7044 17.4663L0.544673 17.4663C0.115032 20.0175 0.850805 29.7122 12.0331 29.7122C20.3348 29.7122 27.1562 25.9786 30.2795 18.1465C31.3072 18.1634 34.0204 18.3307 35.3481 15.7652C35.3803 15.7222 35.686 15.0851 35.686 15.0851L35.1797 14.7449Z" fill="#46A4E0"/>
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="3" y="25" width="12" height="5">
<path d="M9.47079 25.1061C6.71403 26.0623 3.74586 26.2006 3.12549 26.173C5.1935 27.8974 8.87792 31.1516 14.9518 29.2937C13.8426 28.9922 11.2261 27.9837 9.47079 25.1061Z" fill="white"/>
</mask>
<g mask="url(#mask0)">
<mask id="mask1" mask-type="alpha" maskUnits="userSpaceOnUse" x="-19" y="-9" width="70" height="70">
<path d="M-18.804 60.6694L50.1271 60.6694L50.1271 -8.26151L-18.804 -8.26151L-18.804 60.6694Z" fill="white"/>
</mask>
<g mask="url(#mask1)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.43604 30.0002L16.6414 30.0002V23.4167H1.43604L1.43604 30.0002Z" fill="white"/>
</g>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.16281 24.5106C7.83642 25.2677 6.29914 25.6928 4.66865 25.6928C3.84244 25.6928 3.04218 25.5831 2.28076 25.3772C2.76289 26.1877 2.76289 26.1877 2.76289 26.1877C3.37944 26.3094 4.01639 26.3733 4.66865 26.3733C6.42576 26.3733 8.06461 25.914 9.49225 25.106C9.37402 24.9104 9.26406 24.7148 9.16281 24.5106Z" fill="#3371C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.7342 24.0696C12.3829 24.0696 12.9089 23.5401 12.9089 22.887C12.9089 22.2338 12.3829 21.7043 11.7342 21.7043C11.0852 21.7043 10.5593 22.2338 10.5593 22.887C10.5593 23.5401 11.0852 24.0696 11.7342 24.0696Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.911 22.5548C11.911 22.4462 11.9645 22.354 12.043 22.2919C11.9509 22.2441 11.8481 22.2145 11.7375 22.2145C11.3668 22.2145 11.0662 22.5172 11.0662 22.8904C11.0662 23.2635 11.3668 23.5662 11.7375 23.5662C12.1081 23.5662 12.4088 23.2635 12.4088 22.8904L12.4049 22.8515C12.3576 22.8769 12.3061 22.8948 12.2488 22.8948C12.0622 22.8948 11.911 22.7426 11.911 22.5548Z" fill="#46505C"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M35.0795 14.6831C34.5573 14.3871 33.6068 14.2038 32.6253 14.2038C32.0528 14.2038 31.4699 14.2662 30.9552 14.4048C30.9537 14.3741 30.9517 14.3436 30.9495 14.3132C31.4132 14.1907 31.9457 14.1358 32.4772 14.1358C33.5328 14.1358 34.5838 14.3522 35.0795 14.6831Z" fill="#E7E3E1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.7143 29.6104C14.1154 29.5282 14.5278 29.4234 14.9519 29.2937C14.4324 29.1524 13.5821 28.8561 12.6445 28.2852C20.3678 28.0972 26.0235 24.1331 30.5441 14.7766L30.8543 14.3395C30.8855 14.3304 30.9172 14.3217 30.9494 14.3132C30.9517 14.3436 30.9537 14.3741 30.9552 14.4048C31.4699 14.2661 32.0528 14.2038 32.6252 14.2038C33.6067 14.2038 34.5573 14.3871 35.0795 14.6831C35.0802 14.6837 35.0812 14.6843 35.0822 14.685L35.5925 15.0309L35.3355 15.5749C34.1637 17.9501 31.619 18.1285 30.428 18.1285C30.3311 18.1285 30.2432 18.1273 30.1657 18.1261C27.2457 25.3573 21.1794 29.0869 13.7143 29.6104Z" fill="#E7E3E1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.7143 29.6104C14.1154 29.5282 14.5278 29.4234 14.9519 29.2937C14.4324 29.1524 13.5821 28.8561 12.6445 28.2852C20.3678 28.0972 26.0235 24.1331 30.5441 14.7766L30.8543 14.3395C30.8855 14.3304 30.9172 14.3217 30.9494 14.3132C30.9517 14.3436 30.9537 14.3741 30.9552 14.4048C31.4699 14.2661 32.0528 14.2038 32.6252 14.2038C33.6067 14.2038 34.5573 14.3871 35.0795 14.6831C35.0802 14.6837 35.0812 14.6843 35.0822 14.685L35.5925 15.0309L35.3355 15.5749C34.1637 17.9501 31.619 18.1285 30.428 18.1285C30.3311 18.1285 30.2432 18.1273 30.1657 18.1261C27.2457 25.3573 21.1794 29.0869 13.7143 29.6104Z" fill="#4195CA"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.87746 29.21C5.57679 28.6165 3.94076 27.5068 2.78394 26.1917C2.90072 26.2145 3.01817 26.2352 3.1364 26.2538C4.1704 27.4281 5.57776 28.4331 7.47757 29.0522C7.60849 29.1068 7.74195 29.1596 7.87746 29.21Z" fill="#E7E3E1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.4774 29.0522C5.57759 28.4332 4.17023 27.4281 3.13623 26.2538C3.17114 26.2593 3.20628 26.2647 3.2413 26.2697C4.25521 27.1172 5.63935 28.2843 7.4774 29.0522Z" fill="#E7E3E1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.4774 29.0522C5.57759 28.4332 4.17023 27.4281 3.13623 26.2538C3.17114 26.2593 3.20628 26.2647 3.2413 26.2697C4.25521 27.1172 5.63935 28.2843 7.4774 29.0522Z" fill="#4195CA"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.72587 26.1252C0.190161 23.1867 0.00216903 19.2574 0.105579 17.7411H0.506244C0.327521 19.3081 0.54917 22.6683 2.48869 25.4308C2.41914 25.4137 2.34987 25.3959 2.2809 25.3773C2.55776 25.8427 2.67572 26.0409 2.72587 26.1252Z" fill="#E7E3E1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.48017 25.6909C3.79522 25.6771 3.1289 25.5877 2.48874 25.4308C0.549213 22.6683 0.327564 19.3081 0.506287 17.7411H1.45727C1.77706 21.0044 2.53528 23.7998 4.48017 25.6909Z" fill="#E7E3E1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.48017 25.6909C3.79522 25.6771 3.1289 25.5877 2.48874 25.4308C0.549213 22.6683 0.327564 19.3081 0.506287 17.7411H1.45727C1.77706 21.0044 2.53528 23.7998 4.48017 25.6909Z" fill="#4195CA"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.8518 29.6746C10.3449 29.6746 9.02813 29.5069 7.87745 29.21C7.74194 29.1597 7.60848 29.1069 7.47756 29.0522C5.63951 28.2843 4.25537 27.1172 3.24146 26.2697C3.70738 26.3379 4.18384 26.3732 4.6688 26.3732C4.8691 26.3732 5.06762 26.3673 5.26469 26.3555C6.9211 27.5756 9.26948 28.2769 12.6078 28.286C12.6201 28.2857 12.6323 28.2855 12.6446 28.2852C13.5821 28.8561 14.4324 29.1523 14.9519 29.2936C14.5278 29.4234 14.1154 29.5281 13.7144 29.6103C13.1026 29.6532 12.4815 29.6746 11.8518 29.6746Z" fill="#E7E3E1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.8518 29.6746C10.3449 29.6746 9.02813 29.5069 7.87745 29.21C7.74194 29.1597 7.60848 29.1069 7.47756 29.0522C5.63951 28.2843 4.25537 27.1172 3.24146 26.2697C3.70738 26.3379 4.18384 26.3732 4.6688 26.3732C4.8691 26.3732 5.06762 26.3673 5.26469 26.3555C6.9211 27.5756 9.26948 28.2769 12.6078 28.286C12.6201 28.2857 12.6323 28.2855 12.6446 28.2852C13.5821 28.8561 14.4324 29.1523 14.9519 29.2936C14.5278 29.4234 14.1154 29.5281 13.7144 29.6103C13.1026 29.6532 12.4815 29.6746 11.8518 29.6746Z" fill="#E7E3E1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.66865 26.3733C4.18369 26.3733 3.70723 26.338 3.2413 26.2698C3.20629 26.2647 3.17115 26.2594 3.13624 26.2539C3.01801 26.2353 2.90055 26.2146 2.78377 26.1918C2.76417 26.1695 2.74494 26.1475 2.72573 26.1252C2.67558 26.0408 2.55763 25.8427 2.28076 25.3772C2.34973 25.3959 2.419 25.4137 2.48856 25.4308C3.12872 25.5877 3.79504 25.677 4.47999 25.6909C4.72242 25.9267 4.98378 26.1487 5.26454 26.3555C5.06747 26.3673 4.86894 26.3733 4.66865 26.3733Z" fill="#E7E3E1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.66865 26.3733C4.18369 26.3733 3.70723 26.338 3.2413 26.2698C3.20629 26.2647 3.17115 26.2594 3.13624 26.2539C3.01801 26.2353 2.90055 26.2146 2.78377 26.1918C2.76417 26.1695 2.74494 26.1475 2.72573 26.1252C2.67558 26.0408 2.55763 25.8427 2.28076 25.3772C2.34973 25.3959 2.419 25.4137 2.48856 25.4308C3.12872 25.5877 3.79504 25.677 4.47999 25.6909C4.72242 25.9267 4.98378 26.1487 5.26454 26.3555C5.06747 26.3673 4.86894 26.3733 4.66865 26.3733Z" fill="#3068AF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.49731 16.4048H3.83525V14.3773H3.49731L3.49731 16.4048Z" fill="#3271C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.51099 16.4048H4.84873V14.3773H4.51099V16.4048Z" fill="#3271C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.52466 16.4048H5.8625V14.3773H5.52466V16.4048Z" fill="#3271C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.88989 16.4048H8.22773V14.3773H7.88989V16.4048Z" fill="#3271C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.90356 16.4048H9.2415V14.3773H8.90356V16.4048Z" fill="#3271C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.91724 16.4048H10.2551V14.3773H9.91724V16.4048Z" fill="#3271C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.2825 16.4048H12.6203V14.3773H12.2825V16.4048Z" fill="#3271C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.2961 16.4048H13.6341V14.3773H13.2961V16.4048Z" fill="#3271C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.3098 16.4048H14.6478V14.3773H14.3098V16.4048Z" fill="#3271C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.675 16.4048H17.0131V14.3773H16.675V16.4048Z" fill="#3271C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.689 16.4048H18.0267V14.3773H17.689V16.4048Z" fill="#3271C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.7026 16.4048H19.0407V14.3773H18.7026V16.4048Z" fill="#3271C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.0679 16.4048H21.4059V14.3773H21.0679V16.4048Z" fill="#3271C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.0815 16.4048H22.4194V14.3773H22.0815V16.4048Z" fill="#3271C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.0952 16.4048H23.4331V14.3773H23.0952V16.4048Z" fill="#3271C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.90356 12.3501H9.2415V10.3228H8.90356V12.3501Z" fill="#3271C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.91724 12.3501H10.2551V10.3228H9.91724V12.3501Z" fill="#3271C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.2825 12.3501H12.6203V10.3228H12.2825V12.3501Z" fill="#3271C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.2961 12.3501H13.6341V10.3228H13.2961V12.3501Z" fill="#3271C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.3098 12.3501H14.6478V10.3228H14.3098V12.3501Z" fill="#3271C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.675 12.3501H17.0131V10.3228H16.675V12.3501Z" fill="#3271C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.689 12.3501H18.0267V10.3228H17.689V12.3501Z" fill="#3271C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.7026 12.3501H19.0407V10.3228H18.7026V12.3501Z" fill="#3271C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.689 8.29541H18.0267V6.26799H17.689V8.29541Z" fill="#3271C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.675 8.29541H17.0131V6.26799H16.675V8.29541Z" fill="#3271C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.7026 8.29541H19.0407V6.26799H18.7026V8.29541Z" fill="#3271C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.88989 12.3501H8.22773V10.3228H7.88989V12.3501Z" fill="#3271C1"/>
<path d="M35.0944 15.2338C35.0527 15.3191 35.0173 15.3909 34.9898 15.4449C34.975 15.474 34.9631 15.4975 34.9303 15.5558C34.4286 16.525 33.6919 17.1315 32.7576 17.4601C32.0181 17.7201 31.3475 17.7782 30.3428 17.7575C30.3328 17.7572 30.2698 17.7558 30.1672 17.7539L29.9344 17.7502L29.848 17.9673C26.892 25.3801 20.4368 29.3194 11.9152 29.3194C7.04366 29.3194 3.86478 27.4747 2.1111 24.327C1.43435 23.1124 1.00402 21.7495 0.802373 20.3545C0.662378 19.3868 0.643453 18.4479 0.720621 17.7539H2.14554V17.7564L24.7847 17.7564V17.7499C26.1231 17.7348 27.3745 17.3207 28.0027 16.8327L28.2796 16.6176L28.0566 16.3464C27.638 15.838 27.3683 15.093 27.2876 14.3169C27.1917 13.1325 27.4787 11.8782 28.0781 10.9685L28.2279 10.7421L28.4435 10.8868C29.7551 11.9116 30.4287 13.0499 30.5001 14.3708L30.5226 14.7891L30.9247 14.6809C32.2406 14.3265 34.1669 14.4996 34.8739 14.9753L35.1349 15.1508C35.1231 15.1755 35.1112 15.2 35.0944 15.2338ZM2.82142 13.7017H6.53833V17.0736H2.82142V13.7017ZM7.21399 13.7017H10.9309V17.0736H7.21399V13.7017ZM7.21399 9.64684H10.9309V13.0259H7.21399L7.21399 9.64684ZM11.6068 13.7017L15.3237 13.7017V17.0736H11.6068V13.7017ZM11.6068 9.64684L15.3237 9.64684V13.0259L11.6068 13.0259V9.64684ZM15.9994 13.7017H19.7162L19.7162 17.0736H15.9994V13.7017ZM15.9994 9.64684H19.7162V13.0259H15.9994V9.64684ZM15.9994 5.59217H19.7162V8.97105H15.9994L15.9994 5.59217ZM20.392 13.7017H24.1089L24.1089 17.0736L20.392 17.0736L20.392 13.7017ZM35.2493 14.4096C34.4376 13.8632 32.5867 13.6574 31.137 13.9352C30.9499 12.5684 30.1832 11.3821 28.8386 10.3341L28.0404 9.79853L27.5152 10.592C26.8281 11.6349 26.5064 13.0413 26.6148 14.38C26.6947 15.1476 26.932 15.8881 27.3126 16.4746C26.7374 16.7951 25.7937 17.0575 24.7847 17.0709L24.7847 13.0259H20.392V4.91634H15.3237L15.3237 8.97105L6.53833 8.97105L6.53833 13.0259H2.14554L2.14554 17.0736H0.141133L0.0934283 17.3569C-0.0380783 18.1384 -0.036127 19.2782 0.133622 20.4525C0.346588 21.926 0.801983 23.3682 1.52166 24.6598C3.39593 28.0239 6.80093 30 11.9152 30C20.6173 30 27.2834 25.982 30.387 18.4387C31.4352 18.4576 32.1599 18.3907 32.9806 18.1024C34.0675 17.7202 34.9391 17.0044 35.5235 15.8821C35.5562 15.8279 35.6039 15.7319 35.7058 15.5241C35.7425 15.4489 35.7815 15.3684 35.8207 15.288C35.8339 15.2598 35.8467 15.2337 35.8724 15.1805L36 14.9139L35.2493 14.4096Z" fill="#3271C1"/>
</g>
<defs>
<clipPath id="clip0">
<path d="M0 0H36V36H0V0Z" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -4,7 +4,7 @@
<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">{{repoName}}</h2>
<h2 sub-header-title class="custom-h2">{{showCurrentTitle | slice:0:15}}</h2>
</div>
</div>
</section>
@ -19,7 +19,7 @@
</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.IMAGE' | translate}}</button>
type="button" (click)='tabLinkClick("repo-image")'>{{'REPOSITORY.ARTIFACTS' | translate}}</button>
</li>
</ul>
<section id="info" role="tabpanel" aria-labelledby="repo-info" [hidden]='!isCurrentTabContent("info")'>
@ -53,10 +53,10 @@
</form>
</section>
<section id="image" role="tabpanel" aria-labelledby="repo-image" [hidden]='!isCurrentTabContent("image")'>
<div id=images-container>
<hbr-tag ngProjectAs="clr-dg-row-detail" (tagClickEvent)="watchTagClickEvt($event)" (signatureOutput)="saveSignatures($event)"
class="sub-grid-custom" [repoName]="repoName" [registryUrl]="registryUrl" [withNotary]="withNotary" [withAdmiral]="withAdmiral" [hasSignedIn]="hasSignedIn"
[isGuest]="isGuest" [projectId]="projectId" [memberRoleID]="memberRoleID"></hbr-tag>
<div id="images-container">
<artifact-list-tab ngProjectAs="clr-dg-row-detail" (tagClickEvent)="watchTagClickEvt($event)" (signatureOutput)="saveSignatures($event)"
class="sub-grid-custom" [repoName]="repoName" artifact [registryUrl]="registryUrl" [withNotary]="withNotary" [withAdmiral]="withAdmiral" [hasSignedIn]="hasSignedIn"
[isGuest]="isGuest" [projectId]="projectId" [memberRoleID]="memberRoleID" (putReferArtifactArray)="putReferArtifactArray($event)"></artifact-list-tab>
</div>
</section>
</div>

View File

@ -1,27 +1,19 @@
import { ComponentFixture, TestBed, async, } from '@angular/core/testing';
import { DebugElement} from '@angular/core';
import { DebugElement, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
import { SharedModule } from '../../utils/shared/shared.module';
import { ConfirmationDialogComponent } from '../confirmation-dialog';
import { ImageNameInputComponent } from "../image-name-input/image-name-input.component";
import { RepositoryComponent } from './repository.component';
import { GridViewComponent } from '../gridview/grid-view.component';
import { FilterComponent } from '../filter/filter.component';
import { TagComponent } from '../tag/tag.component';
import { ArtifactListComponent } from './artifact-list.component';
import { ErrorHandler } from '../../utils/error-handler';
import { Repository, RepositoryItem, Tag, SystemInfo, Label } from '../../services';
import { Repository, RepositoryItem, Tag, SystemInfo, Label, ArtifactService, ArtifactDefaultService } from '../../services';
import { SERVICE_CONFIG, IServiceConfig } from '../../entities/service.config';
import { RepositoryService, RepositoryDefaultService } from '../../services';
import { SystemInfoService, SystemInfoDefaultService } from '../../services';
import { TagService, TagDefaultService } from '../../services';
import { LabelPieceComponent } from "../label-piece/label-piece.component";
import { LabelDefaultService, LabelService } from "../../services";
import { OperationService } from "../operation/operation.service";
import {
ProjectDefaultService,
ProjectService,
RetagDefaultService,
RetagService, ScanningResultDefaultService,
RetagService,
ScanningResultService
} from "../../services";
import { UserPermissionDefaultService, UserPermissionService } from "../../services";
@ -31,20 +23,19 @@ import { delay } from 'rxjs/operators';
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { ChannelService } from "../../services/channel.service";
import { HarborLibraryModule } from "../../harbor-library.module";
import { Artifact, Reference } from '../artifact/artifact';
import { ClarityModule } from '@clr/angular';
import { ActivatedRoute } from '@angular/router';
class RouterStub {
navigateByUrl(url: string) { return url; }
}
describe('ArtifactListComponent (inline template)', () => {
describe('RepositoryComponent (inline template)', () => {
let compRepo: RepositoryComponent;
let fixture: ComponentFixture<RepositoryComponent>;
let compRepo: ArtifactListComponent;
let fixture: ComponentFixture<ArtifactListComponent>;
let repositoryService: RepositoryService;
let systemInfoService: SystemInfoService;
let userPermissionService: UserPermissionService;
let tagService: TagService;
let artifactService: ArtifactService;
let labelService: LabelService;
let spyRepos: jasmine.Spy;
@ -52,7 +43,21 @@ describe('RepositoryComponent (inline template)', () => {
let spySystemInfo: jasmine.Spy;
let spyLabels: jasmine.Spy;
let spyLabels1: jasmine.Spy;
let mockPojectService = {
getProject: () => of({ name: "library" })
};
let mockActivatedRoute = {
data: of(
{
projectResolver: {
name: 'library'
}
}
)
};
let mockChannelService = {
scanCommand$: of(1)
};
let mockSystemInfo: SystemInfo = {
'with_notary': true,
'with_admiral': false,
@ -87,23 +92,71 @@ describe('RepositoryComponent (inline template)', () => {
];
let mockRepo: Repository = {
metadata: {xTotalCount: 2},
metadata: { xTotalCount: 2 },
data: mockRepoData
};
let mockTagData: Tag[] = [
let mockArtifactData: Artifact[] = [
{
'digest': 'sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55',
'name': '1.11.5',
'size': '2049',
'architecture': 'amd64',
'os': 'linux',
'os.version': '',
'docker_version': '1.12.3',
'author': 'NGINX Docker Maintainers \"docker-maint@nginx.com\"',
'created': new Date('2016-11-08T22:41:15.912313785Z'),
'signature': null,
'labels': []
"id": 1,
type: 'image',
repository: "goharbor/harbor-portal",
tags: [{
id: '1',
artifact_id: 1,
name: 'tag1',
upload_time: '2020-01-06T09:40:08.036866579Z',
},
{
id: '2',
artifact_id: 2,
name: 'tag2',
upload_time: '2020-01-06T09:40:08.036866579Z',
},],
references: [new Reference(1), new Reference(2)],
media_type: 'string',
"digest": "sha256:4875cda368906fd670c9629b5e416ab3d6c0292015f3c3f12ef37dc9a32fc8d4",
"size": 20372934,
"scan_overview": {
"application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0": {
"report_id": "5e64bc05-3102-11ea-93ae-0242ac140004",
"scan_status": "Error",
"severity": "",
"duration": 118,
"summary": null,
"start_time": "2020-01-07T04:01:23.157711Z",
"end_time": "2020-01-07T04:03:21.662766Z"
}
},
"labels": [
{
"id": 3,
"name": "aaa",
"description": "",
"color": "#0095D3",
"scope": "g",
"project_id": 0,
"creation_time": "2020-01-13T05:44:00.580198Z",
"update_time": "2020-01-13T05:44:00.580198Z",
"deleted": false
},
{
"id": 6,
"name": "dbc",
"description": "",
"color": "",
"scope": "g",
"project_id": 0,
"creation_time": "2020-01-13T08:27:19.279123Z",
"update_time": "2020-01-13T08:27:19.279123Z",
"deleted": false
}
],
"push_time": "2020-01-07T03:33:41.162319Z",
"pull_time": "0001-01-01T00:00:00Z",
hasReferenceArtifactList: [],
noReferenceArtifactList: []
}
];
@ -117,16 +170,16 @@ describe('RepositoryComponent (inline template)', () => {
scope: "p",
update_time: "",
},
{
color: "#9b0d54",
creation_time: "",
description: "",
id: 2,
name: "label1-g",
project_id: 0,
scope: "g",
update_time: "",
}];
{
color: "#9b0d54",
creation_time: "",
description: "",
id: 2,
name: "label1-g",
project_id: 0,
scope: "g",
update_time: "",
}];
let mockLabels1: Label[] = [{
color: "#9b0d54",
@ -138,16 +191,16 @@ describe('RepositoryComponent (inline template)', () => {
scope: "p",
update_time: "",
},
{
color: "#9b0d54",
creation_time: "",
description: "",
id: 2,
name: "label1-g",
project_id: 1,
scope: "p",
update_time: "",
}];
{
color: "#9b0d54",
creation_time: "",
description: "",
id: 2,
name: "label1-g",
project_id: 1,
scope: "p",
update_time: "",
}];
let config: IServiceConfig = {
repositoryBaseEndpoint: '/api/repository/testing',
@ -164,10 +217,10 @@ describe('RepositoryComponent (inline template)', () => {
}
};
const permissions = [
{resource: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE},
{resource: USERSTATICPERMISSION.REPOSITORY.KEY, action: USERSTATICPERMISSION.REPOSITORY.VALUE.PULL},
{resource: USERSTATICPERMISSION.REPOSITORY_TAG.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE},
{resource: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE},
{ resource: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE },
{ resource: USERSTATICPERMISSION.REPOSITORY.KEY, action: USERSTATICPERMISSION.REPOSITORY.VALUE.PULL },
{ resource: USERSTATICPERMISSION.REPOSITORY_TAG.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE },
{ resource: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE },
];
beforeEach(async(() => {
TestBed.configureTestingModule({
@ -175,27 +228,32 @@ describe('RepositoryComponent (inline template)', () => {
SharedModule,
RouterTestingModule,
HarborLibraryModule,
BrowserAnimationsModule
BrowserAnimationsModule,
ClarityModule
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
],
providers: [
ErrorHandler,
{ provide: SERVICE_CONFIG, useValue: config },
{ provide: RepositoryService, useClass: RepositoryDefaultService },
{ provide: ChannelService, useValue: mockChannelService },
{ provide: SystemInfoService, useClass: SystemInfoDefaultService },
{ provide: TagService, useClass: TagDefaultService },
{ provide: ProjectService, useClass: ProjectDefaultService },
{ provide: ArtifactService, useClass: ArtifactDefaultService },
{ provide: ProjectService, useValue: mockPojectService },
{ provide: RetagService, useClass: RetagDefaultService },
{ provide: LabelService, useClass: LabelDefaultService},
{ provide: UserPermissionService, useClass: UserPermissionDefaultService},
{ provide: ChannelService},
{ provide: LabelService, useClass: LabelDefaultService },
{ provide: UserPermissionService, useClass: UserPermissionDefaultService },
{ provide: OperationService },
{ provide: ActivatedRoute, useValue: mockActivatedRoute },
{ provide: ScanningResultService, useValue: fakedScanningResultService }
]
});
}));
beforeEach(() => {
fixture = TestBed.createComponent(RepositoryComponent);
fixture = TestBed.createComponent(ArtifactListComponent);
compRepo = fixture.componentInstance;
@ -204,20 +262,23 @@ describe('RepositoryComponent (inline template)', () => {
compRepo.repoName = 'library/nginx';
repositoryService = fixture.debugElement.injector.get(RepositoryService);
systemInfoService = fixture.debugElement.injector.get(SystemInfoService);
tagService = fixture.debugElement.injector.get(TagService);
artifactService = fixture.debugElement.injector.get(ArtifactService);
userPermissionService = fixture.debugElement.injector.get(UserPermissionService);
labelService = fixture.debugElement.injector.get(LabelService);
spyRepos = spyOn(repositoryService, 'getRepositories').and.returnValues(of(mockRepo).pipe(delay(0)));
spySystemInfo = spyOn(systemInfoService, 'getSystemInfo').and.returnValues(of(mockSystemInfo).pipe(delay(0)));
spyTags = spyOn(tagService, 'getTags').and.returnValues(of(mockTagData).pipe(delay(0)));
spyTags = spyOn(artifactService, 'TriggerArtifactChan$').and.returnValues(of('repoName').pipe(delay(0)));
spyTags = spyOn(artifactService, 'getArtifactList').and.returnValues(of(
{
body: mockArtifactData
}).pipe(delay(0)));
spyLabels = spyOn(labelService, 'getGLabels').and.returnValues(of(mockLabels).pipe(delay(0)));
spyLabels1 = spyOn(labelService, 'getPLabels').and.returnValues(of(mockLabels1).pipe(delay(0)));
spyOn(userPermissionService, "hasProjectPermissions")
.withArgs(compRepo.projectId, permissions )
.and.returnValue(of([mockHasAddLabelImagePermission, mockHasRetagImagePermission,
mockHasDeleteImagePermission, mockHasScanImagePermission]));
.withArgs(compRepo.projectId, permissions)
.and.returnValue(of([mockHasAddLabelImagePermission, mockHasRetagImagePermission,
mockHasDeleteImagePermission, mockHasScanImagePermission]));
fixture.detectChanges();
});
let originalTimeout;
@ -239,11 +300,12 @@ describe('RepositoryComponent (inline template)', () => {
fixture.whenStable().then(() => {
fixture.detectChanges();
let de: DebugElement = fixture.debugElement.query(del => del.classes['datagrid-cell']);
// de = fixture.debugElement.query(By.css('datagrid-cell'));
fixture.detectChanges();
expect(de).toBeTruthy();
let el: HTMLElement = de.nativeElement;
expect(el).toBeTruthy();
expect(el.textContent).toEqual('1.11.5');
expect(el.textContent.trim()).toEqual('sha256:4875cda3');
});
}));
});

View File

@ -11,38 +11,39 @@
// 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, Input, Output, EventEmitter } from '@angular/core';
import { Component, OnInit, ViewChild, Input, Output, EventEmitter, OnDestroy } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { State } from '../../services/interface';
import { RepositoryService } from '../../services/repository.service';
import { Repository, RepositoryItem, Tag, TagClickEvent,
SystemInfo, SystemInfoService, TagService } from '../../services';
import {
RepositoryItem, ArtifactClickEvent,
SystemInfo, SystemInfoService, ArtifactService
} from '../../services';
import { ErrorHandler } from '../../utils/error-handler';
import { ConfirmationState, ConfirmationTargets } from '../../entities/shared.const';
import { ConfirmationDialogComponent, ConfirmationMessage, ConfirmationAcknowledgement } from '../confirmation-dialog';
import { map, catchError } from "rxjs/operators";
import { Observable, throwError as observableThrowError } from "rxjs";
const TabLinkContentMap: {[index: string]: string} = {
const TabLinkContentMap: { [index: string]: string } = {
'repo-info': 'info',
'repo-image': 'image'
};
@Component({
selector: 'hbr-repository',
templateUrl: './repository.component.html',
styleUrls: ['./repository.component.scss']
selector: 'artifact-list',
templateUrl: './artifact-list.component.html',
styleUrls: ['./artifact-list.component.scss']
})
export class RepositoryComponent implements OnInit {
signedCon: {[key: string]: any | string[]} = {};
export class ArtifactListComponent implements OnInit, OnDestroy {
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<TagClickEvent>();
@Output() tagClickEvent = new EventEmitter<ArtifactClickEvent>();
@Output() backEvt: EventEmitter<any> = new EventEmitter<any>();
@Output() putArtifactReferenceArr: EventEmitter<string[]> = new EventEmitter<[]>();
onGoing = false;
editing = false;
@ -56,16 +57,17 @@ export class RepositoryComponent implements OnInit {
timerHandler: any;
@ViewChild('confirmationDialog', {static: false})
@ViewChild('confirmationDialog', { static: false })
confirmationDlg: ConfirmationDialogComponent;
showCurrentTitle: string;
constructor(
private errorHandler: ErrorHandler,
private repositoryService: RepositoryService,
private systemInfoService: SystemInfoService,
private tagService: TagService,
private artifactService: ArtifactService,
private translate: TranslateService,
) { }
) { }
public get registryUrl(): string {
return this.systemInfo ? this.systemInfo.registry_url : '';
@ -83,13 +85,26 @@ export class RepositoryComponent implements OnInit {
this.errorHandler.error('Project ID cannot be unset.');
return;
}
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];
}
});
let refer = JSON.parse(sessionStorage.getItem('reference'));
if (refer && refer.projectId === this.projectId && refer.repo === this.repoName) {
this.putReferArtifactArray(refer.referArray);
}
}
retrieve(state?: State) {
this.repositoryService.getRepositories(this.projectId, this.repoName)
.subscribe( response => {
.subscribe(response => {
if (response.metadata.xTotalCount > 0) {
this.orgImageInfo = response.data[0].description;
this.imageInfo = response.data[0].description;
@ -99,7 +114,7 @@ export class RepositoryComponent implements OnInit {
.subscribe(systemInfo => this.systemInfo = systemInfo, error => this.errorHandler.error(error));
}
saveSignatures(event: {[key: string]: string[]}): void {
saveSignatures(event: { [key: string]: string[] }): void {
Object.assign(this.signedCon, event);
}
@ -107,7 +122,7 @@ export class RepositoryComponent implements OnInit {
this.retrieve();
}
watchTagClickEvt(tagClickEvt: TagClickEvent): void {
watchTagClickEvt(tagClickEvt: ArtifactClickEvent): void {
this.tagClickEvent.emit(tagClickEvt);
}
@ -123,24 +138,9 @@ export class RepositoryComponent implements OnInit {
this.currentTabID = tabID;
}
getTagInfo(repoName: string): Observable<void> {
// this.signedNameArr = [];
this.signedCon[repoName] = [];
return this.tagService
.getTags(repoName)
.pipe(map(items => {
items.forEach((t: Tag) => {
if (t.signature !== null) {
this.signedCon[repoName].push(t.name);
}
});
})
, catchError(error => observableThrowError(error)));
}
goBack(): void {
this.backEvt.emit(this.projectId);
}
goBack(): void {
this.backEvt.emit(this.projectId);
}
hasChanges() {
return this.imageInfo !== this.orgImageInfo;
@ -191,8 +191,19 @@ export class RepositoryComponent implements OnInit {
confirmCancel(ack: ConfirmationAcknowledgement): void {
this.editing = false;
if (ack && ack.source === ConfirmationTargets.CONFIG &&
ack.state === ConfirmationState.CONFIRMED) {
this.reset();
ack.state === ConfirmationState.CONFIRMED) {
this.reset();
}
}
ngOnDestroy(): void {
sessionStorage.removeItem('reference');
}
putReferArtifactArray(referArtifactArray) {
if (referArtifactArray.length) {
this.showCurrentTitle = referArtifactArray[referArtifactArray.length - 1];
this.putArtifactReferenceArr.emit(referArtifactArray);
}
}
}

View File

@ -0,0 +1,30 @@
import { TestBed, inject } from '@angular/core/testing';
import { AdditionsService } from "./additions.service";
import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing";
describe('TagRetentionService', () => {
const testLink: string = '/test';
const data: string = 'testData';
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule
],
providers: [AdditionsService]
});
});
it('should be created and get right data', inject([AdditionsService], (service: AdditionsService) => {
expect(service).toBeTruthy();
service.getDetailByLink(testLink).subscribe(res => {
expect(res).toEqual(data);
}
);
const httpTestingController = TestBed.get(HttpTestingController);
const req = httpTestingController.expectOne(testLink);
expect(req.request.method).toEqual('GET');
req.flush(data);
httpTestingController.verify();
}));
});

View File

@ -0,0 +1,15 @@
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";
@Injectable({
providedIn: 'root',
})
export class AdditionsService {
constructor(private http: HttpClient) {
}
getDetailByLink(link: string): Observable<any> {
return this.http.get(link);
}
}

View File

@ -0,0 +1,40 @@
<ng-container *ngIf="additionLinks">
<h4 class="margin-bottom-025">{{'ARTIFACT.ADDITIONS' | translate}}</h4>
<clr-tabs>
<clr-tab *ngIf="getVulnerability()">
<button clrTabLink id="vulnerability">{{'REPOSITORY.VULNERABILITY' | translate}}</button>
<clr-tab-content id="vulnerability-content" *clrIfActive>
<hbr-artifact-vulnerabilities *ngIf="getBuildHistory()"
[vulnerabilitiesLink]="getVulnerability()"></hbr-artifact-vulnerabilities>
</clr-tab-content>
</clr-tab>
<clr-tab *ngIf="getBuildHistory()">
<button clrTabLink id="build-history">{{ 'REPOSITORY.BUILD_HISTORY' | translate }}</button>
<clr-tab-content *clrIfActive>
<hbr-artifact-build-history [buildHistoryLink]="getBuildHistory()"></hbr-artifact-build-history>
</clr-tab-content>
</clr-tab>
<clr-tab *ngIf="getSummary()">
<button clrTabLink id="summary-link">{{'HELM_CHART.SUMMARY' | translate}}</button>
<clr-tab-content id="summary-content" *clrIfActive>
<hbr-artifact-summary [summaryLink]="getSummary()"></hbr-artifact-summary>
</clr-tab-content>
</clr-tab>
<clr-tab *ngIf="getDependencies()">
<button clrTabLink id="depend-link">{{'HELM_CHART.DEPENDENCIES' | translate}}</button>
<clr-tab-content id="depend-content" *clrIfActive>
<hbr-artifact-dependencies [dependenciesLink]="getDependencies()"></hbr-artifact-dependencies>
</clr-tab-content>
</clr-tab>
<clr-tab *ngIf="getValues()">
<button clrTabLink id="value-link">{{'HELM_CHART.VALUES' | translate}}</button>
<clr-tab-content id="value-content" *clrIfActive>
<hbr-artifact-values [valuesLink]="getValues()"></hbr-artifact-values>
</clr-tab-content>
</clr-tab>
</clr-tabs>
</ng-container>

View File

@ -0,0 +1,3 @@
.margin-bottom-025 {
margin-bottom: 0.25rem;
}

View File

@ -0,0 +1,46 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ArtifactAdditionsComponent } from './artifact-additions.component';
import { AdditionLinks } from "../../../../../ng-swagger-gen/models/addition-links";
import { HarborLibraryModule } from "../../../harbor-library.module";
import { IServiceConfig, SERVICE_CONFIG } from "../../../entities/service.config";
describe('ArtifactAdditionsComponent', () => {
const mockedAdditionLinks: AdditionLinks = {
vulnerabilities: {
absolute: false,
href: "api/v2/test"
}
};
const config: IServiceConfig = {
baseEndpoint: "/api/v2"
};
let component: ArtifactAdditionsComponent;
let fixture: ComponentFixture<ArtifactAdditionsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
HarborLibraryModule
],
providers: [
{ provide: SERVICE_CONFIG, useValue: config },
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ArtifactAdditionsComponent);
component = fixture.componentInstance;
component.additionLinks = mockedAdditionLinks;
fixture.detectChanges();
});
it('should create and render vulnerabilities tab', async () => {
expect(component).toBeTruthy();
await fixture.whenStable();
const tabButton: HTMLButtonElement = fixture.nativeElement.querySelector('#vulnerability');
expect(tabButton).toBeTruthy();
});
});

View File

@ -0,0 +1,47 @@
import { Component, OnInit, Input, SimpleChanges } from '@angular/core';
import { AdditionLinks } from "../../../../../ng-swagger-gen/models/addition-links";
import { ADDITIONS } from "./models";
import { AdditionLink } from "../../../../../ng-swagger-gen/models/addition-link";
@Component({
selector: 'artifact-additions',
templateUrl: './artifact-additions.component.html',
styleUrls: ['./artifact-additions.component.scss']
})
export class ArtifactAdditionsComponent implements OnInit {
@Input() additionLinks: AdditionLinks;
constructor() { }
ngOnInit() {
}
getVulnerability(): AdditionLink {
if (this.additionLinks && this.additionLinks[ADDITIONS.VULNERABILITIES]) {
return this.additionLinks[ADDITIONS.VULNERABILITIES];
}
return null;
}
getBuildHistory(): AdditionLink {
if (this.additionLinks && this.additionLinks[ADDITIONS.BUILD_HISTORY]) {
return this.additionLinks[ADDITIONS.BUILD_HISTORY];
}
return null;
}
getSummary(): AdditionLink {
if (this.additionLinks && this.additionLinks[ADDITIONS.SUMMARY]) {
return this.additionLinks[ADDITIONS.SUMMARY];
}
return null;
}
getDependencies(): AdditionLink {
if (this.additionLinks && this.additionLinks[ADDITIONS.DEPENDENCIES]) {
return this.additionLinks[ADDITIONS.DEPENDENCIES];
}
return null;
}
getValues(): AdditionLink {
if (this.additionLinks && this.additionLinks[ADDITIONS.VALUES]) {
return this.additionLinks[ADDITIONS.VALUES];
}
return null;
}
}

View File

@ -0,0 +1,65 @@
<div class="row result-row">
<div>
<div class="row flex-items-xs-right rightPos">
<div class="flex-xs-middle option-right">
<hbr-filter [withDivider]="true" filterPlaceholder="{{'VULNERABILITY.PLACEHOLDER' | translate}}"></hbr-filter>
<span class="refresh-btn" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></span>
</div>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid [clrDgLoading]="loading">
<clr-dg-action-bar>
<button type="button" class="btn btn-secondary" [clrLoading]="scanBtnState" [disabled]="!hasEnabledScanner"><clr-icon shape="shield-check" size="16"></clr-icon>&nbsp;{{'VULNERABILITY.SCAN_NOW' | translate}}</button>
</clr-dg-action-bar>
<clr-dg-column [clrDgField]="'id'">{{'VULNERABILITY.GRID.COLUMN_ID' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="severitySort">{{'VULNERABILITY.GRID.COLUMN_SEVERITY' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'package'">{{'VULNERABILITY.GRID.COLUMN_PACKAGE' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'version'">{{'VULNERABILITY.GRID.COLUMN_VERSION' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'fix_version'">{{'VULNERABILITY.GRID.COLUMN_FIXED' | translate}}</clr-dg-column>
<clr-dg-placeholder>{{'VULNERABILITY.CHART.TOOLTIPS_TITLE_ZERO' | translate}}</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let res of scanningResults">
<clr-dg-cell>
<span *ngIf="!res.links || res.links.length === 0">{{res.id}}</span>
<a *ngIf="res.links && res.links.length === 1" href="{{res.links[0]}}" target="_blank">{{res.id}}</a>
<span *ngIf="res.links && res.links.length > 1">
{{res.id}}
<clr-signpost>
<clr-signpost-content *clrIfOpen>
<div class="mt-5px" *ngFor="let link of res.links">
<a href="{{link}}" target="_blank">{{link}}</a>
</div>
</clr-signpost-content>
</clr-signpost>
</span>
</clr-dg-cell>
<clr-dg-cell [ngSwitch]="res.severity">
<span *ngSwitchCase="'Critical'" class="label label-critical no-border">{{severityText(res.severity) | translate}}</span>
<span *ngSwitchCase="'High'" class="label label-danger no-border">{{severityText(res.severity) | translate}}</span>
<span *ngSwitchCase="'Medium'" class="label label-medium no-border">{{severityText(res.severity) | translate}}</span>
<span *ngSwitchCase="'Low'" class="label label-low no-border">{{severityText(res.severity) | translate}}</span>
<span *ngSwitchCase="'Negligible'" class="label label-negligible no-border">{{severityText(res.severity) | translate}}</span>
<span *ngSwitchCase="'Unknown'" class="label label-unknown no-border">{{severityText(res.severity) | translate}}</span>
<span *ngSwitchDefault>{{severityText(res.severity) | translate}}</span>
</clr-dg-cell>
<clr-dg-cell>{{res.package}}</clr-dg-cell>
<clr-dg-cell>{{res.version}}</clr-dg-cell>
<clr-dg-cell>
<div *ngIf="res.fix_version; else elseBlock">
<clr-icon shape="wrench" class="is-success" size="20"></clr-icon>&nbsp;<span class="font-color-green">{{res.fix_version}}</span>
</div>
<ng-template #elseBlock>{{res.fix_version}}</ng-template>
</clr-dg-cell>
<clr-dg-row-detail *clrIfExpanded>
{{'VULNERABILITY.GRID.COLUMN_DESCRIPTION' | translate}}: {{res.description}}
</clr-dg-row-detail>
</clr-dg-row>
<clr-dg-footer>
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'VULNERABILITY.GRID.FOOT_OF' | translate}}</span> {{pagination.totalItems}} {{'VULNERABILITY.GRID.FOOT_ITEMS' | translate}}
<clr-dg-pagination #pagination [clrDgPageSize]="25" [clrDgTotalItems]="scanningResults.length"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
</div>

View File

@ -0,0 +1,14 @@
.result-row {
position: relative;
}
.rightPos{
position: absolute;
z-index: 100;
right: 35px;
margin-top: 1.25rem;
}
.option-right {
padding-right: 16px;
}

View File

@ -0,0 +1,88 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ArtifactVulnerabilitiesComponent } from './artifact-vulnerabilities.component';
import { NO_ERRORS_SCHEMA } from "@angular/core";
import { ClarityModule } from "@clr/angular";
import { ErrorHandler } from "../../../../utils/error-handler";
import { AdditionsService } from "../additions.service";
import { VulnerabilityItem } from "../../../../services";
import { of } from "rxjs";
import { TranslateFakeLoader, TranslateLoader, TranslateModule } from "@ngx-translate/core";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { AdditionLink } from "../../../../../../ng-swagger-gen/models/addition-link";
describe('ArtifactVulnerabilitiesComponent', () => {
let component: ArtifactVulnerabilitiesComponent;
let fixture: ComponentFixture<ArtifactVulnerabilitiesComponent>;
const mockedVulnerabilities: VulnerabilityItem[] = [
{
id: '123',
severity: 'low',
package: 'test',
version: '1.0',
links: ['testLink'],
fix_version: '1.1.1',
description: 'just a test'
},
{
id: '456',
severity: 'high',
package: 'test',
version: '1.0',
links: ['testLink'],
fix_version: '1.1.1',
description: 'just a test'
},
];
const mockedLink: AdditionLink = {
absolute: false,
href: '/test'
};
const fakedAdditionsService = {
getDetailByLink() {
return of(mockedVulnerabilities);
}
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
BrowserAnimationsModule,
ClarityModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateFakeLoader,
}
})
],
declarations: [ArtifactVulnerabilitiesComponent],
providers: [
ErrorHandler,
{provide: AdditionsService, useValue: fakedAdditionsService}
],
schemas: [
NO_ERRORS_SCHEMA
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ArtifactVulnerabilitiesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should get vulnerability list and render', async () => {
component.vulnerabilitiesLink = mockedLink;
component.ngOnInit();
fixture.detectChanges();
await fixture.whenStable();
const rows = fixture.nativeElement.getElementsByTagName('clr-dg-row');
expect(rows.length).toEqual(2);
});
});

View File

@ -0,0 +1,93 @@
import { Component, Input, OnInit } from '@angular/core';
import { AdditionLink } from "../../../../../../ng-swagger-gen/models/addition-link";
import { ErrorHandler } from "../../../../utils/error-handler";
import { AdditionsService } from "../additions.service";
import {
VulnerabilityItem
} from "../../../../services";
import { ClrDatagridComparatorInterface, ClrLoadingState } from "@clr/angular";
import { SEVERITY_LEVEL_MAP, VULNERABILITY_SEVERITY } from "../../../../utils/utils";
import { finalize } from "rxjs/operators";
@Component({
selector: 'hbr-artifact-vulnerabilities',
templateUrl: './artifact-vulnerabilities.component.html',
styleUrls: ['./artifact-vulnerabilities.component.scss']
})
export class ArtifactVulnerabilitiesComponent implements OnInit {
@Input()
vulnerabilitiesLink: AdditionLink;
scanningResults: VulnerabilityItem[] = [];
loading: boolean = false;
shouldShowLoading: boolean = true;
hasEnabledScanner: boolean = false;
scanBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
severitySort: ClrDatagridComparatorInterface<VulnerabilityItem>;
constructor(
private errorHandler: ErrorHandler,
private additionsService: AdditionsService,
) {
const that = this;
this.severitySort = {
compare(a: VulnerabilityItem, b: VulnerabilityItem): number {
return that.getLevel(a) - that.getLevel(b);
}
};
}
ngOnInit() {
this.getVulnerabilities();
}
getVulnerabilities() {
if (this.vulnerabilitiesLink
&& !this.vulnerabilitiesLink.absolute
&& this.vulnerabilitiesLink.href) {
// only show loading for one time
if (this.shouldShowLoading) {
this.loading = true;
this.shouldShowLoading = false;
}
this.additionsService.getDetailByLink(this.vulnerabilitiesLink.href)
.pipe(finalize(() => this.loading = false))
.subscribe(
res => {
this.scanningResults = res;
}, error => {
this.errorHandler.error(error);
}
);
}
}
getLevel(v: VulnerabilityItem): number {
if (v && v.severity && SEVERITY_LEVEL_MAP[v.severity]) {
return SEVERITY_LEVEL_MAP[v.severity];
}
return 0;
}
refresh(): void {
this.getVulnerabilities();
}
severityText(severity: string): string {
switch (severity) {
case VULNERABILITY_SEVERITY.CRITICAL:
return 'VULNERABILITY.SEVERITY.CRITICAL';
case VULNERABILITY_SEVERITY.HIGH:
return 'VULNERABILITY.SEVERITY.HIGH';
case VULNERABILITY_SEVERITY.MEDIUM:
return 'VULNERABILITY.SEVERITY.MEDIUM';
case VULNERABILITY_SEVERITY.LOW:
return 'VULNERABILITY.SEVERITY.LOW';
case VULNERABILITY_SEVERITY.NEGLIGIBLE:
return 'VULNERABILITY.SEVERITY.NEGLIGIBLE';
case VULNERABILITY_SEVERITY.UNKNOWN:
return 'VULNERABILITY.SEVERITY.UNKNOWN';
default:
return 'UNKNOWN';
}
}
}

View File

@ -1,11 +1,9 @@
<clr-datagrid [clrDgLoading]="loading">
<clr-dg-column class="history-time">{{ 'TAG.CREATION' | translate }}</clr-dg-column>
<clr-dg-column>{{ 'TAG.COMMAND' | translate }}</clr-dg-column>
<clr-dg-row *clrDgItems="let h of history" [clrDgItem]='h' class="history-item">
<clr-dg-row *clrDgItems="let h of historyList" [clrDgItem]='h' class="history-item">
<clr-dg-cell>{{ h.created | date: 'short' }}</clr-dg-cell>
<clr-dg-cell>{{ h.created_by }}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{ history.length }} commands</clr-dg-footer>
<clr-dg-footer>{{ historyList.length }} commands</clr-dg-footer>
</clr-datagrid>

View File

@ -0,0 +1,77 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from "@angular/core";
import { ClarityModule } from "@clr/angular";
import { ErrorHandler } from "../../../../utils/error-handler";
import { AdditionsService } from "../additions.service";
import { of } from "rxjs";
import { TranslateFakeLoader, TranslateLoader, TranslateModule } from "@ngx-translate/core";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { AdditionLink } from "../../../../../../ng-swagger-gen/models/addition-link";
import { BuildHistoryComponent } from "./build-history.component";
import { ArtifactBuildHistory } from "../models";
describe('BuildHistoryComponent', () => {
let component: BuildHistoryComponent;
let fixture: ComponentFixture<BuildHistoryComponent>;
const mockedLink: AdditionLink = {
absolute: false,
href: '/test'
};
const mockedHistoryList: ArtifactBuildHistory[] = [
{
created: new Date(),
created_by: 'test command'
},
{
created: new Date(new Date().getTime() + 123456),
created_by: 'test command'
},
];
const fakedAdditionsService = {
getDetailByLink() {
return of(mockedHistoryList);
}
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
BrowserAnimationsModule,
ClarityModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateFakeLoader,
}
})
],
declarations: [BuildHistoryComponent],
providers: [
ErrorHandler,
{provide: AdditionsService, useValue: fakedAdditionsService}
],
schemas: [
NO_ERRORS_SCHEMA
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BuildHistoryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should get build history list and render', async () => {
component.buildHistoryLink = mockedLink;
component.ngOnInit();
fixture.detectChanges();
await fixture.whenStable();
const rows = fixture.nativeElement.getElementsByTagName('clr-dg-row');
expect(rows.length).toEqual(2);
});
});

View File

@ -0,0 +1,57 @@
import { Component, Input, OnInit } from "@angular/core";
import { ErrorHandler } from "../../../../utils/error-handler";
import { AdditionsService } from "../additions.service";
import { ArtifactBuildHistory } from "../models";
import { AdditionLink } from "../../../../../../ng-swagger-gen/models/addition-link";
import { finalize } from "rxjs/operators";
@Component({
selector: "hbr-artifact-build-history",
templateUrl: "./build-history.component.html",
styleUrls: ["./build-history.component.scss"],
})
export class BuildHistoryComponent implements OnInit {
@Input()
buildHistoryLink: AdditionLink;
historyList: ArtifactBuildHistory[] = [];
loading: Boolean = false;
constructor(
private errorHandler: ErrorHandler,
private additionsService: AdditionsService
) {
}
ngOnInit(): void {
this.getBuildHistory();
}
getBuildHistory() {
if (this.buildHistoryLink
&& !this.buildHistoryLink.absolute
&& this.buildHistoryLink.href) {
this.loading = true;
this.additionsService.getDetailByLink(this.buildHistoryLink.href)
.pipe(finalize(() => this.loading = false))
.subscribe(
res => {
if (res && res.length) {
res.forEach((ele: any) => {
const history: ArtifactBuildHistory = new ArtifactBuildHistory();
history.created = ele.created;
if (ele.created_by !== undefined) {
history.created_by = ele.created_by
.replace("/bin/sh -c #(nop)", "")
.trimLeft()
.replace("/bin/sh -c", "RUN");
} else {
history.created_by = ele.comment;
}
this.historyList.push(history);
});
}
}, error => {
this.errorHandler.error(error);
}
);
}
}
}

View File

@ -0,0 +1,20 @@
<div class="row flex-items-xs-center dep-container">
<div class="col-md-12">
<table class="table">
<thead>
<tr>
<th class="left">{{'HELM_CHART.NAME' | translate}}</th>
<th class="left">{{'HELM_CHART.VERSION' | translate}}</th>
<th class="left">{{'HELM_CHART.REPO' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let dep of dependencyList">
<td class="left">{{dep.name}}</td>
<td class="left">{{dep.version}}</td>
<td class="left"><a href="{{dep.repository}}">{{dep.repository}}</a></td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,3 @@
.dep-container {
margin-top: 30px;
}

View File

@ -0,0 +1,74 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { DependenciesComponent } from "./dependencies.component";
import { ErrorHandler } from '../../../../utils/error-handler';
import { AdditionsService } from '../additions.service';
import { of } from 'rxjs';
import { SERVICE_CONFIG, IServiceConfig } from '../../../../entities/service.config';
import { ArtifactDependency } from "../models";
import { AdditionLink } from "../../../../../../ng-swagger-gen/models/addition-link";
describe('DependenciesComponent', () => {
let component: DependenciesComponent;
let fixture: ComponentFixture<DependenciesComponent>;
const mockErrorHandler = {
error: () => { }
};
const mockedDependencies: ArtifactDependency[] = [
{
name: 'abc',
version: 'v1.0',
repository: 'test1'
},
{
name: 'def',
version: 'v1.1',
repository: 'test2'
}
];
const mockAdditionsService = {
getDetailByLink: () => of(mockedDependencies)
};
const mockedLink: AdditionLink = {
absolute: false,
href: '/test'
};
const config: IServiceConfig = {
repositoryBaseEndpoint: "/api/repositories/testing"
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot()
],
declarations: [DependenciesComponent],
providers: [
TranslateService,
{ provide: SERVICE_CONFIG, useValue: config },
{
provide: ErrorHandler, useValue: mockErrorHandler
},
{ provide: AdditionsService, useValue: mockAdditionsService },
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DependenciesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should get dependencies and render', async () => {
component.dependenciesLink = mockedLink;
component.ngOnInit();
fixture.detectChanges();
await fixture.whenStable();
const trs = fixture.nativeElement.getElementsByTagName('tr');
expect(trs.length).toEqual(3);
});
});

View File

@ -0,0 +1,39 @@
import {
Component,
OnInit,
Input,
} from "@angular/core";
import { ArtifactDependency } from "../models";
import { ErrorHandler } from "../../../../utils/error-handler";
import { AdditionsService } from "../additions.service";
import { AdditionLink } from "../../../../../../ng-swagger-gen/models/addition-link";
@Component({
selector: "hbr-artifact-dependencies",
templateUrl: "./dependencies.component.html",
styleUrls: ["./dependencies.component.scss"],
})
export class DependenciesComponent implements OnInit {
@Input()
dependenciesLink: AdditionLink;
dependencyList: ArtifactDependency[] = [];
constructor( private errorHandler: ErrorHandler,
private additionsService: AdditionsService) {}
ngOnInit(): void {
this.getDependencyList();
}
getDependencyList() {
if (this.dependenciesLink
&& !this.dependenciesLink.absolute
&& this.dependenciesLink.href) {
this.additionsService.getDetailByLink(this.dependenciesLink.href).subscribe(
res => {
this.dependencyList = res;
}, error => {
this.errorHandler.error(error);
}
);
}
}
}

View File

@ -0,0 +1,41 @@
import { HelmChartMaintainer } from "../../../../app/project/helm-chart/helm-chart.interface.service";
export class ArtifactBuildHistory {
created: Date;
created_by: string;
}
export interface ArtifactDependency {
name: string;
version: string;
repository: string;
}
export interface ArtifactSummary {
name: string;
home: string;
sources: string[];
version: string;
description: string;
keywords: string[];
maintainers: HelmChartMaintainer[];
engine: string;
icon: string;
appVersion: string;
urls: string[];
created?: string;
digest: string;
}
export interface Addition {
type: string;
data?: object;
}
export enum ADDITIONS {
VULNERABILITIES = 'vulnerabilities',
BUILD_HISTORY = 'build_history',
SUMMARY = 'readme',
VALUES = 'values.yaml',
DEPENDENCIES = 'dependencies'
}

View File

@ -0,0 +1,7 @@
<div class="row content-wrapper">
<div class="col-md-8 md-container pl-1">
<div *ngIf="readme" class="md-div" [innerHTML]="readme | markdown"></div>
<div *ngIf="!readme">{{'HELM_CHART.NO_README' | translate}}</div>
</div>
</div>
<table class="table"></table>

View File

@ -0,0 +1,7 @@
.content-wrapper {
margin-top: 20px;
padding: 0 0 0 15px;
.md-container {
border: solid 1px #ddd;
}
}

View File

@ -0,0 +1,199 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from "@angular/core";
import { ErrorHandler } from "../../../../utils/error-handler";
import { AdditionsService } from "../additions.service";
import { of } from "rxjs";
import { AdditionLink } from "../../../../../../ng-swagger-gen/models/addition-link";
import { SummaryComponent } from "./summary.component";
import { HarborLibraryModule } from "../../../../harbor-library.module";
import { IServiceConfig, SERVICE_CONFIG } from "../../../../entities/service.config";
describe('SummaryComponent', () => {
let component: SummaryComponent;
let fixture: ComponentFixture<SummaryComponent>;
const mockedLink: AdditionLink = {
absolute: false,
href: '/test'
};
const readme: string = "# Helm Chart for Harbor\n\n## Introduction\n\nThis [Helm](https://github.com/" +
"kubernetes/helm) chart installs [Harbor](http://vmware.github.io/harbor/) in a Kubernetes " +
"cluster. Currently this chart supports Harbor v1.4.0 release. Welcome to [contribute](CONTR" +
"IBUTING.md) to Helm Chart for Harbor.\n\n## Prerequisites\n\n- Kubernetes cluster 1.8+ with " +
"Beta APIs enabled\n- Kubernetes Ingress Controller is enabled\n- kubectl CLI 1.8+\n- Helm CLI" +
" 2.8.0+\n\n## Known Issues\n\n- This chart doesn't work with Kubernetes security update release" +
" 1.8.9+ and 1.9.4+. Refer to [issue 4496](https://github.com/vmware/harbor/issues/4496).\n\n## " +
"Setup a Kubernetes cluster\n\nYou can use any tools to setup a K8s cluster.\nIn this guide," +
" we use [minikube](https://github.com/kubernetes/minikube) 0.25.0 to setup a K8s cluster as " +
"the dev/test env.\n```bash\n# Start minikube\nminikube start --vm-driver=none\n# Enable Ingress" +
" Controller\nminikube addons enable ingress\n```\n## Installing the Chart\n\nFirst install" +
" [Helm CLI](https://github.com/kubernetes/helm#install), then initialize Helm.\n```bash\nhelm" +
" init\n```\nDownload Harbor helm chart code.\n```bash\ngit clone https://github.com/vmware/" +
"harbor\ncd harbor/contrib/helm/harbor\n```\nDownload external dependent charts required by" +
" Harbor chart.\n```bash\nhelm dependency update\n```\n### Secure Registry Mode\n\nBy default " +
"this chart will generate a root CA and SSL certificate for your Harbor.\nYou can also use your" +
" own CA signed certificate:\n\nopen values.yaml, set the value of 'externalDomain' to" +
" your Harbor FQDN, and\nset value of 'tlsCrt', 'tlsKey', 'caCrt'. The common name of the " +
"certificate must match your Harbor FQDN.\n\nInstall the Harbor helm chart with a release " +
"name `my-release`:\n```bash\nhelm install . --debug --name my-release --set externalDomain" +
"=harbor.my.domain\n```\n**Make sure** `harbor.my.domain` resolves to the K8s Ingress Contr" +
"oller IP on the machines where you run docker or access Harbor UI.\nYou can add `harbor.my.domain`" +
" and IP mapping in the DNS server, or in /etc/hosts, or use the FQDN `harbor.\u003cIP\u003e.xip." +
"io`.\n\nFollow the `NOTES` section in the command output to get Harbor admin password and **add " +
"Harbor root CA into docker trusted certificates**.\n\nIf you are using an external service like " +
"[cert-manager](https://github.com/jetstack/cert-manager) for generating the TLS certificates,\nyou" +
"will want to disable the certificate generation by helm by setting the value `generateCertificates` " +
"to _false_. Then the ingress' annotations will be scanned\nby _cert-manager_ and the appropriate " +
"secret will get created and updated by the service.\n\nIf using acme's certificates, do not forget to " +
"add the following annotation to\nyour ingress.\n\n```yaml\ningress:\n annotations:\n kubernetes.io/" +
"tls-acme: \"true\"\n```\n\nThe command deploys Harbor on the Kubernetes cluster in the default " +
"configuration.\nThe [configuration](#configuration) section lists the parameters that can be configured" +
" in values.yaml or via '--set' params during installation.\n\n\u003e **Tip**: List all releases using" +
" `helm list`\n\n\n### Insecure Registry Mode\n\nIf setting Harbor Registry as insecure-registries for " +
"docker,\nyou don't need to generate Root CA and SSL certificate for the Harbor ingress controller.\n\nInstal" +
"l the Harbor helm chart with a release name `my-release`:\n```bash\nhelm install . --debug --name my-release" +
" --set externalDomain=harbor.my.domain,insecureRegistry=true\n```\n**Make sure** `harbor.my.domain` resolves" +
" to the K8s Ingress Controller IP on the machines where you run docker or access Harbor UI.\nYou can add" +
" `harbor.my.domain` and IP mapping in the DNS server, or in /etc/hosts, or use the FQDN " +
"`harbor.\u003cIP\u003e.xip.io`.\n\nThen add `\"insecure-registries\": [\"harbor.my.domain\"]`" +
" in the docker daemon config file and restart docker service.\n\n## Uninstalling the Chart\n\nTo " +
"uninstall/delete the `my-release` deployment:\n\n```bash\nhelm delete my-release\n```\n\nThe command " +
"removes all the Kubernetes components associated with the chart and deletes the release.\n\n## " +
"Configuration\n\nThe following tables lists the configurable parameters of the Harbor chart and the " +
"default values.\n\n| Parameter | Description " +
"| Default |\n| ----------------------- | ---------------------------------- | ----" +
"------------------- |\n| **Harbor** |\n| `harborImageTag` | The tag for Harbor docker images | " +
"`v1.4.0` |\n| `externalDomain` | Harbor will run on (https://`externalDomain`/). Recommend using" +
" K8s Ingress Controller FQDN as `externalDomain`, or make sure this FQDN resolves to the K8s Ingress" +
" Controller IP. | `harbor.my.domain` |\n| `insecureRegistry` | If set to true, you don't need to" +
" set tlsCrt/tlsKey/caCrt, but must add Harbor FQDN as insecure-registries for your docker client. " +
"| `false` |\n| `generateCertificates` | Set to false if TLS certificate will be managed by an external " +
"service | `true` |\n| `tlsCrt` | TLS certificate to use for Harbor's https endpoint. Its" +
" CN must match `externalDomain`. | auto-generated |\n| `tlsKey` | TLS key to use for " +
"Harbor's https endpoint | auto-generated |\n| `caCrt` | CA Cert for self signed TLS cert" +
" | auto-generated |\n| `persistence.enabled` | enable persistent data storage | `false` |\n| `secretKey` " +
"| The secret key used for encryption. Must be a string of 16 chars. | " +
"`not-a-secure-key` |\n| **Adminserver** |\n| `adminserver.image.repository`" +
" | Repository for adminserver image | `vmware/harbor-adminserver` |\n| `adminserver.image.tag`" +
" | Tag for adminserver image | `v1.4.0` |\n| `adminserver.image.pullPolicy` | " +
"Pull Policy for adminserver image | `IfNotPresent` |\n| `adminserver.emailHost` |" +
" email server | `smtp.mydomain.com` |\n| `adminserver.emailPort` | email port | `25` |\n| " +
"`adminserver.emailUser` | email username | `sample_admin@mydomain.com` |\n| `adminserver.emailSsl` " +
"| email uses SSL? | `false` |\n| `adminserver.emailFrom` | send email from address | `admin \u003csample_admin@" +
"mydomain.com\u003e` |\n| `adminserver.emailIdentity` | | \"\" |\n| `adminserver.key` | adminsever key | " +
"`not-a-secure-key` |\n| `adminserver.emailPwd` | password for email | `not-a-secure-password` |\n| `adminserver." +
"adminPassword` | password for admin user | `Harbor12345` |\n| `adminserver.authenticationMode` | authentication" +
" mode for Harbor ( `db_auth` for local database, `ldap_auth` for LDAP, etc...) [Docs](https://github.com/vmware/" +
"harbor/blob/master/docs/user_guide.md#user-account) | `db_auth` |\n| `adminserver.selfRegistration` | Allows users" +
" to register by themselves, otherwise only administrators can add users | `on` |\n| `adminserver.ldap.url` | LDAP" +
" server URL for `ldap_auth` authentication | `ldaps://ldapserver` |\n| `adminserver.ldap.searchDN` |" +
" LDAP Search DN | `` |\n| `adminserver.ldap.baseDN` | LDAP Base DN | `` |\n| `adminserver.ldap.filter` | LDAP Filter " +
"| `(objectClass=person)` |\n| `adminserver.ldap.uid` | LDAP UID | `uid` |\n| `adminserver.ldap.scope` | LDAP Scope" +
" | `2` |\n| `adminserver.ldap.timeout` | LDAP Timeout | `5` |\n| `adminserver.ldap.verifyCert` | LDAP Verify " +
"HTTPS Certificate | `True` |\n| `adminserver.resources` | [resources](https://kubernetes.io/docs/concepts" +
"/configuration/manage-compute-resources-container/) to allocate for container | undefined |\n| " +
"`adminserver.volumes` | used to create PVCs if persistence is enabled (see instructions in values.yaml) | " +
"see values.yaml |\n| `adminserver.nodeSelector` | Node labels for pod assignment | `{}` |\n| `adminserver." +
"tolerations` | Tolerations for pod assignment | `[]` |\n| `adminserver.affinity` | Node/Pod affinities " +
"| `{}` |\n| **Jobservice** |\n| `jobservice.image.repository` | Repository for jobservice image | `vmware" +
"/harbor-jobservice` |\n| `jobservice.image.tag` | Tag for jobservice image | `v1.4.0` |\n| `jobservice." +
"image.pullPolicy` | Pull Policy for jobservice image | `IfNotPresent` |\n| `jobservice.key` | jobservice" +
"key | `not-a-secure-key` |\n| `jobservice.secret` | jobservice secret | `not-a-secure-secret` |\n| " +
"`jobservice.resources` | [resources](https://kubernetes.io/docs/concepts/configuration/" +
"manage-compute-resources-container/) to allocate for container | undefined |\n| `jobservice.nodeSelector` " +
"| Node labels for pod assignment | `{}` |\n| `jobservice.tolerations` | Tolerations for pod assignment |" +
" `[]` |\n| `jobservice.affinity` | Node/Pod affinities | `{}` |\n| **UI** |\n| `ui.image.repository` | " +
"epository for ui image | `vmware/harbor-ui` |\n| `ui.image.tag` | Tag for ui image | `v1.4.0` |\n| `ui." +
"image.pullPolicy` | Pull Policy for ui image | `IfNotPresent` |\n| `ui.key` | ui key | `not-a-secure-key" +
"` |\n| `ui.secret` | ui secret | `not-a-secure-secret` |\n| `ui.privateKeyPem` | ui private key | see " +
"values.yaml |\n| `ui.resources` | [resources](https://kubernetes.io/docs/concepts/configuration/manage-" +
"compute-resources-container/) to allocate for container " +
" | undefined |\n| `ui.nodeSelector` | Node labels for pod assignment " +
"| `{}` |\n| `ui.tolerations` | Tolerations for pod assignment | `[]` |\n| `ui.affinity` | Node/Pod affinities" +
" | `{}` |\n| **MySQL** |\n| `mysql.image.repository` | Repository for mysql image | `vmware/harbor-mysql` " +
"|\n| `mysql.image.tag` | Tag for mysql image | `v1.4.0` |\n| `mysql.image.pullPolicy` | Pull Policy for mysql " +
"image | `IfNotPresent` |\n| `mysql.host` | MySQL Server | `~` |\n| `mysql.port` | MySQL Port | `3306` |\n| " +
"`mysql.user` | MySQL Username | `root` |\n| `mysql.pass` | MySQL Password | `registry` |\n| " +
"`mysql.database` | MySQL Database | `registry` |\n| `mysql.resources` | [resources](https://kubernetes.io/" +
"docs/concepts/configuration/manage-compute-resources-container/) to allocate for container | undefined |\n| " +
"`mysql.volumes` | used to create PVCs if persistence is enabled (see instructions in values.yaml) | " +
"see values.yaml |\n| `mysql.nodeSelector` | Node labels for pod assignment | `{}` |\n| `mysql.tolerations` " +
"| Tolerations for pod assignment | `[]` |\n| `mysql.affinity` | Node/Pod affinities" +
" | `{}` |\n| **Registry** |\n| `registry.image.repository` | Repository for registry image | `" +
"vmware/registry-photon` |\n| `registry.image.tag` | Tag for registry image | `v2.6.2-v1.4.0` |\n| " +
"`registry.image.pullPolicy` | Pull Policy for registry image | `IfNotPresent` |\n| `registry.rootCrt` | " +
"registry root cert " +
"| see values.yaml |\n| `registry.httpSecret` | registry secret | `not-a-secure-secret` |\n| `registry.resources` " +
"| [resources](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to allocate" +
" for container | undefined |\n| `registry.volumes` | used to create PVCs if persistence is enabled (see " +
"instructions in values.yaml) | see values.yaml |\n| `registry.nodeSelector` | Node labels for pod assignment " +
"| `{}` |\n| `registry.tolerations` | Tolerations for pod assignment | `[]` |\n| `registry.affinity` | " +
"Node/Pod affinities | `{}` |\n| **Clair** |\n| `clair.enabled` | Enable Clair? | `true` |\n| " +
"`clair.image.repository` | Repository for clair image | `vmware/clair-photon` |\n| `clair.image.tag` |" +
" Tag for clair image | `v2.0.1-v1.4.0`\n| `clair.resources` | [resources](https://kubernetes.io/docs/concepts/" +
"configuration/manage-compute-resources-container/) to allocate for container | undefined\n| `clair.nodeSelector" +
"` | Node labels for pod assignment | `{}` |\n| `clair.tolerations` | Tolerations for pod assignment | `[]` |\n| " +
"`clair.affinity` | Node/Pod affinities | `{}` |\n| `postgresql` | Overrides for postgresql chart [values.yaml](https" +
"://github.com/kubernetes/charts/blob/f2938a46e3ae8e2512ede1142465004094c3c333/stable/postgresql/values.yaml) | " +
"see values.yaml\n| **Notary** |\n| `notary.enabled` | Enable Notary? | `true` |\n| `notary.server.image.repository`" +
" | Repository for notary server image | `vmware/notary-server-photon` |\n| `notary.server.image.tag` | Tag for " +
"notary server image | `v0.5.1-v1.4.0`\n| `notary.signer.image.repository` | Repository for notary signer image |" +
" `vmware/notary-signer-photon` |\n| `notary.signer.image.tag` | Tag for notary signer image | `v0.5.1-v1.4.0`\n|" +
" `notary.db.image.repository` | Repository for notary database image | `vmware/mariadb-photon` |\n|" +
"`notary.db.image.tag` | Tag for notary database image | `v1.4.0`\n| `notary.db.password` | The password of users " +
"for notary database | Specify your own password |\n| `notary.nodeSelector` | Node labels for pod assignment " +
"| `{}` |\n| `notary.tolerations` | Tolerations for pod assignment | `[]` |\n| `notary.affinity` | " +
"Node/Pod affinities | `{}` |\n| **Ingress** |\n| `ingress.enabled` | Enable ingress objects. | `true` " +
"|\n\nSpecify each parameter using the `--set key=value[,key=value]` argument to `helm install`. " +
"For example:\n\n```bash\nhelm install . --name my-release --set externalDomain=" +
"harbor.\u003cIP\u003e.xip.io\n```\n\nAlternatively," +
" a YAML file that specifies the values for the parameters can be provided while installing the chart. For " +
"example,\n\n```bash\nhelm install . --name my-release -f /path/to/values.yaml\n```\n\n\u003e **Tip**: " +
"You can use the default [values.yaml](values.yaml)\n\n## Persistence\n\nHarbor stores the data and " +
"configurations in emptyDir volumes. You can change the values.yaml to enable persistence and use a " +
"PersistentVolumeClaim instead.\n\n\u003e *\"An emptyDir volume is first created when a Pod is " +
"assigned to a Node, and exists as long as that Pod is running on that node. When a Pod is removed " +
"from a node for any reason, the data in the emptyDir is deleted forever.\"*\n";
const fakedAdditionsService = {
getDetailByLink() {
return of(readme);
}
};
const config: IServiceConfig = {
repositoryBaseEndpoint: "/api/repositories/testing"
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
HarborLibraryModule
],
providers: [
ErrorHandler,
{ provide: AdditionsService, useValue: fakedAdditionsService },
{ provide: SERVICE_CONFIG, useValue: config },
],
schemas: [
NO_ERRORS_SCHEMA
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SummaryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should get readme and render', async () => {
component.summaryLink = mockedLink;
component.ngOnInit();
fixture.detectChanges();
await fixture.whenStable();
const tables = fixture.nativeElement.getElementsByTagName('table');
expect(tables.length).toEqual(2);
});
});

View File

@ -0,0 +1,39 @@
import {
Component,
OnInit,
Input
} from "@angular/core";
import { ErrorHandler } from "../../../../utils/error-handler";
import { AdditionsService } from "../additions.service";
import { AdditionLink } from "../../../../../../ng-swagger-gen/models/addition-link";
@Component({
selector: "hbr-artifact-summary",
templateUrl: "./summary.component.html",
styleUrls: ["./summary.component.scss"],
})
export class SummaryComponent implements OnInit {
@Input() summaryLink: AdditionLink;
readme: string;
constructor(
private errorHandler: ErrorHandler,
private additionsService: AdditionsService
) {}
ngOnInit(): void {
this.getReadme();
}
getReadme() {
if (this.summaryLink
&& !this.summaryLink.absolute
&& this.summaryLink.href) {
this.additionsService.getDetailByLink(this.summaryLink.href).subscribe(
res => {
this.readme = res;
}, error => {
this.errorHandler.error(error);
}
);
}
}
}

View File

@ -0,0 +1,23 @@
<div class="row flex-items-xs-between values-header">
<div *ngIf="valueMode" class="title-container">
<label>{{'HELM_CHART.SHOW_KV' | translate }}</label>
</div>
<div class="switch-container">
<span class="card-btn" (click)="showYamlFile(false)" (mouseenter)="mouseEnter('value') " (mouseleave)="mouseLeave('value')">
<clr-icon size="24" shape="view-list" title='list values' [ngClass]="{'is-highlight': isValueMode || isHovering('value') }"></clr-icon>
</span>
</div>
</div>
<div class="row value-container">
<div class="col-xs-8" *ngIf="valueMode">
<table class="table">
<tbody>
<tr *ngFor="let item of values | keyvalue">
<td class="left">{{item?.key}}</td>
<td class="left">{{item?.value}}</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,15 @@
.value-container {
::ng-deep pre {
min-height: fit-content;
}
}
.values-header {
margin-top: 12px;
}
pre {
max-height: max-content;
padding-left: 21px;
}

View File

@ -0,0 +1,72 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ClarityModule } from '@clr/angular';
import { FormsModule } from '@angular/forms';
import { MarkdownModule, MarkdownService, MarkedOptions } from 'ngx-markdown';
import { BrowserModule } from '@angular/platform-browser';
import { ValuesComponent } from "./values.component";
import { AdditionsService } from "../additions.service";
import { ErrorHandler } from "../../../../utils/error-handler";
import { of } from "rxjs";
import { AdditionLink } from "../../../../../../ng-swagger-gen/models/addition-link";
describe('ValuesComponent', () => {
let component: ValuesComponent;
let fixture: ComponentFixture<ValuesComponent>;
const mockedValues = {
"adminserver.image.pullPolicy": "IfNotPresent",
"adminserver.image.repository": "vmware/harbor-adminserver",
"adminserver.image.tag": "dev"
};
const fakedAdditionsService = {
getDetailByLink() {
return of(mockedValues);
}
};
const mockedLink: AdditionLink = {
absolute: false,
href: '/test'
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
MarkdownModule,
ClarityModule,
FormsModule,
BrowserModule
],
declarations: [ValuesComponent],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
],
providers: [
TranslateService,
MarkdownService,
ErrorHandler,
{provide: AdditionsService, useValue: fakedAdditionsService},
{provide: MarkedOptions, useValue: {}},
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ValuesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
/*it('should create', () => {
expect(component).toBeTruthy();
});*/
it('should get values and render', async () => {
component.valuesLink = mockedLink;
component.ngOnInit();
fixture.detectChanges();
await fixture.whenStable();
const trs = fixture.nativeElement.getElementsByTagName('tr');
expect(trs.length).toEqual(3);
});
});

View File

@ -0,0 +1,73 @@
import {
Component,
Input,
OnInit,
} from "@angular/core";
import { ErrorHandler } from "../../../../utils/error-handler";
import { AdditionsService } from "../additions.service";
import { AdditionLink } from "../../../../../../ng-swagger-gen/models/addition-link";
@Component({
selector: "hbr-artifact-values",
templateUrl: "./values.component.html",
styleUrls: ["./values.component.scss"],
})
export class ValuesComponent implements OnInit {
@Input()
valuesLink: AdditionLink;
values: any;
// Default set to yaml file
valueMode = true;
valueHover = false;
yamlHover = true;
constructor(private errorHandler: ErrorHandler,
private additionsService: AdditionsService) {
}
ngOnInit(): void {
if (this.valuesLink && !this.valuesLink.absolute && this.valuesLink.href) {
this.additionsService.getDetailByLink(this.valuesLink.href).subscribe(
res => {
this.values = res;
}, error => {
this.errorHandler.error(error);
}
);
}
}
public get isValueMode() {
return this.valueMode;
}
isHovering(view: string) {
if (view === 'value') {
return this.valueHover;
} else {
return this.yamlHover;
}
}
showYamlFile(showYaml: boolean) {
this.valueMode = !showYaml;
}
mouseEnter(mode: string) {
if (mode === "value") {
this.valueHover = true;
} else {
this.yamlHover = true;
}
}
mouseLeave(mode: string) {
if (mode === "value") {
this.valueHover = false;
} else {
this.yamlHover = false;
}
}
}

View File

@ -0,0 +1,12 @@
<ng-container *ngIf="hasCommonProperties()">
<h4 class="margin-bottom-075">{{'ARTIFACT.COMMON_PROPERTIES' | translate}}</h4>
<clr-stack-view>
<clr-stack-block [clrSbExpanded] = "true">
<clr-stack-label>{{'ARTIFACT.COMMON_ALL' | translate}}</clr-stack-label>
<clr-stack-block *ngFor="let item of commonProperties | keyvalue">
<clr-stack-label>{{item?.key}}</clr-stack-label>
<clr-stack-content>{{item?.value}}</clr-stack-content>
</clr-stack-block>
</clr-stack-block>
</clr-stack-view>
</ng-container>

View File

@ -0,0 +1,3 @@
.margin-bottom-075 {
margin-bottom: 0.75rem;
}

View File

@ -0,0 +1,57 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ArtifactCommonPropertiesComponent } from './artifact-common-properties.component';
import { ExtraAttrs } from "../../../../../ng-swagger-gen/models/extra-attrs";
import { ClarityModule } from "@clr/angular";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { TranslateFakeLoader, TranslateLoader, TranslateModule, TranslateService } from "@ngx-translate/core";
describe('ArtifactCommonPropertiesComponent', () => {
let component: ArtifactCommonPropertiesComponent;
let fixture: ComponentFixture<ArtifactCommonPropertiesComponent>;
const mockedExtraAttrs: ExtraAttrs = {
architecture: "amd64",
author: "",
created: "2019-11-11T09:42:44.892055836Z",
os: "linux"
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
ClarityModule,
BrowserAnimationsModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateFakeLoader,
}
})
],
declarations: [ ArtifactCommonPropertiesComponent ],
providers: [
TranslateService
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ArtifactCommonPropertiesComponent);
component = fixture.componentInstance;
component.artifactDetails = {};
component.artifactDetails.extra_attrs = mockedExtraAttrs;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should render all properties', async () => {
component.commonProperties = mockedExtraAttrs;
fixture.detectChanges();
await fixture.whenStable();
const contentRows = fixture.nativeElement.getElementsByTagName('clr-stack-content');
expect(contentRows.length).toEqual(4);
});
});

View File

@ -0,0 +1,78 @@
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Artifact } from "../../../../../ng-swagger-gen/models/artifact";
import { DatePipe } from "@angular/common";
import { TranslateService } from "@ngx-translate/core";
import { formatSize } from "../../../utils/utils";
enum Types {
CREATED = 'created',
TYPE = 'type',
MEDIA_TYPE = 'media_type',
MANIFEST_MEDIA_TYPE = 'manifest_media_type',
DIGEST = 'digest',
SIZE = 'size',
PUSH_TIME = 'push_time',
PULL_TIME = 'pull_time',
}
@Component({
selector: 'artifact-common-properties',
templateUrl: './artifact-common-properties.component.html',
styleUrls: ['./artifact-common-properties.component.scss']
})
export class ArtifactCommonPropertiesComponent implements OnInit, OnChanges {
@Input() artifactDetails: Artifact;
commonProperties: { [key: string]: any } = {};
constructor(private translate: TranslateService) {
}
ngOnInit() {
}
ngOnChanges(changes: SimpleChanges) {
if (changes && changes["artifactDetails"]) {
if (this.artifactDetails) {
if (this.artifactDetails.type) {
this.commonProperties[Types.TYPE] = this.artifactDetails.type;
}
if (this.artifactDetails.media_type) {
this.commonProperties[Types.MEDIA_TYPE] = this.artifactDetails.media_type;
}
if (this.artifactDetails.manifest_media_type) {
this.commonProperties[Types.MANIFEST_MEDIA_TYPE] = this.artifactDetails.manifest_media_type;
}
if (this.artifactDetails.digest) {
this.commonProperties[Types.DIGEST] = this.artifactDetails.digest;
}
if (this.artifactDetails.size) {
this.commonProperties[Types.SIZE] = formatSize(this.artifactDetails.size.toString());
}
if (this.artifactDetails.push_time) {
this.commonProperties[Types.PUSH_TIME] = new DatePipe(this.translate.currentLang)
.transform(this.artifactDetails.push_time, 'short');
}
if (this.artifactDetails.pull_time) {
this.commonProperties[Types.PULL_TIME] = new DatePipe(this.translate.currentLang)
.transform(this.artifactDetails.pull_time, 'short');
}
Object.assign(this.commonProperties, this.artifactDetails.extra_attrs, this.artifactDetails.annotations);
for (let name in this.commonProperties) {
if (this.commonProperties.hasOwnProperty(name)) {
if (this.commonProperties[name] && this.commonProperties[name] instanceof Object) {
this.commonProperties[name] = JSON.stringify(this.commonProperties[name]);
}
if (name === Types.CREATED) {
this.commonProperties[name] = new DatePipe(this.translate.currentLang)
.transform(this.commonProperties[name], 'short');
}
}
}
}
}
}
hasCommonProperties(): boolean {
return JSON.stringify(this.commonProperties) !== '{}';
}
}

View File

@ -0,0 +1,286 @@
<confirmation-dialog class="hidden-tag" #confirmationDialog (confirmAction)="confirmDeletion($event)">
</confirmation-dialog>
<clr-modal class="hidden-tag" [(clrModalOpen)]="showTagManifestOpened" [clrModalStaticBackdrop]="staticBackdrop"
[clrModalClosable]="closable">
<h3 class="modal-title">{{ manifestInfoTitle | translate }}</h3>
<div class="modal-body">
<div class="row col-md-12">
<textarea class="clr-textarea w-100" rows="2" #digestTarget>{{digestId}}</textarea>
</div>
</div>
<div class="modal-footer">
<span class="copy-failed" [hidden]="!copyFailed">{{'TAG.COPY_ERROR' | translate}}</span>
<button type="button" class="btn btn-primary" [ngxClipboard]="digestTarget" (cbOnSuccess)="onSuccess($event)"
(cbOnError)="onError($event)">{{'BUTTON.COPY' | translate}}</button>
</div>
</clr-modal>
<clr-modal class="hidden-tag" [(clrModalOpen)]="retagDialogOpened" [clrModalStaticBackdrop]="staticBackdrop">
<h3 class="modal-title">{{ 'REPOSITORY.RETAG' | translate }}</h3>
<div class="modal-body retag-modal-body">
<div class="row col-md-12">
<hbr-image-name-input #imageNameInput></hbr-image-name-input>
</div>
</div>
<div class="modal-footer">
<button type="button"
[disabled]="imageNameInput.projectName.invalid||imageNameInput.repoName.invalid||imageNameInput.tagName.invalid||imageNameInput.noProjectInfo!=''"
class="btn btn-primary" (click)="onRetag()">{{'BUTTON.CONFIRM' | translate}}</button>
</div>
</clr-modal>
<div class="row tag-row">
<div>
<div class="row flex-items-xs-right rightPos">
<div id="filterArea">
<div class='filterLabelPiece' *ngIf="!withAdmiral" [hidden]="!openLabelFilterPiece"
[style.left.px]='32'>
<hbr-label-piece *ngIf="showlabel" [hidden]='!filterOneLabel' [label]="filterOneLabel"
[labelWidth]="130"></hbr-label-piece>
</div>
<div class="flex-xs-middle">
<hbr-filter [readonly]="'readonly'" [withDivider]="true"
filterPlaceholder="{{'ARTIFACT.FILTER_FOR_ARTIFACTS' | translate}}"
(filterEvt)="doSearchArtifactByFilter($event)" (openFlag)="openFlagEvent($event)"
[currentValue]="lastFilteredTagName"></hbr-filter>
<div class="label-filter-panel" *ngIf="!withAdmiral" [hidden]="!openLabelFilterPanel">
<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"
[(ngModel)]="filterName" (keyup)="handleInputFilter()"></div>
<div [hidden]='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'
[hidden]="!label.show" (click)="rightFilterLabel(label)">
<clr-icon shape="check" class='pull-left' [hidden]='!label.iconsShow'></clr-icon>
<div class='labelDiv'>
<hbr-label-piece [label]="label.label" [labelWidth]="160"></hbr-label-piece>
</div>
</button>
</div>
</div>
</div>
</div>
<span class="refresh-btn" (click)="refresh()">
<clr-icon shape="refresh"></clr-icon>
</span>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid [clrDgLoading]="loading" (clrDgRefresh)="clrLoad($event)" class="datagrid-top" [class.embeded-datagrid]="isEmbedded"
[(clrDgSelected)]="selectedRow">
<clr-dg-action-bar>
<button [clrLoading]="scanBtnState" type="button" class="btn btn-secondary scan-btn"
[disabled]="!(canScanNow() && selectedRow.length==1 && hasEnabledScanner && !referArtifactArray.length)" (click)="scanNow()">
<clr-icon shape="shield-check" size="16"></clr-icon>&nbsp;{{'VULNERABILITY.SCAN_NOW' | translate}}
</button>
<clr-dropdown class="btn btn-link mt-1">
<span clrDropdownTrigger id="artifact-list-action" class="btn pl-0">
{{'BUTTON.ACTIONS' | translate}}
<clr-icon shape="caret down"></clr-icon>
</span>
<clr-dropdown-menu class="action-dropdown" clrPosition="bottom-left" *clrIfOpen>
<div class="action-dropdown-item" aria-label="copy digest" clrDropdownItem
[clrDisabled]="!(selectedRow.length==1&& !referArtifactArray.length)" (click)="showDigestId()">
{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</div>
<clr-dropdown *ngIf="!withAdmiral">
<button class="action-dropdown-item" clrDropdownTrigger
[disabled]="!(selectedRow.length==1)||!hasAddLabelImagePermission ||referArtifactArray.length"
(click)="addLabels()">
{{'REPOSITORY.ADD_LABELS' | translate}}
</button>
<clr-dropdown-menu>
<div class="filter-grid">
<label
class="dropdown-header">{{'REPOSITORY.ADD_LABEL_TO_IMAGE' | translate}}</label>
<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">
{{'LABEL.NO_LABELS' | translate }}</div>
<div [hidden]='!imageStickLabels.length' class="has-label">
<button type="button" class="dropdown-item"
*ngFor='let label of imageStickLabels' [hidden]='!label.show'
(click)="stickLabel(label)">
<clr-icon shape="check" class='pull-left' [hidden]='!label.iconsShow'>
</clr-icon>
<div class='labelDiv'>
<hbr-label-piece [label]="label.label" [labelWidth]="130">
</hbr-label-piece>
</div>
</button>
</div>
</div>
</clr-dropdown-menu>
</clr-dropdown>
<div class="action-dropdown-item" aria-label="retag" *ngIf="!withAdmiral"
[clrDisabled]="!(selectedRow.length===1)|| !hasRetagImagePermission||referArtifactArray.length" (click)="retag()"
clrDropdownItem>{{'REPOSITORY.RETAG' | translate}}</div>
<div class="action-dropdown-item" clrDropdownItem *ngIf="hasDeleteImagePermission"
(click)="deleteArtifact()" id="artifact-list-delete" [clrDisabled]="!hasDeleteImagePermission||!selectedRow.length || referArtifactArray.length">
{{'REPOSITORY.DELETE' | translate}}</div>
</clr-dropdown-menu>
</clr-dropdown>
</clr-dg-action-bar>
<clr-dg-column class="flex-max-width" [clrDgField]="'q'">{{'REPOSITORY.ARTIFACTS_COUNT' | translate}}
</clr-dg-column>
<clr-dg-column *ngIf="referArtifactArray.length">{{'REPOSITORY.PLATFORM' | translate}}</clr-dg-column>
<clr-dg-column class="w-rem-4">{{'REPOSITORY.TAGS_COUNT' | translate}}</clr-dg-column>
<clr-dg-column >{{'REPOSITORY.SIZE' | translate}}</clr-dg-column>
<!-- <clr-dg-column>{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column> -->
<clr-dg-column>{{'REPOSITORY.VULNERABILITY' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="withNotary">{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column>
<!-- <clr-dg-column>{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column> -->
<clr-dg-column *ngIf="!withAdmiral">{{'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>{{'TAG.PLACEHOLDER' | translate }}</clr-dg-placeholder>
<clr-dg-row *ngFor="let artifact of artifactList" [clrDgItem]="artifact" >
<clr-dg-cell class="truncated flex-max-width">
<div class="cell white-normal">
<img class="artifact-icon"
[src]="artifact.type==='IMAGE'||artifact.type==='CHART'?'images/artifact-'+artifact.type.toLowerCase()+'.svg':'images/artifact-default.svg'" />
&nbsp; &nbsp;
<a href="javascript:void(0)" class="max-width-100" (click)="onTagClick(artifact)"
title="{{artifact.digest}}">
{{ artifact.digest | slice:0:15}}</a>
<clr-tooltip *ngIf="artifact.references">
<div clrTooltipTrigger class="level-border">
<div class="inner truncated ">
<a href="javascript:void(0)" (click)="refer(artifact)">
<clr-icon class="icon-folder" shape="folder"></clr-icon>
</a>
</div>
</div>
<clr-tooltip-content [clrPosition]="'top-right'" [clrSize]="'lg'" *clrIfOpen>
{{'REPOSITORY.ARTIFACT_TOOTIP' | translate}}
</clr-tooltip-content>
</clr-tooltip>
<span href="javascript:void(0)" *ngIf="!artifact.references">
<clr-icon class="icon-folder" shape="file"></clr-icon>
</span>
</div>
</clr-dg-cell>
<clr-dg-cell *ngIf="referArtifactArray.length">
<div class="cell">
{{artifact.extra_attrs?.os}}/{{artifact.extra_attrs?.architecture}}
</div>
</clr-dg-cell>
<clr-dg-cell class="w-rem-4">
<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 | slice:0:5}}
</span>
<span class="eslip"
*ngIf="artifact.tags.length>1||artifact.tags.length>5">...</span>
<span class=""> ({{artifact.tags.length}})</span>
</div>
</div>
</div>
<clr-tooltip-content [clrPosition]="'top-right'" [clrSize]="'lg'" *clrIfOpen>
<table class="table table-noborder mt-0 table-tag">
<thead class="tag-thead">
<tr>
<th class="left tag-header-color">
{{'REPOSITORY.TAGS_COUNT' | translate | uppercase}}</th>
<th class="left tag-header-color">
{{'REPOSITORY.PUSH_TIME' | translate | uppercase}}</th>
<th class="left tag-header-color">
{{'REPOSITORY.PULL_TIME' | translate | uppercase}}</th>
</tr>
</thead>
<tbody class="tag-tbody">
<tr class="tag-tr" *ngFor="let tag of artifact.tags">
<td class="left tag-body-color">{{tag.name}}</td>
<td class="left tag-body-color">{{tag.push_time | date: 'short'}}</td>
<td class="left tag-body-color">{{tag.pull_time | date: 'short'}}</td>
</tr>
</tbody>
</table>
</clr-tooltip-content>
</clr-tooltip>
</div>
</clr-dg-cell>
<clr-dg-cell>
<div class="cell">
{{artifact.size?sizeTransform(artifact.size): ""}}
</div>
</clr-dg-cell>
<!-- <clr-dg-cell class="truncated" title="docker pull {{registryUrl}}/{{repoName}}:{{artifact.name}}">
<div class="cell">
<hbr-copy-input #copyInput (onCopyError)="onCpError($event)" iconMode="true" defaultValue="docker pull {{registryUrl}}/{{repoName}}:{{artifact.name}}"></hbr-copy-input>
</div>
</clr-dg-cell> -->
<clr-dg-cell>
<div class="cell">
<hbr-vulnerability-bar [scanner]="handleScanOverview(artifact.scan_overview)?.scanner"
(submitFinish)="submitFinish($event)" [projectName]="projectName" [repoName]="repoName"
[artifactId]="artifact.id" [summary]="handleScanOverview(artifact.scan_overview)">
</hbr-vulnerability-bar>
</div>
</clr-dg-cell>
<clr-dg-cell *ngIf="withNotary" [ngSwitch]="artifact.signature !== null">
<div class="cell">
<clr-icon shape="check-circle" *ngSwitchCase="true" size="20" class="color-green"></clr-icon>
<clr-icon shape="times-circle" *ngSwitchCase="false" size="16" class="color-red"></clr-icon>
<a href="javascript:void(0)" *ngSwitchDefault role="tooltip" aria-haspopup="true"
class="tooltip tooltip-top-right">
<clr-icon shape="help" class="color-gray" size="16"></clr-icon>
<span class="tooltip-content">{{'REPOSITORY.NOTARY_IS_UNDETERMINED' | translate}}</span>
</a>
</div>
</clr-dg-cell>
<!-- <clr-dg-cell class="truncated" title="{{artifact.author}}">
<div class="cell">
{{artifact.author}}
</div>
</clr-dg-cell> -->
<clr-dg-cell *ngIf="!withAdmiral">
<div class="cell">
<hbr-label-piece *ngIf="artifact.labels?.length" [label]="artifact.labels[0]" [labelWidth]="90">
</hbr-label-piece>
<div class="signpost-item" [hidden]="artifact.labels?.length<=1">
<div class="trigger-item">
<clr-signpost>
<button class="btn btn-link" clrSignpostTrigger>...</button>
<clr-signpost-content [clrPosition]="'left-top'" *clrIfOpen>
<div>
<hbr-label-piece *ngFor="let label of artifact.labels" [label]="label">
</hbr-label-piece>
</div>
</clr-signpost-content>
</clr-signpost>
</div>
</div>
</div>
</clr-dg-cell>
<clr-dg-cell>
<div class="cell">{{artifact.push_time | date: 'short'}}</div>
</clr-dg-cell>
<clr-dg-cell>
<div class="cell">
{{artifact.pull_time === availableTime ? "" : (artifact.pull_time| date: 'short')}}</div>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<span *ngIf="totalCount">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}}
{{'REPOSITORY.OF' | translate}}</span> {{totalCount}}
{{'REPOSITORY.ITEMS' | translate}}&nbsp;&nbsp;&nbsp;&nbsp;
<clr-dg-pagination #pagination [clrDgTotalItems]="totalCount" [(clrDgPage)]="currentPage" [clrDgPageSize]="pageSize"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
</div>

View File

@ -11,7 +11,7 @@
}
.refresh-btn:hover {
color: #007CBB;
color: #007cbb;
}
.sub-header-title {
@ -28,7 +28,7 @@
height: 0;
}
:host>>>.datagrid-placeholder {
:host >>> .datagrid-placeholder {
display: none;
}
@ -44,7 +44,7 @@
margin-right: 6px;
}
:host>>>.datagrid clr-dg-column {
:host >>> .datagrid clr-dg-column {
min-width: 80px;
}
@ -62,16 +62,16 @@
.dropdown-menu .dropdown-item {
position: relative;
padding-left: .5rem;
padding-right: .5rem;
line-height: 1.0;
padding-left: 0.5rem;
padding-right: 0.5rem;
line-height: 1;
height: 1.2rem;
}
.dropdown-menu input {
position: relative;
margin-left: .5rem;
margin-right: .5rem;
margin-left: 0.5rem;
margin-right: 0.5rem;
}
.pull-left {
@ -86,9 +86,7 @@
.btn-link {
display: inline-flex;
width: 15px;
min-width: 15px;
color: black;
vertical-align: super;
}
@ -98,7 +96,7 @@
}
.signpost-content-body .label {
margin: .3rem;
margin: 0.3rem;
}
.labelDiv {
@ -112,15 +110,15 @@
margin: 6px 0;
}
:host>>>.signpost-content {
:host >>> .signpost-content {
min-width: 4rem;
}
:host>>>.signpost-content-body {
padding: 0 .4rem;
:host >>> .signpost-content-body {
padding: 0 0.4rem;
}
:host>>>.signpost-content-header {
:host >>> .signpost-content-header {
display: none;
}
@ -141,14 +139,14 @@
flex-direction: column;
padding: .5rem 0;
border: 1px solid #ccc;
box-shadow: 0 1px 0.125rem hsla(0, 0%, 45%, .25);
box-shadow: 0 1px 0.125rem hsla(0, 0%, 45%, 0.25);
min-width: 5rem;
max-width: 15rem;
border-radius: .125rem;
border-radius: 0.125rem;
.form-group input {
position: relative;
margin-left: .5rem;
margin-right: .5rem;
margin-left: 0.5rem;
margin-right: 0.5rem;
}
}
@ -168,7 +166,7 @@
.labelBtn {
position: relative;
overflow: hidden;
font-size: .58333rem;
font-size: 0.58333rem;
letter-spacing: normal;
font-weight: 400;
background: transparent;
@ -191,11 +189,11 @@
}
.filterLabelHeader {
font-size: .5rem;
font-size: 0.5rem;
font-weight: 600;
letter-spacing: normal;
padding: 0 .5rem;
line-height: .75rem;
padding: 0 0.5rem;
line-height: 0.75rem;
margin: 0;
}
@ -224,16 +222,23 @@ hbr-image-name-input {
.datagrid-top {
.flex-max-width {
max-width: 220px;
min-width: 130px;
min-width: 200px;
}
.icon-cell {
max-width: 1.5rem;
min-width: 1.5rem;
clr-icon {
cursor: pointer;
}
}
}
.color-green {
color: #1D5100;
color: #1d5100;
}
.color-red {
color: #C92100;
color: #c92100;
}
.color-gray {
@ -268,9 +273,80 @@ clr-datagrid {
white-space: normal;
}
.max-width-100 {
max-width: 100%;
width: 128px;
// overflow: hidden;
// white-space: nowrap;
// text-overflow: ellipsis;
}
.max-width-38 {
max-width: 38px !important;
min-width: 0 !important;
}
clr-datagrid {
height: auto !important;
}
.artifact-icon {
width: 0.8rem;
height: 0.8rem;
}
.width-p-100 {
width: 100%;
}
.w-rem-4 {
width: 4rem !important;
}
.tag-header-color {
color: #fff;
}
.tag-body-color {
color: #ccc;
}
.table {
.tag-tr {
td {
padding-top: 8px;
padding-bottom: 2px;
}
}
.tag-thead {
th {
padding-top: 0;
padding-bottom: 5px;
border: none;
font-weight: 400;
}
}
}
.action-dropdown {
.action-dropdown-item {
position: static;
padding-left: 1rem;
line-height: inherit;
height: auto;
&::before {
margin-top: .5rem;
}
}
.dropdown-header {
text-transform: none;
}
}
.filter-label-input {
::ng-deep {
.clr-input-wrapper {
max-height: 2rem;
}
}
}
.scan-btn{
margin-top: -.3rem;
}
.eslip {
margin-left: -3px;
}

View File

@ -4,14 +4,14 @@ import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from "@angular/core";
import { SharedModule } from "../../utils/shared/shared.module";
import { ConfirmationDialogComponent } from "../confirmation-dialog/confirmation-dialog.component";
import { ImageNameInputComponent } from "../image-name-input/image-name-input.component";
import { TagComponent } from "./tag.component";
import { ArtifactListTabComponent } from "./artifact-list-tab.component";
import { ErrorHandler } from "../../utils/error-handler/error-handler";
import { Label, Tag } from "../../services/interface";
import { SERVICE_CONFIG, IServiceConfig } from "../../entities/service.config";
import {
TagService, TagDefaultService, ScanningResultService, ScanningResultDefaultService,
RetagService, RetagDefaultService, ProjectService, ProjectDefaultService
ScanningResultService, ScanningResultDefaultService,
RetagService, RetagDefaultService, ProjectService, ProjectDefaultService, ArtifactService, ArtifactDefaultService
} from "../../services";
import { CopyInputComponent } from "../push-image/copy-input.component";
import { LabelPieceComponent } from "../label-piece/label-piece.component";
@ -25,12 +25,14 @@ import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { HttpClientTestingModule } from "@angular/common/http/testing";
import { HttpClient } from "@angular/common/http";
import { ChannelService } from "../../services/channel.service";
import { Artifact, Reference } from "./artifact";
import { ActivatedRoute } from "@angular/router";
describe("TagComponent (inline template)", () => {
describe("ArtifactListTabComponent (inline template)", () => {
let comp: TagComponent;
let fixture: ComponentFixture<TagComponent>;
let tagService: TagService;
let comp: ArtifactListTabComponent;
let fixture: ComponentFixture<ArtifactListTabComponent>;
let artifactService: ArtifactService;
let userPermissionService: UserPermissionService;
let spy: jasmine.Spy;
let spyLabels: jasmine.Spy;
@ -40,20 +42,78 @@ describe("TagComponent (inline template)", () => {
disabled: false,
name: "Clair"
};
let mockTags: Tag[] = [
let mockActivatedRoute = {
data: of(
{
projectResolver: {
name: 'library'
}
}
)
};
let mockArtifacts: Artifact[] = [
{
"digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
"name": "1.11.5",
"size": "2049",
"architecture": "amd64",
"os": "linux",
"os.version": "",
"docker_version": "1.12.3",
"author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"",
"created": new Date("2016-11-08T22:41:15.912313785Z"),
"signature": null,
"labels": [],
}
"id": 1,
type: 'image',
repository: "goharbor/harbor-portal",
tags: [{
id: '1',
name: 'tag1',
artifact_id: 1,
upload_time: '2020-01-06T09:40:08.036866579Z',
},
{
id: '2',
name: 'tag2',
artifact_id: 2,
pull_time: '2020-01-06T09:40:08.036866579Z',
push_time: '2020-01-06T09:40:08.036866579Z',
},],
references: [new Reference(1), new Reference(2)],
media_type: 'string',
"digest": "sha256:4875cda368906fd670c9629b5e416ab3d6c0292015f3c3f12ef37dc9a32fc8d4",
"size": 20372934,
"scan_overview": {
"application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0": {
"report_id": "5e64bc05-3102-11ea-93ae-0242ac140004",
"scan_status": "Error",
"severity": "",
"duration": 118,
"summary": null,
"start_time": "2020-01-07T04:01:23.157711Z",
"end_time": "2020-01-07T04:03:21.662766Z"
}
},
"labels": [
{
"id": 3,
"name": "aaa",
"description": "",
"color": "#0095D3",
"scope": "g",
"project_id": 0,
"creation_time": "2020-01-13T05:44:00.580198Z",
"update_time": "2020-01-13T05:44:00.580198Z",
"deleted": false
},
{
"id": 6,
"name": "dbc",
"description": "",
"color": "",
"scope": "g",
"project_id": 0,
"creation_time": "2020-01-13T08:27:19.279123Z",
"update_time": "2020-01-13T08:27:19.279123Z",
"deleted": false
}
],
"push_time": "2020-01-07T03:33:41.162319Z",
"pull_time": "0001-01-01T00:00:00Z",
hasReferenceArtifactList: [],
noReferenceArtifactList: []
}
];
let mockLabels: Label[] = [
@ -129,7 +189,7 @@ describe("TagComponent (inline template)", () => {
CUSTOM_ELEMENTS_SCHEMA
],
declarations: [
TagComponent,
ArtifactListTabComponent,
LabelPieceComponent,
ConfirmationDialogComponent,
ImageNameInputComponent,
@ -139,20 +199,21 @@ describe("TagComponent (inline template)", () => {
ErrorHandler,
ChannelService,
{ provide: SERVICE_CONFIG, useValue: config },
{ provide: TagService, useClass: TagDefaultService },
{ provide: ArtifactService, useClass: ArtifactDefaultService },
{ provide: ProjectService, useClass: ProjectDefaultService },
{ provide: RetagService, useClass: RetagDefaultService },
{ provide: ScanningResultService, useClass: ScanningResultDefaultService },
{ provide: LabelService, useClass: LabelDefaultService },
{ provide: UserPermissionService, useClass: UserPermissionDefaultService },
{ provide: mockErrorHandler, useValue: ErrorHandler },
{ provide: ErrorHandler, useValue: mockErrorHandler },
{ provide: ActivatedRoute, useValue: mockActivatedRoute },
{ provide: OperationService },
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TagComponent);
fixture = TestBed.createComponent(ArtifactListTabComponent);
comp = fixture.componentInstance;
comp.projectId = 1;
@ -168,8 +229,12 @@ describe("TagComponent (inline template)", () => {
let labelService: LabelService;
tagService = fixture.debugElement.injector.get(TagService);
spy = spyOn(tagService, "getTags").and.returnValues(of(mockTags).pipe(delay(0)));
artifactService = fixture.debugElement.injector.get(ArtifactService);
spy = spyOn(artifactService, "getArtifactList").and.returnValues(of(
{
body: mockArtifacts
}
).pipe(delay(0)));
userPermissionService = fixture.debugElement.injector.get(UserPermissionService);
let http: HttpClient;
http = fixture.debugElement.injector.get(HttpClient);
@ -191,10 +256,11 @@ describe("TagComponent (inline template)", () => {
}));
it("should load project scanner", async(() => {
expect(spyScanner.calls.count()).toEqual(1);
expect(spyScanner.calls.count()).toEqual(2);
}));
it("should load and render data", () => {
;
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
@ -203,7 +269,7 @@ describe("TagComponent (inline template)", () => {
expect(de).toBeTruthy();
let el: HTMLElement = de.nativeElement;
expect(el).toBeTruthy();
expect(el.textContent.trim()).toEqual("1.11.5");
expect(el.textContent.trim()).toEqual("sha256:4875cda3");
});
});

View File

@ -20,18 +20,20 @@ import {
Input,
OnInit,
Output,
ViewChild
ViewChild,
} from "@angular/core";
import { forkJoin, Observable, Subject, throwError as observableThrowError, of } from "rxjs";
import { catchError, debounceTime, distinctUntilChanged, finalize, map } from 'rxjs/operators';
import { TranslateService } from "@ngx-translate/core";
import { Comparator, Label, State, Tag, TagClickEvent, VulnerabilitySummary } from "../../services/interface";
import { Comparator, Label, State, Tag, ArtifactClickEvent, VulnerabilitySummary } from "../../services/interface";
import {
RequestQueryParams,
RetagService,
ScanningResultService,
TagService,
ProjectService,
ArtifactService
} from "../../services";
import { ErrorHandler } from "../../utils/error-handler/error-handler";
import { ConfirmationButtons, ConfirmationState, ConfirmationTargets } from "../../entities/shared.const";
@ -46,7 +48,7 @@ import {
CustomComparator,
DEFAULT_PAGE_SIZE, DEFAULT_SUPPORTED_MIME_TYPE,
doFiltering,
doSorting,
doSorting, formatSize,
VULNERABILITY_SCAN_STATUS,
} from "../../utils/utils";
@ -58,26 +60,31 @@ import { operateChanges, OperateInfo, OperationState } from "../operation/operat
import { OperationService } from "../operation/operation.service";
import { ImageNameInputComponent } from "../image-name-input/image-name-input.component";
import { errorHandler as errorHandFn } from "../../utils/shared/shared.utils";
import { ClrLoadingState } from "@clr/angular";
import { ClrLoadingState, ClrDatagridStateInterface, ClrDatagridComparatorInterface } from "@clr/angular";
import { ChannelService } from "../../services/channel.service";
import { Artifact, Reference } from "./artifact";
import { HttpParams } from "@angular/common/http";
import { ActivatedRoute } from "@angular/router";
export interface LabelState {
iconsShow: boolean;
label: Label;
show: boolean;
}
export const AVAILABLE_TIME = '0001-01-01T00:00:00Z';
export const AVAILABLE_TIME = '0001-01-01T00:00:00.000Z';
@Component({
selector: 'hbr-tag',
templateUrl: './tag.component.html',
styleUrls: ['./tag.component.scss']
selector: 'artifact-list-tab',
templateUrl: './artifact-list-tab.component.html',
styleUrls: ['./artifact-list-tab.component.scss']
})
export class TagComponent implements OnInit, AfterViewInit {
export class ArtifactListTabComponent implements OnInit, AfterViewInit {
signedCon: { [key: string]: any | string[] } = {};
@Input() projectId: number;
projectName: string;
@Input() memberRoleID: number;
@Input() repoName: string;
referArtifactArray: string[] = [];
@Input() isEmbedded: boolean;
@Input() hasSignedIn: boolean;
@ -86,11 +93,13 @@ export class TagComponent implements OnInit, AfterViewInit {
@Input() withNotary: boolean;
@Input() withAdmiral: boolean;
@Output() refreshRepo = new EventEmitter<boolean>();
@Output() tagClickEvent = new EventEmitter<TagClickEvent>();
@Output() tagClickEvent = new EventEmitter<ArtifactClickEvent>();
@Output() signatureOutput = new EventEmitter<any>();
@Output() putReferArtifactArray = new EventEmitter<string[]>();
tags: Tag[];
artifactList: Artifact[] = [];
availableTime = AVAILABLE_TIME;
showTagManifestOpened: boolean;
retagDialogOpened: boolean;
@ -105,20 +114,19 @@ export class TagComponent implements OnInit, AfterViewInit {
retagSrcImage: string;
showlabel: boolean;
createdComparator: Comparator<Tag> = new CustomComparator<Tag>("created", "date");
pullComparator: Comparator<Tag> = new CustomComparator<Tag>("pull_time", "date");
pushComparator: Comparator<Tag> = new CustomComparator<Tag>("push_time", "date");
pullComparator: Comparator<Artifact> = new CustomComparator<Artifact>("pull_time", "date");
pushComparator: Comparator<Artifact> = new CustomComparator<Artifact>("push_time", "date");
loading = false;
loading = true;
copyFailed = false;
selectedRow: Tag[] = [];
selectedRow: Artifact[] = [];
imageLabels: LabelState[] = [];
imageStickLabels: LabelState[] = [];
imageFilterLabels: LabelState[] = [];
labelListOpen = false;
selectedTag: Tag[];
selectedTag: Artifact[];
labelNameFilter: Subject<string> = new Subject<string>();
stickLabelNameFilter: Subject<string> = new Subject<string>();
filterOnGoing: boolean;
@ -154,16 +162,19 @@ export class TagComponent implements OnInit, AfterViewInit {
hasEnabledScanner: boolean;
scanBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
onSendingScanCommand: boolean;
constructor(
private errorHandler: ErrorHandler,
private tagService: TagService,
private retagService: RetagService,
private userPermissionService: UserPermissionService,
private labelService: LabelService,
private artifactService: ArtifactService,
private translateService: TranslateService,
private ref: ChangeDetectorRef,
private operationService: OperationService,
private channel: ChannelService,
private projectService: ProjectService,
private activatedRoute: ActivatedRoute,
private scanningService: ScanningResultService
) { }
@ -172,12 +183,23 @@ export class TagComponent implements OnInit, AfterViewInit {
this.errorHandler.error("Project ID cannot be unset.");
return;
}
this.activatedRoute.data.subscribe(res => {
this.projectName = res.projectResolver.name;
});
this.getProjectScanner();
if (!this.repoName) {
this.errorHandler.error("Repo name cannot be unset.");
return;
}
this.retrieve();
let refer = JSON.parse(sessionStorage.getItem('reference'));
if (refer && refer.projectId === this.projectId && refer.repo === this.repoName) {
this.referArtifactArray = refer.referArray;
}
this.artifactService.TriggerArtifactChan$.subscribe(res => {
let st: ClrDatagridStateInterface = { page: {from: 0, to: this.pageSize - 1, size: this.pageSize} };
this.clrLoad(st);
});
this.lastFilteredTagName = '';
this.labelNameFilter
@ -230,8 +252,7 @@ export class TagComponent implements OnInit, AfterViewInit {
let len = this.lastFilteredTagName.length ? this.lastFilteredTagName.length * 6 + 60 : 115;
return len > 210 ? 210 : len;
}
doSearchTagNames(tagName: string) {
doSearchArtifactByFilter(tagName) {
this.lastFilteredTagName = tagName;
this.currentPage = 1;
@ -251,42 +272,107 @@ export class TagComponent implements OnInit, AfterViewInit {
this.clrLoad(st);
}
doSearchArtifactNames(artifactName: string) {
this.lastFilteredTagName = artifactName;
this.currentPage = 1;
clrLoad(state: State): void {
let st: State = this.currentState;
if (!st) {
st = { page: {} };
}
st.page.size = this.pageSize;
st.page.from = 0;
st.page.to = this.pageSize - 1;
let selectedLab = this.imageFilterLabels.find(label => label.iconsShow === true);
if (selectedLab) {
st.filters = [{ property: 'name', value: this.lastFilteredTagName }, { property: 'labels.id', value: selectedLab.label.id }];
} else {
st.filters = [{ property: 'name', value: this.lastFilteredTagName }];
}
this.clrLoad(st);
}
clrLoad(state: ClrDatagridStateInterface): void {
if (!state || !state.page) {
return;
}
this.selectedRow = [];
// Keep it for future filtering and sorting
this.currentState = state;
let pageNumber: number = calculatePage(state);
if (pageNumber <= 0) { pageNumber = 1; }
let sortBy: any = '';
if (state.sort) {
sortBy = state.sort.by as string | ClrDatagridComparatorInterface<any>;
sortBy = sortBy.fieldName ? sortBy.fieldName : sortBy;
sortBy = state.sort.reverse ? `-${sortBy}` : sortBy;
}
this.loading = true;
this.currentState = state;
// Pagination
let params: RequestQueryParams = new RequestQueryParams();
params = params.set("page", "" + pageNumber).set("page_size", "" + this.pageSize);
this.loading = true;
this.tagService.getTags(
this.repoName,
params)
.subscribe((tags: Tag[]) => {
this.signedCon = {};
// Do filtering and sorting
this.tags = doFiltering<Tag>(tags, state);
this.tags = doSorting<Tag>(this.tags, state);
this.loading = false;
}, error => {
this.loading = false;
this.errorHandler.error(error);
let params = new HttpParams();
if (pageNumber && this.pageSize) {
params = params.set('page', pageNumber + '').set('page_size', this.pageSize + '');
}
if (sortBy) {
params = params.set('sort', sortBy);
}
if (state.filters && state.filters.length) {
state.filters.forEach(item => {
params = params.set(item.property, item.value);
});
}
this.projectService.getProject(this.projectId).subscribe(project => {
this.projectName = project.name;
let refer = JSON.parse(sessionStorage.getItem('reference'));
this.referArtifactArray = [];
if (refer && refer.projectId === this.projectId && refer.repo === this.repoName) {
this.referArtifactArray = refer.referArray;
}
// Force refresh view
let hnd = setInterval(() => this.ref.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 5000);
if (this.referArtifactArray.length) {
let observableLists: Observable<Artifact>[] = [];
this.artifactService.getArtifactFromDigest(this.projectName, this.repoName,
this.referArtifactArray[this.referArtifactArray.length - 1]).subscribe(artifact => {
this.totalCount = artifact.references.length;
artifact.references.forEach((child, index) => {
if (index >= (pageNumber - 1) * this.pageSize && index < pageNumber * this.pageSize) {
observableLists.push(this.artifactService.getArtifactFromDigest(this.projectName, this.repoName,
child.child_digest));
}
});
forkJoin(observableLists).pipe(finalize(() => {
this.loading = false;
})).subscribe(artifacts => {
this.artifactList = artifacts;
}, error => {
this.errorHandler.error(error);
});
});
} else {
this.artifactService.getArtifactList(this.projectName, this.repoName, params).subscribe(res => {
if (res.headers) {
let xHeader: string = res.headers.get("X-Total-Count");
if (xHeader) {
this.totalCount = parseInt(xHeader, 0);
}
}
this.artifactList = res.body;
this.loading = false;
}, error => {
// error
this.loading = false;
});
}
});
}
refresh() {
this.doSearchTagNames("");
this.doSearchArtifactNames("");
}
getAllLabels(): void {
@ -301,14 +387,14 @@ export class TagComponent implements OnInit, AfterViewInit {
}, error => this.errorHandler.error(error));
}
labelSelectedChange(tag?: Tag[]): void {
if (tag && tag[0].labels) {
labelSelectedChange(artifact?: Artifact[]): void {
if (artifact && artifact[0].labels) {
this.imageStickLabels.forEach(data => {
data.iconsShow = false;
data.show = true;
});
if (tag[0].labels.length) {
tag[0].labels.forEach((labelInfo: Label) => {
if (artifact[0].labels.length) {
artifact[0].labels.forEach((labelInfo: Label) => {
let findedLabel = this.imageStickLabels.find(data => labelInfo.id === data['label'].id);
this.imageStickLabels.splice(this.imageStickLabels.indexOf(findedLabel), 1);
this.imageStickLabels.unshift(findedLabel);
@ -340,7 +426,8 @@ export class TagComponent implements OnInit, AfterViewInit {
this.inprogress = true;
let labelId = labelInfo.label.id;
this.selectedRow = this.selectedTag;
this.tagService.addLabelToImages(this.repoName, this.selectedRow[0].name, labelId).subscribe(res => {
this.artifactService.addLabelToImages(this.projectName, this.repoName, this.selectedRow[0].digest, labelId).subscribe(res => {
this.refresh();
// set the selected label in front
@ -371,7 +458,7 @@ export class TagComponent implements OnInit, AfterViewInit {
this.inprogress = true;
let labelId = labelInfo.label.id;
this.selectedRow = this.selectedTag;
this.tagService.deleteLabelToImages(this.repoName, this.selectedRow[0].name, labelId).subscribe(res => {
this.artifactService.deleteLabelToImages(this.projectName, this.repoName, this.selectedRow[0].digest, labelId).subscribe(res => {
this.refresh();
// insert the unselected label to groups with the same icons
@ -516,47 +603,49 @@ export class TagComponent implements OnInit, AfterViewInit {
});
}
retrieve() {
this.tags = [];
let signatures: string[] = [];
loadArtifactList(params?) {
this.artifactList = [];
this.loading = true;
let refer = JSON.parse(sessionStorage.getItem('reference'));
if (refer && refer.projectId === this.projectId && refer.repo === this.repoName) {
this.referArtifactArray = refer.referArray;
}
if (this.referArtifactArray.length) {
let observableLists: Observable<Artifact>[] = [];
this.tagService
.getTags(this.repoName)
.subscribe(items => {
// To keep easy use for vulnerability bar
items.forEach((t: Tag) => {
if (t.signature !== null) {
signatures.push(t.name);
this.artifactService.getArtifactFromDigest(this.projectName, this.repoName,
this.referArtifactArray[this.referArtifactArray.length - 1]).subscribe(artifact => {
artifact.references.forEach(child => {
observableLists.push(this.artifactService.getArtifactFromDigest(this.projectName, this.repoName,
child.child_digest));
});
forkJoin(observableLists).subscribe(artifacts => {
this.loading = false;
this.artifactList = artifacts;
});
});
} else {
this.artifactService.getArtifactList(this.projectName, this.repoName, params).subscribe(res => {
if (res.headers) {
let xHeader: string = res.headers.get("X-Total-Count");
if (xHeader) {
this.totalCount = parseInt(xHeader, 0);
}
}
this.artifactList = res.body;
this.loading = false;
}, error => {
// error
this.loading = false;
});
this.tags = items;
let signedName: { [key: string]: string[] } = {};
signedName[this.repoName] = signatures;
this.signatureOutput.emit(signedName);
this.loading = false;
if (this.tags && this.tags.length === 0) {
this.refreshRepo.emit(true);
}
}, error => {
this.errorHandler.error(error);
this.loading = false;
});
let hnd = setInterval(() => this.ref.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 5000);
}
}
sizeTransform(tagSize: string): string {
let size: number = Number.parseInt(tagSize);
if (Math.pow(1024, 1) <= size && size < Math.pow(1024, 2)) {
return (size / Math.pow(1024, 1)).toFixed(2) + "KB";
} else if (Math.pow(1024, 2) <= size && size < Math.pow(1024, 3)) {
return (size / Math.pow(1024, 2)).toFixed(2) + "MB";
} else if (Math.pow(1024, 3) <= size && size < Math.pow(1024, 4)) {
return (size / Math.pow(1024, 3)).toFixed(2) + "GB";
} else {
return size + "B";
}
return formatSize(tagSize);
}
retag() {
@ -584,7 +673,14 @@ export class TagComponent implements OnInit, AfterViewInit {
this.translateService.get('RETAG.MSG_SUCCESS').subscribe((res: string) => {
this.errorHandler.info(res);
if (`${this.imageNameInput.projectName.value}/${this.imageNameInput.repoName.value}` === this.repoName) {
this.retrieve();
let st: State = this.currentState;
if (!st) {
st = { page: {} };
}
st.page.size = this.pageSize;
st.page.from = 0;
st.page.to = this.pageSize - 1;
this.clrLoad(st);
}
});
}, error => {
@ -592,18 +688,18 @@ export class TagComponent implements OnInit, AfterViewInit {
});
}
deleteTags() {
deleteArtifact() {
if (this.selectedRow && this.selectedRow.length) {
let tagNames: string[] = [];
this.selectedRow.forEach(tag => {
tagNames.push(tag.name);
let artifactNames: string[] = [];
this.selectedRow.forEach(artifact => {
artifactNames.push(artifact.digest.slice(0, 15));
});
let titleKey: string, summaryKey: string, content: string, buttons: ConfirmationButtons;
titleKey = "REPOSITORY.DELETION_TITLE_TAG";
summaryKey = "REPOSITORY.DELETION_SUMMARY_TAG";
buttons = ConfirmationButtons.DELETE_CANCEL;
content = tagNames.join(" , ");
content = artifactNames.join(" , ");
let message = new ConfirmationMessage(
titleKey,
summaryKey,
@ -614,64 +710,82 @@ export class TagComponent implements OnInit, AfterViewInit {
this.confirmationDialog.open(message);
}
}
deleteArtifactobservableLists: Observable<any>[] = [];
confirmDeletion(message: ConfirmationAcknowledgement) {
if (message &&
message.source === ConfirmationTargets.TAG
&& message.state === ConfirmationState.CONFIRMED) {
let tags: Tag[] = message.data;
if (tags && tags.length) {
let observableLists: any[] = [];
tags.forEach(tag => {
observableLists.push(this.delOperate(tag));
});
forkJoin(...observableLists).subscribe((items) => {
// if delete one success refresh list
if (items.some(item => !item)) {
this.selectedRow = [];
this.retrieve();
}
});
let artifactList = message.data;
if (artifactList && artifactList.length) {
this.findArtifactFromIndex(artifactList);
}
}
}
findArtifactFromIndex(artifactList: Artifact[]) {
if (artifactList.every(artifact1 => !artifact1.references)) {
artifactList.forEach(artifact => {
this.deleteArtifactobservableLists.push(this.delOperate(artifact));
});
forkJoin(...this.deleteArtifactobservableLists).subscribe((items) => {
// if delete one success refresh list
if (items.some(item => !item)) {
this.selectedRow = [];
let st: ClrDatagridStateInterface = { page: {from: 0, to: this.pageSize - 1, size: this.pageSize} };
this.clrLoad(st);
}
});
} else {
let observArr: Observable<Artifact>[] = [];
artifactList.forEach(artifact => {
this.deleteArtifactobservableLists.push(this.delOperate(artifact));
if (artifact.references) {
artifact.references.forEach(reference => {
observArr.push(this.artifactService.getArtifactFromDigest(this.projectName, this.repoName, reference.child_digest));
});
delOperate(tag: Tag): Observable<any> | null {
}
});
forkJoin(observArr).subscribe((res) => {
this.findArtifactFromIndex(res);
});
}
}
delOperate(artifact: Artifact): Observable<any> | null {
// init operation info
let operMessage = new OperateInfo();
operMessage.name = 'OPERATION.DELETE_TAG';
operMessage.data.id = tag.id;
operMessage.data.id = artifact.id;
operMessage.state = OperationState.progressing;
operMessage.data.name = tag.name;
operMessage.data.name = artifact.digest;
this.operationService.publishInfo(operMessage);
if (tag.signature) {
forkJoin(this.translateService.get("BATCH.DELETED_FAILURE"),
this.translateService.get("REPOSITORY.DELETION_SUMMARY_TAG_DENIED")).subscribe(res => {
let wrongInfo: string = res[1] + "notary -s https://" + this.registryUrl +
":4443 -d ~/.docker/trust remove -p " +
this.registryUrl + "/" + this.repoName +
" " + name;
operateChanges(operMessage, OperationState.failure, wrongInfo);
});
} else {
return this.tagService
.deleteTag(this.repoName, tag.name)
.pipe(map(
response => {
this.translateService.get("BATCH.DELETED_SUCCESS")
.subscribe(res => {
operateChanges(operMessage, OperationState.success);
});
}), catchError(error => {
const message = errorHandFn(error);
this.translateService.get(message).subscribe(res =>
operateChanges(operMessage, OperationState.failure, res)
);
return of(error);
}));
}
// to do signature
// if (tag.signature) {
// forkJoin(this.translateService.get("BATCH.DELETED_FAILURE"),
// this.translateService.get("REPOSITORY.DELETION_SUMMARY_TAG_DENIED")).subscribe(res => {
// let wrongInfo: string = res[1] + "notary -s https://" + this.registryUrl +
// ":4443 -d ~/.docker/trust remove -p " +
// this.registryUrl + "/" + this.repoName +
// " " + name;
// operateChanges(operMessage, OperationState.failure, wrongInfo);
// });
// } else {
return this.artifactService
.deleteArtifact(this.projectName, this.repoName, artifact.digest)
.pipe(map(
response => {
this.translateService.get("BATCH.DELETED_SUCCESS")
.subscribe(res => {
operateChanges(operMessage, OperationState.success);
});
}), catchError(error => {
const message = errorHandFn(error);
this.translateService.get(message).subscribe(res =>
operateChanges(operMessage, OperationState.failure, res)
);
return of(error);
}));
// }
}
showDigestId() {
@ -683,12 +797,13 @@ export class TagComponent implements OnInit, AfterViewInit {
}
}
onTagClick(tag: Tag): void {
if (tag) {
let evt: TagClickEvent = {
onTagClick(artifact: Artifact): void {
if (artifact) {
let evt: ArtifactClickEvent = {
project_id: this.projectId,
repository_name: this.repoName,
tag_name: tag.name
digest: artifact.digest,
artifact_id: artifact.id,
};
this.tagClickEvent.emit(evt);
}
@ -710,9 +825,9 @@ export class TagComponent implements OnInit, AfterViewInit {
}
// Get vulnerability scanning status
scanStatus(t: Tag): string {
if (t) {
let so = this.handleScanOverview(t.scan_overview);
scanStatus(artifact: Artifact): string {
if (artifact) {
let so = this.handleScanOverview(artifact.scan_overview);
if (so && so.scan_status) {
return so.scan_status;
}
@ -728,29 +843,29 @@ export class TagComponent implements OnInit, AfterViewInit {
}
getImagePermissionRule(projectId: number): void {
const permissions = [
{resource: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE},
{resource: USERSTATICPERMISSION.REPOSITORY.KEY, action: USERSTATICPERMISSION.REPOSITORY.VALUE.PULL},
{resource: USERSTATICPERMISSION.REPOSITORY_TAG.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE},
{resource: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE},
{ resource: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE },
{ resource: USERSTATICPERMISSION.REPOSITORY.KEY, action: USERSTATICPERMISSION.REPOSITORY.VALUE.PULL },
{ resource: USERSTATICPERMISSION.REPOSITORY_TAG.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE },
{ resource: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE },
];
this.userPermissionService.hasProjectPermissions(this.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) {
if (!this.withAdmiral) {
this.getAllLabels();
}
this.hasRetagImagePermission = results[1];
this.hasDeleteImagePermission = results[2];
this.hasScanImagePermission = results[3];
// only has label permission
if (this.hasAddLabelImagePermission) {
if (!this.withAdmiral) {
this.getAllLabels();
}
}
}, error => this.errorHandler.error(error));
}
// Trigger scan
scanNow(): void {
if (this.selectedRow && this.selectedRow.length === 1) {
this.onSendingScanCommand = true;
this.channel.publishScanEvent(this.repoName + "/" + this.selectedRow[0].name);
this.onSendingScanCommand = true;
this.channel.publishScanEvent(this.repoName + "/" + this.selectedRow[0].digest);
}
}
submitFinish(e: boolean) {
@ -764,17 +879,17 @@ export class TagComponent implements OnInit, AfterViewInit {
this.hasEnabledScanner = false;
this.scanBtnState = ClrLoadingState.LOADING;
this.scanningService.getProjectScanner(this.projectId)
.subscribe(response => {
if (response && "{}" !== JSON.stringify(response) && !response.disabled
.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.SUCCESS;
this.hasEnabledScanner = true;
} else {
this.scanBtnState = ClrLoadingState.ERROR;
});
}
}, error => {
this.scanBtnState = ClrLoadingState.ERROR;
});
}
handleScanOverview(scanOverview: any): VulnerabilitySummary {
@ -783,4 +898,22 @@ export class TagComponent implements OnInit, AfterViewInit {
}
return null;
}
refer(artifact: Artifact) {
this.referArtifactArray.push(artifact.digest);
sessionStorage.setItem('reference', JSON.stringify({ projectId: this.projectId, repo: this.repoName
, referArray: this.referArtifactArray}));
if (this.referArtifactArray.length) {
this.putReferArtifactArray.emit(this.referArtifactArray);
}
let st: ClrDatagridStateInterface = this.currentState;
if (!st) {
st = { page: {} };
}
st.page.size = this.pageSize;
st.page.from = 0;
st.page.to = this.pageSize - 1;
this.clrLoad(st);
}
}

View File

@ -0,0 +1,24 @@
<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">{{repositoryName}}:{{artifactDetails?.digest | slice:0:15}}</h2> -->
<h2 class="custom-h2">{{artifact?.digest | slice:0:15}}</h2>
</div>
</div>
<ng-container *ngIf="artifact">
<!-- common properties -->
<artifact-common-properties [artifactDetails]="artifact"></artifact-common-properties>
<!-- tags -->
<artifact-tag [artifactDetails]="artifact" [projectName]="projectName" [repositoryName]="repositoryName"
(refreshArtifact)="refreshArtifact()"></artifact-tag>
<!-- Additions -->
<artifact-additions [additionLinks]="artifact?.addition_links"></artifact-additions>
</ng-container>
<div *ngIf="!artifact" class="clr-row mt-3 center">
<span class="spinner spinner-md"></span>
</div>

View File

@ -0,0 +1,8 @@
.margin-top-5px {
margin-top: 5px;
}
.center {
justify-content: center;
align-items: center;
}

View File

@ -0,0 +1,65 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ArtifactSummaryComponent } from "./artifact-summary.component";
import { of } from "rxjs";
import { Artifact } from "../../../../ng-swagger-gen/models/artifact";
import { ProjectService } from "../../services";
import { ArtifactService } from "../../../../ng-swagger-gen/services/artifact.service";
import { ErrorHandler } from "../../utils/error-handler";
import { ClarityModule } from "@clr/angular";
import { NO_ERRORS_SCHEMA } from "@angular/core";
describe('ArtifactSummaryComponent', () => {
const mockedArtifact: Artifact = {
id: 123,
type: 'IMAGE'
};
const fakedProjectService = {
getProject() {
return of({name: 'test'});
}
};
const fakedArtifactService = {
getArtifact() {
return of(mockedArtifact);
}
};
let component: ArtifactSummaryComponent;
let fixture: ComponentFixture<ArtifactSummaryComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
ClarityModule
],
declarations: [
ArtifactSummaryComponent
],
schemas: [
NO_ERRORS_SCHEMA
],
providers: [
{provide: ProjectService, useValue: fakedProjectService},
{provide: ArtifactService, useValue: fakedArtifactService},
ErrorHandler
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ArtifactSummaryComponent);
component = fixture.componentInstance;
component.repositoryName = 'demo';
component.artifactDigest = 'sha: acf4234f';
fixture.detectChanges();
});
it('should create and get artifactDetails', async () => {
expect(component).toBeTruthy();
await fixture.whenStable();
expect(component.artifact.type).toEqual('IMAGE');
});
});

View File

@ -0,0 +1,65 @@
import { Component, Input, Output, EventEmitter, OnInit } from "@angular/core";
import { ProjectService } from "../../services";
import { ErrorHandler } from "../../utils/error-handler";
import { Label } from "../../services/interface";
import { Artifact } from "../../../../ng-swagger-gen/models/artifact";
import { ArtifactService } from "../../../../ng-swagger-gen/services/artifact.service";
@Component({
selector: "artifact-summary",
templateUrl: "./artifact-summary.component.html",
styleUrls: ["./artifact-summary.component.scss"],
providers: []
})
export class ArtifactSummaryComponent implements OnInit {
labels: Label;
@Input()
artifactDigest: string;
@Input()
repositoryName: string;
@Input()
withAdmiral: boolean;
artifact: Artifact;
@Output()
backEvt: EventEmitter<any> = new EventEmitter<any>();
@Input() projectId: number;
projectName: string;
constructor(
private projectService: ProjectService,
private artifactService: ArtifactService,
private errorHandler: ErrorHandler,
) {
}
ngOnInit(): void {
if (this.repositoryName && this.artifactDigest) {
this.projectService.getProject(this.projectId).subscribe(project => {
this.projectName = project.name;
this.getArtifactDetails();
});
}
}
getArtifactDetails(): void {
this.artifactService.getArtifact({
repositoryName: this.repositoryName,
reference: this.artifactDigest,
projectName: this.projectName,
}).subscribe(response => {
this.artifact = response;
}, error => {
this.errorHandler.error(error);
});
}
onBack(): void {
this.backEvt.emit(this.repositoryName);
}
refreshArtifact() {
this.getArtifactDetails();
}
}

View File

@ -0,0 +1,58 @@
<h4>{{'REPOSITORY.TAGS_COUNT' | translate}}</h4>
<clr-datagrid [clrDgLoading]="loading" [(clrDgSelected)]="selectedRow" [clrDgRowSelection]="true">
<clr-dg-action-bar>
<button type="button" class="btn btn-secondary" (click)="addTag()">
<clr-icon shape="plus" size="16"></clr-icon>&nbsp;{{'TAG.ADD_TAG' | translate}}
</button>
<button type="button" class="btn btn-secondary" [disabled]="!(selectedRow.length>=1)" (click)="removeTag()">
<clr-icon shape="trash" size="16"></clr-icon>&nbsp;{{'TAG.REMOVE_TAG' | translate}}
</button>
<form #labelForm="ngForm" [hidden]="!newTagformShow" class="label-form stack-block-label">
<section>
<label>
<label for="name">{{'TAG.NAME' | translate}}</label>
<label class="clr-control-container" [class.clr-error]="isTagNameExist">
<input clrInput type="text" id="name" name="name" required size="20" autocomplete="off"
[(ngModel)]="newTagName.name" #name="ngModel" (keyup)="existValid(newTagName.name)">
<clr-control-error class="position-ab" *ngIf="isTagNameExist">
{{'LABEL.NAME_ALREADY_EXISTS' | translate }}
</clr-control-error>
</label>
</label>
<label>
<button type="button" class="btn btn-sm btn-outline" (click)="cancelAddTag()">{{
'BUTTON.CANCEL' | translate }}
</button>
<button type="submit" class="btn btn-sm btn-primary" (click)="saveAddTag()"
[disabled]="isTagNameExist || !newTagName.name">{{
'BUTTON.OK' | translate }}
</button>
</label>
</section>
</form>
</clr-dg-action-bar>
<clr-dg-column [clrDgField]="'name'">{{'TAG.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'TAG.PULL_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'TAG.PUSH_TIME' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let tag of artifactDetails?.tags" [clrDgItem]="tag">
<clr-dg-cell>
<div class="cell white-normal" [class.immutable]="tag.immutable">
<span href="javascript:void(0)" class="max-width-100" title="{{tag.name}}">{{tag.name}}</span>
<span *ngIf="tag.immutable" class="label label-info ml-8">{{'REPOSITORY.IMMUTABLE' | translate}}</span>
</div>
</clr-dg-cell>
<clr-dg-cell>{{tag.pull_time !== availableTime? (tag.pull_time | date: 'short') : ""}}</clr-dg-cell>
<clr-dg-cell>{{tag.push_time | date: 'short'}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}}
{{'TAG.OF' | translate}} {{pagination.totalItems}} {{'TAG.ITEMS' | translate}}</span>
<clr-dg-pagination #pagination [clrDgPageSize]="10"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
<confirmation-dialog class="hidden-tag" #confirmationDialog (confirmAction)="confirmDeletion($event)">
</confirmation-dialog>

View File

@ -0,0 +1,27 @@
.label-form {
max-width: 100% !important;
}
.clr-control-container {
margin-bottom: 1rem;
}
.btn.remove-btn {
border: none;
height: 0.6rem;
line-height: 1;
}
.immutable {
padding-right: 94px;
position: relative;
.label {
position: absolute;
right: 0;
margin-right: 0;
}
}
.datagrid-action-bar {
margin-top: 0.5rem;
}
.position-ab {
position: absolute;
}

View File

@ -0,0 +1,58 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ErrorHandler } from "../../../utils/error-handler/error-handler";
import { ArtifactTagComponent } from './artifact-tag.component';
import { SharedModule } from '../../../utils/shared/shared.module';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { OperationService } from '../../operation/operation.service';
import { TagService } from '../../../services';
import { of } from 'rxjs';
import { SERVICE_CONFIG, IServiceConfig } from '../../../entities/service.config';
describe('ArtifactTagComponent', () => {
let component: ArtifactTagComponent;
let fixture: ComponentFixture<ArtifactTagComponent>;
const mockErrorHandler = {
error: () => {}
};
const mockTagService = {
newTag: () => of([]),
deleteTag: () => of(null),
};
const config: IServiceConfig = {
repositoryBaseEndpoint: "/api/repositories/testing"
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
SharedModule,
BrowserAnimationsModule,
HttpClientTestingModule
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
],
declarations: [ ArtifactTagComponent ],
providers: [
ErrorHandler,
{ provide: SERVICE_CONFIG, useValue: config },
{ provide: mockErrorHandler, useValue: ErrorHandler },
{ provide: TagService, useValue: mockTagService },
{ provide: OperationService },
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ArtifactTagComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,153 @@
import { Component, OnInit, Input, ViewChild, Output, EventEmitter } from '@angular/core';
import { Artifact } from '../artifact';
import { TagService } from '../../../services';
import { Tag } from '../../../../../ng-swagger-gen/models/tag';
import { ConfirmationButtons, ConfirmationTargets, ConfirmationState } from '../../../entities/shared.const';
import { ConfirmationMessage, ConfirmationDialogComponent, ConfirmationAcknowledgement } from '../../confirmation-dialog';
import { Observable, of, forkJoin } from 'rxjs';
import { OperateInfo, OperationState, operateChanges } from '../../operation/operate';
import { OperationService } from '../../operation/operation.service';
import { map, catchError } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { errorHandler as errorHandFn } from "../../../utils/shared/shared.utils";
import { NgForm } from '@angular/forms';
import { ErrorHandler } from '../../../utils/error-handler';
import { AVAILABLE_TIME } from '../artifact-list-tab.component';
class InitTag {
name = "";
}
@Component({
selector: 'artifact-tag',
templateUrl: './artifact-tag.component.html',
styleUrls: ['./artifact-tag.component.scss']
})
export class ArtifactTagComponent implements OnInit {
@Input() artifactDetails: Artifact;
@Input() projectName: string;
@Input() repositoryName: string;
@Output() refreshArtifact = new EventEmitter();
newTagName = new InitTag();
newTagForm: NgForm;
@ViewChild("newTagForm", { static: true }) currentForm: NgForm;
selectedRow: Tag[] = [];
isTagNameExist = false;
newTagformShow = false;
loading = false;
openTag = false;
availableTime = AVAILABLE_TIME;
@ViewChild("confirmationDialog", { static: false })
confirmationDialog: ConfirmationDialogComponent;
constructor(
private operationService: OperationService,
private tagService: TagService,
private translateService: TranslateService,
private errorHandler: ErrorHandler
) { }
ngOnInit() {
}
addTag() {
this.newTagformShow = true;
}
cancelAddTag() {
this.newTagformShow = false;
this.newTagName = new InitTag();
}
saveAddTag() {
this.tagService.newTag(this.projectName, this.repositoryName, this.artifactDetails.digest, this.newTagName).subscribe(res => {
this.newTagformShow = false;
this.newTagName = new InitTag();
this.refreshArtifact.emit();
}, error => {
this.errorHandler.error(error);
});
}
removeTag() {
if (this.selectedRow && this.selectedRow.length) {
let tagNames: string[] = [];
this.selectedRow.forEach(artifact => {
tagNames.push(artifact.name);
});
let titleKey: string, summaryKey: string, content: string, buttons: ConfirmationButtons;
titleKey = "REPOSITORY.DELETION_TITLE_TAG";
summaryKey = "REPOSITORY.DELETION_SUMMARY_TAG";
buttons = ConfirmationButtons.DELETE_CANCEL;
content = tagNames.join(" , ");
let message = new ConfirmationMessage(
titleKey,
summaryKey,
content,
this.selectedRow,
ConfirmationTargets.TAG,
buttons);
this.confirmationDialog.open(message);
}
}
confirmDeletion(message: ConfirmationAcknowledgement) {
if (message &&
message.source === ConfirmationTargets.TAG
&& message.state === ConfirmationState.CONFIRMED) {
let tagList: Tag[] = message.data;
if (tagList && tagList.length) {
let observableLists: any[] = [];
tagList.forEach(tag => {
observableLists.push(this.delOperate(tag));
});
forkJoin(...observableLists).subscribe((items) => {
// if delete one success refresh list
if (items.some(item => !item)) {
this.selectedRow = [];
this.refreshArtifact.emit();
}
});
}
}
}
delOperate(tag): Observable<any> | null {
// init operation info
let operMessage = new OperateInfo();
operMessage.name = 'OPERATION.DELETE_TAG';
operMessage.state = OperationState.progressing;
operMessage.data.name = tag.name;
this.operationService.publishInfo(operMessage);
return this.tagService
.deleteTag(this.projectName, this.repositoryName, this.artifactDetails.digest, tag.name)
.pipe(map(
response => {
this.translateService.get("BATCH.DELETED_SUCCESS")
.subscribe(res => {
operateChanges(operMessage, OperationState.success);
});
}), catchError(error => {
const message = errorHandFn(error);
this.translateService.get(message).subscribe(res =>
operateChanges(operMessage, OperationState.failure, res)
);
return of(error);
}));
}
existValid(name) {
this.isTagNameExist = false;
if (this.artifactDetails.tags) {
this.artifactDetails.tags.forEach(tag => {
if (tag.name === name) {
this.isTagNameExist = true;
}
});
}
}
toggleTagListOpenOrClose() {
this.openTag = !this.openTag;
this.newTagformShow = false;
}
}

View File

@ -0,0 +1,58 @@
import { Label, Tag } from "../../services";
export class Artifact {
id: number;
type: string;
repository: string;
tags: Tag[];
media_type: string;
digest: string;
size: number;
upload_time?: string;
// labels: string[];
extra_attrs?: Map<string, string>;
addition_links?: Map<string, string>;
references: Reference[];
scan_overview: any;
labels: Label[];
push_time: string;
pull_time: string;
isOpen?: boolean; // front
referenceIndexOpenState?: boolean; // front
referenceDigestOpenState?: boolean; // front
hasReferenceArtifactList?: Artifact[] = []; // front
noReferenceArtifactList?: Artifact[] = []; // front
constructor(digestName, hasReference?) {
this.id = 1;
this.type = 'type';
this.size = 1111111111;
this.upload_time = '2020-01-06T09:40:08.036866579Z';
this.digest = digestName;
this.tags = [
{
id: '1',
artifact_id: 1,
name: 'tag1',
upload_time: '2020-01-06T09:40:08.036866579Z'
},
{
id: '2',
artifact_id: 2,
name: 'tag2',
upload_time: '2020-01-06T09:40:08.036866579Z',
},
];
// tslint:disable-next-line: no-use-before-declare
// this.references = [];
this.references = hasReference ? [new Reference(1), new Reference(2)] : [];
}
}
export class Reference {
child_id: number;
child_digest: string;
parent_id: number;
platform?: any; // json
constructor(artifact_id) {
this.child_id = artifact_id;
}
}

View File

@ -25,16 +25,17 @@
<div *ngIf="!isCardView" class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid (clrDgRefresh)="clrLoad($event)" [clrDgLoading]="loading" [(clrDgSelected)]="selectedRow">
<clr-dg-action-bar>
<button *ngIf="withAdmiral" type="button" class="btn btn-secondary" (click)="provisionItemEvent($event, selectedRow[0])" [disabled]="!(selectedRow.length===1 && hasProjectAdminRole)"><clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'REPOSITORY.DEPLOY' | translate}}</button>
<button *ngIf="withAdmiral" type="button" class="btn btn-secondary" (click)="itemAddInfoEvent($event, selectedRow[0])" [disabled]="!(selectedRow.length===1 && hasProjectAdminRole)"><clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'REPOSITORY.ADDITIONAL_INFO' | translate}}</button>
<!-- <button *ngIf="withAdmiral" type="button" class="btn btn-secondary" (click)="provisionItemEvent($event, selectedRow[0])" [disabled]="!(selectedRow.length===1 && hasProjectAdminRole)"><clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'REPOSITORY.DEPLOY' | translate}}</button>
<button *ngIf="withAdmiral" type="button" class="btn btn-secondary" (click)="itemAddInfoEvent($event, selectedRow[0])" [disabled]="!(selectedRow.length===1 && hasProjectAdminRole)"><clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'REPOSITORY.ADDITIONAL_INFO' | translate}}</button> -->
<button type="button" class="btn btn-secondary" (click)="deleteRepos(selectedRow)" [disabled]="!(selectedRow.length)|| !hasDeleteRepositoryPermission"><clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'REPOSITORY.DELETE' | translate}}</button>
</clr-dg-action-bar>
<clr-dg-column [clrDgField]="'q'" [clrDgSortBy]="'name'">{{'REPOSITORY.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.TAGS_COUNT' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.ARTIFACTS_COUNT' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="'pull_count'">{{'REPOSITORY.PULL_COUNT' | translate}}</clr-dg-column>
<clr-dg-placeholder>{{'REPOSITORY.PLACEHOLDER' | translate }}</clr-dg-placeholder>
<clr-dg-row *ngFor="let r of repositories" [clrDgItem]="r">
<clr-dg-cell><a href="javascript:void(0)" (click)="watchRepoClickEvt(r)"><span *ngIf="withAdmiral" class="list-img"><img [src]="getImgLink(r)"/></span>{{r.name}}</a></clr-dg-cell>
<!-- to do -->
<clr-dg-cell>{{r.tags_count}}</clr-dg-cell>
<clr-dg-cell>{{r.pull_count}}</clr-dg-cell>
</clr-dg-row>
@ -72,7 +73,7 @@
{{item.description || ('REPOSITORY.NO_INFO' | translate)}}
</div>
<div class="form-group">
<label>{{'REPOSITORY.TAGS_COUNT' | translate}}</label>
<label>{{'REPOSITORY.ARTIFACTS_COUNT' | translate}}</label>
<div>{{item.tags_count}}</div>
</div>
<div class="form-group">

View File

@ -22,6 +22,7 @@ import { UserPermissionService } from "../../services/permission.service";
import { of } from "rxjs";
import { HarborLibraryModule } from "../../harbor-library.module";
import { delay } from 'rxjs/operators';
import { RepositoryService as NewRepositoryService } from "../../../../ng-swagger-gen/services/repository.service";
describe('RepositoryComponentGridview (inline template)', () => {
let compRepo: RepositoryGridviewComponent;
@ -117,6 +118,7 @@ describe('RepositoryComponentGridview (inline template)', () => {
{ provide: ErrorHandler, useValue: fakedErrorHandler },
{ provide: SERVICE_CONFIG, useValue: config },
{ provide: RepositoryService, useValue: fakedRepositoryService },
{ provide: NewRepositoryService, useValue: fakedRepositoryService },
{ provide: TagService, useClass: TagDefaultService },
{ provide: ProjectService, useClass: ProjectDefaultService },
{ provide: RetagService, useClass: RetagDefaultService },

View File

@ -42,6 +42,7 @@ import { Observable, throwError as observableThrowError } from "rxjs";
import { errorHandler as errorHandFn } from "../../utils/shared/shared.utils";
import { ClrDatagridStateInterface } from "@clr/angular";
import { FilterComponent } from "../filter/filter.component";
import { RepositoryService as NewRepositoryService} from "../../../../ng-swagger-gen/services/repository.service";
@Component({
selector: "hbr-repository-gridview",
templateUrl: "./repository-gridview.component.html",
@ -89,6 +90,7 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit, OnDestroy
private errorHandler: ErrorHandler,
private translateService: TranslateService,
private repositoryService: RepositoryService,
private newRepoService: NewRepositoryService,
private systemInfoService: SystemInfoService,
private tagService: TagService,
private operationService: OperationService,
@ -169,11 +171,12 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit, OnDestroy
let repArr: any[] = [];
message.data.forEach(repo => {
if (!this.signedCon[repo.name]) {
repArr.push(this.getTagInfo(repo.name));
// to do
// repArr.push(this.getTagInfo(repo.name));
}
});
this.loading = true;
forkJoin(...repArr).subscribe(() => {
// forkJoin(...repArr).subscribe(() => {
if (message &&
message.source === ConfirmationTargets.REPOSITORY &&
message.state === ConfirmationState.CONFIRMED) {
@ -199,10 +202,10 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit, OnDestroy
});
}
}
}, error => {
this.errorHandler.error(error);
this.loading = false;
});
// }, error => {
// this.errorHandler.error(error);
// this.loading = false;
// });
}
delOperate(repo: RepositoryItem): Observable<any> {
@ -214,14 +217,16 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit, OnDestroy
operMessage.data.name = repo.name;
this.operationService.publishInfo(operMessage);
if (this.signedCon[repo.name].length !== 0) {
return forkJoin(this.translateService.get('BATCH.DELETED_FAILURE'),
this.translateService.get('REPOSITORY.DELETION_TITLE_REPO_SIGNED')).pipe(map(res => {
operateChanges(operMessage, OperationState.failure, res[1]);
}));
} else {
return this.repositoryService
.deleteRepository(repo.name)
// if (this.signedCon[repo.name].length !== 0) {
// return forkJoin(this.translateService.get('BATCH.DELETED_FAILURE'),
// this.translateService.get('REPOSITORY.DELETION_TITLE_REPO_SIGNED')).pipe(map(res => {
// operateChanges(operMessage, OperationState.failure, res[1]);
// }));
// } else {
return this.newRepoService
.deleteRepository({
repositoryName: repo.name,
projectName: this.projectName})
.pipe(map(
response => {
this.translateService.get('BATCH.DELETED_SUCCESS').subscribe(res => {
@ -234,7 +239,7 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit, OnDestroy
);
return observableThrowError(message);
}));
}
// }
}
doSearchRepoNames(repoName: string) {
@ -269,19 +274,19 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit, OnDestroy
ConfirmationButtons.DELETE_CANCEL);
}
}
getTagInfo(repoName: string): Observable<void> {
this.signedCon[repoName] = [];
return this.tagService.getTags(repoName)
.pipe(map(items => {
items.forEach((t: Tag) => {
if (t.signature !== null) {
this.signedCon[repoName].push(t.name);
}
});
})
, catchError(error => observableThrowError(error)));
}
// to do delete when user sign
// getTagInfo(repoName: string): Observable<void> {
// this.signedCon[repoName] = [];
// return this.tagService.getTags(repoName)
// .pipe(map(items => {
// items.forEach((t: any) => {
// if (t.signature !== null) {
// this.signedCon[repoName].push(t.name);
// }
// });
// })
// , catchError(error => observableThrowError(error)));
// }
confirmationDialogSet(summaryTitle: string, signature: string,
repoName: string, repoLists: RepositoryItem[],
@ -304,37 +309,6 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit, OnDestroy
});
}
containsLatestTag(repo: RepositoryItem): Observable<boolean> {
return this.tagService.getTags(repo.name)
.pipe(map(items => {
if (items.some((t: Tag) => {
return t.name === 'latest';
})) {
return true;
} else {
return false;
}
})
, catchError(error => observableThrowError(false)));
}
provisionItemEvent(evt: any, repo: RepositoryItem): void {
evt.stopPropagation();
let repoCopy = clone(repo);
repoCopy.name = this.registryUrl + ":443/" + repoCopy.name;
this.containsLatestTag(repo)
.subscribe(containsLatest => {
if (containsLatest) {
this.repoProvisionEvent.emit(repoCopy);
} else {
this.addInfoEvent.emit(repoCopy);
}
}, error => this.errorHandler.error(error));
}
itemAddInfoEvent(evt: any, repo: RepositoryItem): void {
evt.stopPropagation();
let repoCopy = clone(repo);

View File

@ -1,72 +0,0 @@
<div>
<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)="onBack()"></clr-icon>
</div>
<div class="title-block">
<h2 class="custom-h2">{{repositoryId}}:{{tagDetails.name}}</h2>
</div>
</div>
<div class="summary-block row">
<div class="image-summary col-md-4 col-sm-6">
<div class="flex-block">
<section class="image-detail-label">
<section class="detail-row">
<label class="detail-label">{{'TAG.AUTHOR' | translate }}</label>
<div class="image-details" [title]="author | translate">{{author | translate}}</div>
</section>
<section class="detail-row">
<label class="detail-label">{{'TAG.ARCHITECTURE' | translate }}</label>
<div class="image-details" [title]="tagDetails.architecture">{{tagDetails.architecture}}</div>
</section>
<section class="detail-row">
<label class="detail-label">{{'TAG.OS' | translate }}</label>
<div class="image-details" [title]="tagDetails.os">{{tagDetails.os}}</div>
</section>
<section class="detail-row">
<label class="detail-label">{{'TAG.OS_VERSION' | translate }}</label>
<div class="image-details" [title]="tagDetails['os.version']">{{tagDetails['os.version']}}</div>
</section>
<section class="detail-row">
<label class="detail-label">{{'TAG.DOCKER_VERSION' | translate }}</label>
<div class="image-details" [title]="tagDetails.docker_version">{{tagDetails.docker_version}}</div>
</section>
<section class="detail-row" *ngIf="hasCve">
<label class="detail-label">{{'TAG.SCAN_COMPLETION_TIME' | translate }}</label>
<div class="image-details" [title]="scanCompletedDatetime | date">{{scanCompletedDatetime | date}}</div>
</section>
</section>
</div>
</div>
<div class="col-md-4 col-sm-6 margin-top-5px">
<div class="vulnerability" [hidden]="hasCve || showStatBar">
<hbr-vulnerability-bar [repoName]="repositoryId" [tagId]="tagDetails.name" [summary]="vulnerabilitySummary"></hbr-vulnerability-bar>
</div>
<histogram-chart *ngIf="hasCve" [metadata]="passMetadataToChart()" [isWhiteBackground]="isThemeLight()"></histogram-chart>
</div>
<div *ngIf="!withAdmiral && tagDetails?.labels?.length">
<div class="third-column detail-title">{{'TAG.LABELS' | translate }}</div>
<div class="fourth-column">
<div *ngFor="let label of tagDetails.labels" class="detail-tag">
<hbr-label-piece [label]="label"></hbr-label-piece>
</div>
</div>
</div>
</div>
</section>
<clr-tabs>
<clr-tab *ngIf="hasVulnerabilitiesListPermission">
<button clrTabLink [clrTabLinkInOverflow]="false" class="btn btn-link nav-link" id="tag-vulnerability" [class.active]='isCurrentTabLink("tag-vulnerability")' type="button" (click)='tabLinkClick("tag-vulnerability")'>{{'REPOSITORY.VULNERABILITY' | translate}}</button>
<clr-tab-content id="content1" *clrIfActive="true">
<hbr-vulnerabilities-grid [repositoryId]="repositoryId" [projectId]="projectId" [tagId]="tagId"></hbr-vulnerabilities-grid>
</clr-tab-content>
</clr-tab>
<clr-tab *ngIf="hasBuildHistoryPermission">
<button [clrTabLinkInOverflow]="false" id="tag-history" clrTabLink class="btn btn-link nav-link" [class.active]='isCurrentTabLink("tag-history")' type="button" (click)='tabLinkClick("tag-history")'>{{ 'REPOSITORY.BUILD_HISTORY' | translate }}</button>
<clr-tab-content *clrIfActive>
<hbr-tag-history [repositoryId]="repositoryId" [tagId]="tagId">{{ 'REPOSITORY.BUILD_HISTORY' | translate }}</hbr-tag-history>
</clr-tab-content>
</clr-tab>
</clr-tabs>
</div>

View File

@ -1,156 +0,0 @@
@import "../../mixin";
$size24:24px;
.overview-section {
padding-bottom: 36px;
}
.detail-section {
background-color: #fafafa;
padding-left: 12px;
padding-right: $size24;
}
.title-block {
display: inline-block;
}
.tag-name {
font-weight: 300;
font-size: 32px;
}
.tag-name h2{margin-top:0;}
.tag-timestamp {
font-weight: 400;
font-size: 12px;
margin-top: 6px;
}
.rotate-90 {
-webkit-transform: rotate(-90deg);
/*Firefox*/
-moz-transform: rotate(-90deg);
/*Chrome*/
-ms-transform: rotate(-90deg);
/*IE9 、IE10*/
-o-transform: rotate(-90deg);
/*Opera*/
transform: rotate(-90deg);
}
.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;
}
.vulnerability-block {
margin-bottom: 12px;
}
.summary-block {
margin-top: $size24;
display: flex;
flex-wrap: row wrap;
}
.flex-block {
display: inline-flex;
flex-wrap: row wrap;
justify-content: space-around;
}
.third-column {
margin-left: 36px;
}
.vulnerability{
margin-bottom: 20px;}
.vulnerabilities-info {
padding-left: $size24;
.second-column{
text-align: left;
margin-left: 6px;
.second-row {
margin-top: 6px;
}
.row-flex {
display: flex;
align-items: center;
.icon-position {
width: $size24;
height: $size24;
@include flex-center;
}
.detail-count {
height: 20px;
width: 30px;
@include flex-center;
}
}
}
}
.fourth-column{
float: left;
margin-left:20px;
div {
height: $size24;
}
}
.detail-title {
float:left;
font-weight: 600;
font-size: 14px;
}
.detail-tag {
margin-bottom: 2px;
}
.image-detail-label {
margin-right: 10px;
text-align: left;
font-weight: 600;
.detail-row {
display: flex;
.detail-label {
flex:0 0 150px;
}
.image-details {
@include text-overflow-param(200px);
}
}
}
.image-detail-value {
text-align: left;
margin-left: 6px;
font-weight: 500;
div {
height: $size24;
}
}
.tip-icon-medium {
color: orange;
}
.tip-icon-low{
color:yellow;
}
.margin-top-5px {
margin-top: 5px;
}

View File

@ -1,204 +0,0 @@
import { ComponentFixture, TestBed, async } from "@angular/core/testing";
import { SharedModule } from "../../utils/shared/shared.module";
import { ResultGridComponent } from "../vulnerability-scanning/result-grid.component";
import { TagDetailComponent } from "./tag-detail.component";
import { TagHistoryComponent } from "./tag-history.component";
import { ErrorHandler } from "../../utils/error-handler/error-handler";
import {
Tag,
Manifest,
VulnerabilitySummary,
VulnerabilityItem,
VulnerabilitySeverity
} from "../../services/interface";
import { SERVICE_CONFIG, IServiceConfig } from "../../entities/service.config";
import {
TagService,
TagDefaultService,
ScanningResultService,
ScanningResultDefaultService
} from "../../services";
import { VULNERABILITY_SCAN_STATUS, VULNERABILITY_SEVERITY } from "../../utils/utils";
import { LabelPieceComponent } from "../label-piece/label-piece.component";
import { ChannelService } from "../../services/channel.service";
import { of } from "rxjs";
import {
JobLogService,
JobLogDefaultService
} from "../../services/job-log.service";
import { UserPermissionService, UserPermissionDefaultService } from "../../services/permission.service";
import { USERSTATICPERMISSION } from "../../services/permission-static";
import { FilterComponent } from "../filter/filter.component";
import { HarborLibraryModule } from "../../harbor-library.module";
describe("TagDetailComponent (inline template)", () => {
let comp: TagDetailComponent;
let fixture: ComponentFixture<TagDetailComponent>;
let tagService: TagService;
let userPermissionService: UserPermissionService;
let scanningService: ScanningResultService;
let spy: jasmine.Spy;
let vulSpy: jasmine.Spy;
let manifestSpy: jasmine.Spy;
let mockVulnerability: VulnerabilitySummary = {
scan_status: VULNERABILITY_SCAN_STATUS.SUCCESS,
severity: "High",
end_time: new Date(),
summary: {
total: 124,
fixable: 50,
summary: {
"High": 5,
"Low": 5
}
}
};
let mockTag: Tag = {
digest:
"sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
name: "nginx",
size: "2049",
architecture: "amd64",
os: "linux",
'os.version': "",
docker_version: "1.12.3",
author: "steven",
created: new Date("2016-11-08T22:41:15.912313785Z"),
signature: null,
scan_overview: {
"application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0": mockVulnerability
},
labels: []
};
let config: IServiceConfig = {
repositoryBaseEndpoint: "/api/repositories/testing"
};
let mockHasVulnerabilitiesListPermission: boolean = false;
let mockHasBuildHistoryPermission: boolean = true;
let mockManifest: Manifest = {
manifset: {},
// tslint:disable-next-line:max-line-length
config: `{"architecture":"amd64","config":{"Hostname":"","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh"],"ArgsEscaped":true,"Image":"sha256:fbef17698ac8605733924d5662f0cbfc0b27a51e83ab7d7a4b8d8a9a9fe0d1c2","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"30e1a2427aa2325727a092488d304505780501585a6ccf5a6a53c4d83a826101","container_config":{"Hostname":"30e1a2427aa2","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\\"/bin/sh\\"]"],"ArgsEscaped":true,"Image":"sha256:fbef17698ac8605733924d5662f0cbfc0b27a51e83ab7d7a4b8d8a9a9fe0d1c2","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{}},"created":"2018-01-09T21:10:58.579708634Z","docker_version":"17.06.2-ce","history":[{"created":"2018-01-09T21:10:58.365737589Z","created_by":"/bin/sh -c #(nop) ADD file:093f0723fa46f6cdbd6f7bd146448bb70ecce54254c35701feeceb956414622f in / "},{"created":"2018-01-09T21:10:58.579708634Z","created_by":"/bin/sh -c #(nop) CMD [\\"/bin/sh\\"]","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:cd7100a72410606589a54b932cabd804a17f9ae5b42a1882bd56d263e02b6215"]}}`
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
SharedModule,
HarborLibraryModule
],
providers: [
ErrorHandler,
ChannelService,
JobLogDefaultService,
{ provide: JobLogService, useClass: JobLogDefaultService },
{ provide: SERVICE_CONFIG, useValue: config },
{ provide: TagService, useClass: TagDefaultService },
{ provide: UserPermissionService, useClass: UserPermissionDefaultService },
{
provide: ScanningResultService,
useClass: ScanningResultDefaultService
}
]
});
}));
beforeEach(() => {
fixture = TestBed.createComponent(TagDetailComponent);
comp = fixture.componentInstance;
comp.tagId = "mock_tag";
comp.repositoryId = "mock_repo";
comp.projectId = 1;
tagService = fixture.debugElement.injector.get(TagService);
spy = spyOn(tagService, "getTag").and.returnValues(
of(mockTag)
);
let mockData: VulnerabilityItem[] = [];
for (let i = 0; i < 30; i++) {
let res: VulnerabilityItem = {
id: "CVE-2016-" + (8859 + i),
severity:
i % 2 === 0
? VULNERABILITY_SEVERITY.HIGH
: VULNERABILITY_SEVERITY.MEDIUM,
package: "package_" + i,
links: ["https://security-tracker.debian.org/tracker/CVE-2016-4484"],
layer: "layer_" + i,
version: "4." + i + ".0",
fix_version: "4." + i + ".11",
description: "Mock data"
};
mockData.push(res);
}
scanningService = fixture.debugElement.injector.get(ScanningResultService);
vulSpy = spyOn(
scanningService,
"getVulnerabilityScanningResults"
).and.returnValue(of(mockData));
manifestSpy = spyOn(tagService, "getManifest").and.returnValues(
of(mockManifest)
);
userPermissionService = fixture.debugElement.injector.get(UserPermissionService);
spyOn(userPermissionService, "getPermission")
.withArgs(comp.projectId,
USERSTATICPERMISSION.REPOSITORY_TAG_VULNERABILITY.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_VULNERABILITY.VALUE.LIST )
.and.returnValue(of(mockHasVulnerabilitiesListPermission))
.withArgs(comp.projectId, USERSTATICPERMISSION.REPOSITORY_TAG_MANIFEST.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_MANIFEST.VALUE.READ )
.and.returnValue(of(mockHasBuildHistoryPermission));
fixture.detectChanges();
});
it("should load data", async(() => {
expect(spy.calls.any).toBeTruthy();
}));
it("should load history data", async(() => {
expect(manifestSpy.calls.any).toBeTruthy();
}));
it("should rightly display tag name and version", async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let el: HTMLElement = fixture.nativeElement.querySelector(".custom-h2");
expect(el).toBeTruthy();
expect(el.textContent.trim()).toEqual("mock_repo:nginx");
});
}));
it("should display tag details", async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let el: HTMLElement = fixture.nativeElement.querySelector(
".image-detail-label .image-details"
);
expect(el).toBeTruthy();
expect(el.textContent).toEqual("steven");
});
}));
it("should render history info", async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let els: HTMLElement[] = fixture.nativeElement.querySelectorAll(
".history-item"
);
expect(els).toBeTruthy();
expect(els.length).toBe(2);
});
}));
});

View File

@ -1,262 +0,0 @@
import { Component, Input, Output, EventEmitter, OnInit } from "@angular/core";
import { TagService, Tag, VulnerabilitySeverity, VulnerabilitySummary } from "../../services";
import { ErrorHandler } from "../../utils/error-handler";
import { Label } from "../../services/interface";
import { forkJoin } from "rxjs";
import { UserPermissionService } from "../../services/permission.service";
import { USERSTATICPERMISSION } from "../../services/permission-static";
import { ChannelService } from "../../services/channel.service";
import { DEFAULT_SUPPORTED_MIME_TYPE, VULNERABILITY_SCAN_STATUS, VULNERABILITY_SEVERITY } from "../../utils/utils";
const TabLinkContentMap: { [index: string]: string } = {
"tag-history": "history",
"tag-vulnerability": "vulnerability"
};
@Component({
selector: "hbr-tag-detail",
templateUrl: "./tag-detail.component.html",
styleUrls: ["./tag-detail.component.scss"],
providers: []
})
export class TagDetailComponent implements OnInit {
_highCount: number = 0;
_mediumCount: number = 0;
_lowCount: number = 0;
_unknownCount: number = 0;
labels: Label;
vulnerabilitySummary: VulnerabilitySummary;
@Input()
tagId: string;
@Input()
repositoryId: string;
@Input()
withAdmiral: boolean;
tagDetails: Tag = {
name: "--",
size: "--",
author: "--",
created: new Date(),
architecture: "--",
os: "--",
"os.version": "--",
docker_version: "--",
digest: "--",
labels: []
};
@Output()
backEvt: EventEmitter<any> = new EventEmitter<any>();
currentTabID = "tag-vulnerability";
hasVulnerabilitiesListPermission: boolean;
hasBuildHistoryPermission: boolean;
@Input() projectId: number;
showStatBar: boolean = true;
constructor(
private tagService: TagService,
public channel: ChannelService,
private errorHandler: ErrorHandler,
private userPermissionService: UserPermissionService
) {}
ngOnInit(): void {
if (this.repositoryId && this.tagId) {
this.tagService.getTag(this.repositoryId, this.tagId).subscribe(
response => {
this.getTagDetails(response);
},
error => this.errorHandler.error(error)
);
}
this.getTagPermissions(this.projectId);
this.channel.tagDetail$.subscribe(tag => {
this.getTagDetails(tag);
});
}
getTagDetails(tagDetails: Tag): void {
this.tagDetails = tagDetails;
if (tagDetails
&& tagDetails.scan_overview
&& tagDetails.scan_overview[DEFAULT_SUPPORTED_MIME_TYPE]) {
this.vulnerabilitySummary = tagDetails.scan_overview[DEFAULT_SUPPORTED_MIME_TYPE];
this.showStatBar = false;
}
}
onBack(): void {
this.backEvt.emit(this.repositoryId);
}
getPackageText(count: number): string {
return count > 1 ? "VULNERABILITY.PACKAGES" : "VULNERABILITY.PACKAGE";
}
packageText(count: number): string {
return count > 1
? "VULNERABILITY.GRID.COLUMN_PACKAGES"
: "VULNERABILITY.GRID.COLUMN_PACKAGE";
}
haveText(count: number): string {
return count > 1 ? "TAG.HAVE" : "TAG.HAS";
}
public get author(): string {
return this.tagDetails && this.tagDetails.author
? this.tagDetails.author
: "TAG.ANONYMITY";
}
private getCountByLevel(level: string): number {
if (this.vulnerabilitySummary && this.vulnerabilitySummary.summary
&& this.vulnerabilitySummary.summary.summary) {
return this.vulnerabilitySummary.summary.summary[level];
}
return 0;
}
/**
* count of critical level vulnerabilities
*/
get criticalCount(): number {
return this.getCountByLevel(VULNERABILITY_SEVERITY.CRITICAL);
}
/**
* count of high level vulnerabilities
*/
get highCount(): number {
return this.getCountByLevel(VULNERABILITY_SEVERITY.HIGH);
}
/**
* count of medium level vulnerabilities
*/
get mediumCount(): number {
return this.getCountByLevel(VULNERABILITY_SEVERITY.MEDIUM);
}
/**
* count of low level vulnerabilities
*/
get lowCount(): number {
return this.getCountByLevel(VULNERABILITY_SEVERITY.LOW);
}
/**
* count of unknown vulnerabilities
*/
get unknownCount(): number {
return this.getCountByLevel(VULNERABILITY_SEVERITY.UNKNOWN);
}
/**
* count of negligible vulnerabilities
*/
get negligibleCount(): number {
return this.getCountByLevel(VULNERABILITY_SEVERITY.NEGLIGIBLE);
}
get hasCve(): boolean {
return this.vulnerabilitySummary
&& this.vulnerabilitySummary.scan_status === VULNERABILITY_SCAN_STATUS.SUCCESS
&& this.vulnerabilitySummary.severity !== VULNERABILITY_SEVERITY.NONE;
}
public get scanCompletedDatetime(): Date {
return this.tagDetails && this.tagDetails.scan_overview
&& this.tagDetails.scan_overview[DEFAULT_SUPPORTED_MIME_TYPE]
? this.tagDetails.scan_overview[DEFAULT_SUPPORTED_MIME_TYPE].end_time
: null;
}
public get suffixForHigh(): string {
return this.highCount > 1
? "VULNERABILITY.PLURAL"
: "VULNERABILITY.SINGULAR";
}
public get suffixForMedium(): string {
return this.mediumCount > 1
? "VULNERABILITY.PLURAL"
: "VULNERABILITY.SINGULAR";
}
public get suffixForLow(): string {
return this.lowCount > 1
? "VULNERABILITY.PLURAL"
: "VULNERABILITY.SINGULAR";
}
public get suffixForUnknown(): string {
return this.unknownCount > 1
? "VULNERABILITY.PLURAL"
: "VULNERABILITY.SINGULAR";
}
isCurrentTabLink(tabID: string): boolean {
return this.currentTabID === tabID;
}
isCurrentTabContent(ContentID: string): boolean {
return TabLinkContentMap[this.currentTabID] === ContentID;
}
tabLinkClick(tabID: string) {
this.currentTabID = tabID;
}
getTagPermissions(projectId: number): void {
const hasVulnerabilitiesListPermission = this.userPermissionService.getPermission(
projectId,
USERSTATICPERMISSION.REPOSITORY_TAG_VULNERABILITY.KEY,
USERSTATICPERMISSION.REPOSITORY_TAG_VULNERABILITY.VALUE.LIST
);
const hasBuildHistoryPermission = this.userPermissionService.getPermission(
projectId,
USERSTATICPERMISSION.REPOSITORY_TAG_MANIFEST.KEY,
USERSTATICPERMISSION.REPOSITORY_TAG_MANIFEST.VALUE.READ
);
forkJoin(
hasVulnerabilitiesListPermission,
hasBuildHistoryPermission
).subscribe(
permissions => {
this.hasVulnerabilitiesListPermission = permissions[0] as boolean;
this.hasBuildHistoryPermission = permissions[1] as boolean;
},
error => this.errorHandler.error(error)
);
}
passMetadataToChart() {
return [
{
text: 'VULNERABILITY.SEVERITY.CRITICAL',
value: this.criticalCount ? this.criticalCount : 0,
color: 'red'
},
{
text: 'VULNERABILITY.SEVERITY.HIGH',
value: this.highCount ? this.highCount : 0,
color: '#e64524'
},
{
text: 'VULNERABILITY.SEVERITY.MEDIUM',
value: this.mediumCount ? this.mediumCount : 0,
color: 'orange'
},
{
text: 'VULNERABILITY.SEVERITY.LOW',
value: this.lowCount ? this.lowCount : 0,
color: '#007CBB'
},
{
text: 'VULNERABILITY.SEVERITY.NEGLIGIBLE',
value: this.negligibleCount ? this.negligibleCount : 0,
color: 'green'
},
{
text: 'VULNERABILITY.SEVERITY.UNKNOWN',
value: this.unknownCount ? this.unknownCount : 0,
color: 'grey'
},
];
}
isThemeLight() {
return localStorage.getItem('styleModeLocal') === 'LIGHT';
}
}

View File

@ -1,62 +0,0 @@
import { Component, Input, Output, EventEmitter, OnInit } from "@angular/core";
import { TagService, Manifest } from "../../services";
import { ErrorHandler } from "../../utils/error-handler";
@Component({
selector: "hbr-tag-history",
templateUrl: "./tag-history.component.html",
styleUrls: ["./tag-history.component.scss"],
providers: []
})
export class TagHistoryComponent implements OnInit {
@Input()
tagId: string;
@Input()
repositoryId: string;
@Output()
backEvt: EventEmitter<any> = new EventEmitter<any>();
config: any = {};
history: Object[] = [];
loading: Boolean = false;
constructor(
private tagService: TagService,
private errorHandler: ErrorHandler
) {}
ngOnInit(): void {
if (this.repositoryId && this.tagId) {
this.retrieve(this.repositoryId, this.tagId);
}
}
retrieve(repositoryId: string, tagId: string) {
this.loading = true;
this.tagService.getManifest(this.repositoryId, this.tagId)
.subscribe(data => {
this.config = JSON.parse(data.config);
this.config.history.forEach((ele: any) => {
if (ele.created_by !== undefined) {
ele.created_by = ele.created_by
.replace("/bin/sh -c #(nop)", "")
.trimLeft()
.replace("/bin/sh -c", "RUN");
} else {
ele.created_by = ele.comment;
}
this.history.push(ele);
});
this.loading = false;
}, error => {
this.errorHandler.error(error);
this.loading = false;
});
}
onBack(): void {
this.backEvt.emit(this.tagId);
}
}

View File

@ -1,166 +0,0 @@
<confirmation-dialog class="hidden-tag" #confirmationDialog (confirmAction)="confirmDeletion($event)"></confirmation-dialog>
<clr-modal class="hidden-tag" [(clrModalOpen)]="showTagManifestOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{ manifestInfoTitle | translate }}</h3>
<div class="modal-body">
<div class="row col-md-12">
<textarea class="clr-textarea w-100" rows="2" #digestTarget>{{digestId}}</textarea>
</div>
</div>
<div class="modal-footer">
<span class="copy-failed" [hidden]="!copyFailed">{{'TAG.COPY_ERROR' | translate}}</span>
<button type="button" class="btn btn-primary" [ngxClipboard]="digestTarget" (cbOnSuccess)="onSuccess($event)" (cbOnError)="onError($event)">{{'BUTTON.COPY' | translate}}</button>
</div>
</clr-modal>
<clr-modal class="hidden-tag" [(clrModalOpen)]="retagDialogOpened" [clrModalStaticBackdrop]="staticBackdrop">
<h3 class="modal-title">{{ 'REPOSITORY.RETAG' | translate }}</h3>
<div class="modal-body retag-modal-body">
<div class="row col-md-12">
<hbr-image-name-input #imageNameInput></hbr-image-name-input>
</div>
</div>
<div class="modal-footer">
<button type="button" [disabled]="imageNameInput.projectName.invalid||imageNameInput.repoName.invalid||imageNameInput.tagName.invalid||imageNameInput.noProjectInfo!=''" class="btn btn-primary" (click)="onRetag()">{{'BUTTON.CONFIRM' | translate}}</button>
</div>
</clr-modal>
<div class="row tag-row">
<div>
<div class="row flex-items-xs-right rightPos">
<div id="filterArea">
<div class='filterLabelPiece' *ngIf="!withAdmiral" [hidden]="!openLabelFilterPiece" [style.left.px]='32'>
<hbr-label-piece *ngIf="showlabel" [hidden]='!filterOneLabel' [label]="filterOneLabel" [labelWidth]="130"></hbr-label-piece>
</div>
<div class="flex-xs-middle">
<hbr-filter [readonly]="'readonly'" [withDivider]="true" filterPlaceholder="{{'TAG.FILTER_FOR_TAGS' | translate}}" (filterEvt)="doSearchTagNames($event)" (openFlag)="openFlagEvent($event)" [currentValue]="lastFilteredTagName"></hbr-filter>
<div class="label-filter-panel" *ngIf="!withAdmiral" [hidden]="!openLabelFilterPanel">
<a class="filterClose" (click)="closeFilter()">&times;</a>
<label class="filterLabelHeader filter-dark">{{'REPOSITORY.FILTER_BY_LABEL' | translate}}</label>
<div class="form-group"><input clrInput type="text" placeholder="Filter labels" [(ngModel)]="filterName" (keyup)="handleInputFilter()"></div>
<div [hidden]='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' [hidden]="!label.show" (click)="rightFilterLabel(label)">
<clr-icon shape="check" class='pull-left' [hidden]='!label.iconsShow'></clr-icon>
<div class='labelDiv'>
<hbr-label-piece [label]="label.label" [labelWidth]="160"></hbr-label-piece>
</div>
</button>
</div>
</div>
</div>
</div>
<span class="refresh-btn" (click)="refresh()">
<clr-icon shape="refresh"></clr-icon>
</span>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid [clrDgLoading]="loading" class="datagrid-top" [class.embeded-datagrid]="isEmbedded" [(clrDgSelected)]="selectedRow">
<clr-dg-action-bar>
<button [clrLoading]="scanBtnState" type="button" class="btn btn-secondary" [disabled]="!(canScanNow() && selectedRow.length==1 && hasEnabledScanner)" (click)="scanNow()"><clr-icon shape="shield-check" size="16"></clr-icon>&nbsp;{{'VULNERABILITY.SCAN_NOW' | translate}}</button>
<button type="button" class="btn btn-secondary" [disabled]="!(selectedRow.length==1)" (click)="showDigestId()"><clr-icon shape="copy" size="16"></clr-icon>&nbsp;{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
<clr-dropdown *ngIf="!withAdmiral">
<button type="button" class="btn btn-secondary" clrDropdownTrigger [disabled]="!(selectedRow.length==1)||!hasAddLabelImagePermission" (click)="addLabels()"><clr-icon shape="plus" size="16"></clr-icon>{{'REPOSITORY.ADD_LABELS' | translate}}</button>
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
<clr-dropdown>
<div class="filter-grid">
<label class="dropdown-header">{{'REPOSITORY.ADD_LABEL_TO_IMAGE' | translate}}</label>
<div class="form-group"><input clrInput type="text" placeholder="Filter labels" [(ngModel)]="stickName" (keyup)="handleStickInputFilter()"></div>
<div [hidden]='imageStickLabels.length' class="no-labels">{{'LABEL.NO_LABELS' | translate }}</div>
<div [hidden]='!imageStickLabels.length' class="has-label">
<button type="button" class="dropdown-item" *ngFor='let label of imageStickLabels' [hidden]='!label.show' (click)="stickLabel(label)">
<clr-icon shape="check" class='pull-left' [hidden]='!label.iconsShow'></clr-icon>
<div class='labelDiv'><hbr-label-piece [label]="label.label" [labelWidth]="130"></hbr-label-piece></div>
</button>
</div>
</div>
</clr-dropdown>
</clr-dropdown-menu>
</clr-dropdown>
<button type="button" class="btn btn-secondary" *ngIf="!withAdmiral" [disabled]="!(selectedRow.length===1)|| !hasRetagImagePermission" (click)="retag()"><clr-icon shape="copy" size="16"></clr-icon>&nbsp;{{'REPOSITORY.RETAG' | translate}}</button>
<button type="button" class="btn btn-secondary" *ngIf="hasDeleteImagePermission" (click)="deleteTags()" [disabled]="!hasDeleteImagePermission||!selectedRow.length"><clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'REPOSITORY.DELETE' | translate}}</button>
</clr-dg-action-bar>
<clr-dg-column class="flex-max-width" [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'size'">{{'REPOSITORY.SIZE' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.VULNERABILITY' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="withNotary">{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="createdComparator">{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'docker_version'">{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="!withAdmiral">{{'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>{{'TAG.PLACEHOLDER' | translate }}</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
<clr-dg-cell class="truncated flex-max-width">
<div class="cell white-normal" [class.immutable]="t.immutable">
<a href="javascript:void(0)" class="max-width-100" (click)="onTagClick(t)" title="{{t.name}}">{{t.name}}</a>
<span *ngIf="t.immutable" class="label label-info ml-8">{{'REPOSITORY.IMMUTABLE' | translate}}</span>
</div>
</clr-dg-cell>
<clr-dg-cell>
<div class="cell">
{{sizeTransform(t.size)}}
</div>
</clr-dg-cell>
<clr-dg-cell class="truncated" title="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}">
<div class="cell">
<hbr-copy-input #copyInput (onCopyError)="onCpError($event)" iconMode="true" defaultValue="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}"></hbr-copy-input>
</div>
</clr-dg-cell>
<clr-dg-cell>
<div class="cell">
<hbr-vulnerability-bar [scanner]="handleScanOverview(t.scan_overview)?.scanner" (submitFinish)="submitFinish($event)" [repoName]="repoName" [tagId]="t.name" [summary]="handleScanOverview(t.scan_overview)"></hbr-vulnerability-bar>
</div>
</clr-dg-cell>
<clr-dg-cell *ngIf="withNotary" [ngSwitch]="t.signature !== null">
<div class="cell">
<clr-icon shape="check-circle" *ngSwitchCase="true" size="20" class="color-green"></clr-icon>
<clr-icon shape="times-circle" *ngSwitchCase="false" size="16" class="color-red"></clr-icon>
<a href="javascript:void(0)" *ngSwitchDefault role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
<clr-icon shape="help" class="color-gray" size="16"></clr-icon>
<span class="tooltip-content">{{'REPOSITORY.NOTARY_IS_UNDETERMINED' | translate}}</span>
</a>
</div>
</clr-dg-cell>
<clr-dg-cell class="truncated" title="{{t.author}}">
<div class="cell">
{{t.author}}
</div>
</clr-dg-cell>
<clr-dg-cell>
<div class="cell">{{t.created | date: 'short'}}</div>
</clr-dg-cell>
<clr-dg-cell>
<div class="cell">{{t.docker_version}}</div>
</clr-dg-cell>
<clr-dg-cell *ngIf="!withAdmiral">
<div class="cell">
<hbr-label-piece *ngIf="t.labels?.length" [label]="t.labels[0]" [labelWidth]="90"> </hbr-label-piece>
<div class="signpost-item" [hidden]="t.labels?.length<=1">
<div class="trigger-item">
<clr-signpost>
<button class="btn btn-link" clrSignpostTrigger>...</button>
<clr-signpost-content [clrPosition]="'left-top'" *clrIfOpen>
<div>
<hbr-label-piece *ngFor="let label of t.labels" [label]="label"></hbr-label-piece>
</div>
</clr-signpost-content>
</clr-signpost>
</div>
</div>
</div>
</clr-dg-cell>
<clr-dg-cell>
<div class="cell">{{t.push_time | date: 'short'}}</div>
</clr-dg-cell>
<clr-dg-cell>
<div class="cell">{{t.pull_time === availableTime ? "" : (t.pull_time| date: 'short')}}</div>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}</span> {{pagination.totalItems}} {{'REPOSITORY.ITEMS' | translate}}&nbsp;&nbsp;&nbsp;&nbsp;
<clr-dg-pagination #pagination [clrDgPageSize]="50"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
</div>

View File

@ -6,8 +6,8 @@ import { ResultTipComponent } from './result-tip.component';
import {
ScanningResultService,
ScanningResultDefaultService,
TagService,
TagDefaultService,
ArtifactService,
ArtifactDefaultService,
JobLogService,
JobLogDefaultService
} from '../../services';
@ -54,7 +54,7 @@ describe('ResultBarChartComponent (inline template)', () => {
ErrorHandler,
ChannelService,
{ provide: SERVICE_CONFIG, useValue: testConfig },
{ provide: TagService, useValue: TagDefaultService },
{ provide: ArtifactService, useValue: ArtifactDefaultService },
{ provide: ScanningResultService, useValue: ScanningResultDefaultService },
{ provide: JobLogService, useValue: JobLogDefaultService}
]
@ -65,7 +65,7 @@ describe('ResultBarChartComponent (inline template)', () => {
beforeEach(() => {
fixture = TestBed.createComponent(ResultBarChartComponent);
component = fixture.componentInstance;
component.tagId = "mockTag";
component.artifactId = "mockTag";
component.summary = mockData;
serviceConfig = TestBed.get(SERVICE_CONFIG);

View File

@ -12,12 +12,13 @@ import {
VulnerabilitySummary,
TagService,
ScanningResultService,
Tag, ScannerVo
ScannerVo, ArtifactService
} from '../../services';
import { ErrorHandler } from '../../utils/error-handler';
import { JobLogService } from "../../services";
import { finalize } from "rxjs/operators";
import { ChannelService } from "../../services/channel.service";
import { Artifact } from '../artifact/artifact';
const STATE_CHECK_INTERVAL: number = 3000; // 3s
const RETRY_TIMES: number = 3;
@ -30,26 +31,30 @@ const RETRY_TIMES: number = 3;
export class ResultBarChartComponent implements OnInit, OnDestroy {
@Input() scanner: ScannerVo;
@Input() repoName: string = "";
@Input() tagId: string = "";
@Input() projectName: string = "";
@Input() artifactId: string = "";
@Input() summary: VulnerabilitySummary;
onSubmitting: boolean = false;
retryCounter: number = 0;
stateCheckTimer: Subscription;
scanSubscription: Subscription;
timerHandler: any;
@Output()
submitFinish: EventEmitter<boolean> = new EventEmitter<boolean>();
constructor(
private tagService: TagService,
// private tagService: TagService,
private artifactService: ArtifactService,
private scanningService: ScanningResultService,
private errorHandler: ErrorHandler,
private channel: ChannelService,
private ref: ChangeDetectorRef,
private jobLogService: JobLogService,
// private jobLogService: JobLogService,
) { }
ngOnInit(): void {
if ((this.status === VULNERABILITY_SCAN_STATUS.RUNNING ||
this.status === VULNERABILITY_SCAN_STATUS.PENDING) &&
!this.stateCheckTimer) {
@ -58,9 +63,9 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
this.getSummary();
});
}
this.scanSubscription = this.channel.scanCommand$.subscribe((tagId: string) => {
let myFullTag: string = this.repoName + "/" + this.tagId;
if (myFullTag === tagId) {
this.scanSubscription = this.channel.scanCommand$.subscribe((artifactId: string) => {
let myFullTag: string = this.repoName + "/" + this.artifactId;
if (myFullTag === artifactId) {
this.scanNow();
}
});
@ -110,14 +115,14 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
return;
}
if (!this.repoName || !this.tagId) {
if (!this.repoName || !this.artifactId) {
console.log("bad repository or tag");
return;
}
this.onSubmitting = true;
this.scanningService.startVulnerabilityScanning(this.repoName, this.tagId)
this.scanningService.startVulnerabilityScanning(this.projectName, this.repoName, this.artifactId)
.pipe(finalize(() => this.submitFinish.emit(false)))
.subscribe(() => {
this.onSubmitting = false;
@ -148,15 +153,16 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
}
getSummary(): void {
if (!this.repoName || !this.tagId) {
if (!this.repoName || !this.artifactId) {
return;
}
this.tagService.getTag(this.repoName, this.tagId)
.subscribe((t: Tag) => {
// this.tagService.getTag(this.repoName, this.artifactId)
this.artifactService.getArtifactFromDigest(this.projectName, this.repoName, this.artifactId)
.subscribe((artifact: Artifact) => {
// To keep the same summary reference, use value copy.
if (t.scan_overview) {
this.copyValue(t.scan_overview[DEFAULT_SUPPORTED_MIME_TYPE]);
if (artifact.scan_overview) {
this.copyValue(artifact.scan_overview[DEFAULT_SUPPORTED_MIME_TYPE]);
}
// Forcely refresh view
this.forceRefreshView(1000);
@ -168,7 +174,7 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
this.stateCheckTimer = null;
}
}
this.channel.tagDetail$.next(t);
this.channel.ArtifactDetail$.next(artifact);
}, error => {
this.errorHandler.error(error);
this.retryCounter++;
@ -202,6 +208,7 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
}, duration);
}
viewLog(): string {
return `/api/repositories/${this.repoName}/tags/${this.tagId}/scan/${this.summary.report_id}/log`;
return `/api/v2.0/projects/${this.projectName}/repositories/${this.repoName}
/artifacts/${this.artifactId}/scan/${this.summary.report_id}/log`;
}
}

View File

@ -47,7 +47,7 @@ export class ResultGridComponent implements OnInit {
ngOnInit(): void {
this.loadResults(this.repositoryId, this.tagId);
this.getScanPermissions(this.projectId);
this.channel.tagDetail$.subscribe(tag => {
this.channel.ArtifactDetail$.subscribe(tag => {
this.loadResults(this.repositoryId, this.tagId);
});
}

View File

@ -29,7 +29,9 @@ import {
RetagService,
RetagDefaultService,
UserPermissionService,
UserPermissionDefaultService
UserPermissionDefaultService,
ArtifactDefaultService,
ArtifactService
} from './services';
import { GcRepoService } from './components/config/gc/gc.service';
import { ScanAllRepoService } from './components/config/vulnerability/scanAll.service';
@ -72,11 +74,12 @@ import { CopyInputComponent } from "./components/push-image/copy-input.component
import { PushImageButtonComponent } from "./components/push-image/push-image.component";
import { ReplicationTasksComponent } from "./components/replication/replication-tasks/replication-tasks.component";
import { ReplicationComponent } from "./components/replication/replication.component";
import { RepositoryComponent } from "./components/repository/repository.component";
import { ArtifactListComponent } from "./components/artifact-list/artifact-list.component";
import { RepositoryGridviewComponent } from "./components/repository-gridview/repository-gridview.component";
import { TagComponent } from "./components/tag/tag.component";
import { TagDetailComponent } from "./components/tag/tag-detail.component";
import { TagHistoryComponent } from "./components/tag/tag-history.component";
import { ArtifactListTabComponent } from "./components/artifact/artifact-list-tab.component";
import { ArtifactCommonPropertiesComponent } from './components/artifact/artifact-common-properties/artifact-common-properties.component';
import { ArtifactTagComponent } from './components/artifact/artifact-tag/artifact-tag.component';
import { ArtifactAdditionsComponent } from './components/artifact/artifact-additions/artifact-additions.component';
import { HistogramChartComponent } from "./components/vulnerability-scanning/histogram-chart/histogram-chart.component";
import { ResultTipHistogramComponent } from "./components/vulnerability-scanning/result-tip-histogram/result-tip-histogram.component";
import { ResultBarChartComponent } from "./components/vulnerability-scanning/result-bar-chart.component";
@ -84,10 +87,17 @@ import { ResultGridComponent } from "./components/vulnerability-scanning/result-
import { ResultTipComponent } from "./components/vulnerability-scanning/result-tip.component";
import { FilterComponent } from "./components/filter/filter.component";
import { ListReplicationRuleComponent } from "./components/list-replication-rule/list-replication-rule.component";
import { ClipboardDirective } from "./components/third-party/ngx-clipboard/clipboard.directive";
import { ChannelService } from "./services/channel.service";
import { SharedModule } from "./utils/shared/shared.module";
import { TranslateServiceInitializer } from "./i18n";
import { BuildHistoryComponent } from "./components/artifact/artifact-additions/build-history/build-history.component";
import { DependenciesComponent } from "./components/artifact/artifact-additions/dependencies/dependencies.component";
import { SummaryComponent } from "./components/artifact/artifact-additions/summary/summary.component";
import { ValuesComponent } from "./components/artifact/artifact-additions/values/values.component";
import {
ArtifactVulnerabilitiesComponent
} from "./components/artifact/artifact-additions/artifact-vulnerabilities/artifact-vulnerabilities.component";
import { ArtifactSummaryComponent } from "./components/artifact/artifact-summary.component";
/**
* Declare default service configuration; all the endpoints will be defined in
@ -194,6 +204,7 @@ export interface HarborModuleConfig {
helmChartService?: Provider;
// Service implementation for userPermission
userPermissionService?: Provider;
artifactService?: Provider;
// Service implementation for gc
gcApiRepository?: Provider;
@ -206,7 +217,7 @@ export interface HarborModuleConfig {
@NgModule({
imports: [
SharedModule
SharedModule,
],
declarations: [
GcHistoryComponent,
@ -242,16 +253,23 @@ export interface HarborModuleConfig {
PushImageButtonComponent,
ReplicationTasksComponent,
ReplicationComponent,
RepositoryComponent,
ArtifactListComponent,
RepositoryGridviewComponent,
TagComponent,
TagDetailComponent,
TagHistoryComponent,
ArtifactListTabComponent,
ArtifactSummaryComponent,
ArtifactCommonPropertiesComponent,
ArtifactTagComponent,
ArtifactAdditionsComponent,
BuildHistoryComponent,
HistogramChartComponent,
ResultTipHistogramComponent,
ResultBarChartComponent,
ResultGridComponent,
ResultTipComponent
ResultTipComponent,
DependenciesComponent,
SummaryComponent,
ValuesComponent,
ArtifactVulnerabilitiesComponent
],
exports: [
SharedModule,
@ -288,16 +306,23 @@ export interface HarborModuleConfig {
PushImageButtonComponent,
ReplicationTasksComponent,
ReplicationComponent,
RepositoryComponent,
ArtifactListComponent,
RepositoryGridviewComponent,
TagComponent,
TagDetailComponent,
TagHistoryComponent,
ArtifactListTabComponent,
ArtifactSummaryComponent,
ArtifactCommonPropertiesComponent,
ArtifactTagComponent,
ArtifactAdditionsComponent,
BuildHistoryComponent,
HistogramChartComponent,
ResultTipHistogramComponent,
ResultBarChartComponent,
ResultGridComponent,
ResultTipComponent
ResultTipComponent,
DependenciesComponent,
SummaryComponent,
ValuesComponent,
ArtifactVulnerabilitiesComponent
],
providers: []
})
@ -323,6 +348,7 @@ export class HarborLibraryModule {
config.projectPolicyService || { provide: ProjectService, useClass: ProjectDefaultService },
config.labelService || { provide: LabelService, useClass: LabelDefaultService },
config.userPermissionService || { provide: UserPermissionService, useClass: UserPermissionDefaultService },
config.artifactService || { provide: ArtifactService, useClass: ArtifactDefaultService },
config.gcApiRepository || {provide: GcApiRepository, useClass: GcApiDefaultRepository},
config.ScanApiRepository || {provide: ScanApiRepository, useClass: ScanApiDefaultRepository},
// Do initializing
@ -362,6 +388,7 @@ export class HarborLibraryModule {
config.projectPolicyService || { provide: ProjectService, useClass: ProjectDefaultService },
config.labelService || { provide: LabelService, useClass: LabelDefaultService },
config.userPermissionService || { provide: UserPermissionService, useClass: UserPermissionDefaultService },
config.artifactService || { provide: ArtifactService, useClass: ArtifactDefaultService },
config.gcApiRepository || {provide: GcApiRepository, useClass: GcApiDefaultRepository},
config.ScanApiRepository || {provide: ScanApiRepository, useClass: ScanApiDefaultRepository},
ChannelService,

View File

@ -0,0 +1,35 @@
import { TestBed, inject } from '@angular/core/testing';
import { SharedModule } from '../utils/shared/shared.module';
import { SERVICE_CONFIG, IServiceConfig } from '../entities/service.config';
import { TagService, TagDefaultService } from './tag.service';
describe('TagService', () => {
const mockConfig: IServiceConfig = {
repositoryBaseEndpoint: "/api/repositories/testing"
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
SharedModule
],
providers: [
TagDefaultService,
{
provide: TagService,
useClass: TagDefaultService
}, {
provide: SERVICE_CONFIG,
useValue: mockConfig
}]
});
});
it('should be initialized', inject([TagDefaultService], (service: TagService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -0,0 +1,237 @@
import { Injectable, Inject } from "@angular/core";
import { HttpClient, HttpResponse } from "@angular/common/http";
import { SERVICE_CONFIG, IServiceConfig } from "../entities/service.config";
import {
buildHttpRequestOptions,
HTTP_JSON_OPTIONS,
HTTP_GET_OPTIONS,
buildHttpRequestOptionsWithObserveResponse
} from "../utils/utils";
import { RequestQueryParams } from "./RequestQueryParams";
import { Tag, Manifest } from "./interface";
import { Artifact } from "../components/artifact/artifact";
import { map, catchError } from "rxjs/operators";
import { Observable, throwError as observableThrowError, Subject } from "rxjs";
/**
* Define the service methods to handle the repository tag related things.
*
**
* @abstract
* class TagService
*/
export abstract class ArtifactService {
reference: string[];
triggerUploadArtifact = new Subject<string>();
TriggerArtifactChan$ = this.triggerUploadArtifact.asObservable();
/**
* Get all the tags under the specified repository.
* NOTES: If the Notary is enabled, the signatures should be included in the returned data.
*
* @abstract
* ** deprecated param {string} repositoryName
* ** deprecated param {RequestQueryParams} [queryParams]
* returns {(Observable<Tag[]>)}
*
* @memberOf TagService
*/
abstract getArtifactList(
projectName: string,
repositoryName: string,
queryParams?: RequestQueryParams
): Observable<HttpResponse<Artifact[]>>;
/**
* Delete the specified tag.
*
* @abstract
* ** deprecated param {string} repositoryName
* ** deprecated param {string} tag
* returns {(Observable<any> | any)}
*
* @memberOf TagService
*/
abstract getArtifactFromDigest(
projectName: string,
repositoryName: string,
artifactDigest: string
): Observable<Artifact>;
abstract deleteArtifact(
projectName: string,
repositoryName: string,
digest: string
): Observable<any>;
/**
* Get the specified tag.
*
* @abstract
* ** deprecated param {string} repositoryName
* ** deprecated param {string} tag
* returns {(Observable<Tag>)}
*
* @memberOf TagService
*/
abstract addLabelToImages(
projectName: string,
repoName: string,
digest: string,
labelId: number
): Observable<any>;
abstract deleteLabelToImages(
projectName: string,
repoName: string,
digest: string,
labelId: number
): Observable<any>;
/**
* Get manifest of tag under the specified repository.
*
* @abstract
* returns {(Observable<Manifest>)}
*
* @memberOf TagService
*/
abstract getManifest(
repositoryName: string,
tag: string
): Observable<Manifest>;
}
/**
* Implement default service for tag.
*
**
* class TagDefaultService
* extends {TagService}
*/
@Injectable()
export class ArtifactDefaultService extends ArtifactService {
_baseUrl: string;
_labelUrl: string;
reference: string[] = [];
triggerUploadArtifact = new Subject<string>();
TriggerArtifactChan$ = this.triggerUploadArtifact.asObservable();
constructor(
private http: HttpClient,
@Inject(SERVICE_CONFIG) private config: IServiceConfig
) {
super();
this._baseUrl = this.config.repositoryBaseEndpoint
? this.config.repositoryBaseEndpoint
: "/api/repositories";
this._labelUrl = this.config.labelEndpoint
? this.config.labelEndpoint
: "/api/labels";
}
_getArtifacts(
project_id: string, repositoryName: string,
queryParams?: RequestQueryParams
): Observable<HttpResponse<Artifact[]>> {
if (!queryParams) {
queryParams = queryParams = new RequestQueryParams();
}
// queryParams = queryParams.set("detail", "true");
let url: string = `/api/v2.0/projects/${project_id}/repositories/${repositoryName}/artifacts`;
// /api/v2/projects/{project_id}/repositories/{repositoryName}/artifacts
return this.http
.get<HttpResponse<Artifact[]>>(url, buildHttpRequestOptionsWithObserveResponse(queryParams))
.pipe(map(response => response as HttpResponse<Artifact[]>)
, catchError(error => observableThrowError(error)));
}
public getArtifactList(
projectName: string,
repositoryName: string,
queryParams?: RequestQueryParams
): Observable<HttpResponse<Artifact[]>> {
if (!repositoryName) {
return observableThrowError("Bad argument");
}
return this._getArtifacts(projectName, repositoryName, queryParams);
}
public getArtifactFromDigest(
projectName: string,
repositoryName: string,
artifactDigest: string
): Observable<Artifact> {
if (!artifactDigest) {
return observableThrowError("Bad argument");
}
let url = `/api/v2.0/projects/${projectName}/repositories/${repositoryName}/artifacts/${artifactDigest}`;
return this.http.get(url).pipe(catchError(error => observableThrowError(error))) as Observable<Artifact>;
}
public deleteArtifact(
projectName: string,
repositoryName: string,
digest: string
): Observable<any> {
if (!repositoryName || !projectName || !digest) {
return observableThrowError("Bad argument");
}
let url: string = `/api/v2.0/projects/${projectName}/repositories/${repositoryName}/artifacts/${digest}`;
return this.http
.delete(url, HTTP_JSON_OPTIONS)
.pipe(map(response => response)
, catchError(error => observableThrowError(error)));
}
public addLabelToImages(
projectName: string,
repoName: string,
digest: string,
labelId: number
): Observable<any> {
if (!labelId || !digest || !repoName) {
return observableThrowError("Invalid parameters.");
}
let _addLabelToImageUrl = `
/api/v2.0/projects/${projectName}/repositories/${repoName}/artifacts/${digest}/labels`;
return this.http
.post(_addLabelToImageUrl, { id: labelId }, HTTP_JSON_OPTIONS)
.pipe(catchError(error => observableThrowError(error)));
}
public deleteLabelToImages(
projectName: string,
repoName: string,
digest: string,
labelId: number
): Observable<any> {
if (!labelId || !digest || !repoName) {
return observableThrowError("Invalid parameters.");
}
let _addLabelToImageUrl = `
/api/v2.0/projects/${projectName}/repositories/${repoName}/artifacts/${digest}/labels/${labelId}`;
return this.http
.delete(_addLabelToImageUrl)
.pipe(catchError(error => observableThrowError(error)));
}
public getManifest(
repositoryName: string,
tag: string
): Observable<Manifest> {
if (!repositoryName || !tag) {
return observableThrowError("Bad argument");
}
let url: string = `${this._baseUrl}/${repositoryName}/tags/${tag}/manifest`;
return this.http
.get(url, HTTP_GET_OPTIONS)
.pipe(map(response => response as Manifest)
, catchError(error => observableThrowError(error)));
}
}

View File

@ -13,7 +13,8 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { Observable, Subject} from "rxjs";
import { Tag } from './index';
// import { Tag } from './index';
import { Artifact } from '../components/artifact/artifact';
@Injectable()
export class ChannelService {
@ -25,5 +26,5 @@ export class ChannelService {
publishScanEvent(tagId: string): void {
this.scanCommandSource.next(tagId);
}
tagDetail$ = new Subject<Tag>();
ArtifactDetail$ = new Subject<Artifact>();
}

View File

@ -15,3 +15,4 @@ export * from "./retag.service";
export * from "./permission.service";
export * from "./permission-static";
export * from "./quota.service";
export * from "./artifact.service";

Some files were not shown because too many files have changed in this diff Show More