Merge pull request #8339 from jwangyangls/quotaPerProject2

Add project quotas  page in configration ,project
This commit is contained in:
jwangyangls 2019-07-26 10:25:05 +08:00 committed by GitHub
commit fe9eb9da20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 1670 additions and 62 deletions

View File

@ -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);
}
}

View File

@ -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<any>[] = [
GcComponent,
SystemSettingsComponent,
VulnerabilityConfigComponent,
RegistryConfigComponent
RegistryConfigComponent,
ProjectQuotasComponent,
EditProjectQuotasComponent
];

View File

@ -0,0 +1,86 @@
<clr-modal [(clrModalOpen)]="openEditQuota" class="quota-modal" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{ defaultTextsObj.editQuota }}</h3>
<hbr-inline-alert class="modal-title p-0" ></hbr-inline-alert>
<div class="modal-body">
<label>{{defaultTextsObj.setQuota}}</label>
<form #quotaForm="ngForm" class="clr-form-compact"
[class.clr-form-compact-common]="!defaultTextsObj.isSystemDefaultQuota">
<div class="form-group">
<label for="count" class="required">{{ defaultTextsObj.countQuota | translate}}</label>
<label for="count" aria-haspopup="true" role="tooltip"
class="tooltip tooltip-validation tooltip-lg tooltip-top-right mr-3px"
[class.invalid]="countInput.invalid && (countInput.dirty || countInput.touched)">
<input name="count" type="text" #countInput="ngModel" class="quota-input"
[(ngModel)]="quotaHardLimitValue.countLimit" pattern="(^-1$)|(^([1-9]+)([0-9]+)*$)" required id="count"
size="40">
<span class="tooltip-content">
{{ 'PROJECT.COUNT_QUOTA_TIP' | translate }}
</span>
</label>
<div class="select-div"></div>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true"
class="tooltip tooltip-lg tooltip-top-right mr-0">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'PROJECT.QUOTA_UNLIMIT_TIP' | translate }}</span>
</a>
<div class="progress-block progress-min-width progress-div" *ngIf="!defaultTextsObj.isSystemDefaultQuota">
<div class="progress success"
[class.danger]="getDangerStyle(+quotaHardLimitValue.countLimit, quotaHardLimitValue.countUsed)">
<progress value="{{countInput.invalid || +quotaHardLimitValue.countLimit===-1?0:quotaHardLimitValue.countUsed}}"
max="{{countInput.invalid?100:quotaHardLimitValue.countLimit}}" data-displayval="100%"></progress>
</div>
<label class="progress-label">{{ quotaHardLimitValue?.countUsed }} {{ 'QUOTA.OF' | translate }}
{{ countInput?.valid?+quotaHardLimitValue?.countLimit===-1 ? ('QUOTA.UNLIMITED' | translate): quotaHardLimitValue?.countLimit:('QUOTA.INVALID_INPUT' | translate)}}
</label>
</div>
</div>
<div class="form-group">
<label for="storage" class="required">{{ defaultTextsObj?.storageQuota | translate}}</label>
<label for="storage" aria-haspopup="true" role="tooltip"
class="tooltip tooltip-validation tooltip-lg mr-3px tooltip-top-right"
[class.invalid]="(storageInput.invalid && (storageInput.dirty || storageInput.touched))||storageInput.errors">
<input name="storage" type="text" #storageInput="ngModel" class="quota-input"
[(ngModel)]="quotaHardLimitValue.storageLimit"
id="storage" size="40">
<span class="tooltip-content">
{{ 'PROJECT.STORAGE_QUOTA_TIP' | translate }}
</span>
</label>
<div class="select-div">
<select clrSelect name="storageUnit" [(ngModel)]="quotaHardLimitValue.storageUnit">
<ng-template ngFor let-quotaUnit [ngForOf]="quotaUnits" let-i="index">
<option *ngIf="i>1" [value]="quotaUnit.UNIT">{{ quotaUnit?.UNIT }}</option>
</ng-template>
</select>
</div>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true"
class="tooltip tooltip-lg tooltip-top-right mr-0">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'PROJECT.QUOTA_UNLIMIT_TIP' | translate }}</span>
</a>
<div class="progress-block progress-min-width progress-div" *ngIf="!defaultTextsObj.isSystemDefaultQuota">
<div class="progress success" [class.danger]="getDangerStyle(+quotaHardLimitValue.storageLimit,quotaHardLimitValue.storageUsed, quotaHardLimitValue.storageUnit)">
<progress value="{{storageInput.invalid?0:quotaHardLimitValue.storageUsed}}"
max="{{storageInput.invalid?0:getByte(+quotaHardLimitValue.storageLimit, quotaHardLimitValue.storageUnit)}}"
data-displayval="100%"></progress>
</div>
<label class="progress-label">
<!-- the comments of progress , when storageLimit !=-1 get integet and unit in hard storage and used storage;and the unit of used storage <= the unit of hard storage
the other : get suitable number and unit-->
{{ +quotaHardLimitValue.storageLimit !== -1 ?(getIntegerAndUnit(getByte(quotaHardLimitValue.storageLimit,quotaHardLimitValue.storageUnit), quotaHardLimitValue.storageUsed).partNumberUsed
+ getIntegerAndUnit(getByte(quotaHardLimitValue.storageLimit,quotaHardLimitValue.storageUnit), quotaHardLimitValue.storageUsed).partCharacterUsed) : getSuitableUnit(quotaHardLimitValue.storageUsed)}}
{{ 'QUOTA.OF' | translate }}
{{ storageInput?.valid? +quotaHardLimitValue?.storageLimit ===-1? ('QUOTA.UNLIMITED' | translate): quotaHardLimitValue?.storageLimit :('QUOTA.INVALID_INPUT' | translate)}}
{{+quotaHardLimitValue?.storageLimit ===-1?'':quotaHardLimitValue?.storageUnit }}
</label>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="onCancel()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" [disabled]="!isValid"
(click)="onSubmit()">{{'BUTTON.OK' | translate}}</button>
</div>
</clr-modal>

View File

@ -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;
}

View File

@ -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<EditProjectQuotasComponent>;
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();
});
});

View File

@ -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;
}
}

View File

@ -0,0 +1,75 @@
<div class="color-0 pt-1">
<div class="row" class="label-config">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 quota-top">
<div class="default-quota">
<div>
<div class="default-quota-text pr-1"><span>{{'QUOTA.PROJECT_QUOTA_DEFAULT_ARTIFACT' | translate}}</span><span
class="num-count">{{ quotaHardLimitValue?.countLimit === -1? ('QUOTA.UNLIMITED'| translate): quotaHardLimitValue?.countLimit }}</span>
</div>
<div class="default-quota-text pr-1"><span>{{'QUOTA.PROJECT_QUOTA_DEFAULT_DISK' | translate}}</span><span class="num-count">
{{ quotaHardLimitValue?.storageLimit === -1?('QUOTA.UNLIMITED' | translate): getIntegerAndUnit(quotaHardLimitValue?.storageLimit, 0).partNumberHard}}
{{ quotaHardLimitValue?.storageLimit === -1?'':quotaHardLimitValue?.storageUnit }}</span>
</div>
</div>
<button class="btn btn-link btn-sm default-quota-edit-button pt-0 mt-0"
(click)="editDefaultQuota(quotaHardLimitValue)">{{'QUOTA.EDIT' | translate}}</button>
</div>
<div class="refresh-div mr-1">
<span class="refresh-btn" (click)="refresh()">
<clr-icon shape="refresh"></clr-icon>
</span>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid [clrDgLoading]="loading" (clrDgRefresh)="getQuotaList($event)">
<clr-dg-column>{{'QUOTA.PROJECT' | translate}}</clr-dg-column>
<clr-dg-column>{{'QUOTA.OWNER' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="countComparator">{{'QUOTA.COUNT' | translate }}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="storageComparator">{{'QUOTA.STORAGE' | translate }}</clr-dg-column>
<clr-dg-placeholder>{{'QUOTA.PLACEHOLDER' | translate }}</clr-dg-placeholder>
<clr-dg-row *ngFor="let quota of quotaList" [clrDgItem]='quota'>
<clr-dg-action-overflow>
<button class="action-item" (click)="editQuota(quota)">{{'QUOTA.EDIT' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell>
<a href="javascript:void(0)" (click)="goToLink(quota.id)">{{quota?.ref?.name}}</a></clr-dg-cell>
<clr-dg-cell>{{quota?.ref?.owner_name}}</clr-dg-cell>
<clr-dg-cell>
<div class="progress-block progress-min-width">
<div class="progress success"
[class.danger]="quota.hard.count!==-1?quota.used.count/quota.hard.count>0.9:false">
<progress value="{{quota.hard.count===-1? 0 : quota.used.count}}"
max="{{quota.hard.count}}" data-displayval="100%"></progress>
</div>
<label class="min-label-width">{{ quota?.used?.count }} {{ 'QUOTA.OF' | translate }}
{{ quota?.hard.count ===-1?('QUOTA.UNLIMITED' | translate): quota?.hard?.count }}</label>
</div>
</clr-dg-cell>
<clr-dg-cell>
<div class="progress-block progress-min-width">
<div class="progress success"
[class.danger]="quota.hard.storage!==-1?quota.used.storage/quota.hard.storage>0.9:false">
<progress value="{{quota.hard.storage===-1? 0 : quota.used.storage}}"
max="{{quota.hard.storage}}" data-displayval="100%"></progress>
</div>
<label class="min-label-width">{{ quota?.hard?.storage ===-1 ? getSuitableUnit(quota?.used?.storage) :
(getIntegerAndUnit(quota?.hard?.storage, quota?.used?.storage).partNumberUsed + getIntegerAndUnit(quota?.hard?.storage, quota?.used?.storage).partCharacterUsed)}}
{{ 'QUOTA.OF' | translate }}
{{ (quota?.hard?.storage ===-1 ? 'QUOTA.UNLIMITED':
(getIntegerAndUnit(quota?.hard?.storage, quota?.used?.storage).partNumberHard + getIntegerAndUnit(quota?.hard?.storage, quota?.used?.storage).partCharacterHard)) | translate }}
</label>
</div>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<span>{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}}
{{'DESTINATION.OF' | translate}}</span>
{{totalCount}} {{'SUMMARY.QUOTAS' | translate}}
<clr-dg-pagination #pagination [clrDgPageSize]="pageSize" [(clrDgPage)]="currentPage"
[clrDgTotalItems]="totalCount"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
</div>
<edit-project-quotas #editProjectQuotas (confirmAction)="confirmEdit($event)"></edit-project-quotas>
</div>

View File

@ -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;
}

View File

@ -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<ProjectQuotasComponent>;
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();
});
});

View File

@ -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<Configuration> = new EventEmitter<Configuration>();
@Output() refreshAllconfig: EventEmitter<Configuration> = new EventEmitter<Configuration>();
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<Quota> = new CustomComparator<Quota>(quotaSort.count, quotaSort.sortType);
storageComparator: Comparator<Quota> = new CustomComparator<Quota>(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<any>;
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);
}
}

View File

@ -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 },

View File

@ -232,4 +232,6 @@ export interface IServiceConfig {
gcEndpoint?: string;
ScanAllEndpoint?: string;
quotaUrl?: string;
}

View File

@ -14,3 +14,4 @@ export * from "./label.service";
export * from "./retag.service";
export * from "./permission.service";
export * from "./permission-static";
export * from "./quota.service";

View File

@ -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;
}

View File

@ -1,8 +1,10 @@
export const USERSTATICPERMISSION = {
"PROJECT": {
'KEY': 'project',
'KEY': '.',
'VALUE': {
"DELETE": "delete"
"DELETE": "delete",
"UPDATE": "update",
"READ": "read",
}
},
"MEMBER": {

View File

@ -69,11 +69,12 @@ export abstract class ProjectService {
page?: number,
pageSize?: number
): Observable<HttpResponse<Project[]>>;
abstract createProject(name: string, metadata: any): Observable<any>;
abstract createProject(name: string, metadata: any, countLimit: number, storageLimit: number): Observable<any>;
abstract toggleProjectPublic(projectId: number, isPublic: string): Observable<any>;
abstract deleteProject(projectId: number): Observable<any>;
abstract checkProjectExists(projectName: string): Observable<any>;
abstract checkProjectMember(projectId: number): Observable<any>;
abstract getProjectSummary(projectId: number): Observable<any>;
}
/**
@ -149,12 +150,14 @@ export class ProjectDefaultService extends ProjectService {
catchError(error => observableThrowError(error)), );
}
public createProject(name: string, metadata: any): Observable<any> {
public createProject(name: string, metadata: any, countLimit: number, storageLimit: number): Observable<any> {
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<any> {
return this.http
.get(`/api/projects/${projectId}/summary`, HTTP_GET_OPTIONS).pipe(
catchError(error => observableThrowError(error)), );
}
}

View File

@ -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<ReplicationRule[]>)}
*
* @memberOf QuotaService
*/
abstract getQuotaList(quotaType, page?, pageSize?, sortBy?: any):
any;
abstract updateQuota(
id: number,
rep: QuotaHard
): Observable<any>;
}
/**
* 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<HttpResponse<Quota[]>>(this.quotaUrl
, buildHttpRequestOptionsWithObserveResponse(params))
.pipe(map(response => {
return response;
})
, catchError(error => observableThrowError(error)));
}
public updateQuota(
id: number,
quotaHardLimit: QuotaHard
): Observable<any> {
let url = `${this.quotaUrl}/${id}`;
return this.http
.put(url, quotaHardLimit, HTTP_JSON_OPTIONS)
.pipe(catchError(error => observableThrowError(error)));
}
}

View File

@ -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' },

View File

@ -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<T> type.
*
@ -270,8 +271,8 @@ export function doFiltering<T extends { [key: string]: any | any[] }>(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 123 ···
*/
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 GBMBTB
* @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;
};
};

View File

@ -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",

View File

@ -32,6 +32,12 @@
</clr-tab-content>
</ng-template>
</clr-tab>
<clr-tab>
<button id="config-system" clrTabLink>{{'CONFIG.PROJECT_QUOTAS' | translate }}</button>
<clr-tab-content id="project_quotas" *clrIfActive>
<project-quotas [(allConfig)]="allConfig" (refreshAllconfig)="refreshAllconfig()"></project-quotas>
</clr-tab-content>
</clr-tab>
</clr-tabs>
</div>
</div>

View File

@ -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

View File

@ -33,6 +33,52 @@
</a>
</div>
</div>
<div class="form-group" *ngIf="isSystemAdmin">
<label for="create_project_count-limit" class="required col-md-3 form-group-label-override">{{'PROJECT.COUNT_QUOTA' | translate}}</label>
<label for="create_project_count-limit" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left"
[class.invalid]="projectCountLimit.invalid && (projectCountLimit.dirty || projectCountLimit.touched)" >
<input type="text" id="create_project_count-limit" [(ngModel)]="countLimit"
name="create_project_count-limit" class="input-width"
pattern="(^-1$)|(^([1-9]+)([0-9]+)*$)"
required
#projectCountLimit="ngModel"
autocomplete="off" >
<span class="tooltip-content">
{{ 'PROJECT.COUNT_QUOTA_TIP' | translate }}
</span>
</label>
<div class="checkbox-inline">
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-md tooltip-top-left public-tooltip">
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content inline-help-public">{{'PROJECT.QUOTA_UNLIMIT_TIP' | translate }}</span>
</a>
</div>
</div>
<div class="form-group" *ngIf="isSystemAdmin">
<label for="create_project_storage-limit" class="required col-md-3 form-group-label-override">{{'PROJECT.STORAGE_QUOTA' | translate}}</label>
<label for="create_project_storage-limit" aria-haspopup="true" role="tooltip" class="tooltip-quota-storage tooltip tooltip-validation tooltip-md tooltip-top-left"
[class.invalid]="(projectStorageLimit.invalid && (projectStorageLimit.dirty || projectStorageLimit.touched))||projectStorageLimit.errors" >
<input type="text" id="create_project_storage-limit" [(ngModel)]="storageLimit"
name="create_project_storage-limit" size="255" class="input-width"
#projectStorageLimit="ngModel"
autocomplete="off" >
<span class="tooltip-content">
{{ 'PROJECT.STORAGE_QUOTA_TIP' | translate }}
</span>
</label>
<select clrSelect id="create_project_storage-limit-unit" name="create_project_storage-limit-unit" [(ngModel)]="storageLimitUnit">
<ng-template ngFor let-quotaUnit [ngForOf]="quotaUnits" let-i="index">
<option *ngIf="i>1"[value]="quotaUnit.UNIT">{{ quotaUnit.UNIT }}</option>
</ng-template>
</select>
<div class="checkbox-inline">
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-md tooltip-top-left public-tooltip">
<clr-icon shape="info-circle" size="24"></clr-icon>
<span class="tooltip-content inline-help-public">{{'PROJECT.QUOTA_UNLIMIT_TIP' | translate }}</span>
</a>
</div>
</div>
</section>
</form>
</div>

View File

@ -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<string> = new Subject<string>();
@Output() create = new EventEmitter<boolean>();
@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 {

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -4,6 +4,9 @@
<h1 class="custom-h2" sub-header-title>{{currentProject.name}} <span class="role-label" *ngIf="isMember">{{roleName | translate}}</span></h1>
<nav class="subnav sub-nav-bg-color">
<ul class="nav">
<li class="nav-item" *ngIf="hasProjectReadPermission">
<a class="nav-link" routerLink="summary" routerLinkActive="active">{{'PROJECT_DETAIL.SUMMARY' | translate}}</a>
</li>
<li class="nav-item" *ngIf="hasRepositoryListPermission">
<a class="nav-link" routerLink="repositories" routerLinkActive="active">{{'PROJECT_DETAIL.REPOSITORIES' | translate}}</a>
</li>

View File

@ -34,6 +34,7 @@ export class ProjectDetailComponent implements OnInit {
isMember: boolean;
roleName: string;
projectId: number;
hasProjectReadPermission: boolean;
hasHelmChartsListPermission: boolean;
hasRepositoryListPermission: boolean;
hasMemberListPermission: boolean;
@ -64,6 +65,8 @@ export class ProjectDetailComponent implements OnInit {
}
getPermissionsList(projectId: number): void {
let permissionsList = [];
permissionsList.push(this.userPermissionService.getPermission(projectId,
USERSTATICPERMISSION.PROJECT.KEY, USERSTATICPERMISSION.PROJECT.VALUE.READ));
permissionsList.push(this.userPermissionService.getPermission(projectId,
USERSTATICPERMISSION.LOG.KEY, USERSTATICPERMISSION.LOG.VALUE.LIST));
permissionsList.push(this.userPermissionService.getPermission(projectId,
@ -81,7 +84,7 @@ export class ProjectDetailComponent implements OnInit {
permissionsList.push(this.userPermissionService.getPermission(projectId,
USERSTATICPERMISSION.LABEL.KEY, USERSTATICPERMISSION.LABEL.VALUE.CREATE));
forkJoin(...permissionsList).subscribe(Rules => {
[this.hasLogListPermission, this.hasConfigurationListPermission, this.hasMemberListPermission
[this.hasProjectReadPermission, this.hasLogListPermission, this.hasConfigurationListPermission, this.hasMemberListPermission
, this.hasLabelListPermission, this.hasRepositoryListPermission, this.hasHelmChartsListPermission, this.hasRobotListPermission
, this.hasLabelCreatePermission] = Rules;

View File

@ -22,7 +22,7 @@
</span>
</div>
</div>
<create-project (create)="createProject($event)"></create-project>
<create-project (create)="createProject($event)" [quotaObj]="quotaObj" [isSystemAdmin]="isSystemAdmin"></create-project>
<list-project (addProject)="openModal()"></list-project>
</div>
</div>

View File

@ -15,6 +15,9 @@ import { Component, OnInit, ViewChild } from '@angular/core';
import { CreateProjectComponent } from './create-project/create-project.component';
import { ListProjectComponent } from './list-project/list-project.component';
import { ProjectTypes } from '../shared/shared.const';
import { ConfigurationService } from '../config/config.service';
import { Configuration, QuotaHardInterface } from '@harbor/ui';
import { SessionService } from "../shared/session.service";
@Component({
selector: 'project',
@ -23,7 +26,7 @@ import { ProjectTypes } from '../shared/shared.const';
})
export class ProjectComponent implements OnInit {
projectTypes = ProjectTypes;
quotaObj: QuotaHardInterface;
@ViewChild(CreateProjectComponent)
creationProject: CreateProjectComponent;
@ -45,16 +48,33 @@ export class ProjectComponent implements OnInit {
}
}
constructor() {
}
constructor(
public configService: ConfigurationService,
private session: SessionService
) { }
ngOnInit(): void {
if (window.sessionStorage && window.sessionStorage['projectTypeValue'] && window.sessionStorage['fromDetails']) {
this.currentFilteredType = +window.sessionStorage['projectTypeValue'];
window.sessionStorage.removeItem('fromDetails');
}
if (this.isSystemAdmin) {
this.getConfigration();
}
}
getConfigration() {
this.configService.getConfiguration()
.subscribe((configurations: Configuration) => {
this.quotaObj = {
count_per_project: configurations.count_per_project ? configurations.count_per_project.value : -1,
storage_per_project: configurations.storage_per_project ? configurations.storage_per_project.value : -1
};
});
}
public get isSystemAdmin(): boolean {
let account = this.session.getCurrentUser();
return account != null && account.has_admin_role;
}
openModal(): void {
this.creationProject.newProject();
}

View File

@ -17,6 +17,7 @@ import { RouterModule } from '@angular/router';
import { SharedModule } from '../shared/shared.module';
import { RepositoryModule } from '../repository/repository.module';
import { ReplicationModule } from '../replication/replication.module';
import { SummaryModule } from './summary/summary.module';
import { LogModule } from '../log/log.module';
import { ProjectComponent } from './project.component';
@ -47,7 +48,8 @@ import { AddHttpAuthGroupComponent } from './member/add-http-auth-group/add-http
ReplicationModule,
LogModule,
RouterModule,
HelmChartModule
HelmChartModule,
SummaryModule
],
declarations: [
ProjectComponent,

View File

@ -0,0 +1,72 @@
<div class="summary display-flex" *ngIf="summaryInformation">
<div class="summary-left">
<div class="display-flex project-detail pt-1">
<h5 class="mt-0">{{'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">
<h5 class="mt-0">{{'SUMMARY.PROJECT_HELM_CHART' | translate}}</h5>
<ul class="list-unstyled">
<li>{{summaryInformation.chart_count}}</li>
</ul>
</div>
<div class="display-flex project-detail pt-1">
<h5 class="mt-0">{{'SUMMARY.PROJECT_MEMBER' | translate}}</h5>
<ul class="list-unstyled">
<li>{{ summaryInformation.project_admin_count }} {{'SUMMARY.ADMIN' | translate}}</li>
<li>{{ summaryInformation.master_count }} {{'SUMMARY.MASTER' | translate}}</li>
<li>{{ summaryInformation.developer_count }} {{'SUMMARY.DEVELOPER' | translate}}</li>
<li>{{ summaryInformation.guest_count }} {{'SUMMARY.GUEST' | translate}}</li>
</ul>
</div>
</div>
<div class="summary-right pt-1">
<div class="display-flex project-detail">
<h5 class="mt-0">{{'SUMMARY.PROJECT_QUOTAS' | translate}}</h5>
<div class="ml-1">
<div class="display-flex quotas-progress">
<label class="mr-1">{{'SUMMARY.ARTIFACT_COUNT' | translate}}</label>
<label class="progress-label">{{ summaryInformation.quota.used.count }} {{ 'QUOTA.OF' | translate }}
{{ summaryInformation.quota.hard.count ===-1?('QUOTA.UNLIMITED' | translate): summaryInformation.quota.hard.count }}
</label>
</div>
<div>
<div class="progress-block progress-min-width progress-div">
<div class="progress success" [class.danger]="summaryInformation.quota.hard.count!==-1?summaryInformation.quota.used.count/summaryInformation.quota.hard.count>0.9:false">
<progress
value="{{summaryInformation.quota.hard.count===-1? 0 : summaryInformation.quota.used.count}}"
max="{{summaryInformation.quota.hard.count}}" data-displayval="100%"></progress>
</div>
</div>
</div>
<div class="display-flex quotas-progress">
<label class="mr-1">{{'SUMMARY.STORAGE_CONSUMPTION' | translate}}</label>
<label class="progress-label">
{{ summaryInformation.quota.hard.storage !== -1 ?(getIntegerAndUnit(summaryInformation.quota.hard.storage, summaryInformation.quota.used.storage).partNumberUsed
+ getIntegerAndUnit(summaryInformation.quota.hard.storage, summaryInformation.quota.used.storage).partCharacterUsed) : getSuitableUnit(summaryInformation.quota.used.storage)}}
<!-- {{ getSuitableUnit(summaryInformation.quota.used.storage) }} -->
{{ 'QUOTA.OF' | translate }}
{{ summaryInformation.quota.hard.storage ===-1? ('QUOTA.UNLIMITED' | translate) : getIntegerAndUnit(summaryInformation.quota.hard.storage, summaryInformation.quota.used.storage).partNumberHard }}
{{ summaryInformation.quota.hard.storage ===-1? '': getIntegerAndUnit(summaryInformation.quota.hard.storage, summaryInformation.quota.used.storage).partCharacterHard }}
</label>
</div>
<div>
<div class="progress-block progress-min-width progress-div">
<div class="progress success"
[class.danger]="summaryInformation.quota.hard.storage!==-1?summaryInformation.quota.used.storage/summaryInformation.quota.hard.storage>0.9:false">
<progress
value="{{summaryInformation.quota.hard.storage===-1? 0 : summaryInformation.quota.used.storage}}"
max="{{summaryInformation.quota.hard.storage}}" data-displayval="100%"></progress>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,39 @@
.summary {
color: #000;
padding-right: 0.3rem;
font-size: 13px;
.summary-left {
.project-detail {
width: 17rem;
min-height: 3rem;
ul {
width: 8rem;
}
}
}
h5 {
font-size: 13px;
font-weight: 700;
}
.summary-right {
.quotas-progress {
min-width: 10rem;
;
}
}
}
.display-flex {
display: flex;
justify-content: space-between;
}
.progress,
.progress-static {
progress {
max-height: 0.48rem;
}
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SummaryComponent } from './summary.component';
describe('SummaryComponent', () => {
let component: SummaryComponent;
let fixture: ComponentFixture<SummaryComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ SummaryComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SummaryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,41 @@
import { Component, OnInit, Input } from '@angular/core';
import { ProjectService, clone, QuotaUnits, getSuitableUnit, ErrorHandler, GetIntegerAndUnit } from '@harbor/ui';
import { Router, ActivatedRoute } from '@angular/router';
import { forkJoin } from 'rxjs';
import { AppConfigService } from "../../app-config.service";
export const riskRatio = 0.9;
@Component({
selector: 'summary',
templateUrl: './summary.component.html',
styleUrls: ['./summary.component.scss']
})
export class SummaryComponent implements OnInit {
projectId: number;
summaryInformation: any;
constructor(
private projectService: ProjectService,
private errorHandler: ErrorHandler,
private appConfigService: AppConfigService,
private route: ActivatedRoute
) { }
ngOnInit() {
this.projectId = this.route.snapshot.parent.params['id'];
this.projectService.getProjectSummary(this.projectId).subscribe(res => {
this.summaryInformation = res;
}, error => {
this.errorHandler.error(error);
});
}
getSuitableUnit(value) {
const QuotaUnitsCopy = clone(QuotaUnits);
return getSuitableUnit(value, QuotaUnitsCopy);
}
getIntegerAndUnit(hardValue, usedValue) {
return GetIntegerAndUnit(hardValue, clone(QuotaUnits), usedValue, clone(QuotaUnits));
}
public get withHelmChart(): boolean {
return this.appConfigService.getConfig().with_chartmuseum;
}
}

View File

@ -0,0 +1,13 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SummaryComponent } from './summary.component';
import { TranslateModule } from '@ngx-translate/core';
@NgModule({
declarations: [SummaryComponent],
imports: [
CommonModule,
TranslateModule
]
})
export class SummaryModule { }

View File

@ -76,7 +76,8 @@ const uiLibConfig: IServiceConfig = {
helmChartEndpoint: "/api/chartrepo",
downloadChartEndpoint: "/chartrepo",
gcEndpoint: "/api/system/gc",
ScanAllEndpoint: "/api/system/scanAll"
ScanAllEndpoint: "/api/system/scanAll",
quotaUrl: "/api/quotas"
};
@NgModule({

View File

@ -217,9 +217,15 @@
"TOGGLED_SUCCESS": "Toggled project successfully.",
"FAILED_TO_DELETE_PROJECT": "Project contains repositories or replication rules or helm-charts cannot be deleted.",
"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",
"STORAGE_QUOTA": "Storage quota",
"COUNT_QUOTA_TIP": "The upper limit of Count Quota should be integers.",
"STORAGE_QUOTA_TIP": "The upper limit of Storage Quota should be integers,and maximum upper limit is 1024TB",
"QUOTA_UNLIMIT_TIP": "If you want to unlimited this quota, please input -1."
},
"PROJECT_DETAIL": {
"SUMMARY": "Summary",
"REPOSITORIES": "Repositories",
"REPLICATION": "Replication",
"USERS": "Members",
@ -670,6 +676,19 @@
"ADD_LABEL_TO_CHART_VERSION": "Add labels to this chart version",
"STATUS": "Status"
},
"SUMMARY": {
"QUOTAS": "quotas",
"PROJECT_REPOSITORY": "Project repositories",
"PROJECT_HELM_CHART": "Project Helm Chart",
"PROJECT_MEMBER": "Project members",
"PROJECT_QUOTAS": "Project quotas",
"ARTIFACT_COUNT": "Artifact count",
"STORAGE_CONSUMPTION": "Storage consumption",
"ADMIN": "Admin(s)",
"MASTER": "Master(s)",
"DEVELOPER": "Developer(s)",
"GUEST": "Guest(s)"
},
"ALERT": {
"FORM_CHANGE_CONFIRMATION": "Some changes are not saved yet. Do you want to cancel?"
},
@ -695,6 +714,7 @@
"REPOSITORY": "Repository",
"REPO_READ_ONLY": "Repository Read Only",
"SYSTEM": "System Settings",
"PROJECT_QUOTAS": "Project Quotas",
"VULNERABILITY": "Vulnerability",
"GC": "Garbage Collection",
"CONFIRM_TITLE": "Confirm to cancel",
@ -727,6 +747,7 @@
"ROOT_CERT": "Registry Root Certificate",
"ROOT_CERT_LINK": "Download",
"REGISTRY_CERTIFICATE": "Registry certificate",
"NO_CHANGE": "Save abort because nothing changed",
"TOOLTIP": {
"SELF_REGISTRATION_ENABLE": "Enable sign up.",
"SELF_REGISTRATION_DISABLE": "Disable sign up.",
@ -939,6 +960,28 @@
"PLACEHOLDER": "We couldn't find any labels!",
"NAME_ALREADY_EXISTS": "Label name already exists."
},
"QUOTA": {
"PROJECT": "Project",
"OWNER": "Owner",
"COUNT": "Count",
"STORAGE": "Storage",
"EDIT": "Edit",
"DELETE": "Delete",
"OF": "of",
"PROJECT_QUOTA_DEFAULT_ARTIFACT": "Default artifact count per project",
"PROJECT_QUOTA_DEFAULT_DISK": "Default disk space per project",
"EDIT_PROJECT_QUOTAS": "Edit Project Quotas",
"EDIT_DEFAULT_PROJECT_QUOTAS": "Edit Default Project Quotas",
"SET_QUOTAS": "Set the project quotas for project 'library'",
"SET_DEFAULT_QUOTAS": "Set the default project quotas when creating new projects",
"COUNT_QUOTA": "Artifact count",
"COUNT_DEFAULT_QUOTA": "Default artifact count",
"STORAGE_QUOTA": "Storage consumption",
"STORAGE_DEFAULT_QUOTA": "Default storage consumption",
"SAVE_SUCCESS": "Quota edit success",
"UNLIMITED": "unlimited",
"INVALID_INPUT": "invalid input"
},
"WEEKLY": {
"MONDAY": "Monday",
"TUESDAY": "Tuesday",

View File

@ -218,9 +218,15 @@
"TOGGLED_SUCCESS": "Proyecto alternado satisfactoriamente.",
"FAILED_TO_DELETE_PROJECT": "Project contains repositories or replication rules or helm-charts cannot be deleted.",
"INLINE_HELP_PUBLIC": "Cuando un proyecto se marca como público, todo el mundo tiene permisos de lectura sobre los repositorio de dicho proyecto, y no hace falta hacer \"docker login\" antes de subir imágenes a ellos.",
"OF": "of"
"OF": "of",
"COUNT_QUOTA": "Count quota",
"STORAGE_QUOTA": "Storage quota",
"COUNT_QUOTA_TIP": "The upper limit of Count Quota should be integers.",
"STORAGE_QUOTA_TIP": "The upper limit of Storage Quota should be integers,and maximum upper limit is 1024TB",
"QUOTA_UNLIMIT_TIP": "If you want to unlimited this quota, please input -1."
},
"PROJECT_DETAIL": {
"SUMMARY": "Summary",
"REPOSITORIES": "Repositorios",
"REPLICATION": "Replicación",
"USERS": "Miembros",
@ -671,6 +677,19 @@
"ADD_LABEL_TO_CHART_VERSION": "Add labels to this chart version",
"STATUS": "Status"
},
"SUMMARY": {
"QUOTAS": "quotas",
"PROJECT_REPOSITORY": "Project repositories",
"PROJECT_HELM_CHART": "Project Helm Chart",
"PROJECT_MEMBER": "Project members",
"PROJECT_QUOTAS": "Project quotas",
"ARTIFACT_COUNT": "Artifact count",
"STORAGE_CONSUMPTION": "Storage consumption",
"ADMIN": "Admin(s)",
"MASTER": "Master(s)",
"DEVELOPER": "Developer(s)",
"GUEST": "Guest(s)"
},
"ALERT": {
"FORM_CHANGE_CONFIRMATION": "Algunos cambios no se han guardado aún. ¿Quiere cancelar?"
},
@ -695,6 +714,7 @@
"REPOSITORY": "Repository",
"REPO_READ_ONLY": "Repository Read Only",
"SYSTEM": "Opciones del Sistema",
"PROJECT_QUOTAS": "Project Quotas",
"VULNERABILITY": "Vulnerability",
"GC": "Garbage Collection",
"CONFIRM_TITLE": "Confirma cancelación",
@ -727,6 +747,7 @@
"ROOT_CERT": "Registro Certificado Raíz",
"ROOT_CERT_LINK": "Descargar",
"REGISTRY_CERTIFICATE": "Certificado de registro",
"NO_CHANGE": "Save abort because nothing changed",
"TOOLTIP": {
"SELF_REGISTRATION_ENABLE": "Activar registro.",
"SELF_REGISTRATION_DISABLE": "Disable sign up.",
@ -940,6 +961,28 @@
"PLACEHOLDER": "We couldn't find any labels!",
"NAME_ALREADY_EXISTS": "Label name already exists."
},
"QUOTA": {
"PROJECT": "Project",
"OWNER": "Owner",
"COUNT": "Count",
"STORAGE": "Storage",
"EDIT": "Edit",
"DELETE": "Delete",
"OF": "of",
"PROJECT_QUOTA_DEFAULT_ARTIFACT": "Default artifact count per project",
"PROJECT_QUOTA_DEFAULT_DISK": "Default disk space per project",
"EDIT_PROJECT_QUOTAS": "Edit Project Quotas",
"EDIT_DEFAULT_PROJECT_QUOTAS": "Edit Default Project Quotas",
"SET_QUOTAS": "Set the project quotas for project 'library'",
"SET_DEFAULT_QUOTAS": "Set the default project quotas when creating new projects",
"COUNT_QUOTA": "Count quota",
"COUNT_DEFAULT_QUOTA": "Default count quota",
"STORAGE_QUOTA": "Storage quota",
"STORAGE_DEFAULT_QUOTA": "Default storage quota",
"SAVE_SUCCESS": "Quota edit success",
"UNLIMITED": "unlimited",
"INVALID_INPUT": "invalid input"
},
"WEEKLY": {
"MONDAY": "Monday",
"TUESDAY": "Tuesday",

View File

@ -211,9 +211,15 @@
"TOGGLED_SUCCESS": "Projet basculé avec succès.",
"FAILED_TO_DELETE_PROJECT": "Project contains repositories or replication rules or helm-charts cannot be deleted.",
"INLINE_HELP_PUBLIC": "Lorsqu'un projet est mis en public, n'importe qui a l'autorisation de lire les dépôts sous ce projet, et l'utilisateur n' a pas besoin d'exécuter \"docker login\" avant de prendre des images de ce projet.",
"OF": "de"
"OF": "de",
"COUNT_QUOTA": "Count quota",
"STORAGE_QUOTA": "Storage quota",
"COUNT_QUOTA_TIP": "The upper limit of Count Quota should be integers.",
"STORAGE_QUOTA_TIP": "The upper limit of Storage Quota should be integers,and maximum upper limit is 1024TB",
"QUOTA_UNLIMIT_TIP": "If you want to unlimited this quota, please input -1."
},
"PROJECT_DETAIL": {
"SUMMARY": "Summary",
"REPOSITORIES": "Dépôts",
"REPLICATION": "Réplication",
"USERS": "Membres",
@ -657,6 +663,19 @@
"ADD_LABEL_TO_CHART_VERSION": "Add labels to this chart version",
"STATUS": "Status"
},
"SUMMARY": {
"QUOTAS": "quotas",
"PROJECT_REPOSITORY": "Project repositories",
"PROJECT_HELM_CHART": "Project Helm Chart",
"PROJECT_MEMBER": "Project members",
"PROJECT_QUOTAS": "Project quotas",
"ARTIFACT_COUNT": "Artifact count",
"STORAGE_CONSUMPTION": "Storage consumption",
"ADMIN": "Admin(s)",
"MASTER": "Master(s)",
"DEVELOPER": "Developer(s)",
"GUEST": "Guest(s)"
},
"ALERT": {
"FORM_CHANGE_CONFIRMATION": "Certaines modifications ne sont pas encore enregistrées. Voulez-vous annuler ?"
},
@ -679,6 +698,7 @@
"EMAIL": "Email",
"LABEL": "Labels",
"SYSTEM": "Réglages Système",
"PROJECT_QUOTAS": "Project Quotas",
"CONFIRM_TITLE": "Confirmer pour annuler",
"CONFIRM_SUMMARY": "Certaines modifications n'ont pas été sauvegardées. Voulez-vous les défaire ?",
"SAVE_SUCCESS": "La configuration a été sauvegardée avec succès.",
@ -709,6 +729,7 @@
"ROOT_CERT_LINK": "Télécharger",
"GC": "Garbage Collection",
"REGISTRY_CERTIFICATE": "certificat d'enregistrement",
"NO_CHANGE": "Save abort because nothing changed",
"TOOLTIP": {
"SELF_REGISTRATION_ENABLE": "Activer l'inscription.",
"SELF_REGISTRATION_DISABLE": "Désactiver l'inscription.",
@ -912,6 +933,28 @@
"PLACEHOLDER": "We couldn't find any labels!",
"NAME_ALREADY_EXISTS": "Label name already exists."
},
"QUOTA": {
"PROJECT": "Project",
"OWNER": "Owner",
"COUNT": "Count",
"STORAGE": "Storage",
"EDIT": "Edit",
"DELETE": "Delete",
"OF": "of",
"PROJECT_QUOTA_DEFAULT_ARTIFACT": "Default artifact count per project",
"PROJECT_QUOTA_DEFAULT_DISK": "Default disk space per project",
"EDIT_PROJECT_QUOTAS": "Edit Project Quotas",
"EDIT_DEFAULT_PROJECT_QUOTAS": "Edit Default Project Quotas",
"SET_QUOTAS": "Set the project quotas for project 'library'",
"SET_DEFAULT_QUOTAS": "Set the default project quotas when creating new projects",
"COUNT_QUOTA": "Count quota",
"COUNT_DEFAULT_QUOTA": "Default count quota",
"STORAGE_QUOTA": "Storage quota",
"STORAGE_DEFAULT_QUOTA": "Default storage quota",
"SAVE_SUCCESS": "Quota edit success",
"UNLIMITED": "unlimited",
"INVALID_INPUT": "invalid input"
},
"WEEKLY": {
"MONDAY": "Monday",
"TUESDAY": "Tuesday",

View File

@ -215,9 +215,15 @@
"TOGGLED_SUCCESS": "Projeto alterado com sucesso.",
"FAILED_TO_DELETE_PROJECT": "Project contains repositories or replication rules or helm-charts cannot be deleted.",
"INLINE_HELP_PUBLIC": "Quando um projeto é marcado como público, qualquer um tem permissões de leitura aos repositórios desse projeto, e o usuário não precisa executar \"docker login\" antes de baixar imagens desse projeto.",
"OF": "de"
"OF": "de",
"COUNT_QUOTA": "Count quota",
"STORAGE_QUOTA": "Storage quota",
"COUNT_QUOTA_TIP": "The upper limit of Count Quota should be integers.",
"STORAGE_QUOTA_TIP": "The upper limit of Storage Quota should be integers,and maximum upper limit is 1024TB",
"QUOTA_UNLIMIT_TIP": "If you want to unlimited this quota, please input -1."
},
"PROJECT_DETAIL": {
"SUMMARY": "Summary",
"REPOSITORIES": "Repositórios",
"REPLICATION": "Replicação",
"USERS": "Membros",
@ -666,6 +672,19 @@
"ADD_LABEL_TO_CHART_VERSION": "Add labels to this chart version",
"STATUS": "Status"
},
"SUMMARY": {
"QUOTAS": "quotas",
"PROJECT_REPOSITORY": "Project repositories",
"PROJECT_HELM_CHART": "Project Helm Chart",
"PROJECT_MEMBER": "Project members",
"PROJECT_QUOTAS": "Project quotas",
"ARTIFACT_COUNT": "Artifact count",
"STORAGE_CONSUMPTION": "Storage consumption",
"ADMIN": "Admin(s)",
"MASTER": "Master(s)",
"DEVELOPER": "Developer(s)",
"GUEST": "Guest(s)"
},
"ALERT": {
"FORM_CHANGE_CONFIRMATION": "Algumas alterações ainda não foram salvas. Você deseja cancelar?"
},
@ -690,6 +709,7 @@
"REPOSITORY": "Repositório",
"REPO_READ_ONLY": "Repositório somente leitura",
"SYSTEM": "Configurações do Sistema",
"PROJECT_QUOTAS": "Project Quotas",
"VULNERABILITY": "Vulnerabilidade",
"GC": "Garbage Collection",
"CONFIRM_TITLE": "Confirme para cancelar",
@ -721,6 +741,8 @@
"PRO_CREATION_ADMIN": "Apenas Administradores",
"ROOT_CERT": "Certificado Raiz do Registry",
"ROOT_CERT_LINK": "Download",
"REGISTRY_CERTIFICATE": "Registry certificate",
"NO_CHANGE": "Save abort because nothing changed",
"TOOLTIP": {
"SELF_REGISTRATION_ENABLE": "Habilitar registro.",
"SELF_REGISTRATION_DISABLE": "Desabilitar registro.",
@ -930,6 +952,28 @@
"PLACEHOLDER": "Não foi possível encontrar nenhuma Label!",
"NAME_ALREADY_EXISTS": "Nome da Label já existe."
},
"QUOTA": {
"PROJECT": "Project",
"OWNER": "Owner",
"COUNT": "Count",
"STORAGE": "Storage",
"EDIT": "Edit",
"DELETE": "Delete",
"OF": "of",
"PROJECT_QUOTA_DEFAULT_ARTIFACT": "Default artifact count per project",
"PROJECT_QUOTA_DEFAULT_DISK": "Default disk space per project",
"EDIT_PROJECT_QUOTAS": "Edit Project Quotas",
"EDIT_DEFAULT_PROJECT_QUOTAS": "Edit Default Project Quotas",
"SET_QUOTAS": "Set the project quotas for project 'library'",
"SET_DEFAULT_QUOTAS": "Set the default project quotas when creating new projects",
"COUNT_QUOTA": "Count quota",
"COUNT_DEFAULT_QUOTA": "Default count quota",
"STORAGE_QUOTA": "Storage quota",
"STORAGE_DEFAULT_QUOTA": "Default storage quota",
"SAVE_SUCCESS": "Quota edit success",
"UNLIMITED": "unlimited",
"INVALID_INPUT": "invalid input"
},
"WEEKLY": {
"MONDAY": "Segunda Feira",
"TUESDAY": "Terça Feira",

View File

@ -216,9 +216,15 @@
"DELETED_SUCCESS": "成功删除项目。",
"TOGGLED_SUCCESS": "切换状态成功。",
"FAILED_TO_DELETE_PROJECT": "项目包含镜像仓库或同步规则或Helm Charts无法删除。",
"INLINE_HELP_PUBLIC": "当项目设为公开后任何人都有此项目下镜像的读权限。命令行用户不需要“docker login”就可以拉取此项目下的镜像。"
"INLINE_HELP_PUBLIC": "当项目设为公开后任何人都有此项目下镜像的读权限。命令行用户不需要“docker login”就可以拉取此项目下的镜像。",
"COUNT_QUOTA": "存储数量",
"STORAGE_QUOTA": "存储容量",
"COUNT_QUOTA_TIP": "存储数量上限应该是整数.",
"STORAGE_QUOTA_TIP": "存储容量的上限应该设置成为整数.并且最大值不能超过1024TB",
"QUOTA_UNLIMIT_TIP": "如果你想要对存储不设置上限,请输入-1."
},
"PROJECT_DETAIL": {
"SUMMARY": "概要",
"REPOSITORIES": "镜像仓库",
"REPLICATION": "同步",
"USERS": "成员",
@ -671,6 +677,19 @@
"ADD_LABEL_TO_CHART_VERSION": "添加标签到此 Chart Version",
"STATUS": "状态"
},
"SUMMARY": {
"QUOTAS": "容量",
"PROJECT_REPOSITORY": "项目镜像",
"PROJECT_HELM_CHART": "项目 Helm Chart",
"PROJECT_MEMBER": "项目成员",
"PROJECT_QUOTAS": "项目容量",
"ARTIFACT_COUNT": "工件计数",
"STORAGE_CONSUMPTION": "存储消耗",
"ADMIN": "管理员",
"MASTER": "维护人员",
"DEVELOPER": "开发者",
"GUEST": "访客"
},
"ALERT": {
"FORM_CHANGE_CONFIRMATION": "表单内容改变,确认是否取消?"
},
@ -695,6 +714,7 @@
"REPOSITORY": "仓库",
"REPO_READ_ONLY": "仓库只读",
"SYSTEM": "系统设置",
"PROJECT_QUOTAS": "项目定额",
"VULNERABILITY": "漏洞",
"GC": "垃圾清理",
"CONFIRM_TITLE": "确认取消",
@ -727,6 +747,7 @@
"ROOT_CERT": "镜像库根证书",
"ROOT_CERT_LINK": "下载",
"REGISTRY_CERTIFICATE": "注册证书",
"NO_CHANGE": "Save abort because nothing changed",
"TOOLTIP": {
"SELF_REGISTRATION_ENABLE": "激活注册功能。",
"SELF_REGISTRATION_DISABLE": "禁用注册功能。",
@ -737,7 +758,7 @@
"LDAP_UID": "在搜索中用来匹配用户的属性可以是uid,cn,email,sAMAccountName或者其它LDAP/AD服务器支持的属性。",
"LDAP_SCOPE": "搜索用户的范围。",
"TOKEN_EXPIRATION": "由令牌服务创建的令牌的过期时间分钟默认为30分钟。",
"ROBOT_TOKEN_EXPIRATION": "机器人账户的令牌的过期时间默认为30天,显示的结果为分钟转化的天数并向下取整。",
"ROBOT_TOKEN_EXPIRATION": "机器人账户的令牌的过期时间默认为30天,显示的结果为分钟转化的天数并向下取整。",
"PRO_CREATION_RESTRICTION": "用来确定哪些用户有权限创建项目,默认为’所有人‘,设置为’仅管理员‘则只有管理员可以创建项目。",
"ROOT_CERT_DOWNLOAD": "下载镜像库根证书。",
"SCANNING_POLICY": "基于不同需求设置镜像扫描策略。‘无’:不设置任何策略;‘每日定时’:每天在设置的时间定时执行扫描。",
@ -938,6 +959,28 @@
"PLACEHOLDER": "未发现任何标签!",
"NAME_ALREADY_EXISTS": "标签名已存在。"
},
"QUOTA": {
"PROJECT": "项目",
"OWNER": "创建者",
"COUNT": "数量",
"STORAGE": "存储",
"EDIT": "修改",
"DELETE": "删除",
"OF": "of",
"PROJECT_QUOTA_DEFAULT_ARTIFACT": "每个项目的默认项目计数",
"PROJECT_QUOTA_DEFAULT_DISK": "每个项目的默认磁盘空间",
"EDIT_PROJECT_QUOTAS": "修改项目容量",
"EDIT_DEFAULT_PROJECT_QUOTAS": "修改项目默认配额",
"SET_QUOTAS": "设置项目“library”的项目配额",
"SET_DEFAULT_QUOTAS": "创建新项目时设置默认项目配额",
"COUNT_QUOTA": "配额数量",
"COUNT_DEFAULT_QUOTA": "默认配额数量",
"STORAGE_QUOTA": "配额存储",
"STORAGE_DEFAULT_QUOTA": "默认配额存储",
"SAVE_SUCCESS": "项目容量修改成功",
"UNLIMITED": "不设限",
"INVALID_INPUT": "输入错误"
},
"WEEKLY": {
"MONDAY": "周一",
"TUESDAY": "周二",