Commit a13642c2 authored by AllForNothing's avatar AllForNothing
Browse files

Add proxy cache ui


Signed-off-by: default avatarAllForNothing <sshijun@vmware.com>
parent 468ba50a
......@@ -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">
......
......@@ -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();
});
});
......@@ -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;
registries: Endpoint[] = [];
supportedRegistryType: string[] = ['docker-hub', 'harbor'];
constructor(private projectService: ProjectService,
private translateService: TranslateService,
private messageHandlerService: MessageHandlerService) { }
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 {
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 '';
}
}
......@@ -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;
}
......@@ -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",
......
......@@ -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>
......
......@@ -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,
......
<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()">
......
......@@ -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;
}
......@@ -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;
});
......
......@@ -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;
......
<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>
......@@ -57,3 +57,7 @@
}
}
}
.pt-05 {
padding-top: 0.5rem;
}
......@@ -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");
});
});
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;
}