Merge pull request #12374 from AllForNothing/proxy-cache

Add proxy cache ui
This commit is contained in:
Will Sun 2020-07-03 18:14:25 +08:00 committed by GitHub
commit 7dfab5858c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 325 additions and 39 deletions

View File

@ -41,7 +41,7 @@
</clr-tooltip> </clr-tooltip>
</label> </label>
<div class="clr-control-container" [class.clr-error]="(projectStorageLimit.invalid && (projectStorageLimit.dirty || projectStorageLimit.touched))||projectStorageLimit.errors"> <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"> #projectStorageLimit="ngModel" autocomplete="off">
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon> <clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
<div class="clr-select-wrapper"> <div class="clr-select-wrapper">
@ -58,6 +58,45 @@
</clr-control-error> </clr-control-error>
</div> </div>
</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> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">

View File

@ -6,11 +6,14 @@ import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ClarityModule } from '@clr/angular'; import { ClarityModule } from '@clr/angular';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { MessageHandlerService } from '../../shared/message-handler/message-handler.service'; 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 { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { delay } from 'rxjs/operators'; import { delay } from 'rxjs/operators';
import { ErrorHandler } from '../../../lib/utils/error-handler'; 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', () => { describe('CreateProjectComponent', () => {
let component: CreateProjectComponent; let component: CreateProjectComponent;
@ -31,10 +34,14 @@ describe('CreateProjectComponent', () => {
showSuccess: function() { showSuccess: function() {
} }
}; };
const config: IServiceConfig = {
systemInfoEndpoint: CURRENT_BASE_HREF + "/endpoints/testing"
};
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
HttpClientTestingModule,
BrowserAnimationsModule, BrowserAnimationsModule,
FormsModule, FormsModule,
ClarityModule, ClarityModule,
@ -46,8 +53,10 @@ describe('CreateProjectComponent', () => {
], ],
providers: [ providers: [
TranslateService, TranslateService,
{ provide: SERVICE_CONFIG, useValue: config },
{provide: ProjectService, useValue: mockProjectService}, {provide: ProjectService, useValue: mockProjectService},
{provide: MessageHandlerService, useValue: mockMessageHandlerService}, {provide: MessageHandlerService, useValue: mockMessageHandlerService},
{ provide: EndpointService, useClass: EndpointDefaultService },
ErrorHandler ErrorHandler
] ]
}).compileComponents(); }).compileComponents();
@ -111,4 +120,13 @@ describe('CreateProjectComponent', () => {
const modelBody: HTMLDivElement = fixture.nativeElement.querySelector(".modal-body"); const modelBody: HTMLDivElement = fixture.nativeElement.querySelector(".modal-body");
expect(modelBody).toBeFalsy(); 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();
});
}); });

View File

@ -30,7 +30,7 @@ import { MessageHandlerService } from "../../shared/message-handler/message-hand
import { InlineAlertComponent } from "../../shared/inline-alert/inline-alert.component"; import { InlineAlertComponent } from "../../shared/inline-alert/inline-alert.component";
import { Project } from "../project"; import { Project } from "../project";
import { QuotaUnits, QuotaUnlimited } from "../../../lib/entities/shared.const"; 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"; 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", templateUrl: "create-project.component.html",
styleUrls: ["create-project.scss"] styleUrls: ["create-project.scss"]
}) })
export class CreateProjectComponent implements AfterViewInit, OnChanges, OnDestroy { export class CreateProjectComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy {
projectForm: NgForm; projectForm: NgForm;
@ -63,6 +63,8 @@ export class CreateProjectComponent implements AfterViewInit, OnChanges, OnDest
isNameExisted: boolean = false; isNameExisted: boolean = false;
nameTooltipText = "PROJECT.NAME_TOOLTIP"; nameTooltipText = "PROJECT.NAME_TOOLTIP";
checkOnGoing = false; checkOnGoing = false;
enableProxyCache: boolean = false;
endpoint: string = "";
@Output() create = new EventEmitter<boolean>(); @Output() create = new EventEmitter<boolean>();
@Input() quotaObj: QuotaHardInterface; @Input() quotaObj: QuotaHardInterface;
@Input() isSystemAdmin: boolean; @Input() isSystemAdmin: boolean;
@ -70,11 +72,32 @@ export class CreateProjectComponent implements AfterViewInit, OnChanges, OnDest
inlineAlert: InlineAlertComponent; inlineAlert: InlineAlertComponent;
@ViewChild('projectName', {static: false}) projectNameInput: ElementRef; @ViewChild('projectName', {static: false}) projectNameInput: ElementRef;
checkNameSubscribe: Subscription; 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) { if (!this.checkNameSubscribe) {
this.checkNameSubscribe = fromEvent(this.projectNameInput.nativeElement, 'input').pipe( this.checkNameSubscribe = fromEvent(this.projectNameInput.nativeElement, 'input').pipe(
map((e: any) => e.target.value), map((e: any) => e.target.value),
@ -161,7 +184,7 @@ export class CreateProjectComponent implements AfterViewInit, OnChanges, OnDest
this.isSubmitOnGoing = true; this.isSubmitOnGoing = true;
const storageByte = +this.storageLimit === QuotaUnlimited ? this.storageLimit : getByte(+this.storageLimit, this.storageLimitUnit); const storageByte = +this.storageLimit === QuotaUnlimited ? this.storageLimit : getByte(+this.storageLimit, this.storageLimitUnit);
this.projectService this.projectService
.createProject(this.project.name, this.project.metadata, +storageByte) .createProject(this.project.name, this.project.metadata, +storageByte, this.project.registry_id)
.subscribe( .subscribe(
status => { status => {
this.isSubmitOnGoing = false; this.isSubmitOnGoing = false;
@ -184,6 +207,8 @@ export class CreateProjectComponent implements AfterViewInit, OnChanges, OnDest
this.project = new Project(); this.project = new Project();
this.hasChanged = false; this.hasChanged = false;
this.createProjectOpened = true; this.createProjectOpened = true;
this.enableProxyCache = false;
this.endpoint = "";
if (this.currentForm && this.currentForm.controls && this.currentForm.controls["create_project_name"]) { if (this.currentForm && this.currentForm.controls && this.currentForm.controls["create_project_name"]) {
this.currentForm.controls["create_project_name"].reset(); this.currentForm.controls["create_project_name"].reset();
} }
@ -199,5 +224,16 @@ export class CreateProjectComponent implements AfterViewInit, OnChanges, OnDest
this.isNameValid && this.isNameValid &&
!this.checkOnGoing; !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 '';
}
} }

View File

@ -15,4 +15,38 @@
.clr-select-wrapper::after { .clr-select-wrapper::after {
right: 0.25rem !important; right: 0.25rem !important;
} }
} }
.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;
}

View File

@ -140,6 +140,7 @@ describe('ChartDetailComponent', () => {
"role_name": 'master', "role_name": 'master',
"repo_count": 0, "repo_count": 0,
"chart_count": 1, "chart_count": 1,
"registry_id" : 0,
"metadata": { "metadata": {
"public": "true", "public": "true",
"enable_content_trust": "string", "enable_content_trust": "string",

View File

@ -9,6 +9,7 @@
<clr-dg-column [clrDgField]="'name'">{{'PROJECT.NAME' | translate}}</clr-dg-column> <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]="accessLevelComparator">{{'PROJECT.ACCESS_LEVEL' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="roleComparator">{{'PROJECT.ROLE' | 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 [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 *ngIf="withChartMuseum" [clrDgSortBy]="chartCountComparator">{{'PROJECT.CHART_COUNT'| translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="timeComparator">{{'PROJECT.CREATION_TIME' | 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>
<clr-dg-cell>{{ (p.metadata.public === 'true' ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE') | translate}}</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>{{ 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>{{p.repo_count}}</clr-dg-cell>
<clr-dg-cell *ngIf="withChartMuseum">{{p.chart_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> <clr-dg-cell>{{p.creation_time | date: 'short'}}</clr-dg-cell>

View File

@ -55,11 +55,16 @@ export class ListProjectComponent implements OnDestroy {
timeComparator: Comparator<Project> = new CustomComparator<Project>("creation_time", "date"); timeComparator: Comparator<Project> = new CustomComparator<Project>("creation_time", "date");
accessLevelComparator: Comparator<Project> = new CustomComparator<Project>("public", "string"); accessLevelComparator: Comparator<Project> = new CustomComparator<Project>("public", "string");
roleComparator: Comparator<Project> = new CustomComparator<Project>("current_user_role_id", "number"); roleComparator: Comparator<Project> = new CustomComparator<Project>("current_user_role_id", "number");
typeComparator: Comparator<Project> = new CustomComparator<Project>("registry_id", "number");
currentPage = 1; currentPage = 1;
totalCount = 0; totalCount = 0;
pageSize = 15; pageSize = 15;
currentState: State; currentState: State;
subscription: Subscription; subscription: Subscription;
projectTypeMap: any = {
0: "Project",
1: "Proxy Cache"
};
constructor( constructor(
private session: SessionService, private session: SessionService,

View File

@ -1,8 +1,14 @@
<a *ngIf="hasSignedIn" (click)="backToProject()" class="backStyle"> {{'PROJECT_DETAIL.PROJECTS' | translate}}</a> <a *ngIf="hasSignedIn" (click)="backToProject()" class="backStyle"> {{'PROJECT_DETAIL.PROJECTS' | translate}}</a>
<a *ngIf="!hasSignedIn" [routerLink]="['/harbor', 'sign-in']"> {{'SEARCH.BACK' | 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" <h1 class="custom-h2" sub-header-title>
*ngIf="isMember">{{roleName | translate}}</span></h1> <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()"> <clr-tabs id="project-tabs" class="tabs" [class.in-overflow]="isTabLinkInOverFlow()">
<ng-container *ngFor="let tab of tabLinkNavList;let i=index"> <ng-container *ngFor="let tab of tabLinkNavList;let i=index">
<ng-container *ngIf="tab.permissions()"> <ng-container *ngIf="tab.permissions()">

View File

@ -42,3 +42,15 @@ button {
padding: 0; 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;
}

View File

@ -118,6 +118,7 @@ export class ProjectDetailComponent implements OnInit, AfterViewInit, OnDestroy
previousWindowWidth: number; previousWindowWidth: number;
private _subject = new Subject<string>(); private _subject = new Subject<string>();
private _subscription: Subscription; private _subscription: Subscription;
isProxyCacheProject: boolean = false;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
@ -129,6 +130,9 @@ export class ProjectDetailComponent implements OnInit, AfterViewInit, OnDestroy
this.hasSignedIn = this.sessionService.getCurrentUser() !== null; this.hasSignedIn = this.sessionService.getCurrentUser() !== null;
this.route.data.subscribe(data => { this.route.data.subscribe(data => {
this.currentProject = <Project>data['projectResolver']; this.currentProject = <Project>data['projectResolver'];
if (this.currentProject.registry_id) {
this.isProxyCacheProject = true;
}
this.isMember = this.currentProject.is_member; this.isMember = this.currentProject.is_member;
this.roleName = this.currentProject.role_name; this.roleName = this.currentProject.role_name;
}); });

View File

@ -27,6 +27,7 @@ export class Project {
has_project_admin_role: boolean; has_project_admin_role: boolean;
is_member: boolean; is_member: boolean;
role_name: string; role_name: string;
registry_id: number;
metadata: { metadata: {
public: string | boolean; public: string | boolean;
enable_content_trust: string | boolean; enable_content_trust: string | boolean;

View File

@ -1,18 +1,24 @@
<div class="summary summary-dark display-flex" *ngIf="summaryInformation"> <div class="summary summary-dark display-flex" *ngIf="summaryInformation">
<div class="summary-left"> <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> <h5 class="mt-0 width-7-5">{{'SUMMARY.PROJECT_REPOSITORY' | translate}}</h5>
<ul class="list-unstyled"> <ul class="list-unstyled">
<li>{{summaryInformation?.repo_count}}</li> <li>{{summaryInformation?.repo_count}}</li>
</ul> </ul>
</div> </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> <h5 class="mt-0 width-7-5">{{'SUMMARY.PROJECT_HELM_CHART' | translate}}</h5>
<ul class="list-unstyled"> <ul class="list-unstyled">
<li>{{summaryInformation?.chart_count}}</li> <li>{{summaryInformation?.chart_count}}</li>
</ul> </ul>
</div> </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> <h5 class="mt-0 width-7-5">{{'SUMMARY.PROJECT_MEMBER' | translate}}</h5>
<ul class="list-unstyled"> <ul class="list-unstyled">
<li>{{ summaryInformation?.project_admin_count }} {{'SUMMARY.ADMIN' | translate}}</li> <li>{{ summaryInformation?.project_admin_count }} {{'SUMMARY.ADMIN' | translate}}</li>
@ -23,7 +29,7 @@
</ul> </ul>
</div> </div>
</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"> <div class="display-flex project-detail">
<h5 class="mt-0">{{'SUMMARY.PROJECT_QUOTAS' | translate}}</h5> <h5 class="mt-0">{{'SUMMARY.PROJECT_QUOTAS' | translate}}</h5>
<div class="ml-1"> <div class="ml-1">
@ -55,4 +61,4 @@
</div> </div>
</div> </div>
</div> </div>

View File

@ -57,3 +57,7 @@
} }
} }
} }
.pt-05 {
padding-top: 0.5rem;
}

View File

@ -6,14 +6,23 @@ import { ActivatedRoute } from '@angular/router';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { AppConfigService } from "../../services/app-config.service"; import { AppConfigService } from "../../services/app-config.service";
import { SummaryComponent } from './summary.component'; 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 { 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', () => { describe('SummaryComponent', () => {
let component: SummaryComponent; let component: SummaryComponent;
let fixture: ComponentFixture<SummaryComponent>; let fixture: ComponentFixture<SummaryComponent>;
let fakeAppConfigService = null; let fakeAppConfigService = {
getConfig() {
return {
with_chartmuseum: false
};
}
};
let fakeProjectService = { let fakeProjectService = {
getProjectSummary: function () { getProjectSummary: function () {
return of(); return of();
@ -25,6 +34,34 @@ describe('SummaryComponent', () => {
return of([true, true]); 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(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@ -42,14 +79,19 @@ describe('SummaryComponent', () => {
{ provide: ProjectService, useValue: fakeProjectService }, { provide: ProjectService, useValue: fakeProjectService },
{ provide: ErrorHandler, useValue: fakeErrorHandler }, { provide: ErrorHandler, useValue: fakeErrorHandler },
{ provide: UserPermissionService, useValue: fakeUserPermissionService }, { provide: UserPermissionService, useValue: fakeUserPermissionService },
{ provide: EndpointService, useValue: fakedEndpointService },
{ provide: SERVICE_CONFIG, useValue: config },
{ provide: SessionService, useValue: fakedSessionService},
{ {
provide: ActivatedRoute, useValue: { provide: ActivatedRoute, useValue: {
paramMap: of({ get: (key) => 'value' }), paramMap: of({ get: (key) => 'value' }),
snapshot: { snapshot: {
parent: { parent: {
params: { id: 1 } params: { id: 1 },
data: {
projectResolver: {registry_id: 3}
}
}, },
data: 1
} }
} }
}, },
@ -66,4 +108,13 @@ describe('SummaryComponent', () => {
it('should create', () => { it('should create', () => {
expect(component).toBeTruthy(); 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");
});
}); });

View File

@ -1,10 +1,18 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { AppConfigService } from "../../services/app-config.service"; import { AppConfigService } from "../../services/app-config.service";
import { QUOTA_DANGER_COEFFICIENT, QUOTA_WARNING_COEFFICIENT, QuotaUnits } from "../../../lib/entities/shared.const"; 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 { ErrorHandler } from "../../../lib/utils/error-handler";
import { clone, GetIntegerAndUnit, getSuitableUnit as getSuitableUnitFn } from "../../../lib/utils/utils"; import { clone, GetIntegerAndUnit, getSuitableUnit as getSuitableUnitFn } from "../../../lib/utils/utils";
import { SessionService } from '../../shared/session.service';
import { Project } from '../project';
@Component({ @Component({
selector: 'summary', selector: 'summary',
@ -19,20 +27,30 @@ export class SummaryComponent implements OnInit {
summaryInformation: any; summaryInformation: any;
quotaDangerCoefficient: number = QUOTA_DANGER_COEFFICIENT; quotaDangerCoefficient: number = QUOTA_DANGER_COEFFICIENT;
quotaWarningCoefficient: number = QUOTA_WARNING_COEFFICIENT; quotaWarningCoefficient: number = QUOTA_WARNING_COEFFICIENT;
endpoint: Endpoint;
constructor( constructor(
private projectService: ProjectService, private projectService: ProjectService,
private userPermissionService: UserPermissionService, private userPermissionService: UserPermissionService,
private errorHandler: ErrorHandler, private errorHandler: ErrorHandler,
private appConfigService: AppConfigService, private appConfigService: AppConfigService,
private route: ActivatedRoute private route: ActivatedRoute,
private session: SessionService,
private endpointService: EndpointService
) { } ) { }
ngOnInit() { ngOnInit() {
this.projectId = this.route.snapshot.parent.params['id']; 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 = [ const permissions = [
{ resource: USERSTATICPERMISSION.MEMBER.KEY, action: USERSTATICPERMISSION.MEMBER.VALUE.LIST }, {resource: USERSTATICPERMISSION.MEMBER.KEY, action: USERSTATICPERMISSION.MEMBER.VALUE.LIST},
{ resource: USERSTATICPERMISSION.QUOTA.KEY, action: USERSTATICPERMISSION.QUOTA.VALUE.READ }, {resource: USERSTATICPERMISSION.QUOTA.KEY, action: USERSTATICPERMISSION.QUOTA.VALUE.READ},
]; ];
this.userPermissionService.hasProjectPermissions(this.projectId, permissions).subscribe((results: Array<boolean>) => { 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) { getSuitableUnit(value) {
const QuotaUnitsCopy = clone(QuotaUnits); const QuotaUnitsCopy = clone(QuotaUnits);
return getSuitableUnitFn(value, QuotaUnitsCopy); return getSuitableUnitFn(value, QuotaUnitsCopy);
@ -60,4 +86,9 @@ export class SummaryComponent implements OnInit {
return this.appConfigService.getConfig().with_chartmuseum; return this.appConfigService.getConfig().with_chartmuseum;
} }
public get isSystemAdmin(): boolean {
const account = this.session.getCurrentUser();
return account && account.has_admin_role;
}
} }

View File

@ -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.", "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", "OF": "of",
"COUNT_QUOTA": "Count quota", "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.", "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", "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": { "PROJECT_DETAIL": {
"SUMMARY": "Summary", "SUMMARY": "Summary",

View File

@ -239,7 +239,12 @@
"STORAGE_QUOTA": "Storage quota", "STORAGE_QUOTA": "Storage quota",
"COUNT_QUOTA_TIP": "Please enter an integer between '1' & '100,000,000', '-1' for unlimited", "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", "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": { "PROJECT_DETAIL": {
"SUMMARY": "Summary", "SUMMARY": "Summary",

View File

@ -232,7 +232,12 @@
"STORAGE_QUOTA": "Storage quota", "STORAGE_QUOTA": "Storage quota",
"COUNT_QUOTA_TIP": "Please enter an integer between '1' & '100,000,000', '-1' for unlimited", "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", "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": { "PROJECT_DETAIL": {
"SUMMARY": "Summary", "SUMMARY": "Summary",

View File

@ -236,7 +236,12 @@
"STORAGE_QUOTA": "Storage quota", "STORAGE_QUOTA": "Storage quota",
"COUNT_QUOTA_TIP": "Please enter an integer between '1' & '100,000,000', '-1' for unlimited", "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", "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": { "PROJECT_DETAIL": {
"SUMMARY": "Summary", "SUMMARY": "Summary",

View File

@ -238,7 +238,12 @@
"STORAGE_QUOTA": "Depolama kotası", "STORAGE_QUOTA": "Depolama kotası",
"COUNT_QUOTA_TIP": "Sayı Kotasının üst sınırı tamsayı olmalıdır.\n", "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", "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": { "PROJECT_DETAIL": {
"SUMMARY": "Özet", "SUMMARY": "Özet",

View File

@ -237,7 +237,12 @@
"STORAGE_QUOTA": "存储容量", "STORAGE_QUOTA": "存储容量",
"COUNT_QUOTA_TIP": "请输入一个'1' ~ '100000000'之间的整数, '-1'表示不设置上限。", "COUNT_QUOTA_TIP": "请输入一个'1' ~ '100000000'之间的整数, '-1'表示不设置上限。",
"STORAGE_QUOTA_TIP": "存储配额的上限仅采用整数值上限为1024TB。输入“-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": { "PROJECT_DETAIL": {
"SUMMARY": "概要", "SUMMARY": "概要",

View File

@ -235,7 +235,12 @@
"STORAGE_QUOTA": "存儲容量", "STORAGE_QUOTA": "存儲容量",
"COUNT_QUOTA_TIP": "請輸入一個'1' ~ '100000000'之間的整數, '-1'表示不設置上限。", "COUNT_QUOTA_TIP": "請輸入一個'1' ~ '100000000'之間的整數, '-1'表示不設置上限。",
"STORAGE_QUOTA_TIP": "存儲配額的上限僅採用整數值,上限為1024TB。輸入“-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":{ "PROJECT_DETAIL":{
"SUMMARY": "概要", "SUMMARY": "概要",

View File

@ -69,7 +69,7 @@ export abstract class ProjectService {
page?: number, page?: number,
pageSize?: number pageSize?: number
): Observable<HttpResponse<Project[]>>; ): 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 deleteProject(projectId: number): Observable<any>;
abstract checkProjectExists(projectName: string): Observable<any>; abstract checkProjectExists(projectName: string): Observable<any>;
abstract checkProjectMember(projectId: number): Observable<any>; abstract checkProjectMember(projectId: number): Observable<any>;
@ -149,12 +149,13 @@ export class ProjectDefaultService extends ProjectService {
catchError(error => observableThrowError(error)), ); 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 return this.http
.post(`${ CURRENT_BASE_HREF }/projects`, .post(`${ CURRENT_BASE_HREF }/projects`,
JSON.stringify({'project_name': name, 'metadata': { JSON.stringify({
public: metadata.public ? 'true' : 'false', 'project_name': name, registry_id: +registryId, 'metadata': {
}, public: metadata.public ? 'true' : 'false'
},
storage_limit: storageLimit storage_limit: storageLimit
}) })
, HTTP_JSON_OPTIONS).pipe( , HTTP_JSON_OPTIONS).pipe(