diff --git a/src/portal/src/app/project/create-project/create-project.component.html b/src/portal/src/app/project/create-project/create-project.component.html index 2ec099597..2f10b87b5 100644 --- a/src/portal/src/app/project/create-project/create-project.component.html +++ b/src/portal/src/app/project/create-project/create-project.component.html @@ -41,7 +41,7 @@ </clr-tooltip> </label> <div class="clr-control-container" [class.clr-error]="(projectStorageLimit.invalid && (projectStorageLimit.dirty || projectStorageLimit.touched))||projectStorageLimit.errors"> - <input type="text" id="create_project_storage_limit" [(ngModel)]="storageLimit" name="create_project_storage_limit" class="mr-10 clr-input" + <input type="text" id="create_project_storage_limit" [(ngModel)]="storageLimit" name="create_project_storage_limit" class="mr-10 clr-input width-182" #projectStorageLimit="ngModel" autocomplete="off"> <clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon> <div class="clr-select-wrapper"> @@ -58,6 +58,45 @@ </clr-control-error> </div> </div> + <div class="clr-form-control" *ngIf="isSystemAdmin"> + <label for="create_project_storage_limit" class="clr-control-label">{{ 'PROJECT.PROXY_CACHE' | translate }} + <clr-tooltip> + <clr-icon clrTooltipTrigger shape="info-circle" size="24"></clr-icon> + <clr-tooltip-content clrPosition="bottom-left" clrSize="lg" *clrIfOpen> + <span>{{ 'PROJECT.PROXY_CACHE_TOOLTIP' | translate }}</span> + </clr-tooltip-content> + </clr-tooltip> + </label> + <clr-toggle-wrapper class="mt-02"> + <input type="checkbox" clrToggle name="proxy-cache" id="proxy-cache" + [(ngModel)]="enableProxyCache"/> + </clr-toggle-wrapper> + <div *ngIf="enableProxyCache" class="clr-select-wrapper ml-1"> + <select class="width-164" id="registry" name="registry" [(ngModel)]="project.registry_id"> + <option class="display-none" value=""></option> + <option *ngFor="let r of registries" [value]="r.id">{{r.name}}-{{r.url}}</option> + </select> + </div> + </div> + <div class="clr-form-control mt-0" *ngIf="isSystemAdmin && enableProxyCache"> + <label for="create_project_storage_limit" class="clr-control-label"></label> + <div class="clr-control-container input-width"> + <div class="space-between" *ngIf=" enableProxyCache && !registries.length" > + <span class="alert-label">{{"REPLICATION.NO_ENDPOINT_INFO" | translate}}</span> + <a class="alert-label go-link" routerLink="/harbor/registries">{{'REPLICATION.ENDPOINTS' | translate}}</a> + </div> + </div> + </div> + <div class="clr-form-control mt-05" *ngIf="isSystemAdmin && enableProxyCache"> + <label for="create_project_storage_limit" class="clr-control-label"></label> + <div class="clr-control-container"> + <div class="clr-input-wrapper"> + <label class="clr-control-label endpoint">{{ 'PROJECT.ENDPOINT' | translate }}</label> + <input placeholder="http(s)://192.168.1.1" [value]="getEndpoint()" readonly class="clr-input" type="text" id="endpoint" + autocomplete="off"> + </div> + </div> + </div> </form> </div> <div class="modal-footer"> diff --git a/src/portal/src/app/project/create-project/create-project.component.spec.ts b/src/portal/src/app/project/create-project/create-project.component.spec.ts index 5b2d288f6..6d9bd373c 100644 --- a/src/portal/src/app/project/create-project/create-project.component.spec.ts +++ b/src/portal/src/app/project/create-project/create-project.component.spec.ts @@ -6,11 +6,14 @@ import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { ClarityModule } from '@clr/angular'; import { FormsModule } from '@angular/forms'; import { MessageHandlerService } from '../../shared/message-handler/message-handler.service'; -import { ProjectService } from "../../../lib/services"; +import { EndpointDefaultService, EndpointService, ProjectService } from '../../../lib/services'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { of } from 'rxjs'; import { delay } from 'rxjs/operators'; import { ErrorHandler } from '../../../lib/utils/error-handler'; +import { IServiceConfig, SERVICE_CONFIG } from '../../../lib/entities/service.config'; +import { CURRENT_BASE_HREF } from '../../../lib/utils/utils'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; describe('CreateProjectComponent', () => { let component: CreateProjectComponent; @@ -31,10 +34,14 @@ describe('CreateProjectComponent', () => { showSuccess: function() { } }; + const config: IServiceConfig = { + systemInfoEndpoint: CURRENT_BASE_HREF + "/endpoints/testing" + }; beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ + HttpClientTestingModule, BrowserAnimationsModule, FormsModule, ClarityModule, @@ -46,8 +53,10 @@ describe('CreateProjectComponent', () => { ], providers: [ TranslateService, + { provide: SERVICE_CONFIG, useValue: config }, {provide: ProjectService, useValue: mockProjectService}, {provide: MessageHandlerService, useValue: mockMessageHandlerService}, + { provide: EndpointService, useClass: EndpointDefaultService }, ErrorHandler ] }).compileComponents(); @@ -111,4 +120,13 @@ describe('CreateProjectComponent', () => { const modelBody: HTMLDivElement = fixture.nativeElement.querySelector(".modal-body"); expect(modelBody).toBeFalsy(); }); + + it('should enable proxy cache', async () => { + component.enableProxyCache = true; + component.isSystemAdmin = true; + fixture.detectChanges(); + await fixture.whenStable(); + const endpoint: HTMLDivElement = fixture.nativeElement.querySelector("#endpoint"); + expect(endpoint).toBeFalsy(); + }); }); diff --git a/src/portal/src/app/project/create-project/create-project.component.ts b/src/portal/src/app/project/create-project/create-project.component.ts index 9d87fa390..1ab54826d 100644 --- a/src/portal/src/app/project/create-project/create-project.component.ts +++ b/src/portal/src/app/project/create-project/create-project.component.ts @@ -30,7 +30,7 @@ import { MessageHandlerService } from "../../shared/message-handler/message-hand import { InlineAlertComponent } from "../../shared/inline-alert/inline-alert.component"; import { Project } from "../project"; import { QuotaUnits, QuotaUnlimited } from "../../../lib/entities/shared.const"; -import { ProjectService, QuotaHardInterface } from "../../../lib/services"; +import { Endpoint, EndpointService, ProjectService, QuotaHardInterface } from '../../../lib/services'; import { clone, getByte, GetIntegerAndUnit, validateLimit } from "../../../lib/utils/utils"; @@ -39,7 +39,7 @@ import { clone, getByte, GetIntegerAndUnit, validateLimit } from "../../../lib/u templateUrl: "create-project.component.html", styleUrls: ["create-project.scss"] }) -export class CreateProjectComponent implements AfterViewInit, OnChanges, OnDestroy { +export class CreateProjectComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy { projectForm: NgForm; @@ -63,6 +63,8 @@ export class CreateProjectComponent implements AfterViewInit, OnChanges, OnDest isNameExisted: boolean = false; nameTooltipText = "PROJECT.NAME_TOOLTIP"; checkOnGoing = false; + enableProxyCache: boolean = false; + endpoint: string = ""; @Output() create = new EventEmitter<boolean>(); @Input() quotaObj: QuotaHardInterface; @Input() isSystemAdmin: boolean; @@ -70,11 +72,32 @@ export class CreateProjectComponent implements AfterViewInit, OnChanges, OnDest inlineAlert: InlineAlertComponent; @ViewChild('projectName', {static: false}) projectNameInput: ElementRef; checkNameSubscribe: Subscription; - constructor(private projectService: ProjectService, - private translateService: TranslateService, - private messageHandlerService: MessageHandlerService) { } - ngAfterViewInit(): void { + registries: Endpoint[] = []; + supportedRegistryType: string[] = ['docker-hub', 'harbor']; + + constructor(private projectService: ProjectService, + private translateService: TranslateService, + private messageHandlerService: MessageHandlerService, + private endpointService: EndpointService) { + } + + ngOnInit(): void { + this.getRegistries(); + } + + getRegistries() { + this.endpointService.getEndpoints() + .subscribe(targets => { + if (targets && targets.length) { + this.registries = targets.filter(item => this.supportedRegistryType.indexOf(item.type) !== -1); + } + }, error => { + this.messageHandlerService.handleError(error); + }); + } + + ngAfterViewInit(): void { if (!this.checkNameSubscribe) { this.checkNameSubscribe = fromEvent(this.projectNameInput.nativeElement, 'input').pipe( map((e: any) => e.target.value), @@ -161,7 +184,7 @@ export class CreateProjectComponent implements AfterViewInit, OnChanges, OnDest this.isSubmitOnGoing = true; const storageByte = +this.storageLimit === QuotaUnlimited ? this.storageLimit : getByte(+this.storageLimit, this.storageLimitUnit); this.projectService - .createProject(this.project.name, this.project.metadata, +storageByte) + .createProject(this.project.name, this.project.metadata, +storageByte, this.project.registry_id) .subscribe( status => { this.isSubmitOnGoing = false; @@ -184,6 +207,8 @@ export class CreateProjectComponent implements AfterViewInit, OnChanges, OnDest this.project = new Project(); this.hasChanged = false; this.createProjectOpened = true; + this.enableProxyCache = false; + this.endpoint = ""; if (this.currentForm && this.currentForm.controls && this.currentForm.controls["create_project_name"]) { this.currentForm.controls["create_project_name"].reset(); } @@ -199,5 +224,16 @@ export class CreateProjectComponent implements AfterViewInit, OnChanges, OnDest this.isNameValid && !this.checkOnGoing; } + + getEndpoint(): string { + if (this.registries && this.registries.length && this.project.registry_id) { + for (let i = 0; i < this.registries.length; i++) { + if (+this.registries[i].id === +this.project.registry_id) { + return this.registries[i].url; + } + } + } + return ''; + } } diff --git a/src/portal/src/app/project/create-project/create-project.scss b/src/portal/src/app/project/create-project/create-project.scss index c8471ed93..8169c6199 100644 --- a/src/portal/src/app/project/create-project/create-project.scss +++ b/src/portal/src/app/project/create-project/create-project.scss @@ -15,4 +15,38 @@ .clr-select-wrapper::after { right: 0.25rem !important; } -} \ No newline at end of file +} +.input-width { + width: 242px; +} +.mt-02 { + margin-top: 0.2rem; +} +.width-164 { + width: 164px; +} +.endpoint { + display: inline-block; + width: 72px; +} +.display-none { + display: none +} +.space-between { + display: flex; + justify-content: space-between; +} +.go-link { + line-height: 1rem; + cursor: pointer; +} +.alert-label { + color:red; + font-size: 12px; +} +.width-182 { + width: 182px; +} +.mt-05 { + margin-top: 0.5rem; +} diff --git a/src/portal/src/app/project/helm-chart/helm-chart-detail/chart-detail/chart-detail.component.spec.ts b/src/portal/src/app/project/helm-chart/helm-chart-detail/chart-detail/chart-detail.component.spec.ts index 6165056db..e21e5b7d0 100644 --- a/src/portal/src/app/project/helm-chart/helm-chart-detail/chart-detail/chart-detail.component.spec.ts +++ b/src/portal/src/app/project/helm-chart/helm-chart-detail/chart-detail/chart-detail.component.spec.ts @@ -140,6 +140,7 @@ describe('ChartDetailComponent', () => { "role_name": 'master', "repo_count": 0, "chart_count": 1, + "registry_id" : 0, "metadata": { "public": "true", "enable_content_trust": "string", diff --git a/src/portal/src/app/project/list-project/list-project.component.html b/src/portal/src/app/project/list-project/list-project.component.html index a2c3acff3..88eb23f70 100644 --- a/src/portal/src/app/project/list-project/list-project.component.html +++ b/src/portal/src/app/project/list-project/list-project.component.html @@ -9,6 +9,7 @@ <clr-dg-column [clrDgField]="'name'">{{'PROJECT.NAME' | translate}}</clr-dg-column> <clr-dg-column [clrDgSortBy]="accessLevelComparator">{{'PROJECT.ACCESS_LEVEL' | translate}}</clr-dg-column> <clr-dg-column [clrDgSortBy]="roleComparator">{{'PROJECT.ROLE' | translate}}</clr-dg-column> + <clr-dg-column [clrDgSortBy]="typeComparator">{{'PROJECT.TYPE' | translate}}</clr-dg-column> <clr-dg-column [clrDgSortBy]="repoCountComparator">{{'PROJECT.REPO_COUNT'| translate}}</clr-dg-column> <clr-dg-column *ngIf="withChartMuseum" [clrDgSortBy]="chartCountComparator">{{'PROJECT.CHART_COUNT'| translate}}</clr-dg-column> <clr-dg-column [clrDgSortBy]="timeComparator">{{'PROJECT.CREATION_TIME' | translate}}</clr-dg-column> @@ -18,6 +19,7 @@ </clr-dg-cell> <clr-dg-cell>{{ (p.metadata.public === 'true' ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE') | translate}}</clr-dg-cell> <clr-dg-cell>{{ roleInfo[p.current_user_role_id]? (roleInfo[p.current_user_role_id] | translate): "-"}}</clr-dg-cell> + <clr-dg-cell>{{projectTypeMap[p.registry_id ? 1 : 0]}}</clr-dg-cell> <clr-dg-cell>{{p.repo_count}}</clr-dg-cell> <clr-dg-cell *ngIf="withChartMuseum">{{p.chart_count}}</clr-dg-cell> <clr-dg-cell>{{p.creation_time | date: 'short'}}</clr-dg-cell> diff --git a/src/portal/src/app/project/list-project/list-project.component.ts b/src/portal/src/app/project/list-project/list-project.component.ts index 8fbfcf701..dfcfaad27 100644 --- a/src/portal/src/app/project/list-project/list-project.component.ts +++ b/src/portal/src/app/project/list-project/list-project.component.ts @@ -55,11 +55,16 @@ export class ListProjectComponent implements OnDestroy { timeComparator: Comparator<Project> = new CustomComparator<Project>("creation_time", "date"); accessLevelComparator: Comparator<Project> = new CustomComparator<Project>("public", "string"); roleComparator: Comparator<Project> = new CustomComparator<Project>("current_user_role_id", "number"); + typeComparator: Comparator<Project> = new CustomComparator<Project>("registry_id", "number"); currentPage = 1; totalCount = 0; pageSize = 15; currentState: State; subscription: Subscription; + projectTypeMap: any = { + 0: "Project", + 1: "Proxy Cache" + }; constructor( private session: SessionService, diff --git a/src/portal/src/app/project/project-detail/project-detail.component.html b/src/portal/src/app/project/project-detail/project-detail.component.html index 248973f18..5a767a6db 100644 --- a/src/portal/src/app/project/project-detail/project-detail.component.html +++ b/src/portal/src/app/project/project-detail/project-detail.component.html @@ -1,8 +1,14 @@ <a *ngIf="hasSignedIn" (click)="backToProject()" class="backStyle"> {{'PROJECT_DETAIL.PROJECTS' | translate}}</a> <a *ngIf="!hasSignedIn" [routerLink]="['/harbor', 'sign-in']"> {{'SEARCH.BACK' | translate}}</a> -<h1 class="custom-h2" sub-header-title>{{currentProject.name}} <span class="role-label" - *ngIf="isMember">{{roleName | translate}}</span></h1> +<h1 class="custom-h2" sub-header-title> + <clr-icon *ngIf="isProxyCacheProject" shape="cloud-traffic" size="30"></clr-icon> + <span class="ml-05">{{currentProject.name}}</span> + <span class="ml-05 role-label" *ngIf="isMember">{{roleName | translate}}</span> +</h1> +<div class="clr-row mt-0 line-height-10" *ngIf="isProxyCacheProject"> + <span class="proxy-cache">{{ 'PROJECT.PROXY_CACHE' | translate }}</span> +</div> <clr-tabs id="project-tabs" class="tabs" [class.in-overflow]="isTabLinkInOverFlow()"> <ng-container *ngFor="let tab of tabLinkNavList;let i=index"> <ng-container *ngIf="tab.permissions()"> diff --git a/src/portal/src/app/project/project-detail/project-detail.component.scss b/src/portal/src/app/project/project-detail/project-detail.component.scss index 0e7e8ab29..8993a9027 100644 --- a/src/portal/src/app/project/project-detail/project-detail.component.scss +++ b/src/portal/src/app/project/project-detail/project-detail.component.scss @@ -42,3 +42,15 @@ button { padding: 0; } } +.ml-05 { + margin-left: 0.5rem; +} +.proxy-cache { + margin-left: 2.5rem; + font-size: 10px; + font-weight: 300; + opacity: 0.8; +} +.line-height-10 { + line-height: 10px; +} diff --git a/src/portal/src/app/project/project-detail/project-detail.component.ts b/src/portal/src/app/project/project-detail/project-detail.component.ts index b3cc40aec..2ddf00806 100644 --- a/src/portal/src/app/project/project-detail/project-detail.component.ts +++ b/src/portal/src/app/project/project-detail/project-detail.component.ts @@ -118,6 +118,7 @@ export class ProjectDetailComponent implements OnInit, AfterViewInit, OnDestroy previousWindowWidth: number; private _subject = new Subject<string>(); private _subscription: Subscription; + isProxyCacheProject: boolean = false; constructor( private route: ActivatedRoute, private router: Router, @@ -129,6 +130,9 @@ export class ProjectDetailComponent implements OnInit, AfterViewInit, OnDestroy this.hasSignedIn = this.sessionService.getCurrentUser() !== null; this.route.data.subscribe(data => { this.currentProject = <Project>data['projectResolver']; + if (this.currentProject.registry_id) { + this.isProxyCacheProject = true; + } this.isMember = this.currentProject.is_member; this.roleName = this.currentProject.role_name; }); diff --git a/src/portal/src/app/project/project.ts b/src/portal/src/app/project/project.ts index d43408bbe..7fea628a7 100644 --- a/src/portal/src/app/project/project.ts +++ b/src/portal/src/app/project/project.ts @@ -27,6 +27,7 @@ export class Project { has_project_admin_role: boolean; is_member: boolean; role_name: string; + registry_id: number; metadata: { public: string | boolean; enable_content_trust: string | boolean; diff --git a/src/portal/src/app/project/summary/summary.component.html b/src/portal/src/app/project/summary/summary.component.html index b2d86bc42..6af3f544e 100644 --- a/src/portal/src/app/project/summary/summary.component.html +++ b/src/portal/src/app/project/summary/summary.component.html @@ -1,18 +1,24 @@ <div class="summary summary-dark display-flex" *ngIf="summaryInformation"> <div class="summary-left"> - <div class="display-flex project-detail pt-1"> + <div class="display-flex project-detail pt-05" *ngIf="isSystemAdmin && endpoint"> + <h5 class="mt-0 width-7-5">{{'PROJECT.PROXY_CACHE_ENDPOINT' | translate}}</h5> + <ul class="list-unstyled"> + <li id="endpoint">{{endpoint?.name}}-{{endpoint?.url}}</li> + </ul> + </div> + <div class="display-flex project-detail pt-05"> <h5 class="mt-0 width-7-5">{{'SUMMARY.PROJECT_REPOSITORY' | translate}}</h5> <ul class="list-unstyled"> <li>{{summaryInformation?.repo_count}}</li> </ul> </div> - <div class="display-flex project-detail pt-1" *ngIf="withHelmChart"> + <div class="display-flex project-detail pt-05" *ngIf="withHelmChart"> <h5 class="mt-0 width-7-5">{{'SUMMARY.PROJECT_HELM_CHART' | translate}}</h5> <ul class="list-unstyled"> <li>{{summaryInformation?.chart_count}}</li> </ul> </div> - <div *ngIf="showProjectMemberInfo" class="display-flex project-detail pt-1"> + <div *ngIf="showProjectMemberInfo" class="display-flex project-detail pt-05"> <h5 class="mt-0 width-7-5">{{'SUMMARY.PROJECT_MEMBER' | translate}}</h5> <ul class="list-unstyled"> <li>{{ summaryInformation?.project_admin_count }} {{'SUMMARY.ADMIN' | translate}}</li> @@ -23,7 +29,7 @@ </ul> </div> </div> - <div *ngIf="showQuotaInfo && summaryInformation?.quota" class="summary-right pt-1"> + <div *ngIf="showQuotaInfo && summaryInformation?.quota" class="summary-right pt-05"> <div class="display-flex project-detail"> <h5 class="mt-0">{{'SUMMARY.PROJECT_QUOTAS' | translate}}</h5> <div class="ml-1"> @@ -55,4 +61,4 @@ </div> </div> -</div> \ No newline at end of file +</div> diff --git a/src/portal/src/app/project/summary/summary.component.scss b/src/portal/src/app/project/summary/summary.component.scss index a0fae889f..e0dc7f511 100644 --- a/src/portal/src/app/project/summary/summary.component.scss +++ b/src/portal/src/app/project/summary/summary.component.scss @@ -57,3 +57,7 @@ } } } + +.pt-05 { + padding-top: 0.5rem; +} diff --git a/src/portal/src/app/project/summary/summary.component.spec.ts b/src/portal/src/app/project/summary/summary.component.spec.ts index ad20319e2..4b150581b 100644 --- a/src/portal/src/app/project/summary/summary.component.spec.ts +++ b/src/portal/src/app/project/summary/summary.component.spec.ts @@ -6,14 +6,23 @@ import { ActivatedRoute } from '@angular/router'; import { of } from 'rxjs'; import { AppConfigService } from "../../services/app-config.service"; import { SummaryComponent } from './summary.component'; -import { ProjectService, UserPermissionService } from "../../../lib/services"; +import { EndpointDefaultService, EndpointService, ProjectService, UserPermissionService } from '../../../lib/services'; import { ErrorHandler } from "../../../lib/utils/error-handler"; +import { IServiceConfig, SERVICE_CONFIG } from '../../../lib/entities/service.config'; +import { CURRENT_BASE_HREF } from '../../../lib/utils/utils'; +import { SessionService } from '../../shared/session.service'; describe('SummaryComponent', () => { let component: SummaryComponent; let fixture: ComponentFixture<SummaryComponent>; - let fakeAppConfigService = null; + let fakeAppConfigService = { + getConfig() { + return { + with_chartmuseum: false + }; + } + }; let fakeProjectService = { getProjectSummary: function () { return of(); @@ -25,6 +34,34 @@ describe('SummaryComponent', () => { return of([true, true]); } }; + const config: IServiceConfig = { + systemInfoEndpoint: CURRENT_BASE_HREF + "/endpoints/testing" + }; + + const fakedSessionService = { + getCurrentUser() { + return { + has_admin_role: true + }; + } + }; + + const fakedEndpointService = { + getEndpoint() { + return of({ + name: "test", + url: "https://test.com" + }); + } + }; + + const mockedSummaryInformation = { + repo_count: 0, + chart_count: 0, + project_admin_count: 1, + master_count: 0, + developer_count: 0 + }; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -42,14 +79,19 @@ describe('SummaryComponent', () => { { provide: ProjectService, useValue: fakeProjectService }, { provide: ErrorHandler, useValue: fakeErrorHandler }, { provide: UserPermissionService, useValue: fakeUserPermissionService }, + { provide: EndpointService, useValue: fakedEndpointService }, + { provide: SERVICE_CONFIG, useValue: config }, + { provide: SessionService, useValue: fakedSessionService}, { provide: ActivatedRoute, useValue: { paramMap: of({ get: (key) => 'value' }), snapshot: { parent: { - params: { id: 1 } + params: { id: 1 }, + data: { + projectResolver: {registry_id: 3} + } }, - data: 1 } } }, @@ -66,4 +108,13 @@ describe('SummaryComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should show proxy cache endpoint', async () => { + component.summaryInformation = mockedSummaryInformation; + fixture.detectChanges(); + await fixture.whenStable(); + const endpoint: HTMLElement = fixture.nativeElement.querySelector("#endpoint"); + expect(endpoint).toBeTruthy(); + expect(endpoint.innerText).toEqual("test-https://test.com"); + }); }); diff --git a/src/portal/src/app/project/summary/summary.component.ts b/src/portal/src/app/project/summary/summary.component.ts index 74fc05b92..51f64e290 100644 --- a/src/portal/src/app/project/summary/summary.component.ts +++ b/src/portal/src/app/project/summary/summary.component.ts @@ -1,10 +1,18 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { AppConfigService } from "../../services/app-config.service"; import { QUOTA_DANGER_COEFFICIENT, QUOTA_WARNING_COEFFICIENT, QuotaUnits } from "../../../lib/entities/shared.const"; -import { ProjectService, UserPermissionService, USERSTATICPERMISSION } from "../../../lib/services"; +import { + Endpoint, + EndpointService, + ProjectService, + UserPermissionService, + USERSTATICPERMISSION +} from '../../../lib/services'; import { ErrorHandler } from "../../../lib/utils/error-handler"; import { clone, GetIntegerAndUnit, getSuitableUnit as getSuitableUnitFn } from "../../../lib/utils/utils"; +import { SessionService } from '../../shared/session.service'; +import { Project } from '../project'; @Component({ selector: 'summary', @@ -19,20 +27,30 @@ export class SummaryComponent implements OnInit { summaryInformation: any; quotaDangerCoefficient: number = QUOTA_DANGER_COEFFICIENT; quotaWarningCoefficient: number = QUOTA_WARNING_COEFFICIENT; + endpoint: Endpoint; constructor( private projectService: ProjectService, private userPermissionService: UserPermissionService, private errorHandler: ErrorHandler, private appConfigService: AppConfigService, - private route: ActivatedRoute + private route: ActivatedRoute, + private session: SessionService, + private endpointService: EndpointService ) { } ngOnInit() { this.projectId = this.route.snapshot.parent.params['id']; + const resolverData = this.route.snapshot.parent.data; + if (resolverData) { + const pro: Project = <Project>resolverData['projectResolver']; + if (pro && pro.registry_id && this.isSystemAdmin) { + this.getRegistry(pro.registry_id); + } + } const permissions = [ - { resource: USERSTATICPERMISSION.MEMBER.KEY, action: USERSTATICPERMISSION.MEMBER.VALUE.LIST }, - { resource: USERSTATICPERMISSION.QUOTA.KEY, action: USERSTATICPERMISSION.QUOTA.VALUE.READ }, + {resource: USERSTATICPERMISSION.MEMBER.KEY, action: USERSTATICPERMISSION.MEMBER.VALUE.LIST}, + {resource: USERSTATICPERMISSION.QUOTA.KEY, action: USERSTATICPERMISSION.QUOTA.VALUE.READ}, ]; this.userPermissionService.hasProjectPermissions(this.projectId, permissions).subscribe((results: Array<boolean>) => { @@ -47,6 +65,14 @@ export class SummaryComponent implements OnInit { }); } + getRegistry(registryId: number) { + this.endpointService.getEndpoint(registryId).subscribe(res => { + this.endpoint = res; + }, error => { + this.errorHandler.error(error); + }); + } + getSuitableUnit(value) { const QuotaUnitsCopy = clone(QuotaUnits); return getSuitableUnitFn(value, QuotaUnitsCopy); @@ -60,4 +86,9 @@ export class SummaryComponent implements OnInit { return this.appConfigService.getConfig().with_chartmuseum; } + public get isSystemAdmin(): boolean { + const account = this.session.getCurrentUser(); + return account && account.has_admin_role; + } + } diff --git a/src/portal/src/i18n/lang/en-us-lang.json b/src/portal/src/i18n/lang/en-us-lang.json index d99663a19..c4a480786 100644 --- a/src/portal/src/i18n/lang/en-us-lang.json +++ b/src/portal/src/i18n/lang/en-us-lang.json @@ -235,10 +235,15 @@ "INLINE_HELP_PUBLIC": "When a project is set to public, anyone has read permission to the repositories under this project, and the user does not need to run \"docker login\" before pulling images under this project.", "OF": "of", "COUNT_QUOTA": "Count quota", - "STORAGE_QUOTA": "Storage quota", + "STORAGE_QUOTA": "Storage Quota", "COUNT_QUOTA_TIP": "Please enter an integer between '1' & '100,000,000', '-1' for unlimited.", "STORAGE_QUOTA_TIP": "The upper limit of Storage Quota only takes integer values, capped at '1024TB'. Enter '-1' for unlimited quota", - "QUOTA_UNLIMIT_TIP": "For unlimited quota enter '-1'." + "QUOTA_UNLIMIT_TIP": "For unlimited quota enter '-1'.", + "TYPE": "Type", + "PROXY_CACHE": "Proxy Cache", + "PROXY_CACHE_TOOLTIP": "Enable this to allow this project to act as a pull-through cache for a particular namespace within a target registry.Harbor can only act a proxy for DockerHub and Harbor registries.", + "ENDPOINT": "Endpoint", + "PROXY_CACHE_ENDPOINT": "Proxy Cache Endpoint" }, "PROJECT_DETAIL": { "SUMMARY": "Summary", diff --git a/src/portal/src/i18n/lang/es-es-lang.json b/src/portal/src/i18n/lang/es-es-lang.json index 375008ec1..b7d21acff 100644 --- a/src/portal/src/i18n/lang/es-es-lang.json +++ b/src/portal/src/i18n/lang/es-es-lang.json @@ -239,7 +239,12 @@ "STORAGE_QUOTA": "Storage quota", "COUNT_QUOTA_TIP": "Please enter an integer between '1' & '100,000,000', '-1' for unlimited", "STORAGE_QUOTA_TIP": "The upper limit of Storage Quota only takes integer values, capped at '1024TB'. Enter '-1' for unlimited quota", - "QUOTA_UNLIMIT_TIP": "For unlimited quota enter '-1'." + "QUOTA_UNLIMIT_TIP": "For unlimited quota enter '-1'.", + "TYPE": "Type", + "PROXY_CACHE": "Proxy Cache", + "PROXY_CACHE_TOOLTIP": "Enable this to allow this project to act as a pull-through cache for a particular namespace within a target registry.Harbor can only act a proxy for DockerHub and Harbor registries.", + "ENDPOINT": "Endpoint", + "PROXY_CACHE_ENDPOINT": "Proxy Cache Endpoint" }, "PROJECT_DETAIL": { "SUMMARY": "Summary", diff --git a/src/portal/src/i18n/lang/fr-fr-lang.json b/src/portal/src/i18n/lang/fr-fr-lang.json index f30003282..5f918dbb3 100644 --- a/src/portal/src/i18n/lang/fr-fr-lang.json +++ b/src/portal/src/i18n/lang/fr-fr-lang.json @@ -232,7 +232,12 @@ "STORAGE_QUOTA": "Storage quota", "COUNT_QUOTA_TIP": "Please enter an integer between '1' & '100,000,000', '-1' for unlimited", "STORAGE_QUOTA_TIP": "The upper limit of Storage Quota only takes integer values, capped at '1024TB'. Enter '-1' for unlimited quota", - "QUOTA_UNLIMIT_TIP": "For unlimited quota enter '-1'." + "QUOTA_UNLIMIT_TIP": "For unlimited quota enter '-1'.", + "TYPE": "Type", + "PROXY_CACHE": "Proxy Cache", + "PROXY_CACHE_TOOLTIP": "Enable this to allow this project to act as a pull-through cache for a particular namespace within a target registry.Harbor can only act a proxy for DockerHub and Harbor registries.", + "ENDPOINT": "Endpoint", + "PROXY_CACHE_ENDPOINT": "Proxy Cache Endpoint" }, "PROJECT_DETAIL": { "SUMMARY": "Summary", diff --git a/src/portal/src/i18n/lang/pt-br-lang.json b/src/portal/src/i18n/lang/pt-br-lang.json index ebc099a4c..c63fe0ad3 100644 --- a/src/portal/src/i18n/lang/pt-br-lang.json +++ b/src/portal/src/i18n/lang/pt-br-lang.json @@ -236,7 +236,12 @@ "STORAGE_QUOTA": "Storage quota", "COUNT_QUOTA_TIP": "Please enter an integer between '1' & '100,000,000', '-1' for unlimited", "STORAGE_QUOTA_TIP": "The upper limit of Storage Quota only takes integer values, capped at '1024TB'. Enter '-1' for unlimited quota", - "QUOTA_UNLIMIT_TIP": "For unlimited quota enter '-1'." + "QUOTA_UNLIMIT_TIP": "For unlimited quota enter '-1'.", + "TYPE": "Type", + "PROXY_CACHE": "Proxy Cache", + "PROXY_CACHE_TOOLTIP": "Enable this to allow this project to act as a pull-through cache for a particular namespace within a target registry.Harbor can only act a proxy for DockerHub and Harbor registries.", + "ENDPOINT": "Endpoint", + "PROXY_CACHE_ENDPOINT": "Proxy Cache Endpoint" }, "PROJECT_DETAIL": { "SUMMARY": "Summary", diff --git a/src/portal/src/i18n/lang/tr-tr-lang.json b/src/portal/src/i18n/lang/tr-tr-lang.json index 7112c4ee0..7767e241c 100644 --- a/src/portal/src/i18n/lang/tr-tr-lang.json +++ b/src/portal/src/i18n/lang/tr-tr-lang.json @@ -238,7 +238,12 @@ "STORAGE_QUOTA": "Depolama kotası", "COUNT_QUOTA_TIP": "Sayı Kotasının üst sınırı tamsayı olmalıdır.\n", "STORAGE_QUOTA_TIP": "Depolama Kotasının üst sınırı tamsayı olmalı ve maksimum üst sınır 1024 TB", - "QUOTA_UNLIMIT_TIP": "Bu kotayı sınırsız istiyorsanız, lütfen -1 girin." + "QUOTA_UNLIMIT_TIP": "Bu kotayı sınırsız istiyorsanız, lütfen -1 girin.", + "TYPE": "Type", + "PROXY_CACHE": "Proxy Cache", + "PROXY_CACHE_TOOLTIP": "Enable this to allow this project to act as a pull-through cache for a particular namespace within a target registry.Harbor can only act a proxy for DockerHub and Harbor registries.", + "ENDPOINT": "Endpoint", + "PROXY_CACHE_ENDPOINT": "Proxy Cache Endpoint" }, "PROJECT_DETAIL": { "SUMMARY": "Özet", diff --git a/src/portal/src/i18n/lang/zh-cn-lang.json b/src/portal/src/i18n/lang/zh-cn-lang.json index 5e015fc9c..9fadd1b0f 100644 --- a/src/portal/src/i18n/lang/zh-cn-lang.json +++ b/src/portal/src/i18n/lang/zh-cn-lang.json @@ -237,7 +237,12 @@ "STORAGE_QUOTA": "存储容量", "COUNT_QUOTA_TIP": "请输入一个'1' ~ '100000000'之间的整数, '-1'表示不设置上限。", "STORAGE_QUOTA_TIP": "存储配额的上限仅采用整数值,上限为1024TB。输入“-1”作为无限制配额。", - "QUOTA_UNLIMIT_TIP": "如果你想要对存储不设置上限,请输入-1。" + "QUOTA_UNLIMIT_TIP": "如果你想要对存储不设置上限,请输入-1。", + "TYPE": "类型", + "PROXY_CACHE": "镜像代理", + "PROXY_CACHE_TOOLTIP": "开启此项,以使得该项目成为目标仓库的镜像代理.仅支持 Docker Hub 和 Harbor 类型的仓库", + "ENDPOINT": "地址", + "PROXY_CACHE_ENDPOINT": "镜像代理地址" }, "PROJECT_DETAIL": { "SUMMARY": "概要", diff --git a/src/portal/src/i18n/lang/zh-tw-lang.json b/src/portal/src/i18n/lang/zh-tw-lang.json index 20369fd98..318f4adae 100644 --- a/src/portal/src/i18n/lang/zh-tw-lang.json +++ b/src/portal/src/i18n/lang/zh-tw-lang.json @@ -235,7 +235,12 @@ "STORAGE_QUOTA": "存儲容量", "COUNT_QUOTA_TIP": "請輸入一個'1' ~ '100000000'之間的整數, '-1'表示不設置上限。", "STORAGE_QUOTA_TIP": "存儲配額的上限僅採用整數值,上限為1024TB。輸入“-1”作為無限製配額。", - "QUOTA_UNLIMIT_TIP": "如果你想要對存儲不設置上限,請輸入-1。" + "QUOTA_UNLIMIT_TIP": "如果你想要對存儲不設置上限,請輸入-1。", + "TYPE": "Type", + "PROXY_CACHE": "Proxy Cache", + "PROXY_CACHE_TOOLTIP": "Enable this to allow this project to act as a pull-through cache for a particular namespace within a target registry.Harbor can only act a proxy for DockerHub and Harbor registries.", + "ENDPOINT": "Endpoint", + "PROXY_CACHE_ENDPOINT": "Proxy Cache Endpoint" }, "PROJECT_DETAIL":{ "SUMMARY": "概要", diff --git a/src/portal/src/lib/services/project.service.ts b/src/portal/src/lib/services/project.service.ts index 265a2f913..40f86ec17 100644 --- a/src/portal/src/lib/services/project.service.ts +++ b/src/portal/src/lib/services/project.service.ts @@ -69,7 +69,7 @@ export abstract class ProjectService { page?: number, pageSize?: number ): Observable<HttpResponse<Project[]>>; - abstract createProject(name: string, metadata: any, storageLimit: number): Observable<any>; + abstract createProject(name: string, metadata: any, storageLimit: number, registryId: number): Observable<any>; abstract deleteProject(projectId: number): Observable<any>; abstract checkProjectExists(projectName: string): Observable<any>; abstract checkProjectMember(projectId: number): Observable<any>; @@ -149,12 +149,13 @@ export class ProjectDefaultService extends ProjectService { catchError(error => observableThrowError(error)), ); } - public createProject(name: string, metadata: any, storageLimit: number): Observable<any> { + public createProject(name: string, metadata: any, storageLimit: number, registryId: number): Observable<any> { return this.http .post(`${ CURRENT_BASE_HREF }/projects`, - JSON.stringify({'project_name': name, 'metadata': { - public: metadata.public ? 'true' : 'false', - }, + JSON.stringify({ + 'project_name': name, registry_id: +registryId, 'metadata': { + public: metadata.public ? 'true' : 'false' + }, storage_limit: storageLimit }) , HTTP_JSON_OPTIONS).pipe(