diff --git a/src/portal/src/app/harbor-routing.module.ts b/src/portal/src/app/harbor-routing.module.ts index bb525c4c6..959c3e629 100644 --- a/src/portal/src/app/harbor-routing.module.ts +++ b/src/portal/src/app/harbor-routing.module.ts @@ -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', diff --git a/src/portal/src/app/project/helm-chart/label-filter/label-filter.component.ts b/src/portal/src/app/project/helm-chart/label-filter/label-filter.component.ts index 02ded85c6..3cdd7da41 100644 --- a/src/portal/src/app/project/helm-chart/label-filter/label-filter.component.ts +++ b/src/portal/src/app/project/helm-chart/label-filter/label-filter.component.ts @@ -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, 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; } diff --git a/src/portal/src/app/repository/artifact-list-page/artifact-list-page.component.html b/src/portal/src/app/repository/artifact-list-page/artifact-list-page.component.html new file mode 100644 index 000000000..d23caabbf --- /dev/null +++ b/src/portal/src/app/repository/artifact-list-page/artifact-list-page.component.html @@ -0,0 +1,15 @@ +
+ + +
diff --git a/src/portal/src/app/repository/tag-repository/tag-repository.component.scss b/src/portal/src/app/repository/artifact-list-page/artifact-list-page.component.scss similarity index 100% rename from src/portal/src/app/repository/tag-repository/tag-repository.component.scss rename to src/portal/src/app/repository/artifact-list-page/artifact-list-page.component.scss diff --git a/src/portal/src/app/repository/tag-repository/tag-repository.component.spec.ts b/src/portal/src/app/repository/artifact-list-page/artifact-list-page.component.spec.ts similarity index 82% rename from src/portal/src/app/repository/tag-repository/tag-repository.component.spec.ts rename to src/portal/src/app/repository/artifact-list-page/artifact-list-page.component.spec.ts index c5310ed2b..ee177c9b5 100644 --- a/src/portal/src/app/repository/tag-repository/tag-repository.component.spec.ts +++ b/src/portal/src/app/repository/artifact-list-page/artifact-list-page.component.spec.ts @@ -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; +import { ArtifactService } from '../../../lib/services'; +describe('ArtifactListPageComponent', () => { + let component: ArtifactListPageComponent; + let fixture: ComponentFixture; 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(); }); diff --git a/src/portal/src/app/repository/tag-repository/tag-repository.component.ts b/src/portal/src/app/repository/artifact-list-page/artifact-list-page.component.ts similarity index 55% rename from src/portal/src/app/repository/tag-repository/tag-repository.component.ts rename to src/portal/src/app/repository/artifact-list-page/artifact-list-page.component.ts index bf25c879b..5cc1ee43d 100644 --- a/src/portal/src/app/repository/tag-repository/tag-repository.component.ts +++ b/src/portal/src/app/repository/artifact-list-page/artifact-list-page.component.ts @@ -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 = (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; + } } diff --git a/src/portal/src/app/repository/artifact-summary-page/artifact-summary-page.component.html b/src/portal/src/app/repository/artifact-summary-page/artifact-summary-page.component.html new file mode 100644 index 000000000..36cebe56b --- /dev/null +++ b/src/portal/src/app/repository/artifact-summary-page/artifact-summary-page.component.html @@ -0,0 +1,17 @@ +
+ + +
diff --git a/src/portal/src/app/repository/tag-detail/tag-detail-page.component.scss b/src/portal/src/app/repository/artifact-summary-page/artifact-summary-page.component.scss similarity index 100% rename from src/portal/src/app/repository/tag-detail/tag-detail-page.component.scss rename to src/portal/src/app/repository/artifact-summary-page/artifact-summary-page.component.scss diff --git a/src/portal/src/app/repository/tag-detail/tag-detail-page.component.spec.ts b/src/portal/src/app/repository/artifact-summary-page/artifact-summary-page.component.spec.ts similarity index 87% rename from src/portal/src/app/repository/tag-detail/tag-detail-page.component.spec.ts rename to src/portal/src/app/repository/artifact-summary-page/artifact-summary-page.component.spec.ts index eb0859c97..eed8fc554 100644 --- a/src/portal/src/app/repository/tag-detail/tag-detail-page.component.spec.ts +++ b/src/portal/src/app/repository/artifact-summary-page/artifact-summary-page.component.spec.ts @@ -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; +describe('ArtifactSummaryPageComponent', () => { + let component: ArtifactSummaryPageComponent; + let fixture: ComponentFixture; 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(); }); diff --git a/src/portal/src/app/repository/artifact-summary-page/artifact-summary-page.component.ts b/src/portal/src/app/repository/artifact-summary-page/artifact-summary-page.component.ts new file mode 100644 index 000000000..8edd05bd8 --- /dev/null +++ b/src/portal/src/app/repository/artifact-summary-page/artifact-summary-page.component.ts @@ -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]); + } +} diff --git a/src/portal/src/app/repository/repository-page.component.ts b/src/portal/src/app/repository/repository-page.component.ts index 730c00b1b..ae6e60a4a 100644 --- a/src/portal/src/app/repository/repository-page.component.ts +++ b/src/portal/src/app/repository/repository-page.component.ts @@ -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); } } diff --git a/src/portal/src/app/repository/repository.module.ts b/src/portal/src/app/repository/repository.module.ts index 7215e834e..1d9741d52 100644 --- a/src/portal/src/app/repository/repository.module.ts +++ b/src/portal/src/app/repository/repository.module.ts @@ -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: [] }) diff --git a/src/portal/src/app/repository/tag-detail/tag-detail-page.component.html b/src/portal/src/app/repository/tag-detail/tag-detail-page.component.html deleted file mode 100644 index f1f769562..000000000 --- a/src/portal/src/app/repository/tag-detail/tag-detail-page.component.html +++ /dev/null @@ -1,14 +0,0 @@ - diff --git a/src/portal/src/app/repository/tag-detail/tag-detail-page.component.ts b/src/portal/src/app/repository/tag-detail/tag-detail-page.component.ts deleted file mode 100644 index 30d884428..000000000 --- a/src/portal/src/app/repository/tag-detail/tag-detail-page.component.ts +++ /dev/null @@ -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"]); - } -} diff --git a/src/portal/src/app/repository/tag-repository/tag-repository.component.html b/src/portal/src/app/repository/tag-repository/tag-repository.component.html deleted file mode 100644 index dc3a049d0..000000000 --- a/src/portal/src/app/repository/tag-repository/tag-repository.component.html +++ /dev/null @@ -1,10 +0,0 @@ - diff --git a/src/portal/src/app/shared/list-repository-ro/list-repository-ro.component.html b/src/portal/src/app/shared/list-repository-ro/list-repository-ro.component.html index d4b622465..3be971bc5 100644 --- a/src/portal/src/app/shared/list-repository-ro/list-repository-ro.component.html +++ b/src/portal/src/app/shared/list-repository-ro/list-repository-ro.component.html @@ -1,6 +1,6 @@ {{'REPOSITORY.NAME' | translate}} - {{'REPOSITORY.TAGS_COUNT' | translate}} + {{'REPOSITORY.ARTIFACTS_COUNT' | translate}} {{'REPOSITORY.PULL_COUNT' | translate}} {{r.name || r.repository_name}} diff --git a/src/portal/src/app/shared/route/artifact-guard-activate.service.spec.ts b/src/portal/src/app/shared/route/artifact-guard-activate.service.spec.ts new file mode 100644 index 000000000..f5264ee0f --- /dev/null +++ b/src/portal/src/app/shared/route/artifact-guard-activate.service.spec.ts @@ -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(); + }); +}); diff --git a/src/portal/src/app/shared/route/artifact-guard-activate.service.ts b/src/portal/src/app/shared/route/artifact-guard-activate.service.ts new file mode 100644 index 000000000..d92c57649 --- /dev/null +++ b/src/portal/src/app/shared/route/artifact-guard-activate.service.ts @@ -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 { + 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 { + return this.canActivate(route, state); + } + + hasArtifactPerm(projectName: string, repoName: string, digest): Observable { + // 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); + }) + ); + } +} diff --git a/src/portal/src/app/shared/route/leaving-artifact-summary-deactivate.service.spec.ts b/src/portal/src/app/shared/route/leaving-artifact-summary-deactivate.service.spec.ts new file mode 100644 index 000000000..495676f2e --- /dev/null +++ b/src/portal/src/app/shared/route/leaving-artifact-summary-deactivate.service.spec.ts @@ -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(); + })); +}); diff --git a/src/portal/src/app/shared/route/leaving-artifact-summary-deactivate.service.ts b/src/portal/src/app/shared/route/leaving-artifact-summary-deactivate.service.ts new file mode 100644 index 000000000..f34e9478f --- /dev/null +++ b/src/portal/src/app/shared/route/leaving-artifact-summary-deactivate.service.ts @@ -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 { + constructor( + private router: Router, + private confirmation: ConfirmationDialogService) { } + + canDeactivate( + tagRepo: ArtifactListPageComponent, + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot): Observable | boolean { + // Confirmation before leaving config route + return new Observable((observer) => { + sessionStorage.removeItem('referenceSummary'); + + return observer.next(true); + }); + } +} diff --git a/src/portal/src/app/shared/route/leaving-repository-deactivate.service.ts b/src/portal/src/app/shared/route/leaving-repository-deactivate.service.ts index 382c52ebc..7dc27a8db 100644 --- a/src/portal/src/app/shared/route/leaving-repository-deactivate.service.ts +++ b/src/portal/src/app/shared/route/leaving-repository-deactivate.service.ts @@ -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 { +export class LeavingRepositoryRouteDeactivate implements CanDeactivate { constructor( private router: Router, private confirmation: ConfirmationDialogService) { } canDeactivate( - tagRepo: TagRepositoryComponent, + tagRepo: ArtifactListPageComponent, route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | 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 { 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); } }); diff --git a/src/portal/src/app/shared/shared.module.ts b/src/portal/src/app/shared/shared.module.ts index d0e2e08f5..dd3e16ea9 100644 --- a/src/portal/src/app/shared/shared.module.ts +++ b/src/portal/src/app/shared/shared.module.ts @@ -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, diff --git a/src/portal/src/css/common.scss b/src/portal/src/css/common.scss index 8154d244c..b3b20de9f 100644 --- a/src/portal/src/css/common.scss +++ b/src/portal/src/css/common.scss @@ -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; + } + } + } + +} diff --git a/src/portal/src/css/dark-theme.scss b/src/portal/src/css/dark-theme.scss index c3f4e82e1..70e3aed74 100644 --- a/src/portal/src/css/dark-theme.scss +++ b/src/portal/src/css/dark-theme.scss @@ -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"; diff --git a/src/portal/src/css/light-theme.scss b/src/portal/src/css/light-theme.scss index fa6e0e42b..9d3a8c435 100644 --- a/src/portal/src/css/light-theme.scss +++ b/src/portal/src/css/light-theme.scss @@ -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"; diff --git a/src/portal/src/global.scss b/src/portal/src/global.scss index 1379615dd..1ed5ce094 100644 --- a/src/portal/src/global.scss +++ b/src/portal/src/global.scss @@ -447,3 +447,9 @@ clr-datagrid { button:focus { outline: none !important; } +.cursor-pointer { + cursor: pointer +} +.text-align-r { + text-align: right !important; +} diff --git a/src/portal/src/i18n/lang/en-us-lang.json b/src/portal/src/i18n/lang/en-us-lang.json index 19a9ad112..604eeff5c 100644 --- a/src/portal/src/i18n/lang/en-us-lang.json +++ b/src/portal/src/i18n/lang/en-us-lang.json @@ -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", diff --git a/src/portal/src/i18n/lang/es-es-lang.json b/src/portal/src/i18n/lang/es-es-lang.json index 024f229f4..c32b8a343 100644 --- a/src/portal/src/i18n/lang/es-es-lang.json +++ b/src/portal/src/i18n/lang/es-es-lang.json @@ -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": { diff --git a/src/portal/src/i18n/lang/fr-fr-lang.json b/src/portal/src/i18n/lang/fr-fr-lang.json index 0b69abd77..df1bfd620 100644 --- a/src/portal/src/i18n/lang/fr-fr-lang.json +++ b/src/portal/src/i18n/lang/fr-fr-lang.json @@ -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." } -} +} \ No newline at end of file diff --git a/src/portal/src/i18n/lang/pt-br-lang.json b/src/portal/src/i18n/lang/pt-br-lang.json index d447c295c..652096f09 100644 --- a/src/portal/src/i18n/lang/pt-br-lang.json +++ b/src/portal/src/i18n/lang/pt-br-lang.json @@ -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", diff --git a/src/portal/src/i18n/lang/tr-tr-lang.json b/src/portal/src/i18n/lang/tr-tr-lang.json index 4fe4fa9dc..c55326ac0 100644 --- a/src/portal/src/i18n/lang/tr-tr-lang.json +++ b/src/portal/src/i18n/lang/tr-tr-lang.json @@ -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", diff --git a/src/portal/src/i18n/lang/zh-cn-lang.json b/src/portal/src/i18n/lang/zh-cn-lang.json index 0487f1ee0..4d926f10a 100644 --- a/src/portal/src/i18n/lang/zh-cn-lang.json +++ b/src/portal/src/i18n/lang/zh-cn-lang.json @@ -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": "标签", diff --git a/src/portal/src/images/artifact-chart.svg b/src/portal/src/images/artifact-chart.svg new file mode 100644 index 000000000..6014e6d04 --- /dev/null +++ b/src/portal/src/images/artifact-chart.svg @@ -0,0 +1 @@ +helm-logo \ No newline at end of file diff --git a/src/portal/src/images/artifact-default.svg b/src/portal/src/images/artifact-default.svg new file mode 100644 index 000000000..85e97af6b --- /dev/null +++ b/src/portal/src/images/artifact-default.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/portal/src/images/artifact-image.svg b/src/portal/src/images/artifact-image.svg new file mode 100644 index 000000000..ba26e82d8 --- /dev/null +++ b/src/portal/src/images/artifact-image.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/portal/src/lib/components/repository/repository.component.html b/src/portal/src/lib/components/artifact-list/artifact-list.component.html similarity index 84% rename from src/portal/src/lib/components/repository/repository.component.html rename to src/portal/src/lib/components/artifact-list/artifact-list.component.html index ac8c3220d..2d7ff62ac 100644 --- a/src/portal/src/lib/components/repository/repository.component.html +++ b/src/portal/src/lib/components/artifact-list/artifact-list.component.html @@ -4,7 +4,7 @@
-

{{repoName}}

+

{{showCurrentTitle | slice:0:15}}

@@ -19,7 +19,7 @@
@@ -53,10 +53,10 @@
-
- +
+
diff --git a/src/portal/src/lib/components/repository/repository.component.scss b/src/portal/src/lib/components/artifact-list/artifact-list.component.scss similarity index 100% rename from src/portal/src/lib/components/repository/repository.component.scss rename to src/portal/src/lib/components/artifact-list/artifact-list.component.scss diff --git a/src/portal/src/lib/components/repository/repository.component.spec.ts b/src/portal/src/lib/components/artifact-list/artifact-list.component.spec.ts similarity index 54% rename from src/portal/src/lib/components/repository/repository.component.spec.ts rename to src/portal/src/lib/components/artifact-list/artifact-list.component.spec.ts index 2b1958c6a..723ef8be9 100644 --- a/src/portal/src/lib/components/repository/repository.component.spec.ts +++ b/src/portal/src/lib/components/artifact-list/artifact-list.component.spec.ts @@ -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; + let compRepo: ArtifactListComponent; + let fixture: ComponentFixture; 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'); }); })); }); diff --git a/src/portal/src/lib/components/repository/repository.component.ts b/src/portal/src/lib/components/artifact-list/artifact-list.component.ts similarity index 70% rename from src/portal/src/lib/components/repository/repository.component.ts rename to src/portal/src/lib/components/artifact-list/artifact-list.component.ts index 8ff9ff3bb..849cc86bb 100644 --- a/src/portal/src/lib/components/repository/repository.component.ts +++ b/src/portal/src/lib/components/artifact-list/artifact-list.component.ts @@ -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(); + @Output() tagClickEvent = new EventEmitter(); @Output() backEvt: EventEmitter = new EventEmitter(); + @Output() putArtifactReferenceArr: EventEmitter = 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 { - // 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); } } } diff --git a/src/portal/src/lib/components/artifact/artifact-additions/additions.service.spec.ts b/src/portal/src/lib/components/artifact/artifact-additions/additions.service.spec.ts new file mode 100644 index 000000000..8e9b16eb0 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-additions/additions.service.spec.ts @@ -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(); + })); +}); diff --git a/src/portal/src/lib/components/artifact/artifact-additions/additions.service.ts b/src/portal/src/lib/components/artifact/artifact-additions/additions.service.ts new file mode 100644 index 000000000..da6e2d6d4 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-additions/additions.service.ts @@ -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 { + return this.http.get(link); + } +} diff --git a/src/portal/src/lib/components/artifact/artifact-additions/artifact-additions.component.html b/src/portal/src/lib/components/artifact/artifact-additions/artifact-additions.component.html new file mode 100644 index 000000000..362b688d4 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-additions/artifact-additions.component.html @@ -0,0 +1,40 @@ + +

{{'ARTIFACT.ADDITIONS' | translate}}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/src/portal/src/lib/components/artifact/artifact-additions/artifact-additions.component.scss b/src/portal/src/lib/components/artifact/artifact-additions/artifact-additions.component.scss new file mode 100644 index 000000000..8a25088f6 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-additions/artifact-additions.component.scss @@ -0,0 +1,3 @@ +.margin-bottom-025 { + margin-bottom: 0.25rem; +} \ No newline at end of file diff --git a/src/portal/src/lib/components/artifact/artifact-additions/artifact-additions.component.spec.ts b/src/portal/src/lib/components/artifact/artifact-additions/artifact-additions.component.spec.ts new file mode 100644 index 000000000..fded6fa6c --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-additions/artifact-additions.component.spec.ts @@ -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; + + 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(); + }); +}); diff --git a/src/portal/src/lib/components/artifact/artifact-additions/artifact-additions.component.ts b/src/portal/src/lib/components/artifact/artifact-additions/artifact-additions.component.ts new file mode 100644 index 000000000..1719121ad --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-additions/artifact-additions.component.ts @@ -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; + } +} diff --git a/src/portal/src/lib/components/artifact/artifact-additions/artifact-vulnerabilities/artifact-vulnerabilities.component.html b/src/portal/src/lib/components/artifact/artifact-additions/artifact-vulnerabilities/artifact-vulnerabilities.component.html new file mode 100644 index 000000000..a6fea15a0 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-additions/artifact-vulnerabilities/artifact-vulnerabilities.component.html @@ -0,0 +1,65 @@ +
+
+
+
+ + +
+
+
+
+ + + + + {{'VULNERABILITY.GRID.COLUMN_ID' | translate}} + {{'VULNERABILITY.GRID.COLUMN_SEVERITY' | translate}} + {{'VULNERABILITY.GRID.COLUMN_PACKAGE' | translate}} + {{'VULNERABILITY.GRID.COLUMN_VERSION' | translate}} + {{'VULNERABILITY.GRID.COLUMN_FIXED' | translate}} + + {{'VULNERABILITY.CHART.TOOLTIPS_TITLE_ZERO' | translate}} + + + {{res.id}} + {{res.id}} + + {{res.id}} + + +
+ {{link}} +
+
+
+
+
+ + {{severityText(res.severity) | translate}} + {{severityText(res.severity) | translate}} + {{severityText(res.severity) | translate}} + {{severityText(res.severity) | translate}} + {{severityText(res.severity) | translate}} + {{severityText(res.severity) | translate}} + {{severityText(res.severity) | translate}} + + {{res.package}} + {{res.version}} + +
+  {{res.fix_version}} +
+ {{res.fix_version}} +
+ + {{'VULNERABILITY.GRID.COLUMN_DESCRIPTION' | translate}}: {{res.description}} + +
+ + + {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'VULNERABILITY.GRID.FOOT_OF' | translate}} {{pagination.totalItems}} {{'VULNERABILITY.GRID.FOOT_ITEMS' | translate}} + + +
+
+
\ No newline at end of file diff --git a/src/portal/src/lib/components/artifact/artifact-additions/artifact-vulnerabilities/artifact-vulnerabilities.component.scss b/src/portal/src/lib/components/artifact/artifact-additions/artifact-vulnerabilities/artifact-vulnerabilities.component.scss new file mode 100644 index 000000000..cd1370a40 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-additions/artifact-vulnerabilities/artifact-vulnerabilities.component.scss @@ -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; +} diff --git a/src/portal/src/lib/components/artifact/artifact-additions/artifact-vulnerabilities/artifact-vulnerabilities.component.spec.ts b/src/portal/src/lib/components/artifact/artifact-additions/artifact-vulnerabilities/artifact-vulnerabilities.component.spec.ts new file mode 100644 index 000000000..cc1d845a6 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-additions/artifact-vulnerabilities/artifact-vulnerabilities.component.spec.ts @@ -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; + 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); + }); +}); diff --git a/src/portal/src/lib/components/artifact/artifact-additions/artifact-vulnerabilities/artifact-vulnerabilities.component.ts b/src/portal/src/lib/components/artifact/artifact-additions/artifact-vulnerabilities/artifact-vulnerabilities.component.ts new file mode 100644 index 000000000..656aadec3 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-additions/artifact-vulnerabilities/artifact-vulnerabilities.component.ts @@ -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; + + 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'; + } + } +} diff --git a/src/portal/src/lib/components/tag/tag-history.component.html b/src/portal/src/lib/components/artifact/artifact-additions/build-history/build-history.component.html similarity index 68% rename from src/portal/src/lib/components/tag/tag-history.component.html rename to src/portal/src/lib/components/artifact/artifact-additions/build-history/build-history.component.html index 0b6cfff52..7ab789e7c 100644 --- a/src/portal/src/lib/components/tag/tag-history.component.html +++ b/src/portal/src/lib/components/artifact/artifact-additions/build-history/build-history.component.html @@ -1,11 +1,9 @@ {{ 'TAG.CREATION' | translate }} {{ 'TAG.COMMAND' | translate }} - - + {{ h.created | date: 'short' }} {{ h.created_by }} - - {{ history.length }} commands + {{ historyList.length }} commands \ No newline at end of file diff --git a/src/portal/src/lib/components/tag/tag-history.component.scss b/src/portal/src/lib/components/artifact/artifact-additions/build-history/build-history.component.scss similarity index 100% rename from src/portal/src/lib/components/tag/tag-history.component.scss rename to src/portal/src/lib/components/artifact/artifact-additions/build-history/build-history.component.scss diff --git a/src/portal/src/lib/components/artifact/artifact-additions/build-history/build-history.component.spec.ts b/src/portal/src/lib/components/artifact/artifact-additions/build-history/build-history.component.spec.ts new file mode 100644 index 000000000..19fa406b2 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-additions/build-history/build-history.component.spec.ts @@ -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; + 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); + }); +}); diff --git a/src/portal/src/lib/components/artifact/artifact-additions/build-history/build-history.component.ts b/src/portal/src/lib/components/artifact/artifact-additions/build-history/build-history.component.ts new file mode 100644 index 000000000..3fe4d5195 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-additions/build-history/build-history.component.ts @@ -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); + } + ); + } + } +} diff --git a/src/portal/src/lib/components/artifact/artifact-additions/dependencies/dependencies.component.html b/src/portal/src/lib/components/artifact/artifact-additions/dependencies/dependencies.component.html new file mode 100644 index 000000000..9653610f5 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-additions/dependencies/dependencies.component.html @@ -0,0 +1,20 @@ +
+
+ + + + + + + + + + + + + + + +
{{'HELM_CHART.NAME' | translate}}{{'HELM_CHART.VERSION' | translate}}{{'HELM_CHART.REPO' | translate}}
{{dep.name}}{{dep.version}}{{dep.repository}}
+
+
\ No newline at end of file diff --git a/src/portal/src/lib/components/artifact/artifact-additions/dependencies/dependencies.component.scss b/src/portal/src/lib/components/artifact/artifact-additions/dependencies/dependencies.component.scss new file mode 100644 index 000000000..ef01e937e --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-additions/dependencies/dependencies.component.scss @@ -0,0 +1,3 @@ +.dep-container { + margin-top: 30px; +} \ No newline at end of file diff --git a/src/portal/src/lib/components/artifact/artifact-additions/dependencies/dependencies.component.spec.ts b/src/portal/src/lib/components/artifact/artifact-additions/dependencies/dependencies.component.spec.ts new file mode 100644 index 000000000..226bda9f2 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-additions/dependencies/dependencies.component.spec.ts @@ -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; + 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); + }); +}); diff --git a/src/portal/src/lib/components/artifact/artifact-additions/dependencies/dependencies.component.ts b/src/portal/src/lib/components/artifact/artifact-additions/dependencies/dependencies.component.ts new file mode 100644 index 000000000..79852abea --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-additions/dependencies/dependencies.component.ts @@ -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); + } + ); + } + } +} diff --git a/src/portal/src/lib/components/artifact/artifact-additions/models.ts b/src/portal/src/lib/components/artifact/artifact-additions/models.ts new file mode 100644 index 000000000..63f292bc1 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-additions/models.ts @@ -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' +} + diff --git a/src/portal/src/lib/components/artifact/artifact-additions/summary/summary.component.html b/src/portal/src/lib/components/artifact/artifact-additions/summary/summary.component.html new file mode 100644 index 000000000..6013333b7 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-additions/summary/summary.component.html @@ -0,0 +1,7 @@ +
+
+
+
{{'HELM_CHART.NO_README' | translate}}
+
+
+
\ No newline at end of file diff --git a/src/portal/src/lib/components/artifact/artifact-additions/summary/summary.component.scss b/src/portal/src/lib/components/artifact/artifact-additions/summary/summary.component.scss new file mode 100644 index 000000000..5f49c1479 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-additions/summary/summary.component.scss @@ -0,0 +1,7 @@ +.content-wrapper { + margin-top: 20px; + padding: 0 0 0 15px; + .md-container { + border: solid 1px #ddd; + } +} diff --git a/src/portal/src/lib/components/artifact/artifact-additions/summary/summary.component.spec.ts b/src/portal/src/lib/components/artifact/artifact-additions/summary/summary.component.spec.ts new file mode 100644 index 000000000..03d2211d9 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-additions/summary/summary.component.spec.ts @@ -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; + 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); + }); +}); diff --git a/src/portal/src/lib/components/artifact/artifact-additions/summary/summary.component.ts b/src/portal/src/lib/components/artifact/artifact-additions/summary/summary.component.ts new file mode 100644 index 000000000..e9041c4b0 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-additions/summary/summary.component.ts @@ -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); + } + ); + } + } +} diff --git a/src/portal/src/lib/components/artifact/artifact-additions/values/values.component.html b/src/portal/src/lib/components/artifact/artifact-additions/values/values.component.html new file mode 100644 index 000000000..7807061d4 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-additions/values/values.component.html @@ -0,0 +1,23 @@ +
+
+ +
+
+ + + +
+
+ +
+
+ + + + + + + +
{{item?.key}}{{item?.value}}
+
+
\ No newline at end of file diff --git a/src/portal/src/lib/components/artifact/artifact-additions/values/values.component.scss b/src/portal/src/lib/components/artifact/artifact-additions/values/values.component.scss new file mode 100644 index 000000000..59ebcf2bf --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-additions/values/values.component.scss @@ -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; +} \ No newline at end of file diff --git a/src/portal/src/lib/components/artifact/artifact-additions/values/values.component.spec.ts b/src/portal/src/lib/components/artifact/artifact-additions/values/values.component.spec.ts new file mode 100644 index 000000000..b93c1d410 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-additions/values/values.component.spec.ts @@ -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; + + 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); + }); +}); diff --git a/src/portal/src/lib/components/artifact/artifact-additions/values/values.component.ts b/src/portal/src/lib/components/artifact/artifact-additions/values/values.component.ts new file mode 100644 index 000000000..0995814e5 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-additions/values/values.component.ts @@ -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; + } + } +} diff --git a/src/portal/src/lib/components/artifact/artifact-common-properties/artifact-common-properties.component.html b/src/portal/src/lib/components/artifact/artifact-common-properties/artifact-common-properties.component.html new file mode 100644 index 000000000..032fff5ac --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-common-properties/artifact-common-properties.component.html @@ -0,0 +1,12 @@ + +

{{'ARTIFACT.COMMON_PROPERTIES' | translate}}

+ + + {{'ARTIFACT.COMMON_ALL' | translate}} + + {{item?.key}} + {{item?.value}} + + + +
diff --git a/src/portal/src/lib/components/artifact/artifact-common-properties/artifact-common-properties.component.scss b/src/portal/src/lib/components/artifact/artifact-common-properties/artifact-common-properties.component.scss new file mode 100644 index 000000000..74315070d --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-common-properties/artifact-common-properties.component.scss @@ -0,0 +1,3 @@ +.margin-bottom-075 { + margin-bottom: 0.75rem; +} \ No newline at end of file diff --git a/src/portal/src/lib/components/artifact/artifact-common-properties/artifact-common-properties.component.spec.ts b/src/portal/src/lib/components/artifact/artifact-common-properties/artifact-common-properties.component.spec.ts new file mode 100644 index 000000000..755fdefb3 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-common-properties/artifact-common-properties.component.spec.ts @@ -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; + 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); + }); +}); diff --git a/src/portal/src/lib/components/artifact/artifact-common-properties/artifact-common-properties.component.ts b/src/portal/src/lib/components/artifact/artifact-common-properties/artifact-common-properties.component.ts new file mode 100644 index 000000000..4e4760e56 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-common-properties/artifact-common-properties.component.ts @@ -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) !== '{}'; + } +} diff --git a/src/portal/src/lib/components/artifact/artifact-list-tab.component.html b/src/portal/src/lib/components/artifact/artifact-list-tab.component.html new file mode 100644 index 000000000..5408d73b4 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-list-tab.component.html @@ -0,0 +1,286 @@ + + + + + + + + + + + + +
+
+
+
+
+ +
+
+ +
+ × + +
+
{{'LABEL.NO_LABELS' | translate }} +
+
+ +
+
+
+
+ + + +
+
+
+ + + + + + + {{'BUTTON.ACTIONS' | translate}} + + + +
+ {{'REPOSITORY.COPY_DIGEST_ID' | translate}}
+ + + +
+ +
+
+ {{'LABEL.NO_LABELS' | translate }}
+
+ +
+
+ +
+
+
{{'REPOSITORY.RETAG' | translate}}
+
+ {{'REPOSITORY.DELETE' | translate}}
+
+
+ +
+ + {{'REPOSITORY.ARTIFACTS_COUNT' | translate}} + + {{'REPOSITORY.PLATFORM' | translate}} + {{'REPOSITORY.TAGS_COUNT' | translate}} + {{'REPOSITORY.SIZE' | translate}} + + {{'REPOSITORY.VULNERABILITY' | translate}} + {{'REPOSITORY.SIGNED' | translate}} + + + {{'REPOSITORY.LABELS' | translate}} + {{'REPOSITORY.PUSH_TIME' | translate}} + {{'REPOSITORY.PULL_TIME' | translate}} + {{'TAG.PLACEHOLDER' | translate }} + + +
+ +     + + {{ artifact.digest | slice:0:15}} + +
+
+ + + +
+
+ + {{'REPOSITORY.ARTIFACT_TOOTIP' | translate}} + +
+ + + +
+
+ +
+ {{artifact.extra_attrs?.os}}/{{artifact.extra_attrs?.architecture}} +
+
+ +
+ + +
+
+
+ + {{artifact.tags[0].name | slice:0:5}} + + ... + ({{artifact.tags.length}}) + +
+
+
+ + + + + + + + + + + + + + + + +
+ {{'REPOSITORY.TAGS_COUNT' | translate | uppercase}} + {{'REPOSITORY.PUSH_TIME' | translate | uppercase}} + {{'REPOSITORY.PULL_TIME' | translate | uppercase}}
{{tag.name}}{{tag.push_time | date: 'short'}}{{tag.pull_time | date: 'short'}}
+
+
+ +
+ +
+ +
+ {{artifact.size?sizeTransform(artifact.size): ""}} +
+
+ + +
+ + +
+
+ + + + + +
+ + +
+
+ + + +
+ + +
+
+
+
+
+
+
+ +
{{artifact.push_time | date: 'short'}}
+
+ +
+ {{artifact.pull_time === availableTime ? "" : (artifact.pull_time| date: 'short')}}
+
+
+ + {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} + {{'REPOSITORY.OF' | translate}} {{totalCount}} + {{'REPOSITORY.ITEMS' | translate}}     + + +
+
+
\ No newline at end of file diff --git a/src/portal/src/lib/components/tag/tag.component.scss b/src/portal/src/lib/components/artifact/artifact-list-tab.component.scss similarity index 63% rename from src/portal/src/lib/components/tag/tag.component.scss rename to src/portal/src/lib/components/artifact/artifact-list-tab.component.scss index 7930e9d87..fdd7a2899 100644 --- a/src/portal/src/lib/components/tag/tag.component.scss +++ b/src/portal/src/lib/components/artifact/artifact-list-tab.component.scss @@ -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; +} diff --git a/src/portal/src/lib/components/tag/tag.component.spec.ts b/src/portal/src/lib/components/artifact/artifact-list-tab.component.spec.ts similarity index 66% rename from src/portal/src/lib/components/tag/tag.component.spec.ts rename to src/portal/src/lib/components/artifact/artifact-list-tab.component.spec.ts index 69252feaf..fc4f17cf9 100644 --- a/src/portal/src/lib/components/tag/tag.component.spec.ts +++ b/src/portal/src/lib/components/artifact/artifact-list-tab.component.spec.ts @@ -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; - let tagService: TagService; + let comp: ArtifactListTabComponent; + let fixture: ComponentFixture; + 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"); }); }); diff --git a/src/portal/src/lib/components/tag/tag.component.ts b/src/portal/src/lib/components/artifact/artifact-list-tab.component.ts similarity index 60% rename from src/portal/src/lib/components/tag/tag.component.ts rename to src/portal/src/lib/components/artifact/artifact-list-tab.component.ts index c69f626ff..c74540687 100644 --- a/src/portal/src/lib/components/tag/tag.component.ts +++ b/src/portal/src/lib/components/artifact/artifact-list-tab.component.ts @@ -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(); - @Output() tagClickEvent = new EventEmitter(); + @Output() tagClickEvent = new EventEmitter(); @Output() signatureOutput = new EventEmitter(); + @Output() putReferArtifactArray = new EventEmitter(); 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 = new CustomComparator("created", "date"); - pullComparator: Comparator = new CustomComparator("pull_time", "date"); - pushComparator: Comparator = new CustomComparator("push_time", "date"); + pullComparator: Comparator = new CustomComparator("pull_time", "date"); + pushComparator: Comparator = new CustomComparator("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 = new Subject(); stickLabelNameFilter: Subject = new Subject(); 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; + 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(tags, state); - this.tags = doSorting(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[] = []; + + 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[] = []; - 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[] = []; 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[] = []; + 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 | null { + } + }); + forkJoin(observArr).subscribe((res) => { + this.findArtifactFromIndex(res); + }); + } + } + + delOperate(artifact: Artifact): Observable | 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) => { 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); + } } diff --git a/src/portal/src/lib/components/artifact/artifact-summary.component.html b/src/portal/src/lib/components/artifact/artifact-summary.component.html new file mode 100644 index 000000000..6109cea42 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-summary.component.html @@ -0,0 +1,24 @@ +
+
+ +
+
+ +

{{artifact?.digest | slice:0:15}}

+
+
+ + + + + + + + + + +
+ +
+ diff --git a/src/portal/src/lib/components/artifact/artifact-summary.component.scss b/src/portal/src/lib/components/artifact/artifact-summary.component.scss new file mode 100644 index 000000000..c92a7060e --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-summary.component.scss @@ -0,0 +1,8 @@ +.margin-top-5px { + margin-top: 5px; +} + +.center { + justify-content: center; + align-items: center; +} \ No newline at end of file diff --git a/src/portal/src/lib/components/artifact/artifact-summary.component.spec.ts b/src/portal/src/lib/components/artifact/artifact-summary.component.spec.ts new file mode 100644 index 000000000..3a3126942 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-summary.component.spec.ts @@ -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; + + 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'); + }); +}); diff --git a/src/portal/src/lib/components/artifact/artifact-summary.component.ts b/src/portal/src/lib/components/artifact/artifact-summary.component.ts new file mode 100644 index 000000000..1e70a6000 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-summary.component.ts @@ -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 = new EventEmitter(); + @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(); + } +} diff --git a/src/portal/src/lib/components/artifact/artifact-tag/artifact-tag.component.html b/src/portal/src/lib/components/artifact/artifact-tag/artifact-tag.component.html new file mode 100644 index 000000000..f72b336ea --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-tag/artifact-tag.component.html @@ -0,0 +1,58 @@ +

{{'REPOSITORY.TAGS_COUNT' | translate}}

+ + + + +
+ +
+ + +
+
+
+ {{'TAG.NAME' | translate}} + {{'TAG.PULL_TIME' | translate}} + {{'TAG.PUSH_TIME' | translate}} + + + +
+ {{tag.name}} + {{'REPOSITORY.IMMUTABLE' | translate}} +
+
+ {{tag.pull_time !== availableTime? (tag.pull_time | date: 'short') : ""}} + {{tag.push_time | date: 'short'}} +
+ + + {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} + {{'TAG.OF' | translate}} {{pagination.totalItems}} {{'TAG.ITEMS' | translate}} + + +
+ + + \ No newline at end of file diff --git a/src/portal/src/lib/components/artifact/artifact-tag/artifact-tag.component.scss b/src/portal/src/lib/components/artifact/artifact-tag/artifact-tag.component.scss new file mode 100644 index 000000000..1a5c7ba4b --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-tag/artifact-tag.component.scss @@ -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; +} diff --git a/src/portal/src/lib/components/artifact/artifact-tag/artifact-tag.component.spec.ts b/src/portal/src/lib/components/artifact/artifact-tag/artifact-tag.component.spec.ts new file mode 100644 index 000000000..1188e2b43 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-tag/artifact-tag.component.spec.ts @@ -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; + 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(); + }); +}); diff --git a/src/portal/src/lib/components/artifact/artifact-tag/artifact-tag.component.ts b/src/portal/src/lib/components/artifact/artifact-tag/artifact-tag.component.ts new file mode 100644 index 000000000..e0b3b0388 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact-tag/artifact-tag.component.ts @@ -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 | 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; + } +} diff --git a/src/portal/src/lib/components/artifact/artifact.ts b/src/portal/src/lib/components/artifact/artifact.ts new file mode 100644 index 000000000..8cb6c7fa1 --- /dev/null +++ b/src/portal/src/lib/components/artifact/artifact.ts @@ -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; + addition_links?: Map; + 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; + } +} diff --git a/src/portal/src/lib/components/repository-gridview/repository-gridview.component.html b/src/portal/src/lib/components/repository-gridview/repository-gridview.component.html index ce0e0b717..8e105e295 100644 --- a/src/portal/src/lib/components/repository-gridview/repository-gridview.component.html +++ b/src/portal/src/lib/components/repository-gridview/repository-gridview.component.html @@ -25,16 +25,17 @@
- - + {{'REPOSITORY.NAME' | translate}} - {{'REPOSITORY.TAGS_COUNT' | translate}} + {{'REPOSITORY.ARTIFACTS_COUNT' | translate}} {{'REPOSITORY.PULL_COUNT' | translate}} {{'REPOSITORY.PLACEHOLDER' | translate }} {{r.name}} + {{r.tags_count}} {{r.pull_count}} @@ -72,7 +73,7 @@ {{item.description || ('REPOSITORY.NO_INFO' | translate)}}
- +
{{item.tags_count}}
diff --git a/src/portal/src/lib/components/repository-gridview/repository-gridview.component.spec.ts b/src/portal/src/lib/components/repository-gridview/repository-gridview.component.spec.ts index 83642abfc..36561bfdb 100644 --- a/src/portal/src/lib/components/repository-gridview/repository-gridview.component.spec.ts +++ b/src/portal/src/lib/components/repository-gridview/repository-gridview.component.spec.ts @@ -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 }, diff --git a/src/portal/src/lib/components/repository-gridview/repository-gridview.component.ts b/src/portal/src/lib/components/repository-gridview/repository-gridview.component.ts index f861af776..901f00737 100644 --- a/src/portal/src/lib/components/repository-gridview/repository-gridview.component.ts +++ b/src/portal/src/lib/components/repository-gridview/repository-gridview.component.ts @@ -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 { @@ -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 { - 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 { + // 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 { - 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); diff --git a/src/portal/src/lib/components/tag/tag-detail.component.html b/src/portal/src/lib/components/tag/tag-detail.component.html deleted file mode 100644 index 09eac3be8..000000000 --- a/src/portal/src/lib/components/tag/tag-detail.component.html +++ /dev/null @@ -1,72 +0,0 @@ -
-
-
-
- -
-
-

{{repositoryId}}:{{tagDetails.name}}

-
-
-
-
-
-
-
- -
{{author | translate}}
-
-
- -
{{tagDetails.architecture}}
-
-
- -
{{tagDetails.os}}
-
-
- -
{{tagDetails['os.version']}}
-
-
- -
{{tagDetails.docker_version}}
-
-
- -
{{scanCompletedDatetime | date}}
-
-
-
-
-
-
- -
- -
-
-
{{'TAG.LABELS' | translate }}
-
-
- -
-
-
-
-
- - - - - - - - - - - {{ 'REPOSITORY.BUILD_HISTORY' | translate }} - - - -
diff --git a/src/portal/src/lib/components/tag/tag-detail.component.scss b/src/portal/src/lib/components/tag/tag-detail.component.scss deleted file mode 100644 index 7eea611b8..000000000 --- a/src/portal/src/lib/components/tag/tag-detail.component.scss +++ /dev/null @@ -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; -} - - diff --git a/src/portal/src/lib/components/tag/tag-detail.component.spec.ts b/src/portal/src/lib/components/tag/tag-detail.component.spec.ts deleted file mode 100644 index de7da41b3..000000000 --- a/src/portal/src/lib/components/tag/tag-detail.component.spec.ts +++ /dev/null @@ -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; - 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); - }); - })); -}); diff --git a/src/portal/src/lib/components/tag/tag-detail.component.ts b/src/portal/src/lib/components/tag/tag-detail.component.ts deleted file mode 100644 index 358860b20..000000000 --- a/src/portal/src/lib/components/tag/tag-detail.component.ts +++ /dev/null @@ -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 = new EventEmitter(); - - 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'; - } -} diff --git a/src/portal/src/lib/components/tag/tag-history.component.ts b/src/portal/src/lib/components/tag/tag-history.component.ts deleted file mode 100644 index 0831c5917..000000000 --- a/src/portal/src/lib/components/tag/tag-history.component.ts +++ /dev/null @@ -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 = new EventEmitter(); - - 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); - } -} diff --git a/src/portal/src/lib/components/tag/tag.component.html b/src/portal/src/lib/components/tag/tag.component.html deleted file mode 100644 index f46a0ef9e..000000000 --- a/src/portal/src/lib/components/tag/tag.component.html +++ /dev/null @@ -1,166 +0,0 @@ - - - - - - - - - - - -
-
-
-
-
- -
-
- -
- × - -
-
{{'LABEL.NO_LABELS' | translate }}
-
- -
-
-
-
- - - -
-
-
- - - - - - - - -
- -
-
{{'LABEL.NO_LABELS' | translate }}
-
- -
-
-
-
-
- - -
- {{'REPOSITORY.TAG' | translate}} - {{'REPOSITORY.SIZE' | translate}} - {{'REPOSITORY.PULL_COMMAND' | translate}} - {{'REPOSITORY.VULNERABILITY' | translate}} - {{'REPOSITORY.SIGNED' | translate}} - {{'REPOSITORY.AUTHOR' | translate}} - {{'REPOSITORY.CREATED' | translate}} - {{'REPOSITORY.DOCKER_VERSION' | translate}} - {{'REPOSITORY.LABELS' | translate}} - {{'REPOSITORY.PUSH_TIME' | translate}} - {{'REPOSITORY.PULL_TIME' | translate}} - {{'TAG.PLACEHOLDER' | translate }} - - -
- {{t.name}} - {{'REPOSITORY.IMMUTABLE' | translate}} -
-
- -
- {{sizeTransform(t.size)}} -
-
- -
- -
-
- -
- -
-
- - - - -
- {{t.author}} -
-
- -
{{t.created | date: 'short'}}
-
- -
{{t.docker_version}}
-
- -
- -
-
- - - -
- -
-
-
-
-
-
-
- -
{{t.push_time | date: 'short'}}
-
- -
{{t.pull_time === availableTime ? "" : (t.pull_time| date: 'short')}}
-
-
- - {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}} {{pagination.totalItems}} {{'REPOSITORY.ITEMS' | translate}}     - - -
-
-
diff --git a/src/portal/src/lib/components/vulnerability-scanning/result-bar-chart.component.spec.ts b/src/portal/src/lib/components/vulnerability-scanning/result-bar-chart.component.spec.ts index 5e5b2e802..4ae7e865b 100644 --- a/src/portal/src/lib/components/vulnerability-scanning/result-bar-chart.component.spec.ts +++ b/src/portal/src/lib/components/vulnerability-scanning/result-bar-chart.component.spec.ts @@ -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); diff --git a/src/portal/src/lib/components/vulnerability-scanning/result-bar-chart.component.ts b/src/portal/src/lib/components/vulnerability-scanning/result-bar-chart.component.ts index 5b7dc2ba5..1ccb961ff 100644 --- a/src/portal/src/lib/components/vulnerability-scanning/result-bar-chart.component.ts +++ b/src/portal/src/lib/components/vulnerability-scanning/result-bar-chart.component.ts @@ -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 = new EventEmitter(); 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`; } } diff --git a/src/portal/src/lib/components/vulnerability-scanning/result-grid.component.ts b/src/portal/src/lib/components/vulnerability-scanning/result-grid.component.ts index be2ace338..fc6b268b7 100644 --- a/src/portal/src/lib/components/vulnerability-scanning/result-grid.component.ts +++ b/src/portal/src/lib/components/vulnerability-scanning/result-grid.component.ts @@ -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); }); } diff --git a/src/portal/src/lib/harbor-library.module.ts b/src/portal/src/lib/harbor-library.module.ts index a4043e5f1..28fd6d871 100644 --- a/src/portal/src/lib/harbor-library.module.ts +++ b/src/portal/src/lib/harbor-library.module.ts @@ -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, diff --git a/src/portal/src/lib/services/artifact.service.spec.ts b/src/portal/src/lib/services/artifact.service.spec.ts new file mode 100644 index 000000000..351fa508d --- /dev/null +++ b/src/portal/src/lib/services/artifact.service.spec.ts @@ -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(); + })); + +}); diff --git a/src/portal/src/lib/services/artifact.service.ts b/src/portal/src/lib/services/artifact.service.ts new file mode 100644 index 000000000..c59f8c846 --- /dev/null +++ b/src/portal/src/lib/services/artifact.service.ts @@ -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(); + 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)} + * + * @memberOf TagService + */ + abstract getArtifactList( + projectName: string, + repositoryName: string, + queryParams?: RequestQueryParams + ): Observable>; + + /** + * Delete the specified tag. + * + * @abstract + * ** deprecated param {string} repositoryName + * ** deprecated param {string} tag + * returns {(Observable | any)} + * + * @memberOf TagService + */ + abstract getArtifactFromDigest( + projectName: string, + repositoryName: string, + artifactDigest: string + ): Observable; + + abstract deleteArtifact( + projectName: string, + repositoryName: string, + digest: string + ): Observable; + + /** + * Get the specified tag. + * + * @abstract + * ** deprecated param {string} repositoryName + * ** deprecated param {string} tag + * returns {(Observable)} + * + * @memberOf TagService + */ + + abstract addLabelToImages( + projectName: string, + repoName: string, + digest: string, + labelId: number + ): Observable; + abstract deleteLabelToImages( + projectName: string, + repoName: string, + digest: string, + labelId: number + ): Observable; + + /** + * Get manifest of tag under the specified repository. + * + * @abstract + * returns {(Observable)} + * + * @memberOf TagService + */ + abstract getManifest( + repositoryName: string, + tag: string + ): Observable; +} + +/** + * Implement default service for tag. + * + ** + * class TagDefaultService + * extends {TagService} + */ +@Injectable() +export class ArtifactDefaultService extends ArtifactService { + _baseUrl: string; + _labelUrl: string; + reference: string[] = []; + triggerUploadArtifact = new Subject(); + 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> { + 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>(url, buildHttpRequestOptionsWithObserveResponse(queryParams)) + .pipe(map(response => response as HttpResponse) + , catchError(error => observableThrowError(error))); + } + + public getArtifactList( + projectName: string, + repositoryName: string, + queryParams?: RequestQueryParams + ): Observable> { + if (!repositoryName) { + return observableThrowError("Bad argument"); + } + return this._getArtifacts(projectName, repositoryName, queryParams); + } + public getArtifactFromDigest( + projectName: string, + repositoryName: string, + artifactDigest: string + ): Observable { + 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; + } + public deleteArtifact( + projectName: string, + repositoryName: string, + digest: string + ): Observable { + 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 { + 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 { + 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 { + 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))); + } +} diff --git a/src/portal/src/lib/services/channel.service.ts b/src/portal/src/lib/services/channel.service.ts index c3f4ddd1b..3be4f8e3b 100644 --- a/src/portal/src/lib/services/channel.service.ts +++ b/src/portal/src/lib/services/channel.service.ts @@ -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(); + ArtifactDetail$ = new Subject(); } diff --git a/src/portal/src/lib/services/index.ts b/src/portal/src/lib/services/index.ts index 311501a80..eb27c18aa 100644 --- a/src/portal/src/lib/services/index.ts +++ b/src/portal/src/lib/services/index.ts @@ -15,3 +15,4 @@ export * from "./retag.service"; export * from "./permission.service"; export * from "./permission-static"; export * from "./quota.service"; +export * from "./artifact.service"; diff --git a/src/portal/src/lib/services/interface.ts b/src/portal/src/lib/services/interface.ts index 260760b14..781c1a530 100644 --- a/src/portal/src/lib/services/interface.ts +++ b/src/portal/src/lib/services/interface.ts @@ -54,21 +54,13 @@ export interface Repository { */ export interface Tag extends Base { - digest: string; + artifact_id: number; name: string; - size: string; - architecture: string; - os: string; - 'os.version': string; - docker_version: string; - author: string; - created: Date; - signature?: string; - scan_overview?: ScanOverview; - labels: Label[]; push_time?: string; pull_time?: string; immutable?: boolean; + repository_id?: number; + upload_time?: string; } /** @@ -347,10 +339,11 @@ export interface VulnerabilitySeverityMetrics { count: number; } -export interface TagClickEvent { +export interface ArtifactClickEvent { project_id: string | number; repository_name: string; - tag_name: string; + digest: string; + artifact_id: number; } export interface Label { diff --git a/src/portal/src/lib/services/scanning.service.ts b/src/portal/src/lib/services/scanning.service.ts index fa765f9ec..3f50a0254 100644 --- a/src/portal/src/lib/services/scanning.service.ts +++ b/src/portal/src/lib/services/scanning.service.ts @@ -60,8 +60,9 @@ export abstract class ScanningResultService { * @memberOf ScanningResultService */ abstract startVulnerabilityScanning( + projectName: string, repoName: string, - tagId: string + artifactId: string ): Observable; /** @@ -90,7 +91,7 @@ export abstract class ScanningResultService { @Injectable() export class ScanningResultDefaultService extends ScanningResultService { - _baseUrl: string = "/api/repositories"; + _baseUrl: string = "/api/v2.0/projects"; constructor( private http: HttpClient, @@ -140,16 +141,17 @@ export class ScanningResultDefaultService extends ScanningResultService { } startVulnerabilityScanning( + projectName: string, repoName: string, - tagId: string + artifactId: string ): Observable { - if (!repoName || repoName.trim() === "" || !tagId || tagId.trim() === "") { + if (!repoName || repoName.trim() === "" || !artifactId || artifactId.trim() === "") { return observableThrowError("Bad argument"); } return this.http .post( - `${this._baseUrl}/${repoName}/tags/${tagId}/scan`, + `/api/v2.0/projects//${projectName}/repositories/${repoName}/artifacts/${artifactId}/scan`, HTTP_JSON_OPTIONS ) .pipe(map(() => { diff --git a/src/portal/src/lib/services/tag.service.ts b/src/portal/src/lib/services/tag.service.ts index fc2360d79..e33539e21 100644 --- a/src/portal/src/lib/services/tag.service.ts +++ b/src/portal/src/lib/services/tag.service.ts @@ -34,6 +34,7 @@ export class VerifiedSignature { * class TagService */ export abstract class TagService { + /** * Get all the tags under the specified repository. * NOTES: If the Notary is enabled, the signatures should be included in the returned data. @@ -45,10 +46,13 @@ export abstract class TagService { * * @memberOf TagService */ - abstract getTags( + // to delete + abstract newTag( + projectName: string, repositoryName: string, - queryParams?: RequestQueryParams - ): Observable; + digest: string, + tagName: {name: string} + ): Observable; /** * Delete the specified tag. @@ -61,51 +65,12 @@ export abstract class TagService { * @memberOf TagService */ abstract deleteTag( + projectName: string, repositoryName: string, - tag: string + digest: string, + tagName: string ): Observable; - - /** - * Get the specified tag. - * - * @abstract - * ** deprecated param {string} repositoryName - * ** deprecated param {string} tag - * returns {(Observable)} - * - * @memberOf TagService - */ - abstract getTag( - repositoryName: string, - tag: string, - queryParams?: RequestQueryParams - ): Observable; - - abstract addLabelToImages( - repoName: string, - tagName: string, - labelId: number - ): Observable; - abstract deleteLabelToImages( - repoName: string, - tagName: string, - labelId: number - ): Observable; - - /** - * Get manifest of tag under the specified repository. - * - * @abstract - * returns {(Observable)} - * - * @memberOf TagService - */ - abstract getManifest( - repositoryName: string, - tag: string - ): Observable; -} - + } /** * Implement default service for tag. * @@ -130,119 +95,36 @@ export class TagDefaultService extends TagService { : "/api/labels"; } - // Private methods - // These two methods are temporary, will be deleted in future after API refactored - _getTags( + public newTag( + projectName: string, repositoryName: string, - queryParams?: RequestQueryParams - ): Observable { - if (!queryParams) { - queryParams = queryParams = new RequestQueryParams(); - } - - queryParams = queryParams.set("detail", "true"); - let url: string = `${this._baseUrl}/${repositoryName}/tags`; - - return this.http - .get(url, buildHttpRequestOptions(queryParams)) - .pipe(map(response => response as Tag[]) - , catchError(error => observableThrowError(error))); - } - - _getSignatures(repositoryName: string): Observable { - let url: string = `${this._baseUrl}/${repositoryName}/signatures`; - return this.http - .get(url, HTTP_GET_OPTIONS) - .pipe(map(response => response as VerifiedSignature[]) - , catchError(error => observableThrowError(error))); - } - - public getTags( - repositoryName: string, - queryParams?: RequestQueryParams - ): Observable { - if (!repositoryName) { + digest: string, + tagName: {name: string} + ): Observable { + if (!projectName || !repositoryName || !digest || !tagName) { return observableThrowError("Bad argument"); } - return this._getTags(repositoryName, queryParams); + let url: string = `/api/v2.0/projects/${projectName}/repositories/${repositoryName}/artifacts/${digest}/tags`; + return this.http + .post(url, tagName, HTTP_JSON_OPTIONS) + .pipe(map(response => response) + , catchError(error => observableThrowError(error))); } public deleteTag( + projectName: string, repositoryName: string, - tag: string + digest: string, + tagName: string ): Observable { - if (!repositoryName || !tag) { + if (!projectName || !repositoryName || !digest || !tagName) { return observableThrowError("Bad argument"); } - let url: string = `${this._baseUrl}/${repositoryName}/tags/${tag}`; + let url: string = `/api/v2.0/projects/${projectName}/repositories/${repositoryName}/artifacts/${digest}/tags/${tagName}`; return this.http .delete(url, HTTP_JSON_OPTIONS) .pipe(map(response => response) - , catchError(error => observableThrowError(error))); - } - - public getTag( - repositoryName: string, - tag: string, - queryParams?: RequestQueryParams - ): Observable { - if (!repositoryName || !tag) { - return observableThrowError("Bad argument"); - } - - let url: string = `${this._baseUrl}/${repositoryName}/tags/${tag}`; - return this.http - .get(url, HTTP_GET_OPTIONS) - .pipe(map(response => response as Tag) - , catchError(error => observableThrowError(error))); - } - - public addLabelToImages( - repoName: string, - tagName: string, - labelId: number - ): Observable { - if (!labelId || !tagName || !repoName) { - return observableThrowError("Invalid parameters."); - } - - let _addLabelToImageUrl = `${ - this._baseUrl - }/${repoName}/tags/${tagName}/labels`; - return this.http - .post(_addLabelToImageUrl, { id: labelId }, HTTP_JSON_OPTIONS) - .pipe(catchError(error => observableThrowError(error))); - } - - public deleteLabelToImages( - repoName: string, - tagName: string, - labelId: number - ): Observable { - if (!labelId || !tagName || !repoName) { - return observableThrowError("Invalid parameters."); - } - - let _addLabelToImageUrl = `${ - this._baseUrl - }/${repoName}/tags/${tagName}/labels/${labelId}`; - return this.http - .delete(_addLabelToImageUrl) - .pipe(catchError(error => observableThrowError(error))); - } - - public getManifest( - repositoryName: string, - tag: string - ): Observable { - 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))); + , catchError(error => observableThrowError(error))); } } diff --git a/src/portal/src/lib/utils/utils.ts b/src/portal/src/lib/utils/utils.ts index 338c2b7e3..78caa4d17 100644 --- a/src/portal/src/lib/utils/utils.ts +++ b/src/portal/src/lib/utils/utils.ts @@ -574,3 +574,15 @@ export const validateLimit = unitContrl => { }; }; +export function formatSize(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"; + } +}