From b1b915354ee19cd1abc7dbabcfd95e6aa7ed8d9d Mon Sep 17 00:00:00 2001 From: Yogi_Wang Date: Wed, 10 Jul 2019 17:28:05 +0800 Subject: [PATCH] Add project quotas in configration page include quotas page and edit quota page add quota field when create project Signed-off-by: Yogi_Wang --- src/portal/lib/src/config/config.ts | 4 + src/portal/lib/src/config/index.ts | 6 +- .../edit-project-quotas.component.html | 86 +++++++ .../edit-project-quotas.component.scss | 61 +++++ .../edit-project-quotas.component.spec.ts | 37 +++ .../edit-project-quotas.component.ts | 143 +++++++++++ .../project-quotas.component.html | 75 ++++++ .../project-quotas.component.scss | 42 ++++ .../project-quotas.component.spec.ts | 93 +++++++ .../project-quotas.component.ts | 238 ++++++++++++++++++ src/portal/lib/src/harbor-library.module.ts | 7 + src/portal/lib/src/service.config.ts | 2 + src/portal/lib/src/service/index.ts | 1 + src/portal/lib/src/service/interface.ts | 81 ++++-- .../lib/src/service/permission-static.ts | 6 +- src/portal/lib/src/service/project.service.ts | 14 +- src/portal/lib/src/service/quota.service.ts | 92 +++++++ src/portal/lib/src/shared/shared.const.ts | 25 ++ src/portal/lib/src/utils.ts | 113 ++++++++- src/portal/package.json | 2 +- .../src/app/config/config.component.html | 6 + src/portal/src/app/harbor-routing.module.ts | 5 + .../create-project.component.html | 46 ++++ .../create-project.component.ts | 64 ++++- .../create-project/create-project.scss | 18 +- .../list-project/list-project.component.ts | 2 +- .../project-detail.component.html | 3 + .../project-detail.component.ts | 5 +- .../src/app/project/project.component.html | 2 +- .../src/app/project/project.component.ts | 28 ++- src/portal/src/app/project/project.module.ts | 4 +- .../project/summary/summary.component.html | 72 ++++++ .../project/summary/summary.component.scss | 39 +++ .../project/summary/summary.component.spec.ts | 25 ++ .../app/project/summary/summary.component.ts | 41 +++ .../src/app/project/summary/summary.module.ts | 13 + src/portal/src/app/shared/shared.module.ts | 3 +- src/portal/src/i18n/lang/en-us-lang.json | 45 +++- src/portal/src/i18n/lang/es-es-lang.json | 45 +++- src/portal/src/i18n/lang/fr-fr-lang.json | 45 +++- src/portal/src/i18n/lang/pt-br-lang.json | 46 +++- src/portal/src/i18n/lang/zh-cn-lang.json | 47 +++- 42 files changed, 1670 insertions(+), 62 deletions(-) create mode 100644 src/portal/lib/src/config/project-quotas/edit-project-quotas/edit-project-quotas.component.html create mode 100644 src/portal/lib/src/config/project-quotas/edit-project-quotas/edit-project-quotas.component.scss create mode 100644 src/portal/lib/src/config/project-quotas/edit-project-quotas/edit-project-quotas.component.spec.ts create mode 100644 src/portal/lib/src/config/project-quotas/edit-project-quotas/edit-project-quotas.component.ts create mode 100644 src/portal/lib/src/config/project-quotas/project-quotas.component.html create mode 100644 src/portal/lib/src/config/project-quotas/project-quotas.component.scss create mode 100644 src/portal/lib/src/config/project-quotas/project-quotas.component.spec.ts create mode 100644 src/portal/lib/src/config/project-quotas/project-quotas.component.ts create mode 100644 src/portal/lib/src/service/quota.service.ts create mode 100644 src/portal/src/app/project/summary/summary.component.html create mode 100644 src/portal/src/app/project/summary/summary.component.scss create mode 100644 src/portal/src/app/project/summary/summary.component.spec.ts create mode 100644 src/portal/src/app/project/summary/summary.component.ts create mode 100644 src/portal/src/app/project/summary/summary.module.ts diff --git a/src/portal/lib/src/config/config.ts b/src/portal/lib/src/config/config.ts index e24aa30dc..23078e106 100644 --- a/src/portal/lib/src/config/config.ts +++ b/src/portal/lib/src/config/config.ts @@ -97,6 +97,8 @@ export class Configuration { oidc_client_secret?: StringValueItem; oidc_verify_cert?: BoolValueItem; oidc_scope?: StringValueItem; + count_per_project: NumberValueItem; + storage_per_project: NumberValueItem; public constructor() { this.auth_mode = new StringValueItem("db_auth", true); this.project_creation_restriction = new StringValueItem("everyone", true); @@ -148,5 +150,7 @@ export class Configuration { this.oidc_client_secret = new StringValueItem('', true); this.oidc_verify_cert = new BoolValueItem(false, true); this.oidc_scope = new StringValueItem('', true); + this.count_per_project = new NumberValueItem(-1, true); + this.storage_per_project = new NumberValueItem(-1, true); } } diff --git a/src/portal/lib/src/config/index.ts b/src/portal/lib/src/config/index.ts index 5ecae2c6e..a43adbce6 100644 --- a/src/portal/lib/src/config/index.ts +++ b/src/portal/lib/src/config/index.ts @@ -6,6 +6,8 @@ import { VulnerabilityConfigComponent } from './vulnerability/vulnerability-conf import { RegistryConfigComponent } from './registry-config.component'; import { GcComponent } from './gc/gc.component'; import { GcHistoryComponent } from './gc/gc-history/gc-history.component'; +import { ProjectQuotasComponent } from './project-quotas/project-quotas.component'; +import { EditProjectQuotasComponent } from './project-quotas/edit-project-quotas/edit-project-quotas.component'; export * from './config'; export * from './replication/replication-config.component'; @@ -20,5 +22,7 @@ export const CONFIGURATION_DIRECTIVES: Type[] = [ GcComponent, SystemSettingsComponent, VulnerabilityConfigComponent, - RegistryConfigComponent + RegistryConfigComponent, + ProjectQuotasComponent, + EditProjectQuotasComponent ]; diff --git a/src/portal/lib/src/config/project-quotas/edit-project-quotas/edit-project-quotas.component.html b/src/portal/lib/src/config/project-quotas/edit-project-quotas/edit-project-quotas.component.html new file mode 100644 index 000000000..98e57589a --- /dev/null +++ b/src/portal/lib/src/config/project-quotas/edit-project-quotas/edit-project-quotas.component.html @@ -0,0 +1,86 @@ + + + + + + \ No newline at end of file diff --git a/src/portal/lib/src/config/project-quotas/edit-project-quotas/edit-project-quotas.component.scss b/src/portal/lib/src/config/project-quotas/edit-project-quotas/edit-project-quotas.component.scss new file mode 100644 index 000000000..024a058d6 --- /dev/null +++ b/src/portal/lib/src/config/project-quotas/edit-project-quotas/edit-project-quotas.component.scss @@ -0,0 +1,61 @@ + +::ng-deep .modal-dialog { + width: 25rem; +} +.modal-body { + padding-top: 0.8rem; + overflow-y: visible; + overflow-x: visible; + .clr-form-compact { + div.form-group { + padding-left: 8.5rem; + .mr-3px { + margin-right: 3px; + } + .quota-input { + width: 2rem; + padding-right: 0.8rem; + } + .select-div { + width: 2.5rem; + + ::ng-deep .clr-form-control { + margin-top: 0.28rem; + + select { + padding-right: 15px; + } + } + } + } + } + + .clr-form-compact-common { + div.form-group { + padding-left: 6rem; + + .select-div { + width: 1.6rem; + } + } + } +} + +.progress-block { + width: 8rem; +} + +.progress-div { + position: relative; + padding-right: 0.6rem; + width: 9rem; +} + +.progress-label { + position: absolute; + right: -2.3rem; + top: 0; + width: 3.5rem; + font-weight: 100; + font-size: 10px; +} \ No newline at end of file diff --git a/src/portal/lib/src/config/project-quotas/edit-project-quotas/edit-project-quotas.component.spec.ts b/src/portal/lib/src/config/project-quotas/edit-project-quotas/edit-project-quotas.component.spec.ts new file mode 100644 index 000000000..595f1ab1b --- /dev/null +++ b/src/portal/lib/src/config/project-quotas/edit-project-quotas/edit-project-quotas.component.spec.ts @@ -0,0 +1,37 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EditProjectQuotasComponent } from './edit-project-quotas.component'; +import { SharedModule } from '../../../shared/shared.module'; +import { InlineAlertComponent } from '../../../inline-alert/inline-alert.component'; +import { SERVICE_CONFIG, IServiceConfig } from '../../../service.config'; +import { RouterModule } from '@angular/router'; + +describe('EditProjectQuotasComponent', () => { + let component: EditProjectQuotasComponent; + let fixture: ComponentFixture; + let config: IServiceConfig = { + quotaUrl: "/api/quotas/testing" + }; + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + SharedModule, + RouterModule.forRoot([]) + ], + declarations: [ EditProjectQuotasComponent, InlineAlertComponent ], + providers: [ + { provide: SERVICE_CONFIG, useValue: config }, + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(EditProjectQuotasComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/portal/lib/src/config/project-quotas/edit-project-quotas/edit-project-quotas.component.ts b/src/portal/lib/src/config/project-quotas/edit-project-quotas/edit-project-quotas.component.ts new file mode 100644 index 000000000..a0e435c91 --- /dev/null +++ b/src/portal/lib/src/config/project-quotas/edit-project-quotas/edit-project-quotas.component.ts @@ -0,0 +1,143 @@ +import { + Component, + EventEmitter, + Output, + ViewChild, + OnInit, +} from '@angular/core'; +import { NgForm, Validators } from '@angular/forms'; +import { ActivatedRoute } from "@angular/router"; + +import { TranslateService } from '@ngx-translate/core'; + +import { InlineAlertComponent } from '../../../inline-alert/inline-alert.component'; + +import { QuotaUnits, QuotaUnlimited } from "../../../shared/shared.const"; + +import { clone, getSuitableUnit, getByte, GetIntegerAndUnit, validateLimit } from '../../../utils'; +import { EditQuotaQuotaInterface, QuotaHardLimitInterface } from '../../../service'; +import { distinctUntilChanged } from 'rxjs/operators'; + +@Component({ + selector: 'edit-project-quotas', + templateUrl: './edit-project-quotas.component.html', + styleUrls: ['./edit-project-quotas.component.scss'] +}) +export class EditProjectQuotasComponent implements OnInit { + openEditQuota: boolean; + defaultTextsObj: { editQuota: string; setQuota: string; countQuota: string; storageQuota: string; isSystemDefaultQuota: boolean } = { + editQuota: '', + setQuota: '', + countQuota: '', + storageQuota: '', + isSystemDefaultQuota: false, + }; + quotaHardLimitValue: QuotaHardLimitInterface = { + storageLimit: -1 + , storageUnit: '' + , countLimit: -1 + }; + quotaUnits = QuotaUnits; + staticBackdrop = true; + closable = false; + quotaForm: NgForm; + @ViewChild(InlineAlertComponent) + inlineAlert: InlineAlertComponent; + + @ViewChild('quotaForm') + currentForm: NgForm; + @Output() confirmAction = new EventEmitter(); + constructor( + private translateService: TranslateService, + private route: ActivatedRoute) { } + + ngOnInit() { + } + + onSubmit(): void { + const emitData = { + formValue: this.currentForm.value, + isSystemDefaultQuota: this.defaultTextsObj.isSystemDefaultQuota, + id: this.quotaHardLimitValue.id + }; + this.confirmAction.emit(emitData); + } + onCancel() { + this.openEditQuota = false; + } + + openEditQuotaModal(defaultTextsObj: EditQuotaQuotaInterface): void { + this.defaultTextsObj = defaultTextsObj; + if (this.defaultTextsObj.isSystemDefaultQuota) { + this.quotaHardLimitValue = { + storageLimit: defaultTextsObj.quotaHardLimitValue.storageLimit === QuotaUnlimited ? + QuotaUnlimited : GetIntegerAndUnit(defaultTextsObj.quotaHardLimitValue.storageLimit + , clone(QuotaUnits), 0, clone(QuotaUnits)).partNumberHard + , storageUnit: defaultTextsObj.quotaHardLimitValue.storageLimit === QuotaUnlimited ? + QuotaUnits[3].UNIT : GetIntegerAndUnit(defaultTextsObj.quotaHardLimitValue.storageLimit + , clone(QuotaUnits), 0, clone(QuotaUnits)).partCharacterHard + , countLimit: defaultTextsObj.quotaHardLimitValue.countLimit + }; + } else { + this.quotaHardLimitValue = { + storageLimit: defaultTextsObj.quotaHardLimitValue.hard.storage === QuotaUnlimited ? + QuotaUnlimited : GetIntegerAndUnit(defaultTextsObj.quotaHardLimitValue.hard.storage + , clone(QuotaUnits), defaultTextsObj.quotaHardLimitValue.used.storage, clone(QuotaUnits)).partNumberHard + , storageUnit: defaultTextsObj.quotaHardLimitValue.hard.storage === QuotaUnlimited ? + QuotaUnits[3].UNIT : GetIntegerAndUnit(defaultTextsObj.quotaHardLimitValue.hard.storage + , clone(QuotaUnits), defaultTextsObj.quotaHardLimitValue.used.storage, clone(QuotaUnits)).partCharacterHard + , countLimit: defaultTextsObj.quotaHardLimitValue.hard.count + , id: defaultTextsObj.quotaHardLimitValue.id + , countUsed: defaultTextsObj.quotaHardLimitValue.used.count + , storageUsed: defaultTextsObj.quotaHardLimitValue.used.storage + }; + } + let defaultForm = { + count: this.quotaHardLimitValue.countLimit + , storage: this.quotaHardLimitValue.storageLimit + , storageUnit: this.quotaHardLimitValue.storageUnit + }; + this.currentForm.resetForm(defaultForm); + this.openEditQuota = true; + + this.currentForm.form.controls['storage'].setValidators( + [ + Validators.required, + Validators.pattern('(^-1$)|(^([1-9]+)([0-9]+)*$)'), + validateLimit(this.currentForm.form.controls['storageUnit']) + ]); + this.currentForm.form.valueChanges + .pipe(distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b))) + .subscribe((data) => { + ['storage', 'storageUnit'].forEach(fieldName => { + if (this.currentForm.form.get(fieldName) && this.currentForm.form.get(fieldName).value !== null) { + this.currentForm.form.get(fieldName).updateValueAndValidity(); + } + }); + }); + } + + get isValid() { + return this.currentForm.valid && this.currentForm.dirty; + } + getSuitableUnit(value) { + const QuotaUnitsCopy = clone(QuotaUnits); + return getSuitableUnit(value, QuotaUnitsCopy); + } + getIntegerAndUnit(valueHard, valueUsed) { + return GetIntegerAndUnit(valueHard + , clone(QuotaUnits), valueUsed, clone(QuotaUnits)); + } + getByte(count: number, unit: string) { + if (+count === +count) { + return getByte(+count, unit); + } + return 0; + } + getDangerStyle(limit: number | string, used: number | string, unit?: string) { + if (unit) { + return limit !== QuotaUnlimited ? +used / getByte(+limit, unit) > 0.9 : false; + } + return limit !== QuotaUnlimited ? +used / +limit > 0.9 : false; + } +} diff --git a/src/portal/lib/src/config/project-quotas/project-quotas.component.html b/src/portal/lib/src/config/project-quotas/project-quotas.component.html new file mode 100644 index 000000000..6f9e9cad2 --- /dev/null +++ b/src/portal/lib/src/config/project-quotas/project-quotas.component.html @@ -0,0 +1,75 @@ +
+
+
+
+
+
{{'QUOTA.PROJECT_QUOTA_DEFAULT_ARTIFACT' | translate}}{{ quotaHardLimitValue?.countLimit === -1? ('QUOTA.UNLIMITED'| translate): quotaHardLimitValue?.countLimit }} +
+
{{'QUOTA.PROJECT_QUOTA_DEFAULT_DISK' | translate}} + {{ quotaHardLimitValue?.storageLimit === -1?('QUOTA.UNLIMITED' | translate): getIntegerAndUnit(quotaHardLimitValue?.storageLimit, 0).partNumberHard}} + {{ quotaHardLimitValue?.storageLimit === -1?'':quotaHardLimitValue?.storageUnit }} +
+
+ +
+
+ + + +
+
+
+ + {{'QUOTA.PROJECT' | translate}} + {{'QUOTA.OWNER' | translate}} + {{'QUOTA.COUNT' | translate }} + {{'QUOTA.STORAGE' | translate }} + {{'QUOTA.PLACEHOLDER' | translate }} + + + + + + {{quota?.ref?.name}} + {{quota?.ref?.owner_name}} + +
+
+ +
+ +
+
+ +
+
+ +
+ +
+
+
+ + {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} + {{'DESTINATION.OF' | translate}} + {{totalCount}} {{'SUMMARY.QUOTAS' | translate}} + + +
+
+
+ +
\ No newline at end of file diff --git a/src/portal/lib/src/config/project-quotas/project-quotas.component.scss b/src/portal/lib/src/config/project-quotas/project-quotas.component.scss new file mode 100644 index 000000000..eeb09db91 --- /dev/null +++ b/src/portal/lib/src/config/project-quotas/project-quotas.component.scss @@ -0,0 +1,42 @@ +.default-quota { + display: flex; + + .default-quota-text { + display: flex; + justify-content: space-between; + min-width: 13rem; + + .num-count { + display: inline-block; + min-width: 2rem; + } + } +} + +.color-0 { + color: #000; +} + +.progress-block { + label { + font-weight: 400 !important; + } +} + +.default-quota-edit-button { + height: 1rem; +} + +.min-label-width { + min-width: 120px; +} + +.quota-top { + display: flex; + justify-content: space-between; +} + +.refresh-div { + margin-top: auto; + cursor: pointer; +} \ No newline at end of file diff --git a/src/portal/lib/src/config/project-quotas/project-quotas.component.spec.ts b/src/portal/lib/src/config/project-quotas/project-quotas.component.spec.ts new file mode 100644 index 000000000..168685550 --- /dev/null +++ b/src/portal/lib/src/config/project-quotas/project-quotas.component.spec.ts @@ -0,0 +1,93 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProjectQuotasComponent } from './project-quotas.component'; +import { IServiceConfig, SERVICE_CONFIG } from '../../service.config'; +import { SharedModule } from '../../shared/shared.module'; +import { RouterModule } from '@angular/router'; +import { EditProjectQuotasComponent } from './edit-project-quotas/edit-project-quotas.component'; +import { InlineAlertComponent } from '../../inline-alert/inline-alert.component'; +import { + ConfigurationService, ConfigurationDefaultService, QuotaService + , QuotaDefaultService, Quota, RequestQueryParams +} from '../../service'; +import { ErrorHandler } from '../../error-handler'; +import { of } from 'rxjs'; +import { delay } from 'rxjs/operators'; +import {APP_BASE_HREF} from '@angular/common'; +describe('ProjectQuotasComponent', () => { + let spy: jasmine.Spy; + let quotaService: QuotaService; + + let component: ProjectQuotasComponent; + let fixture: ComponentFixture; + + let config: IServiceConfig = { + quotaUrl: "/api/quotas/testing" + }; + let mockQuotaList: Quota[] = [{ + id: 1111, + ref: { + id: 1111, + name: "project1", + owner_name: "project1" + }, + creation_time: "12212112121", + update_time: "12212112121", + hard: { + count: -1, + storage: -1, + }, + used: { + count: 1234, + storage: 1234 + }, + } + ]; + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + SharedModule, + RouterModule.forRoot([]) + ], + declarations: [ProjectQuotasComponent, EditProjectQuotasComponent, InlineAlertComponent], + providers: [ + ErrorHandler, + { provide: SERVICE_CONFIG, useValue: config }, + { provide: ConfigurationService, useClass: ConfigurationDefaultService }, + { provide: QuotaService, useClass: QuotaDefaultService }, + { provide: APP_BASE_HREF, useValue : '/' } + + ] + }) + .compileComponents(); + })); + + beforeEach(async(() => { + + fixture = TestBed.createComponent(ProjectQuotasComponent); + component = fixture.componentInstance; + component.quotaHardLimitValue = { + countLimit: 1111, + storageLimit: 23, + storageUnit: 'GB' + }; + component.loading = true; + quotaService = fixture.debugElement.injector.get(QuotaService); + spy = spyOn(quotaService, 'getQuotaList') + .and.callFake(function (params: RequestQueryParams) { + let header = new Map(); + header.set("X-Total-Count", 123); + const httpRes = { + headers: header, + body: mockQuotaList + }; + return of(httpRes).pipe(delay(0)); + }); + + fixture.detectChanges(); + })); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/portal/lib/src/config/project-quotas/project-quotas.component.ts b/src/portal/lib/src/config/project-quotas/project-quotas.component.ts new file mode 100644 index 000000000..ffaff1336 --- /dev/null +++ b/src/portal/lib/src/config/project-quotas/project-quotas.component.ts @@ -0,0 +1,238 @@ +import { Component, Input, Output, EventEmitter, ViewChild, SimpleChanges, OnChanges } from '@angular/core'; +import { Configuration } from '../config'; +import { + Quota, State, Comparator, ClrDatagridComparatorInterface, QuotaHardLimitInterface, QuotaHard +} from '../../service/interface'; +import { + clone, isEmpty, getChanges, getSuitableUnit, calculatePage, CustomComparator + , getByte, GetIntegerAndUnit +} from '../../utils'; +import { ErrorHandler } from '../../error-handler/index'; +import { QuotaUnits, QuotaUnlimited } from '../../shared/shared.const'; +import { EditProjectQuotasComponent } from './edit-project-quotas/edit-project-quotas.component'; +import { + ConfigurationService +} from '../../service/index'; +import { TranslateService } from '@ngx-translate/core'; +import { forkJoin } from 'rxjs'; +import { QuotaService } from "../../service/quota.service"; +import { Router } from '@angular/router'; +import { finalize } from 'rxjs/operators'; +const quotaSort = { + count: 'used.count', + storage: "used.storage", + sortType: 'string' +}; +const QuotaType = 'project'; + +@Component({ + selector: 'project-quotas', + templateUrl: './project-quotas.component.html', + styleUrls: ['./project-quotas.component.scss'] +}) +export class ProjectQuotasComponent implements OnChanges { + + config: Configuration = new Configuration(); + @ViewChild('editProjectQuotas') + editQuotaDialog: EditProjectQuotasComponent; + loading = true; + quotaHardLimitValue: QuotaHardLimitInterface; + currentState: State; + + @Output() configChange: EventEmitter = new EventEmitter(); + @Output() refreshAllconfig: EventEmitter = new EventEmitter(); + quotaList: Quota[] = []; + originalConfig: Configuration; + currentPage = 1; + totalCount = 0; + pageSize = 15; + @Input() + get allConfig(): Configuration { + return this.config; + } + set allConfig(cfg: Configuration) { + this.config = cfg; + this.configChange.emit(this.config); + } + countComparator: Comparator = new CustomComparator(quotaSort.count, quotaSort.sortType); + storageComparator: Comparator = new CustomComparator(quotaSort.storage, quotaSort.sortType); + + constructor( + private configService: ConfigurationService, + private quotaService: QuotaService, + private translate: TranslateService, + private router: Router, + private errorHandler: ErrorHandler) { } + + editQuota(quotaHardLimitValue: QuotaHardLimitInterface) { + const defaultTexts = [this.translate.get('QUOTA.EDIT_PROJECT_QUOTAS'), this.translate.get('QUOTA.SET_QUOTAS') + , this.translate.get('QUOTA.COUNT_QUOTA'), this.translate.get('QUOTA.STORAGE_QUOTA')]; + forkJoin(...defaultTexts).subscribe(res => { + const defaultTextsObj = { + editQuota: res[0], + setQuota: res[1], + countQuota: res[2], + storageQuota: res[3], + quotaHardLimitValue: quotaHardLimitValue, + isSystemDefaultQuota: false + }; + this.editQuotaDialog.openEditQuotaModal(defaultTextsObj); + }); + } + + editDefaultQuota(quotaHardLimitValue: QuotaHardLimitInterface) { + const defaultTexts = [this.translate.get('QUOTA.EDIT_DEFAULT_PROJECT_QUOTAS'), this.translate.get('QUOTA.SET_DEFAULT_QUOTAS') + , this.translate.get('QUOTA.COUNT_DEFAULT_QUOTA'), this.translate.get('QUOTA.STORAGE_DEFAULT_QUOTA')]; + forkJoin(...defaultTexts).subscribe(res => { + const defaultTextsObj = { + editQuota: res[0], + setQuota: res[1], + countQuota: res[2], + storageQuota: res[3], + quotaHardLimitValue: quotaHardLimitValue, + isSystemDefaultQuota: true + }; + this.editQuotaDialog.openEditQuotaModal(defaultTextsObj); + + }); + } + public getChanges() { + let allChanges = getChanges(this.originalConfig, this.config); + if (allChanges) { + return this.getQuotaChanges(allChanges); + } + return null; + } + + getQuotaChanges(allChanges) { + let changes = {}; + for (let prop in allChanges) { + if (prop === 'storage_per_project' + || prop === 'count_per_project' + ) { + changes[prop] = allChanges[prop]; + } + } + return changes; + } + + public saveConfig(configQuota): void { + this.allConfig.count_per_project.value = configQuota.count; + this.allConfig.storage_per_project.value = +configQuota.storage === QuotaUnlimited ? + configQuota.storage : getByte(configQuota.storage, configQuota.storageUnit); + let changes = this.getChanges(); + if (!isEmpty(changes)) { + this.loading = true; + this.configService.saveConfigurations(changes) + .pipe(finalize(() => { + this.loading = false; + this.editQuotaDialog.openEditQuota = false; + })) + .subscribe(response => { + this.refreshAllconfig.emit(); + this.errorHandler.info('CONFIG.SAVE_SUCCESS'); + } + , error => { + this.errorHandler.error(error); + }); + } else { + // Inprop situation, should not come here + this.translate.get('CONFIG.NO_CHANGE').subscribe(res => { + this.editQuotaDialog.inlineAlert.showInlineError(res); + }); + } + } + + confirmEdit(event) { + if (event.isSystemDefaultQuota) { + this.saveConfig(event.formValue); + } else { + this.saveCurrentQuota(event); + } + } + saveCurrentQuota(event) { + let count = +event.formValue.count; + let storage = +event.formValue.storage === QuotaUnlimited ? + +event.formValue.storage : getByte(+event.formValue.storage, event.formValue.storageUnit); + let rep: QuotaHard = { hard: { count, storage } }; + this.loading = true; + this.quotaService.updateQuota(event.id, rep).subscribe(res => { + this.editQuotaDialog.openEditQuota = false; + this.getQuotaList(this.currentState); + this.errorHandler.info('QUOTA.SAVE_SUCCESS'); + }, error => { + this.errorHandler.error(error); + this.loading = false; + }); + } + + getquotaHardLimitValue() { + const storageNumberAndUnit = this.allConfig.storage_per_project ? this.allConfig.storage_per_project.value : QuotaUnlimited; + const storageLimit = storageNumberAndUnit; + const storageUnit = this.getIntegerAndUnit(storageNumberAndUnit, 0).partCharacterHard; + const countLimit = this.allConfig.count_per_project ? this.allConfig.count_per_project.value : QuotaUnlimited; + this.quotaHardLimitValue = { storageLimit, storageUnit, countLimit }; + } + getQuotaList(state: State) { + if (!state || !state.page) { + return; + } + // Keep state for future filtering and sorting + this.currentState = state; + + let pageNumber: number = calculatePage(state); + if (pageNumber <= 0) { pageNumber = 1; } + let sortBy: any = ''; + if (state.sort) { + sortBy = state.sort.by as string | ClrDatagridComparatorInterface; + sortBy = sortBy.fieldName ? sortBy.fieldName : sortBy; + sortBy = state.sort.reverse ? `-${sortBy}` : sortBy; + } + this.loading = true; + + this.quotaService.getQuotaList(QuotaType, pageNumber, this.pageSize, sortBy).pipe(finalize(() => { + this.loading = false; + })).subscribe(res => { + if (res.headers) { + let xHeader: string = res.headers.get("X-Total-Count"); + if (xHeader) { + this.totalCount = parseInt(xHeader, 0); + } + } + this.quotaList = res.body.filter((quota) => { + return quota.ref !== null; + }) as Quota[]; + }, error => { + this.errorHandler.error(error); + }); + } + ngOnChanges(changes: SimpleChanges): void { + if (changes && changes["allConfig"]) { + this.originalConfig = clone(this.config); + this.getquotaHardLimitValue(); + } + } + getSuitableUnit(value) { + const QuotaUnitsCopy = clone(QuotaUnits); + return getSuitableUnit(value, QuotaUnitsCopy); + } + getIntegerAndUnit(valueHard, valueUsed) { + return GetIntegerAndUnit(valueHard + , clone(QuotaUnits), valueUsed, clone(QuotaUnits)); + } + + goToLink(proId) { + let linkUrl = ["harbor", "projects", proId, "summary"]; + this.router.navigate(linkUrl); + } + refresh() { + const state: State = { + page: { + from: 0, + to: 14, + size: 15 + }, + }; + this.getQuotaList(state); + } +} diff --git a/src/portal/lib/src/harbor-library.module.ts b/src/portal/lib/src/harbor-library.module.ts index 4c764b439..51babecf6 100644 --- a/src/portal/lib/src/harbor-library.module.ts +++ b/src/portal/lib/src/harbor-library.module.ts @@ -38,6 +38,8 @@ import { EndpointDefaultService, ReplicationService, ReplicationDefaultService, + QuotaService, + QuotaDefaultService, RepositoryService, RepositoryDefaultService, TagService, @@ -131,6 +133,9 @@ export interface HarborModuleConfig { // Service implementation for replication replicationService?: Provider; + // Service implementation for replication + QuotaService?: Provider; + // Service implementation for repository repositoryService?: Provider; @@ -257,6 +262,7 @@ export class HarborLibraryModule { config.logService || { provide: AccessLogService, useClass: AccessLogDefaultService }, config.endpointService || { provide: EndpointService, useClass: EndpointDefaultService }, config.replicationService || { provide: ReplicationService, useClass: ReplicationDefaultService }, + config.QuotaService || { provide: QuotaService, useClass: QuotaDefaultService }, config.repositoryService || { provide: RepositoryService, useClass: RepositoryDefaultService }, config.tagService || { provide: TagService, useClass: TagDefaultService }, config.retagService || { provide: RetagService, useClass: RetagDefaultService }, @@ -295,6 +301,7 @@ export class HarborLibraryModule { config.logService || { provide: AccessLogService, useClass: AccessLogDefaultService }, config.endpointService || { provide: EndpointService, useClass: EndpointDefaultService }, config.replicationService || { provide: ReplicationService, useClass: ReplicationDefaultService }, + config.QuotaService || { provide: QuotaService, useClass: QuotaDefaultService }, config.repositoryService || { provide: RepositoryService, useClass: RepositoryDefaultService }, config.tagService || { provide: TagService, useClass: TagDefaultService }, config.retagService || { provide: RetagService, useClass: RetagDefaultService }, diff --git a/src/portal/lib/src/service.config.ts b/src/portal/lib/src/service.config.ts index b5bca0cd4..545e60e0a 100644 --- a/src/portal/lib/src/service.config.ts +++ b/src/portal/lib/src/service.config.ts @@ -232,4 +232,6 @@ export interface IServiceConfig { gcEndpoint?: string; ScanAllEndpoint?: string; + + quotaUrl?: string; } diff --git a/src/portal/lib/src/service/index.ts b/src/portal/lib/src/service/index.ts index 2c22c3bd8..311501a80 100644 --- a/src/portal/lib/src/service/index.ts +++ b/src/portal/lib/src/service/index.ts @@ -14,3 +14,4 @@ export * from "./label.service"; export * from "./retag.service"; export * from "./permission.service"; export * from "./permission-static"; +export * from "./quota.service"; diff --git a/src/portal/lib/src/service/interface.ts b/src/portal/lib/src/service/interface.ts index f1410c056..5de814f2b 100644 --- a/src/portal/lib/src/service/interface.ts +++ b/src/portal/lib/src/service/interface.ts @@ -99,9 +99,9 @@ export interface PingEndpoint extends Base { } export interface Filter { - type: string; - style: string; - values ?: string[]; + type: string; + style: string; + values?: string[]; } /** @@ -122,7 +122,7 @@ export interface ReplicationRule extends Base { deletion?: boolean; src_registry?: any; dest_registry?: any; - src_namespaces: string []; + src_namespaces: string[]; dest_namespace?: string; enabled: boolean; override: boolean; @@ -333,6 +333,33 @@ export interface Label { scope: string; project_id: number; } + +export interface Quota { + id: number; + ref: { + name: string; + owner_name: string; + id: number; + } | null; + creation_time: string; + update_time: string; + hard: { + count: number; + storage: number; + }; + used: { + count: number; + storage: number; + }; +} +export interface QuotaHard { + hard: QuotaCountStorage; +} +export interface QuotaCountStorage { + count: number; + storage: number; +} + export interface CardItemEvent { event_type: string; item: any; @@ -408,26 +435,26 @@ export class OriginCron { cron: string; } -export interface HttpOptionInterface { +export interface HttpOptionInterface { headers?: HttpHeaders | { - [header: string]: string | string[]; + [header: string]: string | string[]; }; observe?: 'body'; params?: HttpParams | { - [param: string]: string | string[]; + [param: string]: string | string[]; }; reportProgress?: boolean; responseType: 'json'; withCredentials?: boolean; } -export interface HttpOptionTextInterface { +export interface HttpOptionTextInterface { headers?: HttpHeaders | { - [header: string]: string | string[]; + [header: string]: string | string[]; }; observe?: 'body'; params?: HttpParams | { - [param: string]: string | string[]; + [param: string]: string | string[]; }; reportProgress?: boolean; responseType: 'text'; @@ -435,14 +462,38 @@ export interface HttpOptionTextInterface { } -export interface ProjectRootInterface { +export interface ProjectRootInterface { NAME: string; VALUE: number; LABEL: string; } export interface SystemCVEWhitelist { - id: number; - project_id: number; - expires_at: number; - items: Array<{ "cve_id": string; }>; + id: number; + project_id: number; + expires_at: number; + items: Array<{ "cve_id": string; }>; +} +export interface QuotaHardInterface { + count_per_project: number; + storage_per_project: number; +} + +export interface QuotaUnitInterface { + UNIT: string; +} +export interface QuotaHardLimitInterface { + countLimit: number; + storageLimit: number; + storageUnit: string; + id?: string; + countUsed?: string; + storageUsed?: string; +} +export interface EditQuotaQuotaInterface { + editQuota: string; + setQuota: string; + countQuota: string; + storageQuota: string; + quotaHardLimitValue: QuotaHardLimitInterface | any; + isSystemDefaultQuota: boolean; } diff --git a/src/portal/lib/src/service/permission-static.ts b/src/portal/lib/src/service/permission-static.ts index da0aaf62c..b0ef1fa25 100644 --- a/src/portal/lib/src/service/permission-static.ts +++ b/src/portal/lib/src/service/permission-static.ts @@ -1,8 +1,10 @@ export const USERSTATICPERMISSION = { "PROJECT": { - 'KEY': 'project', + 'KEY': '.', 'VALUE': { - "DELETE": "delete" + "DELETE": "delete", + "UPDATE": "update", + "READ": "read", } }, "MEMBER": { diff --git a/src/portal/lib/src/service/project.service.ts b/src/portal/lib/src/service/project.service.ts index e6f6ef0a8..a0f34ad4c 100644 --- a/src/portal/lib/src/service/project.service.ts +++ b/src/portal/lib/src/service/project.service.ts @@ -69,11 +69,12 @@ export abstract class ProjectService { page?: number, pageSize?: number ): Observable>; - abstract createProject(name: string, metadata: any): Observable; + abstract createProject(name: string, metadata: any, countLimit: number, storageLimit: number): Observable; abstract toggleProjectPublic(projectId: number, isPublic: string): Observable; abstract deleteProject(projectId: number): Observable; abstract checkProjectExists(projectName: string): Observable; abstract checkProjectMember(projectId: number): Observable; + abstract getProjectSummary(projectId: number): Observable; } /** @@ -149,12 +150,14 @@ export class ProjectDefaultService extends ProjectService { catchError(error => observableThrowError(error)), ); } - public createProject(name: string, metadata: any): Observable { + public createProject(name: string, metadata: any, countLimit: number, storageLimit: number): Observable { return this.http .post(`/api/projects`, JSON.stringify({'project_name': name, 'metadata': { public: metadata.public ? 'true' : 'false', - }}) + }, + count_limit: countLimit, storage_limit: storageLimit + }) , HTTP_JSON_OPTIONS).pipe( catchError(error => observableThrowError(error)), ); } @@ -182,4 +185,9 @@ export class ProjectDefaultService extends ProjectService { .get(`/api/projects/${projectId}/members`, HTTP_GET_OPTIONS).pipe( catchError(error => observableThrowError(error)), ); } + public getProjectSummary(projectId: number): Observable { + return this.http + .get(`/api/projects/${projectId}/summary`, HTTP_GET_OPTIONS).pipe( + catchError(error => observableThrowError(error)), ); + } } diff --git a/src/portal/lib/src/service/quota.service.ts b/src/portal/lib/src/service/quota.service.ts new file mode 100644 index 000000000..a5f7a0853 --- /dev/null +++ b/src/portal/lib/src/service/quota.service.ts @@ -0,0 +1,92 @@ +import { HttpClient, HttpResponse, HttpParams } from "@angular/common/http"; +import { Injectable, Inject } from "@angular/core"; +import { + HTTP_JSON_OPTIONS, + buildHttpRequestOptionsWithObserveResponse, +} from "../utils"; +import { + QuotaHard +} from "./interface"; +import { map, catchError } from "rxjs/operators"; +import { Observable, throwError as observableThrowError } from "rxjs"; +import { Quota } from "./interface"; +import { SERVICE_CONFIG, IServiceConfig } from "../service.config"; +/** + * Define the service methods to handle the replication (rule and job) related things. + * + ** + * @abstract + * class QuotaService + */ +export abstract class QuotaService { + /** + * + * @abstract + * returns {(Observable)} + * + * @memberOf QuotaService + */ + abstract getQuotaList(quotaType, page?, pageSize?, sortBy?: any): + any; + + abstract updateQuota( + id: number, + rep: QuotaHard + ): Observable; +} + +/** + * Implement default service for replication rule and job. + * + ** + * class QuotaDefaultService + * extends {QuotaService} + */ +@Injectable() +export class QuotaDefaultService extends QuotaService { + quotaUrl: string; + constructor( + private http: HttpClient, + @Inject(SERVICE_CONFIG) private config: IServiceConfig + ) { + super(); + if (this.config && this.config.quotaUrl) { + this.quotaUrl = this.config.quotaUrl; + } + } + + public getQuotaList(quotaType: string, page?, pageSize?, sortBy?: any): + any { + + let params = new HttpParams(); + if (quotaType) { + params = params.set('reference', quotaType); + } + if (page && pageSize) { + params = params.set('page', page + '').set('page_size', pageSize + ''); + } + if (sortBy) { + params = params.set('sort', sortBy); + } + + return this.http + .get>(this.quotaUrl + , buildHttpRequestOptionsWithObserveResponse(params)) + .pipe(map(response => { + return response; + }) + , catchError(error => observableThrowError(error))); + } + + public updateQuota( + id: number, + quotaHardLimit: QuotaHard + ): Observable { + + let url = `${this.quotaUrl}/${id}`; + return this.http + .put(url, quotaHardLimit, HTTP_JSON_OPTIONS) + .pipe(catchError(error => observableThrowError(error))); + } + +} diff --git a/src/portal/lib/src/shared/shared.const.ts b/src/portal/lib/src/shared/shared.const.ts index a74ed9b16..4b5169abd 100644 --- a/src/portal/lib/src/shared/shared.const.ts +++ b/src/portal/lib/src/shared/shared.const.ts @@ -71,6 +71,31 @@ export const FilterType = { export const enum ConfirmationButtons { CONFIRM_CANCEL, YES_NO, DELETE_CANCEL, CLOSE, REPLICATE_CANCEL, STOP_CANCEL } +export const QuotaUnits = [ + { + UNIT: "Byte", + }, + { + UNIT: "KB", + }, + { + UNIT: "MB", + }, + { + UNIT: "GB", + }, + { + UNIT: "TB", + }, +]; +export const QuotaUnlimited = -1; +export const StorageMultipleConstant = 1024; +export enum QuotaUnit { + TB = "TB", GB = "GB", MB = "MB", KB = "KB", BIT = "Byte" +} +export enum QuotaProgress { + COUNT_USED = "COUNT_USED", COUNT_HARD = "COUNT_HARD", STROAGE_USED = "STORAGE_USED", STORAGE_HARD = "STORAGE_HARD" +} export const LabelColor = [ { 'color': '#000000', 'textColor': 'white' }, { 'color': '#61717D', 'textColor': 'white' }, diff --git a/src/portal/lib/src/utils.ts b/src/portal/lib/src/utils.ts index a4500b8b5..7c3ede678 100644 --- a/src/portal/lib/src/utils.ts +++ b/src/portal/lib/src/utils.ts @@ -1,10 +1,11 @@ import { Observable } from "rxjs"; -import { HttpHeaders, HttpParams } from '@angular/common/http'; +import { HttpHeaders } from '@angular/common/http'; import { RequestQueryParams } from './service/RequestQueryParams'; import { DebugElement } from '@angular/core'; -import { Comparator, State, HttpOptionInterface, HttpOptionTextInterface } from './service/interface'; - +import { Comparator, State, HttpOptionInterface, HttpOptionTextInterface, QuotaUnitInterface } from './service/interface'; +import { QuotaUnits, StorageMultipleConstant } from './shared/shared.const'; +import { AbstractControl } from "@angular/forms"; /** * Convert the different async channels to the Promise type. * @@ -270,8 +271,8 @@ export function doFiltering(items: T[] if (filter['property'].indexOf('.') !== -1) { let arr = filter['property'].split('.'); if (Array.isArray(item[arr[0]]) && item[arr[0]].length) { - return item[arr[0]].some((data: any) => { - return filter['value'] === data[arr[1]]; + return item[arr[0]].some((data: any) => { + return filter['value'] === data[arr[1]]; }); } } else { @@ -382,14 +383,14 @@ export function isEmpty(obj: any): boolean { export function downloadFile(fileData) { let url = window.URL.createObjectURL(fileData.data); - let a = document.createElement("a"); - document.body.appendChild(a); - a.setAttribute("style", "display: none"); - a.href = url; - a.download = fileData.filename; - a.click(); - window.URL.revokeObjectURL(url); - a.remove(); + let a = document.createElement("a"); + document.body.appendChild(a); + a.setAttribute("style", "display: none"); + a.href = url; + a.download = fileData.filename; + a.click(); + window.URL.revokeObjectURL(url); + a.remove(); } export function getChanges(original: any, afterChange: any): { [key: string]: any | any[] } { @@ -429,3 +430,89 @@ export function cronRegex(testValue: any): boolean { let reg = new RegExp(regEx, "i"); return reg.test(testValue.trim()); } + +/** + * Keep decimal digits + * @param count number + * @param decimals number 1、2、3 ··· + */ +export const roundDecimals = (count, decimals = 0) => { + return Number(`${Math.round(+`${count}e${decimals}`)}e-${decimals}`); +}; +/** + * get suitable unit + * @param count number ;bit + * @param quotaUnitsDeep Array link QuotaUnits; + */ +export const getSuitableUnit = (count: number, quotaUnitsDeep: QuotaUnitInterface[]): string => { + for (let unitObj of quotaUnitsDeep) { + if (count / StorageMultipleConstant >= 1 && quotaUnitsDeep.length > 1) { + quotaUnitsDeep.shift(); + return getSuitableUnit(count / StorageMultipleConstant, quotaUnitsDeep); + } else { + return +count ? `${roundDecimals(count, 2)}${unitObj.UNIT}` : `0${unitObj.UNIT}`; + } + } + return `${roundDecimals(count, 2)}${QuotaUnits[0].UNIT}`; +}; +/** + * get byte from GB、MB、TB + * @param count number + * @param unit MB /GB / TB + */ +export const getByte = (count: number, unit: string): number => { + let flagIndex; + return QuotaUnits.reduce((totalValue, currentValue, index) => { + if (currentValue.UNIT === unit) { + flagIndex = index; + return totalValue; + } else { + if (!flagIndex) { + return totalValue * StorageMultipleConstant; + } + return totalValue; + } + }, count); +}; +/** + * get integet and unit in hard storage and used storage;and the unit of used storage <= the unit of hard storage + * @param hardNumber hard storage number + * @param quotaUnitsDeep clone(Quotas) + * @param usedNumber used storage number + * @param quotaUnitsDeepClone clone(Quotas) + */ +export const GetIntegerAndUnit = (hardNumber: number, quotaUnitsDeep: QuotaUnitInterface[] + , usedNumber: number, quotaUnitsDeepClone: QuotaUnitInterface[]) => { + + for (let unitObj of quotaUnitsDeep) { + if (hardNumber % StorageMultipleConstant === 0 && quotaUnitsDeep.length > 1) { + quotaUnitsDeep.shift(); + if (usedNumber / StorageMultipleConstant >= 1) { + quotaUnitsDeepClone.shift(); + return GetIntegerAndUnit(hardNumber / StorageMultipleConstant + , quotaUnitsDeep, usedNumber / StorageMultipleConstant, quotaUnitsDeepClone); + } else { + return GetIntegerAndUnit(hardNumber / StorageMultipleConstant, quotaUnitsDeep, usedNumber, quotaUnitsDeepClone); + } + } else { + return { + partNumberHard: +hardNumber, + partCharacterHard: unitObj.UNIT, + partNumberUsed: roundDecimals(+usedNumber, 2), + partCharacterUsed: quotaUnitsDeepClone[0].UNIT + }; + } + } +}; +export const validateLimit = (unitContrl) => { + return (control: AbstractControl) => { + if (getByte(control.value, unitContrl.value) > StorageMultipleConstant * StorageMultipleConstant + * StorageMultipleConstant * StorageMultipleConstant * StorageMultipleConstant) { + return { + error: true + }; + } + return null; + }; +}; + diff --git a/src/portal/package.json b/src/portal/package.json index 9c9dd34eb..3ba57d668 100644 --- a/src/portal/package.json +++ b/src/portal/package.json @@ -76,7 +76,7 @@ "karma-chrome-launcher": "~2.2.0", "karma-cli": "^1.0.1", "karma-coverage-istanbul-reporter": "~2.0.0", - "karma-jasmine": "^1.1.2", + "karma-jasmine": "^2.0.0", "karma-jasmine-html-reporter": "^0.2.2", "karma-mocha-reporter": "^2.2.4", "karma-remap-istanbul": "^0.6.0", diff --git a/src/portal/src/app/config/config.component.html b/src/portal/src/app/config/config.component.html index 1b89bb2b5..a56f0adf4 100644 --- a/src/portal/src/app/config/config.component.html +++ b/src/portal/src/app/config/config.component.html @@ -32,6 +32,12 @@ + + + + + + \ No newline at end of file diff --git a/src/portal/src/app/harbor-routing.module.ts b/src/portal/src/app/harbor-routing.module.ts index 8d8e10eb9..26ec644df 100644 --- a/src/portal/src/app/harbor-routing.module.ts +++ b/src/portal/src/app/harbor-routing.module.ts @@ -56,6 +56,7 @@ import { ListChartsComponent } from './project/helm-chart/list-charts.component' import { ListChartVersionsComponent } from './project/helm-chart/list-chart-versions/list-chart-versions.component'; import { HelmChartDetailComponent } from './project/helm-chart/helm-chart-detail/chart-detail.component'; import { OidcOnboardComponent } from './oidc-onboard/oidc-onboard.component'; +import { SummaryComponent } from './project/summary/summary.component'; const harborRoutes: Routes = [ { path: '', redirectTo: 'harbor', pathMatch: 'full' }, @@ -165,6 +166,10 @@ const harborRoutes: Routes = [ projectResolver: ProjectRoutingResolver }, children: [ + { + path: 'summary', + component: SummaryComponent + }, { path: 'repositories', component: RepositoryPageComponent 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 3b285000e..e3fa55024 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 @@ -33,6 +33,52 @@ + +
+ + + +
+
+ + + + +
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 68bf3140f..eff57e473 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 @@ -1,5 +1,5 @@ -import {debounceTime} from 'rxjs/operators'; +import {debounceTime, distinctUntilChanged} from 'rxjs/operators'; // Copyright (c) 2017 VMware, Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,9 +19,12 @@ import { Output, ViewChild, OnInit, - OnDestroy + OnDestroy, + Input, + OnChanges, + SimpleChanges } from "@angular/core"; -import { NgForm } from "@angular/forms"; +import { NgForm, Validators, AbstractControl } from "@angular/forms"; import { Subject } from "rxjs"; import { TranslateService } from "@ngx-translate/core"; @@ -30,24 +33,29 @@ import { MessageHandlerService } from "../../shared/message-handler/message-hand import { InlineAlertComponent } from "../../shared/inline-alert/inline-alert.component"; import { Project } from "../project"; -import { ProjectService } from "@harbor/ui"; +import { ProjectService, QuotaUnits, QuotaHardInterface, QuotaUnlimited, getByte + , GetIntegerAndUnit, clone, StorageMultipleConstant, validateLimit} from "@harbor/ui"; import { errorHandler } from '@angular/platform-browser/src/browser'; - - @Component({ selector: "create-project", templateUrl: "create-project.component.html", styleUrls: ["create-project.scss"] }) -export class CreateProjectComponent implements OnInit, OnDestroy { +export class CreateProjectComponent implements OnInit, OnChanges, OnDestroy { projectForm: NgForm; @ViewChild("projectForm") currentForm: NgForm; - + quotaUnits = QuotaUnits; project: Project = new Project(); + countLimit: number; + storageLimit: number; + storageLimitUnit: string = QuotaUnits[3].UNIT; + storageDefaultLimit: number; + storageDefaultLimitUnit: string; + countDefaultLimit: number; initVal: Project = new Project(); createProjectOpened: boolean; @@ -64,6 +72,8 @@ export class CreateProjectComponent implements OnInit, OnDestroy { proNameChecker: Subject = new Subject(); @Output() create = new EventEmitter(); + @Input() quotaObj: QuotaHardInterface; + @Input() isSystemAdmin: boolean; @ViewChild(InlineAlertComponent) inlineAlert: InlineAlertComponent; @@ -97,6 +107,35 @@ export class CreateProjectComponent implements OnInit, OnDestroy { }); } + ngOnChanges(changes: SimpleChanges): void { + if (changes && changes["quotaObj"] && changes["quotaObj"].currentValue) { + this.countLimit = this.quotaObj.count_per_project; + this.storageLimit = GetIntegerAndUnit(this.quotaObj.storage_per_project, clone(QuotaUnits), 0, clone(QuotaUnits)).partNumberHard; + this.storageLimitUnit = this.storageLimit === QuotaUnlimited ? QuotaUnits[3].UNIT + : GetIntegerAndUnit(this.quotaObj.storage_per_project, clone(QuotaUnits), 0, clone(QuotaUnits)).partCharacterHard; + + this.countDefaultLimit = this.countLimit; + this.storageDefaultLimit = this.storageLimit; + this.storageDefaultLimitUnit = this.storageLimitUnit; + if (this.isSystemAdmin) { + this.currentForm.form.controls['create_project_storage-limit'].setValidators( + [ + Validators.required, + Validators.pattern('(^-1$)|(^([1-9]+)([0-9]+)*$)'), + validateLimit(this.currentForm.form.controls['create_project_storage-limit-unit']) + ]); + } + this.currentForm.form.valueChanges + .pipe(distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b))) + .subscribe((data) => { + ['create_project_storage-limit', 'create_project_storage-limit-unit'].forEach(fieldName => { + if (this.currentForm.form.get(fieldName) && this.currentForm.form.get(fieldName).value !== null) { + this.currentForm.form.get(fieldName).updateValueAndValidity(); + } + }); + }); + } +} ngOnDestroy(): void { this.proNameChecker.unsubscribe(); } @@ -105,10 +144,10 @@ export class CreateProjectComponent implements OnInit, OnDestroy { if (this.isSubmitOnGoing) { return ; } - this.isSubmitOnGoing = true; + const storageByte = +this.storageLimit === QuotaUnlimited ? this.storageLimit : getByte(+this.storageLimit, this.storageLimitUnit); this.projectService - .createProject(this.project.name, this.project.metadata) + .createProject(this.project.name, this.project.metadata, +this.countLimit, +storageByte) .subscribe( status => { this.isSubmitOnGoing = false; @@ -127,7 +166,6 @@ export class CreateProjectComponent implements OnInit, OnDestroy { this.createProjectOpened = false; } - newProject() { this.project = new Project(); this.hasChanged = false; @@ -135,6 +173,10 @@ export class CreateProjectComponent implements OnInit, OnDestroy { this.createProjectOpened = true; this.inlineAlert.close(); + + this.countLimit = this.countDefaultLimit ; + this.storageLimit = this.storageDefaultLimit; + this.storageLimitUnit = this.storageDefaultLimitUnit; } public get isValid(): boolean { 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 8661ba903..ec7820ed9 100644 --- a/src/portal/src/app/project/create-project/create-project.scss +++ b/src/portal/src/app/project/create-project/create-project.scss @@ -17,7 +17,7 @@ .form-block > div { padding-left: 135px; .input-width { - width: 296px; + width: 196px; } .public-tooltip { top: -8px; @@ -29,3 +29,19 @@ } } + +.form-group { + ::ng-deep { + clr-select-container { + margin-top: 0.3rem; + } + } + select { + display: inline; + } + .checkbox-inline { + margin-left: 5px; + height: 1rem; + } +} + 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 dd9c64a79..59477512d 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 @@ -143,7 +143,7 @@ export class ListProjectComponent implements OnDestroy { goToLink(proId: number): void { this.searchTrigger.closeSearch(true); - let linkUrl = ["harbor", "projects", proId, "repositories"]; + let linkUrl = ["harbor", "projects", proId, "summary"]; this.router.navigate(linkUrl); } 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 89820aa8a..0589d5138 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 @@ -4,6 +4,9 @@

{{currentProject.name}} {{roleName | translate}}