diff --git a/src/portal/scripts/convert-yaml-to-json.js b/src/portal/scripts/convert-yaml-to-json.js index 9f393f216..8ab88dee5 100644 --- a/src/portal/scripts/convert-yaml-to-json.js +++ b/src/portal/scripts/convert-yaml-to-json.js @@ -21,4 +21,32 @@ const swaggerObj = yaml.load(fs.readFileSync(inputFile, {encoding: 'utf-8'})); if (swaggerObj.host) { delete swaggerObj.host; } +// enhancement for property 'additionalProperties' +traverseObject(swaggerObj); + fs.writeFileSync(outputDir + '/swagger.json', JSON.stringify(swaggerObj, null, 2)); + + +function traverseObject(obj) { + if (obj) { + if (Array.isArray(obj)) { + for (let i = 0; i < obj.length; i++) { + traverseObject(obj[i]) + } + } + if (typeof obj === 'object') { + for (let name in obj) { + if (obj.hasOwnProperty(name)) { + if (name === 'additionalProperties' + && obj[name].type === 'object' + && obj[name].additionalProperties === true) { + obj[name] = true; + } else { + traverseObject(obj[name]) + } + } + } + } + } +} + diff --git a/src/portal/src/app/app.module.ts b/src/portal/src/app/app.module.ts index a9b969856..054ac5835 100644 --- a/src/portal/src/app/app.module.ts +++ b/src/portal/src/app/app.module.ts @@ -46,6 +46,7 @@ import { ProjectQuotasComponent } from './project-quotas/project-quotas.componen import { HarborLibraryModule } from "../lib/harbor-library.module"; import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AllPipesModule } from './all-pipes/all-pipes.module'; +import { DistributionModule } from './distribution/distribution.module'; registerLocaleData(zh, 'zh-cn'); registerLocaleData(es, 'es-es'); registerLocaleData(localeFr, 'fr-fr'); @@ -85,7 +86,8 @@ export function getCurrentLanguage(translateService: TranslateService) { OidcOnboardModule, LicenseModule, HarborLibraryModule, - AllPipesModule + AllPipesModule, + DistributionModule, ], exports: [ ], diff --git a/src/portal/src/app/base/harbor-shell/harbor-shell.component.html b/src/portal/src/app/base/harbor-shell/harbor-shell.component.html index 9c30266b7..66ed45dc0 100644 --- a/src/portal/src/app/base/harbor-shell/harbor-shell.component.html +++ b/src/portal/src/app/base/harbor-shell/harbor-shell.component.html @@ -56,6 +56,10 @@ {{'SIDE_NAV.SYSTEM_MGMT.REPLICATION' | translate}} + + + {{'SIDE_NAV.DISTRIBUTIONS.NAME' | translate}} + diff --git a/src/portal/src/app/distribution/base.scss b/src/portal/src/app/distribution/base.scss new file mode 100644 index 000000000..ac4af5c97 --- /dev/null +++ b/src/portal/src/app/distribution/base.scss @@ -0,0 +1,20 @@ +@mixin refresh-button { + cursor: pointer; + margin-top: 7px; +} + +@mixin refresh-button-hover($color) { + color: $color; +} + +@mixin float-right($right-margin) { + display: flex; + justify-content: flex-end; + margin-right: $right-margin; +} + +@mixin square($edge) { + width: $edge; + height: $edge; + min-height: $edge; +} diff --git a/src/portal/src/app/distribution/distribution-instances/distribution-instances.component.html b/src/portal/src/app/distribution/distribution-instances/distribution-instances.component.html new file mode 100644 index 000000000..681aa1cfe --- /dev/null +++ b/src/portal/src/app/distribution/distribution-instances/distribution-instances.component.html @@ -0,0 +1,137 @@ +
+
+

+ {{ 'SIDE_NAV.DISTRIBUTIONS.INSTANCES' | translate }} +

+
+ + +
+
+ + + + {{ 'BUTTON.ACTIONS' | translate}} + + + + + + + + + + +
+
+
+ + + + + +
+
+
+
+ {{ 'DISTRIBUTION.NAME' | translate }} + {{ 'DISTRIBUTION.ENDPOINT' | translate }} + {{ 'DISTRIBUTION.PROVIDER' | translate }} + {{ 'DISTRIBUTION.STATUS' | translate }} + {{ 'DISTRIBUTION.ENABLED' | translate }} + {{'SCANNER.AUTH' | translate}} + {{'DISTRIBUTION.SETUP_TIMESTAMP' | translate}} + {{'DISTRIBUTION.DESCRIPTION' | translate}} + {{ + 'DISTRIBUTION.NOT_FOUND' | translate + }} + + + {{ instance.name }} + {{'SCANNER.DEFAULT' | translate}} + + {{ instance.endpoint }} + + {{ instance.vendor }} + + +
+ + + +
+
+ {{'DISTRIBUTION.NAME' | translate}}: + {{providerMap[instance.vendor].name}} +
+
+ + {{'DISTRIBUTION.MAINTAINER' | translate}}: + {{providerMap[instance.vendor].maintainers?.join(',')}} + +
+
+
+ {{'DISTRIBUTION.VERSION' | translate}}: + {{providerMap[instance.vendor].version}} +
+ + + + + {{ instance.status }} + {{ instance.status }} + + {{ instance.enabled || false }} + {{ instance.auth_mode }} + {{fmtTime(instance.setup_timestamp) | date: 'short'}} + {{ instance.description }} + + + + {{ pagination.firstItem + 1 }} - {{ pagination.lastItem + 1 }}{{ 'HELM_CHART.OF' | translate }} + {{ pagination.totalItems }} {{ 'HELM_CHART.ITEMS' | translate }} + + + +
+
+
+
+ +
diff --git a/src/portal/src/app/distribution/distribution-instances/distribution-instances.component.scss b/src/portal/src/app/distribution/distribution-instances/distribution-instances.component.scss new file mode 100644 index 000000000..775bcbfda --- /dev/null +++ b/src/portal/src/app/distribution/distribution-instances/distribution-instances.component.scss @@ -0,0 +1,32 @@ +@import "../base.scss"; +$refrsh-btn-color: #007CBB; +.refresh-btn { + @include refresh-button +} + +.refresh-btn:hover { + @include refresh-button-hover($refrsh-btn-color); +} + +.filter-pos { + float: right; + margin-right: 24px; + position: relative; + top: 10px; +} + +.action-head-pos { + padding-right: 18px; + height: 24px; + display: flex; + justify-content: flex-end; +} +.no-wrapper { + white-space: nowrap; +} +.margin-top-5px { + margin-top: 5px; +} +.height-24 { + height: 24px; +} diff --git a/src/portal/src/app/distribution/distribution-instances/distribution-instances.component.spec.ts b/src/portal/src/app/distribution/distribution-instances/distribution-instances.component.spec.ts new file mode 100644 index 000000000..b261debec --- /dev/null +++ b/src/portal/src/app/distribution/distribution-instances/distribution-instances.component.spec.ts @@ -0,0 +1,142 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { ClarityModule } from '@clr/angular'; +import { SharedModule } from '../../shared/shared.module'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { DistributionInstancesComponent } from './distribution-instances.component'; +import { PreheatService } from "../../../../ng-swagger-gen/services/preheat.service"; +import { Instance } from '../../../../ng-swagger-gen/models/instance'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { of } from 'rxjs'; +import { delay } from 'rxjs/operators'; +import { Metadata } from '../../../../ng-swagger-gen/models/metadata'; +import { DistributionSetupModalComponent } from '../distribution-setup-modal/distribution-setup-modal.component'; + +describe('DistributionInstanceComponent', () => { + let component: DistributionInstancesComponent; + let fixture: ComponentFixture; + + const instance1: Instance = { + name: 'Test1', + default: true, + enabled: true, + description: 'Test1', + endpoint: 'http://test.com', + id: 1, + setup_timestamp: new Date().getTime(), + auth_mode: 'NONE', + vendor: 'kraken', + status: 'Healthy' + }; + + const instance2: Instance = { + name: 'Test2', + default: false, + enabled: false, + description: 'Test2', + endpoint: 'http://test2.com', + id: 2, + setup_timestamp: new Date().getTime() + 3600000, + auth_mode: 'BASIC', + auth_info: { + password: '123', + username: 'abc' + }, + vendor: 'kraken', + status: 'Healthy' + }; + + const instance3: Instance = { + name: 'Test3', + default: false, + enabled: true, + description: 'Test3', + endpoint: 'http://test3.com', + id: 3, + setup_timestamp: new Date().getTime() + 7200000, + auth_mode: 'OAUTH', + auth_info: { + token: 'xxxxxxxxxxxxxxxxxxxx' + }, + vendor: 'kraken', + status: 'Unhealthy' + }; + + const mockedProviders: Metadata[] = [{ + 'icon': 'https://raw.githubusercontent.com/alibaba/Dragonfly/master/docs/images/logo.png', + 'id': 'dragonfly', + 'maintainers': ['Jin Zhang/taiyun.zj@alibaba-inc.com'], + 'name': 'Dragonfly', + 'source': 'https://github.com/alibaba/Dragonfly', + 'version': '0.10.1' + }, { + 'icon': 'https://github.com/uber/kraken/blob/master/assets/kraken-logo-color.svg', + 'id': 'kraken', + 'maintainers': ['mmpei/peimingming@corp.netease.com'], + 'name': 'Kraken', + 'source': 'https://github.com/uber/kraken', + 'version': '0.1.3' + }]; + + + const fakedPreheatService = { + ListInstancesResponse() { + const res: HttpResponse> = new HttpResponse>({ + headers: new HttpHeaders({'x-total-count': '3'}), + body: [instance1, instance2, instance3] + }); + return of(res).pipe(delay(10)); + }, + ListProviders() { + return of(mockedProviders).pipe(delay(10)); + } + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ClarityModule, + TranslateModule, + SharedModule, + HttpClientTestingModule + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + providers: [ + { provide: PreheatService, useValue: fakedPreheatService } + ], + declarations: [ + DistributionInstancesComponent, + DistributionSetupModalComponent + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DistributionInstancesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should render list and get providers', async () => { + fixture.autoDetectChanges(true); + await fixture.whenStable(); + expect(component.providers.length).toEqual(2); + const rows = fixture.nativeElement.getElementsByTagName('clr-dg-row'); + expect(rows.length).toEqual(3); + }); + + it('should open modal', async () => { + fixture.autoDetectChanges(true); + await fixture.whenStable(); + const addButton: HTMLButtonElement = fixture.nativeElement.querySelector("#new-instance"); + addButton.click(); + await fixture.whenStable(); + const modal: HTMLElement = fixture.nativeElement.querySelector("clr-modal"); + expect(modal).toBeTruthy(); + }); +}); diff --git a/src/portal/src/app/distribution/distribution-instances/distribution-instances.component.ts b/src/portal/src/app/distribution/distribution-instances/distribution-instances.component.ts new file mode 100644 index 000000000..51e12d240 --- /dev/null +++ b/src/portal/src/app/distribution/distribution-instances/distribution-instances.component.ts @@ -0,0 +1,377 @@ +import { MessageHandlerService } from '../../shared/message-handler/message-handler.service'; +import { Component, OnInit, ViewChild, OnDestroy } from '@angular/core'; +import { + Subscription, + Observable, + forkJoin, + throwError as observableThrowError +} from 'rxjs'; +import { DistributionSetupModalComponent } from '../distribution-setup-modal/distribution-setup-modal.component'; +import { OperationService } from '../../../lib/components/operation/operation.service'; +import { + ConfirmationState, + ConfirmationTargets, + ConfirmationButtons +} from '../../shared/shared.const'; +import { ConfirmationDialogService } from '../../shared/confirmation-dialog/confirmation-dialog.service'; +import { ConfirmationMessage } from '../../shared/confirmation-dialog/confirmation-message'; +import { + operateChanges, + OperateInfo, + OperationState +} from '../../../lib/components/operation/operate'; +import { TranslateService } from '@ngx-translate/core'; +import { map, catchError, finalize } from 'rxjs/operators'; +import { errorHandler } from '../../../lib/utils/shared/shared.utils'; +import { clone, DEFAULT_PAGE_SIZE } from '../../../lib/utils/utils'; +import { Instance } from "../../../../ng-swagger-gen/models/instance"; +import { PreheatService } from "../../../../ng-swagger-gen/services/preheat.service"; +import { Metadata } from '../../../../ng-swagger-gen/models/metadata'; + +interface MultiOperateData { + operation: string; + instances: Instance[]; +} + +const DEFAULT_ICON: string = 'images/harbor-logo.svg'; +const KRAKEN_ICON: string = 'images/kraken-logo-color.svg'; +const ONE_THOUSAND: number = 1000; +const KRAKEN: string = 'kraken'; + +@Component({ + selector: 'dist-instances', + templateUrl: './distribution-instances.component.html', + styleUrls: ['./distribution-instances.component.scss'] +}) +export class DistributionInstancesComponent implements OnInit, OnDestroy { + instances: Instance[] = []; + selectedRow: Instance[] = []; + + pageSize: number = DEFAULT_PAGE_SIZE; + currentPage: number = 1; + totalCount: number = 0; + queryString: string; + + chanSub: Subscription; + + private loading: boolean = true; + private operationSubscription: Subscription; + + @ViewChild('setupModal', { static: false }) + setupModal: DistributionSetupModalComponent; + providerMap: {[key: string]: Metadata} = {}; + providers: Metadata[] = []; + constructor( + private disService: PreheatService, + private msgHandler: MessageHandlerService, + private translate: TranslateService, + private operationDialogService: ConfirmationDialogService, + private operationService: OperationService + ) { + // subscribe operation + this.operationSubscription = operationDialogService.confirmationConfirm$.subscribe( + confirmed => { + if ( + confirmed && + confirmed.source === ConfirmationTargets.INSTANCE && + confirmed.state === ConfirmationState.CONFIRMED + ) { + this.operateInstance(confirmed.data); + } + } + ); + } + + public get inProgress(): boolean { + return this.loading; + } + + ngOnInit() { + this.loadData(); + this.getProviders(); + } + + ngOnDestroy() { + if (this.operationSubscription) { + this.operationSubscription.unsubscribe(); + } + if (this.chanSub) { + this.chanSub.unsubscribe(); + } + } + + getProviders() { + this.disService.ListProviders().subscribe( + providers => { + if (providers && providers.length) { + this.providers = providers; + providers.forEach(item => { + this.providerMap[item.id] = item; + }); + } + }, + err => this.msgHandler.error(err) + ); + } + + loadData() { + this.selectedRow = []; + const queryParam: PreheatService.ListInstancesParams = { + page: this.currentPage, + pageSize: this.pageSize + }; + if (this.queryString) { + queryParam.q = encodeURIComponent(`name=~${this.queryString}`); + } + this.loading = true; + this.disService.ListInstancesResponse(queryParam) + .pipe(finalize(() => this.loading = false)) + .subscribe( + response => { + this.totalCount = Number.parseInt( + response.headers.get('x-total-count') + ); + this.instances = response.body as Instance[]; + }, + err => this.msgHandler.error(err) + ); + } + + refresh() { + this.queryString = null; + this.currentPage = 1; + this.loadData(); + } + + doFilter($evt: any) { + this.currentPage = 1; + this.queryString = $evt; + this.loadData(); + } + + addInstance() { + this.setupModal.openSetupModal(false); + } + + editInstance() { + if (this.selectedRow && this.selectedRow.length === 1) { + this.setupModal.openSetupModal(true, clone(this.selectedRow[0])); + } + } + + setAsDefault() { + if (this.selectedRow && this.selectedRow.length === 1) { + const operMessage = new OperateInfo(); + operMessage.name = 'DISTRIBUTION.SET_AS_DEFAULT'; + operMessage.data.id = this.selectedRow[0].id; + operMessage.state = OperationState.progressing; + operMessage.data.name = this.selectedRow[0].name; + this.operationService.publishInfo(operMessage); + this.disService.UpdateInstance({ + propertySet: {default: true}, + instanceId: this.selectedRow[0].id + }) + .subscribe( + () => { + this.translate.get('DISTRIBUTION.SET_DEFAULT_SUCCESS').subscribe(msg => { + operateChanges(operMessage, OperationState.success); + this.msgHandler.info(msg); + }); + this.refresh(); + }, + error => { + const message = errorHandler(error); + this.translate.get('DISTRIBUTION.SET_DEFAULT_FAILED').subscribe(msg => { + operateChanges(operMessage, OperationState.failure, msg); + this.translate.get(message).subscribe(errMsg => { + this.msgHandler.error(msg + ': ' + errMsg); + }); + }); + } + ); + } + } + // Operate the specified Instance + operateInstances(operation: string, instances: Instance[]): void { + let arr: string[] = []; + let title: string; + let summary: string; + let buttons: ConfirmationButtons; + + switch (operation) { + case 'delete': + title = 'DISTRIBUTION.DELETION_TITLE'; + summary = 'DISTRIBUTION.DELETION_SUMMARY'; + buttons = ConfirmationButtons.DELETE_CANCEL; + break; + case 'enable': + title = 'DISTRIBUTION.ENABLE_TITLE'; + summary = 'DISTRIBUTION.ENABLE_SUMMARY'; + buttons = ConfirmationButtons.ENABLE_CANCEL; + break; + case 'disable': + title = 'DISTRIBUTION.DISABLE_TITLE'; + summary = 'DISTRIBUTION.DISABLE_SUMMARY'; + buttons = ConfirmationButtons.DISABLE_CANCEL; + break; + + default: + return; + } + + if (instances && instances.length) { + instances.forEach(instance => { + arr.push(instance.name); + }); + } + // Confirm + let msg: ConfirmationMessage = new ConfirmationMessage( + title, + summary, + arr.join(','), + { operation: operation, instances: instances }, + ConfirmationTargets.INSTANCE, + buttons + ); + this.operationDialogService.openComfirmDialog(msg); + } + + operateInstance(data: MultiOperateData) { + let observableLists: any[] = []; + if (data.instances && data.instances.length) { + switch (data.operation) { + case 'delete': + data.instances.forEach(instance => { + observableLists.push(this.deleteInstance(instance)); + }); + break; + + case 'enable': + data.instances.forEach(instance => { + observableLists.push(this.enableInstance(instance)); + }); + break; + + case 'disable': + data.instances.forEach(instance => { + observableLists.push(this.disableInstance(instance)); + }); + break; + } + + forkJoin(...observableLists).subscribe(item => { + this.selectedRow = []; + this.refresh(); + }); + } + } + deleteInstance(instance: Instance): Observable { + let operMessage = new OperateInfo(); + operMessage.name = 'DISTRIBUTION.DELETE_INSTANCE'; + operMessage.data.id = instance.id; + operMessage.state = OperationState.progressing; + operMessage.data.name = instance.name; + this.operationService.publishInfo(operMessage); + + return this.disService.DeleteInstance({instanceId: instance.id}).pipe( + map(() => { + this.translate.get('DISTRIBUTION.DELETED_SUCCESS').subscribe(msg => { + operateChanges(operMessage, OperationState.success); + this.msgHandler.info(msg); + }); + }), + catchError(error => { + const message = errorHandler(error); + this.translate.get('DISTRIBUTION.DELETED_FAILED').subscribe(msg => { + operateChanges(operMessage, OperationState.failure, msg); + this.translate.get(message).subscribe(errMsg => { + this.msgHandler.error(msg + ': ' + errMsg); + }); + }); + return observableThrowError(message); + }) + ); + } + + enableInstance(instance: Instance) { + let operMessage = new OperateInfo(); + operMessage.name = 'DISTRIBUTION.ENABLE_INSTANCE'; + operMessage.data.id = instance.id; + operMessage.state = OperationState.progressing; + operMessage.data.name = instance.name; + this.operationService.publishInfo(operMessage); + + instance.enabled = true; + return this.disService + .UpdateInstance({ + propertySet: {enabled: true}, + instanceId: instance.id + }) + .pipe( + map(() => { + this.translate.get('DISTRIBUTION.ENABLE_SUCCESS').subscribe(msg => { + operateChanges(operMessage, OperationState.success); + this.msgHandler.info(msg); + }); + }), + catchError(error => { + const message = errorHandler(error); + this.translate.get('DISTRIBUTION.ENABLE_FAILED').subscribe(msg => { + operateChanges(operMessage, OperationState.failure, msg); + this.translate.get(message).subscribe(errMsg => { + this.msgHandler.error(msg + ': ' + errMsg); + }); + }); + return observableThrowError(message); + }) + ); + } + + disableInstance(instance: Instance) { + let operMessage = new OperateInfo(); + operMessage.name = 'DISTRIBUTION.DISABLE_INSTANCE'; + operMessage.data.id = instance.id; + operMessage.state = OperationState.progressing; + operMessage.data.name = instance.name; + this.operationService.publishInfo(operMessage); + + instance.enabled = false; + return this.disService + .UpdateInstance({ + propertySet: {enabled: false}, + instanceId: instance.id + }) + .pipe( + map(() => { + this.translate.get('DISTRIBUTION.DISABLE_SUCCESS').subscribe(msg => { + operateChanges(operMessage, OperationState.success); + this.msgHandler.info(msg); + }); + }), + catchError(error => { + const message = errorHandler(error); + this.translate.get('DISTRIBUTION.DISABLE_FAILED').subscribe(msg => { + operateChanges(operMessage, OperationState.failure, msg); + this.translate.get(message).subscribe(errMsg => { + this.msgHandler.error(msg + ': ' + errMsg); + }); + }); + return observableThrowError(message); + }) + ); + } + + fmtTime(time: number) { + let date = new Date(); + return date.setTime(time * ONE_THOUSAND); + } + showDefaultIcon(event: any, vendor: string) { + if (event && event.target) { + if (KRAKEN === vendor) { + event.target.src = KRAKEN_ICON; + } else { + event.target.src = DEFAULT_ICON; + } + } + } +} diff --git a/src/portal/src/app/distribution/distribution-interface.ts b/src/portal/src/app/distribution/distribution-interface.ts new file mode 100644 index 000000000..c29871336 --- /dev/null +++ b/src/portal/src/app/distribution/distribution-interface.ts @@ -0,0 +1,16 @@ +export class AuthMode { + static NONE = 'NONE'; + static BASIC = 'BASIC'; + static OAUTH = 'OAUTH'; + static CUSTOM = 'CUSTOM'; +} + +export enum PreheatingStatusEnum { + // front status + NOT_PREHEATED = 'NOT_PREHEATED', + // back-end status + PENDING = 'PENDING', + RUNNING = 'RUNNING', + SUCCESS = 'SUCCESS', + FAIL = 'FAIL', +} diff --git a/src/portal/src/app/distribution/distribution-setup-modal/distribution-setup-modal.component.html b/src/portal/src/app/distribution/distribution-setup-modal/distribution-setup-modal.component.html new file mode 100644 index 000000000..167525030 --- /dev/null +++ b/src/portal/src/app/distribution/distribution-setup-modal/distribution-setup-modal.component.html @@ -0,0 +1,238 @@ + + + + + diff --git a/src/portal/src/app/distribution/distribution-setup-modal/distribution-setup-modal.component.scss b/src/portal/src/app/distribution/distribution-setup-modal/distribution-setup-modal.component.scss new file mode 100644 index 000000000..1cda2ae16 --- /dev/null +++ b/src/portal/src/app/distribution/distribution-setup-modal/distribution-setup-modal.component.scss @@ -0,0 +1,3 @@ +.display-none { + display: none; +} diff --git a/src/portal/src/app/distribution/distribution-setup-modal/distribution-setup-modal.component.spec.ts b/src/portal/src/app/distribution/distribution-setup-modal/distribution-setup-modal.component.spec.ts new file mode 100644 index 000000000..cc9a20126 --- /dev/null +++ b/src/portal/src/app/distribution/distribution-setup-modal/distribution-setup-modal.component.spec.ts @@ -0,0 +1,100 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { ClarityModule } from '@clr/angular'; +import { SharedModule } from '../../shared/shared.module'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { DistributionSetupModalComponent } from './distribution-setup-modal.component'; +import { PreheatService } from "../../../../ng-swagger-gen/services/preheat.service"; +import { Instance } from '../../../../ng-swagger-gen/models/instance'; + +describe('DistributionSetupModalComponent', () => { + let component: DistributionSetupModalComponent; + let fixture: ComponentFixture; + + const instance1: Instance = { + name: 'Test1', + default: true, + enabled: true, + description: 'Test1', + endpoint: 'http://test.com', + id: 1, + setup_timestamp: new Date().getTime(), + auth_mode: 'NONE', + vendor: 'kraken', + status: 'Healthy' + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ClarityModule, + TranslateModule, + SharedModule, + HttpClientTestingModule + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + providers: [PreheatService], + declarations: [DistributionSetupModalComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DistributionSetupModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should show "name is required"', async () => { + fixture.autoDetectChanges(); + component._open(); + await fixture.whenStable(); + const nameInput = fixture.nativeElement.querySelector('#name'); + nameInput.value = ""; + nameInput.dispatchEvent(new Event('input')); + nameInput.blur(); + nameInput.dispatchEvent(new Event('blur')); + let el = fixture.nativeElement.querySelector('clr-control-error'); + expect(el).toBeTruthy(); + }); + + it('should show "endpoint is required"', async () => { + fixture.autoDetectChanges(); + component._open(); + await fixture.whenStable(); + const endpointInput = fixture.nativeElement.querySelector('#endpoint'); + endpointInput.value = "svn://test.com"; + endpointInput.dispatchEvent(new Event('input')); + endpointInput.blur(); + endpointInput.dispatchEvent(new Event('blur')); + let el = fixture.nativeElement.querySelector('clr-control-error'); + expect(el).toBeTruthy(); + }); + + it('should be edit model', async () => { + fixture.autoDetectChanges(); + component.openSetupModal(true, instance1); + await fixture.whenStable(); + const nameInput = fixture.nativeElement.querySelector('#name'); + expect(nameInput.value).toEqual('Test1'); + }); + + it('should be valid', async () => { + fixture.autoDetectChanges(); + component._open(); + await fixture.whenStable(); + component.model.vendor = 'kraken'; + const nameInput = fixture.nativeElement.querySelector('#name'); + nameInput.value = "test"; + nameInput.dispatchEvent(new Event('input')); + const endpointInput = fixture.nativeElement.querySelector('#endpoint'); + endpointInput.value = "https://test.com"; + endpointInput.dispatchEvent(new Event('input')); + await fixture.whenStable(); + expect(component.isValid).toBeTruthy(); + }); +}); diff --git a/src/portal/src/app/distribution/distribution-setup-modal/distribution-setup-modal.component.ts b/src/portal/src/app/distribution/distribution-setup-modal/distribution-setup-modal.component.ts new file mode 100644 index 000000000..ae372a680 --- /dev/null +++ b/src/portal/src/app/distribution/distribution-setup-modal/distribution-setup-modal.component.ts @@ -0,0 +1,219 @@ +import { MessageHandlerService } from '../../shared/message-handler/message-handler.service'; +import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; +import { NgForm } from '@angular/forms'; +import { TranslateService } from '@ngx-translate/core'; +import { errorHandler } from '../../../lib/utils/shared/shared.utils'; +import { PreheatService } from "../../../../ng-swagger-gen/services/preheat.service"; +import { Instance } from "../../../../ng-swagger-gen/models/instance"; +import { AuthMode } from "../distribution-interface"; +import { clone } from '../../../lib/utils/utils'; +import { InlineAlertComponent } from "../../shared/inline-alert/inline-alert.component"; +import { ClrLoadingState } from "@clr/angular"; +import { Metadata } from '../../../../ng-swagger-gen/models/metadata'; +import { operateChanges, OperateInfo, OperationState } from '../../../lib/components/operation/operate'; +import { OperationService } from '../../../lib/components/operation/operation.service'; + +@Component({ + selector: 'dist-setup-modal', + templateUrl: './distribution-setup-modal.component.html', + styleUrls: ['./distribution-setup-modal.component.scss'] +}) +export class DistributionSetupModalComponent implements OnInit { + @Input() + providers: Metadata[] = []; + model: Instance; + originModelForEdit: Instance; + opened: boolean = false; + editingMode: boolean = false; + authData: {[key: string]: any} = {}; + @ViewChild('instanceForm', { static: true }) instanceForm: NgForm; + @ViewChild(InlineAlertComponent, { static: false }) inlineAlert: InlineAlertComponent; + saveBtnState: ClrLoadingState = ClrLoadingState.DEFAULT; + + @Output() + refresh: EventEmitter = new EventEmitter(); + + constructor( + private distributionService: PreheatService, + private msgHandler: MessageHandlerService, + private translate: TranslateService, + private operationService: OperationService + ) {} + + ngOnInit() { + this.reset(); + } + + public get isValid(): boolean { + return this.instanceForm && this.instanceForm.valid; + } + + get title(): string { + return this.editingMode + ? 'DISTRIBUTION.EDIT_INSTANCE' + : 'DISTRIBUTION.SETUP_NEW_INSTANCE'; + } + + authModeChange() { + if (this.editingMode && this.model.auth_mode === this.originModelForEdit.auth_mode) { + this.authData = clone(this.originModelForEdit.auth_info); + } else { + switch (this.model.auth_mode) { + case AuthMode.BASIC: + this.authData = { + password: '', + username: '' + }; + break; + case AuthMode.OAUTH: + this.authData = { + token: '' + }; + break; + default: + this.authData = null; + break; + } + } + } + + _open() { + this.inlineAlert.close(); + this.opened = true; + } + + _close() { + this.opened = false; + this.reset(); + } + + reset() { + this.model = { + name: '', + endpoint: '', + enabled: true, + vendor: '', + auth_mode: AuthMode.NONE, + auth_info: this.authData + }; + this.instanceForm.reset(); + } + + cancel() { + this._close(); + } + + submit() { + if (this.editingMode) { + const data: Instance = { + endpoint: this.model.endpoint, + enabled: this.model.enabled, + description: this.model.description, + auth_mode: this.model.auth_mode, + auth_info: this.model.auth_info + }; + const operMessageForEdit = new OperateInfo(); + operMessageForEdit.name = 'DISTRIBUTION.UPDATE_INSTANCE'; + operMessageForEdit.data.id = this.model.id; + operMessageForEdit.state = OperationState.progressing; + operMessageForEdit.data.name = this.model.name; + this.operationService.publishInfo(operMessageForEdit); + this.saveBtnState = ClrLoadingState.LOADING; + this.distributionService.UpdateInstance({instanceId: this.model.id, propertySet: data + }).subscribe( + response => { + this.translate.get('DISTRIBUTION.UPDATE_SUCCESS').subscribe(msg => { + operateChanges(operMessageForEdit, OperationState.success); + this.msgHandler.info(msg); + }); + this.saveBtnState = ClrLoadingState.SUCCESS; + this._close(); + this.refresh.emit(); + }, + err => { + const message = errorHandler(err); + this.translate.get('DISTRIBUTION.UPDATE_FAILED').subscribe(msg => { + this.translate.get(message).subscribe(errMsg => { + operateChanges(operMessageForEdit, OperationState.failure, msg); + this.inlineAlert.showInlineError(msg + ': ' + errMsg); + this.saveBtnState = ClrLoadingState.ERROR; + }); + }); + } + ); + } else { + const operMessage = new OperateInfo(); + operMessage.name = 'DISTRIBUTION.CREATE_INSTANCE'; + operMessage.state = OperationState.progressing; + operMessage.data.name = this.model.name; + this.operationService.publishInfo(operMessage); + this.saveBtnState = ClrLoadingState.LOADING; + if (this.model.auth_mode !== AuthMode.NONE) { + this.model.auth_info = this.authData; + } else { + delete this.model.auth_info; + } + this.distributionService.CreateInstance({instance: this.model}).subscribe( + response => { + this.translate.get('DISTRIBUTION.CREATE_SUCCESS').subscribe(msg => { + operateChanges(operMessage, OperationState.success); + this.msgHandler.info(msg); + }); + this.saveBtnState = ClrLoadingState.SUCCESS; + this._close(); + this.refresh.emit(); + }, + err => { + const message = errorHandler(err); + this.translate.get('DISTRIBUTION.CREATE_FAILED').subscribe(msg => { + this.translate.get(message).subscribe(errMsg => { + operateChanges(operMessage, OperationState.failure, msg); + this.inlineAlert.showInlineError(msg + ': ' + errMsg); + this.saveBtnState = ClrLoadingState.ERROR; + }); + }); + } + ); + } + } + + openSetupModal(editingMode: boolean, data?: Instance): void { + this.editingMode = editingMode; + this._open(); + if (editingMode) { + this.model = clone(data); + this.originModelForEdit = clone(data); + this.authData = this.model.auth_info || {}; + } + } + + hasChangesForEdit(): boolean { + if ( this.editingMode) { + if ( this.model.description !== this.originModelForEdit.description) { + return true; + } + if ( this.model.endpoint !== this.originModelForEdit.endpoint) { + return true; + } + if (this.model.auth_mode !== this.originModelForEdit.auth_mode) { + return true; + } else { + if (this.model.auth_mode === AuthMode.BASIC) { + if (this.originModelForEdit.auth_info['username'] !== this.authData['username']) { + return true; + } + if (this.originModelForEdit.auth_info['password'] !== this.authData['password']) { + return true; + } + } + if (this.model.auth_mode === AuthMode.OAUTH) { + if (this.originModelForEdit.auth_info['token'] !== this.authData['token']) { + return true; + } + } + return false; + } + } + return true; + } +} diff --git a/src/portal/src/app/distribution/distribution.module.ts b/src/portal/src/app/distribution/distribution.module.ts new file mode 100644 index 000000000..2a850b8d8 --- /dev/null +++ b/src/portal/src/app/distribution/distribution.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { DistributionInstancesComponent } from './distribution-instances/distribution-instances.component'; +import { DistributionSetupModalComponent } from './distribution-setup-modal/distribution-setup-modal.component'; +import { SharedModule } from '../shared/shared.module'; + +@NgModule({ + imports: [CommonModule, SharedModule], + declarations: [ + DistributionSetupModalComponent, + DistributionInstancesComponent + ], +}) +export class DistributionModule {} diff --git a/src/portal/src/app/harbor-routing.module.ts b/src/portal/src/app/harbor-routing.module.ts index 36f908594..5ec062edf 100644 --- a/src/portal/src/app/harbor-routing.module.ts +++ b/src/portal/src/app/harbor-routing.module.ts @@ -63,6 +63,7 @@ import { ArtifactSummaryComponent } from "./project/repository/artifact/artifact import { ReplicationTasksComponent } from "../lib/components/replication/replication-tasks/replication-tasks.component"; import { ReplicationTasksRoutingResolverService } from "./services/routing-resolvers/replication-tasks-routing-resolver.service"; import { ArtifactDetailRoutingResolverService } from "./services/routing-resolvers/artifact-detail-routing-resolver.service"; +import { DistributionInstancesComponent } from './distribution/distribution-instances/distribution-instances.component'; const harborRoutes: Routes = [ { path: '', redirectTo: 'harbor', pathMatch: 'full' }, @@ -124,6 +125,11 @@ const harborRoutes: Routes = [ canActivate: [SystemAdminGuard], canActivateChild: [SystemAdminGuard] }, + { + path: 'distribution/instances', + component: DistributionInstancesComponent, + canActivate: [SystemAdminGuard] + }, { path: 'interrogation-services', component: InterrogationServicesComponent, diff --git a/src/portal/src/app/shared/shared.const.ts b/src/portal/src/app/shared/shared.const.ts index e53673ba9..edc4250ae 100644 --- a/src/portal/src/app/shared/shared.const.ts +++ b/src/portal/src/app/shared/shared.const.ts @@ -43,7 +43,8 @@ export const enum ConfirmationTargets { HELM_CHART, HELM_CHART_VERSION, WEBHOOK, - SCANNER + SCANNER, + INSTANCE } export const enum ActionType { @@ -99,4 +100,4 @@ export enum ResourceType { REPOSITORY = 1, CHART_VERSION = 2, REPOSITORY_TAG = 3, -} \ No newline at end of file +} diff --git a/src/portal/src/i18n/lang/en-us-lang.json b/src/portal/src/i18n/lang/en-us-lang.json index c4a480786..a3a7b16cb 100644 --- a/src/portal/src/i18n/lang/en-us-lang.json +++ b/src/portal/src/i18n/lang/en-us-lang.json @@ -175,7 +175,11 @@ "TASKS": "Tasks", "API_EXPLORER": "Api Explorer", "HARBOR_API_MANAGEMENT": "Harbor API V2.0", - "HELM_API_MANAGEMENT": "Harbor API" + "HELM_API_MANAGEMENT": "Harbor API", + "DISTRIBUTIONS": { + "NAME": "Distributions", + "INSTANCES": "Instances" + } }, "USER": { "ADD_ACTION": "New User", @@ -1425,6 +1429,71 @@ "HELP_INFO_1": "The default scanner has been installed. To install other scanners refer to the ", "HELP_INFO_2": "documentation.", "NO_DEFAULT_SCANNER": "No default scanner" - + }, + "DISTRIBUTION": { + "FILTER_INSTANCE_PLACEHOLDER": "Filter instances", + "FILTER_HISTORIES_PLACEHOLDER": "Filter histories", + "ADD_ACTION": "NEW INSTANCE", + "PREHEAT_ACTION": "Preheat", + "EDIT_ACTION": "Edit", + "ENABLE_ACTION": "Enable", + "DISABLE_ACTION": "Disable", + "DELETE_ACTION": "Delete", + "NOT_FOUND": "Not found", + "NAME": "Name", + "ENDPOINT": "Endpoint", + "STATUS": "Status", + "ENABLED": "Enable", + "SETUP_TIMESTAMP": "Setup Timestamp", + "PROVIDER": "Provider", + "DELETION_TITLE": "Confirm instance deletion", + "DELETION_SUMMARY": "Do you want to delete instance(s) {{param}}?", + "ENABLE_TITLE": "Enable instance(s)", + "ENABLE_SUMMARY": "Do you want to enable instance(s) {{param}}?", + "DISABLE_TITLE": "Disable instance(s)", + "DISABLE_SUMMARY": "Do you want to disable instance(s) {{param}}?", + "IMAGE": "Image", + "START_TIME": "Start time", + "FINISH_TIME": "Finish Time", + "INSTANCE": "Instance", + "HISTORIES": "Histories", + "CREATE_SUCCESS": "Instance created successfully", + "CREATE_FAILED": "Creating instance failed", + "DELETED_SUCCESS": "Instance(s) deleted successfully", + "DELETED_FAILED": "Deleting instance(s) failed", + "ENABLE_SUCCESS": "Instance(s) enabled successfully", + "ENABLE_FAILED": "Enabling instance(s) failed", + "DISABLE_SUCCESS": "Instance(s) disabled successfully", + "DISABLE_FAILED": "Disabling instance(s) failed", + "UPDATE_SUCCESS": "Instance updated successfully", + "UPDATE_FAILED": "Updating instance failed", + "REQUEST_PREHEAT_SUCCESS": "Preheat request successfully", + "REQUEST_PREHEAT_FAILED": "Preheat request failed", + "DESCRIPTION": "description", + "AUTH_MODE": "Auth Mode", + "USERNAME": "Username", + "PASSWORD": "Password", + "TOKEN": "Token", + "SETUP_NEW_INSTANCE": "Setup new instance", + "EDIT_INSTANCE": "Edit instance", + "SETUP": { + "NAME_PLACEHOLDER": "Input instance's name", + "DESCRIPTION_PLACEHOLDER": "Input instance's description", + "ENDPOINT_PLACEHOLDER": "Input instance's endpoint ", + "USERNAME_PLACEHOLDER": "Input username", + "PASSWORD_PLACEHOLDER": "Input password", + "TOKEN_PLACEHOLDER": "Input token" + }, + "MAINTAINER": "Maintainer(s)", + "SOURCE": "Source", + "VERSION": "Version", + "SET_AS_DEFAULT": "Set as default", + "DELETE_INSTANCE": "Delete instance", + "ENABLE_INSTANCE": "Enable instance", + "DISABLE_INSTANCE": "Disable instance", + "SET_DEFAULT_SUCCESS": "Set as default successfully", + "SET_DEFAULT_FAILED": "Setting as default failed", + "UPDATE_INSTANCE": "Update instance", + "CREATE_INSTANCE": "Create instance" } } diff --git a/src/portal/src/i18n/lang/es-es-lang.json b/src/portal/src/i18n/lang/es-es-lang.json index b7d21acff..524945e86 100644 --- a/src/portal/src/i18n/lang/es-es-lang.json +++ b/src/portal/src/i18n/lang/es-es-lang.json @@ -175,7 +175,11 @@ "TASKS": "Tasks", "API_EXPLORER": "Api Explorer", "HARBOR_API_MANAGEMENT": "Harbor API V2.0", - "HELM_API_MANAGEMENT": "Harbor API" + "HELM_API_MANAGEMENT": "Harbor API", + "DISTRIBUTIONS": { + "NAME": "Distributions", + "INSTANCES": "Instances" + } }, "USER": { "ADD_ACTION": "New User", @@ -1423,5 +1427,71 @@ "HELP_INFO_1": "The default scanner has been installed. To install other scanners refer to the ", "HELP_INFO_2": "documentation.", "NO_DEFAULT_SCANNER": "No default scanner" + }, + "DISTRIBUTION": { + "FILTER_INSTANCE_PLACEHOLDER": "Filter instances", + "FILTER_HISTORIES_PLACEHOLDER": "Filter histories", + "ADD_ACTION": "NEW INSTANCE", + "PREHEAT_ACTION": "Preheat", + "EDIT_ACTION": "Edit", + "ENABLE_ACTION": "Enable", + "DISABLE_ACTION": "Disable", + "DELETE_ACTION": "Delete", + "NOT_FOUND": "Not found", + "NAME": "Name", + "ENDPOINT": "Endpoint", + "STATUS": "Status", + "ENABLED": "Enable", + "SETUP_TIMESTAMP": "Setup Timestamp", + "PROVIDER": "Provider", + "DELETION_TITLE": "Confirm instance deletion", + "DELETION_SUMMARY": "Do you want to delete instance(s) {{param}}?", + "ENABLE_TITLE": "Enable instance(s)", + "ENABLE_SUMMARY": "Do you want to enable instance(s) {{param}}?", + "DISABLE_TITLE": "Disable instance(s)", + "DISABLE_SUMMARY": "Do you want to disable instance(s) {{param}}?", + "IMAGE": "Image", + "START_TIME": "Start time", + "FINISH_TIME": "Finish Time", + "INSTANCE": "Instance", + "HISTORIES": "Histories", + "CREATE_SUCCESS": "Instance created successfully", + "CREATE_FAILED": "Creating instance failed", + "DELETED_SUCCESS": "Instance(s) deleted successfully", + "DELETED_FAILED": "Deleting instance(s) failed", + "ENABLE_SUCCESS": "Instance(s) enabled successfully", + "ENABLE_FAILED": "Enabling instance(s) failed", + "DISABLE_SUCCESS": "Instance(s) disabled successfully", + "DISABLE_FAILED": "Disabling instance(s) failed", + "UPDATE_SUCCESS": "Instance updated successfully", + "UPDATE_FAILED": "Updating instance failed", + "REQUEST_PREHEAT_SUCCESS": "Preheat request successfully", + "REQUEST_PREHEAT_FAILED": "Preheat request failed", + "DESCRIPTION": "description", + "AUTH_MODE": "Auth Mode", + "USERNAME": "Username", + "PASSWORD": "Password", + "TOKEN": "Token", + "SETUP_NEW_INSTANCE": "Setup new instance", + "EDIT_INSTANCE": "Edit instance", + "SETUP": { + "NAME_PLACEHOLDER": "Input instance's name", + "DESCRIPTION_PLACEHOLDER": "Input instance's description", + "ENDPOINT_PLACEHOLDER": "Input instance's endpoint ", + "USERNAME_PLACEHOLDER": "Input username", + "PASSWORD_PLACEHOLDER": "Input password", + "TOKEN_PLACEHOLDER": "Input token" + }, + "MAINTAINER": "Maintainer(s)", + "SOURCE": "Source", + "VERSION": "Version", + "SET_AS_DEFAULT": "Set as default", + "DELETE_INSTANCE": "Delete instance", + "ENABLE_INSTANCE": "Enable instance", + "DISABLE_INSTANCE": "Disable instance", + "SET_DEFAULT_SUCCESS": "Set as default successfully", + "SET_DEFAULT_FAILED": "Setting as default failed", + "UPDATE_INSTANCE": "Update instance", + "CREATE_INSTANCE": "Create instance" } } diff --git a/src/portal/src/i18n/lang/fr-fr-lang.json b/src/portal/src/i18n/lang/fr-fr-lang.json index 5f918dbb3..0d446c0da 100644 --- a/src/portal/src/i18n/lang/fr-fr-lang.json +++ b/src/portal/src/i18n/lang/fr-fr-lang.json @@ -169,7 +169,11 @@ "TASKS": "Tasks", "API_EXPLORER": "Api Explorer", "HARBOR_API_MANAGEMENT": "Harbor API V2.0", - "HELM_API_MANAGEMENT": "Harbor API" + "HELM_API_MANAGEMENT": "Harbor API", + "DISTRIBUTIONS": { + "NAME": "Distributions", + "INSTANCES": "Instances" + } }, "USER": { "ADD_ACTION": "UTILISATEUR", @@ -1393,5 +1397,71 @@ "HELP_INFO_1": "The default scanner has been installed. To install other scanners refer to the ", "HELP_INFO_2": "documentation.", "NO_DEFAULT_SCANNER": "No default scanner" + }, + "DISTRIBUTION": { + "FILTER_INSTANCE_PLACEHOLDER": "Filter instances", + "FILTER_HISTORIES_PLACEHOLDER": "Filter histories", + "ADD_ACTION": "NEW INSTANCE", + "PREHEAT_ACTION": "Preheat", + "EDIT_ACTION": "Edit", + "ENABLE_ACTION": "Enable", + "DISABLE_ACTION": "Disable", + "DELETE_ACTION": "Delete", + "NOT_FOUND": "Not found", + "NAME": "Name", + "ENDPOINT": "Endpoint", + "STATUS": "Status", + "ENABLED": "Enable", + "SETUP_TIMESTAMP": "Setup Timestamp", + "PROVIDER": "Provider", + "DELETION_TITLE": "Confirm instance deletion", + "DELETION_SUMMARY": "Do you want to delete instance(s) {{param}}?", + "ENABLE_TITLE": "Enable instance(s)", + "ENABLE_SUMMARY": "Do you want to enable instance(s) {{param}}?", + "DISABLE_TITLE": "Disable instance(s)", + "DISABLE_SUMMARY": "Do you want to disable instance(s) {{param}}?", + "IMAGE": "Image", + "START_TIME": "Start time", + "FINISH_TIME": "Finish Time", + "INSTANCE": "Instance", + "HISTORIES": "Histories", + "CREATE_SUCCESS": "Instance created successfully", + "CREATE_FAILED": "Creating instance failed", + "DELETED_SUCCESS": "Instance(s) deleted successfully", + "DELETED_FAILED": "Deleting instance(s) failed", + "ENABLE_SUCCESS": "Instance(s) enabled successfully", + "ENABLE_FAILED": "Enabling instance(s) failed", + "DISABLE_SUCCESS": "Instance(s) disabled successfully", + "DISABLE_FAILED": "Disabling instance(s) failed", + "UPDATE_SUCCESS": "Instance updated successfully", + "UPDATE_FAILED": "Updating instance failed", + "REQUEST_PREHEAT_SUCCESS": "Preheat request successfully", + "REQUEST_PREHEAT_FAILED": "Preheat request failed", + "DESCRIPTION": "description", + "AUTH_MODE": "Auth Mode", + "USERNAME": "Username", + "PASSWORD": "Password", + "TOKEN": "Token", + "SETUP_NEW_INSTANCE": "Setup new instance", + "EDIT_INSTANCE": "Edit instance", + "SETUP": { + "NAME_PLACEHOLDER": "Input instance's name", + "DESCRIPTION_PLACEHOLDER": "Input instance's description", + "ENDPOINT_PLACEHOLDER": "Input instance's endpoint ", + "USERNAME_PLACEHOLDER": "Input username", + "PASSWORD_PLACEHOLDER": "Input password", + "TOKEN_PLACEHOLDER": "Input token" + }, + "MAINTAINER": "Maintainer(s)", + "SOURCE": "Source", + "VERSION": "Version", + "SET_AS_DEFAULT": "Set as default", + "DELETE_INSTANCE": "Delete instance", + "ENABLE_INSTANCE": "Enable instance", + "DISABLE_INSTANCE": "Disable instance", + "SET_DEFAULT_SUCCESS": "Set as default successfully", + "SET_DEFAULT_FAILED": "Setting as default failed", + "UPDATE_INSTANCE": "Update instance", + "CREATE_INSTANCE": "Create instance" } } diff --git a/src/portal/src/i18n/lang/pt-br-lang.json b/src/portal/src/i18n/lang/pt-br-lang.json index c63fe0ad3..698c749cf 100644 --- a/src/portal/src/i18n/lang/pt-br-lang.json +++ b/src/portal/src/i18n/lang/pt-br-lang.json @@ -173,7 +173,11 @@ "TASKS": "Tasks", "API_EXPLORER": "Api Explorer", "HARBOR_API_MANAGEMENT": "Harbor API V2.0", - "HELM_API_MANAGEMENT": "Harbor API" + "HELM_API_MANAGEMENT": "Harbor API", + "DISTRIBUTIONS": { + "NAME": "Distributions", + "INSTANCES": "Instances" + } }, "USER": { "ADD_ACTION": "Novo Usuário", @@ -1421,6 +1425,72 @@ "HELP_INFO_1": "The default scanner has been installed. To install other scanners refer to the ", "HELP_INFO_2": "documentation.", "NO_DEFAULT_SCANNER": "No default scanner" + }, + "DISTRIBUTION": { + "FILTER_INSTANCE_PLACEHOLDER": "Filter instances", + "FILTER_HISTORIES_PLACEHOLDER": "Filter histories", + "ADD_ACTION": "NEW INSTANCE", + "PREHEAT_ACTION": "Preheat", + "EDIT_ACTION": "Edit", + "ENABLE_ACTION": "Enable", + "DISABLE_ACTION": "Disable", + "DELETE_ACTION": "Delete", + "NOT_FOUND": "Not found", + "NAME": "Name", + "ENDPOINT": "Endpoint", + "STATUS": "Status", + "ENABLED": "Enable", + "SETUP_TIMESTAMP": "Setup Timestamp", + "PROVIDER": "Provider", + "DELETION_TITLE": "Confirm instance deletion", + "DELETION_SUMMARY": "Do you want to delete instance(s) {{param}}?", + "ENABLE_TITLE": "Enable instance(s)", + "ENABLE_SUMMARY": "Do you want to enable instance(s) {{param}}?", + "DISABLE_TITLE": "Disable instance(s)", + "DISABLE_SUMMARY": "Do you want to disable instance(s) {{param}}?", + "IMAGE": "Image", + "START_TIME": "Start time", + "FINISH_TIME": "Finish Time", + "INSTANCE": "Instance", + "HISTORIES": "Histories", + "CREATE_SUCCESS": "Instance created successfully", + "CREATE_FAILED": "Creating instance failed", + "DELETED_SUCCESS": "Instance(s) deleted successfully", + "DELETED_FAILED": "Deleting instance(s) failed", + "ENABLE_SUCCESS": "Instance(s) enabled successfully", + "ENABLE_FAILED": "Enabling instance(s) failed", + "DISABLE_SUCCESS": "Instance(s) disabled successfully", + "DISABLE_FAILED": "Disabling instance(s) failed", + "UPDATE_SUCCESS": "Instance updated successfully", + "UPDATE_FAILED": "Updating instance failed", + "REQUEST_PREHEAT_SUCCESS": "Preheat request successfully", + "REQUEST_PREHEAT_FAILED": "Preheat request failed", + "DESCRIPTION": "description", + "AUTH_MODE": "Auth Mode", + "USERNAME": "Username", + "PASSWORD": "Password", + "TOKEN": "Token", + "SETUP_NEW_INSTANCE": "Setup new instance", + "EDIT_INSTANCE": "Edit instance", + "SETUP": { + "NAME_PLACEHOLDER": "Input instance's name", + "DESCRIPTION_PLACEHOLDER": "Input instance's description", + "ENDPOINT_PLACEHOLDER": "Input instance's endpoint ", + "USERNAME_PLACEHOLDER": "Input username", + "PASSWORD_PLACEHOLDER": "Input password", + "TOKEN_PLACEHOLDER": "Input token" + }, + "MAINTAINER": "Maintainer(s)", + "SOURCE": "Source", + "VERSION": "Version", + "SET_AS_DEFAULT": "Set as default", + "DELETE_INSTANCE": "Delete instance", + "ENABLE_INSTANCE": "Enable instance", + "DISABLE_INSTANCE": "Disable instance", + "SET_DEFAULT_SUCCESS": "Set as default successfully", + "SET_DEFAULT_FAILED": "Setting as default failed", + "UPDATE_INSTANCE": "Update instance", + "CREATE_INSTANCE": "Create instance" } } diff --git a/src/portal/src/i18n/lang/tr-tr-lang.json b/src/portal/src/i18n/lang/tr-tr-lang.json index 7767e241c..296ce08a1 100644 --- a/src/portal/src/i18n/lang/tr-tr-lang.json +++ b/src/portal/src/i18n/lang/tr-tr-lang.json @@ -175,7 +175,11 @@ "TASKS": "Görevler", "API_EXPLORER": "Api Explorer", "HARBOR_API_MANAGEMENT": "Harbor API V2.0", - "HELM_API_MANAGEMENT": "Harbor API" + "HELM_API_MANAGEMENT": "Harbor API", + "DISTRIBUTIONS": { + "NAME": "Distributions", + "INSTANCES": "Instances" + } }, "USER": { "ADD_ACTION": "Yeni Kullanıcı", @@ -1425,5 +1429,71 @@ "HELP_INFO_1": "The default scanner has been installed. To install other scanners refer to the ", "HELP_INFO_2": "documentation.", "NO_DEFAULT_SCANNER": "No default scanner" + }, + "DISTRIBUTION": { + "FILTER_INSTANCE_PLACEHOLDER": "Filter instances", + "FILTER_HISTORIES_PLACEHOLDER": "Filter histories", + "ADD_ACTION": "NEW INSTANCE", + "PREHEAT_ACTION": "Preheat", + "EDIT_ACTION": "Edit", + "ENABLE_ACTION": "Enable", + "DISABLE_ACTION": "Disable", + "DELETE_ACTION": "Delete", + "NOT_FOUND": "Not found", + "NAME": "Name", + "ENDPOINT": "Endpoint", + "STATUS": "Status", + "ENABLED": "Enable", + "SETUP_TIMESTAMP": "Setup Timestamp", + "PROVIDER": "Provider", + "DELETION_TITLE": "Confirm instance deletion", + "DELETION_SUMMARY": "Do you want to delete instance(s) {{param}}?", + "ENABLE_TITLE": "Enable instance(s)", + "ENABLE_SUMMARY": "Do you want to enable instance(s) {{param}}?", + "DISABLE_TITLE": "Disable instance(s)", + "DISABLE_SUMMARY": "Do you want to disable instance(s) {{param}}?", + "IMAGE": "Image", + "START_TIME": "Start time", + "FINISH_TIME": "Finish Time", + "INSTANCE": "Instance", + "HISTORIES": "Histories", + "CREATE_SUCCESS": "Instance created successfully", + "CREATE_FAILED": "Creating instance failed", + "DELETED_SUCCESS": "Instance(s) deleted successfully", + "DELETED_FAILED": "Deleting instance(s) failed", + "ENABLE_SUCCESS": "Instance(s) enabled successfully", + "ENABLE_FAILED": "Enabling instance(s) failed", + "DISABLE_SUCCESS": "Instance(s) disabled successfully", + "DISABLE_FAILED": "Disabling instance(s) failed", + "UPDATE_SUCCESS": "Instance updated successfully", + "UPDATE_FAILED": "Updating instance failed", + "REQUEST_PREHEAT_SUCCESS": "Preheat request successfully", + "REQUEST_PREHEAT_FAILED": "Preheat request failed", + "DESCRIPTION": "description", + "AUTH_MODE": "Auth Mode", + "USERNAME": "Username", + "PASSWORD": "Password", + "TOKEN": "Token", + "SETUP_NEW_INSTANCE": "Setup new instance", + "EDIT_INSTANCE": "Edit instance", + "SETUP": { + "NAME_PLACEHOLDER": "Input instance's name", + "DESCRIPTION_PLACEHOLDER": "Input instance's description", + "ENDPOINT_PLACEHOLDER": "Input instance's endpoint ", + "USERNAME_PLACEHOLDER": "Input username", + "PASSWORD_PLACEHOLDER": "Input password", + "TOKEN_PLACEHOLDER": "Input token" + }, + "MAINTAINER": "Maintainer(s)", + "SOURCE": "Source", + "VERSION": "Version", + "SET_AS_DEFAULT": "Set as default", + "DELETE_INSTANCE": "Delete instance", + "ENABLE_INSTANCE": "Enable instance", + "DISABLE_INSTANCE": "Disable instance", + "SET_DEFAULT_SUCCESS": "Set as default successfully", + "SET_DEFAULT_FAILED": "Setting as default failed", + "UPDATE_INSTANCE": "Update instance", + "CREATE_INSTANCE": "Create instance" } } diff --git a/src/portal/src/i18n/lang/zh-cn-lang.json b/src/portal/src/i18n/lang/zh-cn-lang.json index 9fadd1b0f..62d7988c4 100644 --- a/src/portal/src/i18n/lang/zh-cn-lang.json +++ b/src/portal/src/i18n/lang/zh-cn-lang.json @@ -174,7 +174,11 @@ "TASKS": "任务", "API_EXPLORER": "API控制中心", "HARBOR_API_MANAGEMENT": "Harbor Api V2.0", - "HELM_API_MANAGEMENT": "Harbor Api" + "HELM_API_MANAGEMENT": "Harbor Api", + "DISTRIBUTIONS": { + "NAME": "分布式分发", + "INSTANCES": "实例" + } }, "USER": { "ADD_ACTION": "创建用户", @@ -1422,5 +1426,71 @@ "HELP_INFO_1": "默认扫描器已安装。获取扫描器安装帮助,请查看", "HELP_INFO_2": "文档。", "NO_DEFAULT_SCANNER": "未配置默认扫描器" + }, + "DISTRIBUTION": { + "FILTER_INSTANCE_PLACEHOLDER": "过滤实例", + "FILTER_HISTORIES_PLACEHOLDER": "过滤历史记录", + "ADD_ACTION": "新建实例", + "PREHEAT_ACTION": "预热", + "EDIT_ACTION": "编辑", + "ENABLE_ACTION": "启用", + "DISABLE_ACTION": "禁用", + "DELETE_ACTION": "删除", + "NOT_FOUND": "未发现任何记录", + "NAME": "名称", + "ENDPOINT": "端点", + "STATUS": "状态", + "ENABLED": "启用", + "SETUP_TIMESTAMP": "设置时间", + "PROVIDER": "供应商", + "DELETION_TITLE": "删除实例", + "DELETION_SUMMARY": "你确认删除实例 {{param}}?", + "ENABLE_TITLE": "启用实例", + "ENABLE_SUMMARY": "你确认启用实例 {{param}}?", + "DISABLE_TITLE": "禁用实例", + "DISABLE_SUMMARY": "你确认禁用实例 {{param}}?", + "IMAGE": "镜像", + "START_TIME": "开始时间", + "FINISH_TIME": "完成时间", + "INSTANCE": "实例", + "HISTORIES": "历史记录", + "CREATE_SUCCESS": "添加实例成功", + "CREATE_FAILED": "添加实例失败", + "DELETED_SUCCESS": "删除实例成功", + "DELETED_FAILED": "删除实例失败", + "ENABLE_SUCCESS": "启用实例成功", + "ENABLE_FAILED": "启用实例失败", + "DISABLE_SUCCESS": "禁用实例成功", + "DISABLE_FAILED": "禁用实例失败", + "UPDATE_SUCCESS": "更新实例成功", + "UPDATE_FAILED": "更新实例失败", + "REQUEST_PREHEAT_SUCCESS": "请求预热成功", + "REQUEST_PREHEAT_FAILED": "请求预热失败", + "DESCRIPTION": "描述", + "AUTH_MODE": "认证模式", + "USERNAME": "用户名", + "PASSWORD": "密码", + "TOKEN": "令牌", + "SETUP_NEW_INSTANCE": "设置新实例", + "EDIT_INSTANCE": "编辑实例", + "SETUP": { + "NAME_PLACEHOLDER": "输入实例名称", + "DESCRIPTION_PLACEHOLDER": "输入实例描述", + "ENDPOINT_PLACEHOLDER": "输入实例 endpoint ", + "USERNAME_PLACEHOLDER": "输入认证用户名", + "PASSWORD_PLACEHOLDER": "输入认证密码", + "TOKEN_PLACEHOLDER": "输入认证令牌" + }, + "MAINTAINER": "维护人员", + "SOURCE": "资源", + "VERSION": "版本", + "SET_AS_DEFAULT": "设为默认", + "DELETE_INSTANCE": "删除实例", + "ENABLE_INSTANCE": "启用实例", + "DISABLE_INSTANCE": "禁用实例", + "SET_DEFAULT_SUCCESS": "设为默认成功", + "SET_DEFAULT_FAILED": "设为默认失败", + "UPDATE_INSTANCE": "更新实例", + "CREATE_INSTANCE": "新建实例" } } diff --git a/src/portal/src/i18n/lang/zh-tw-lang.json b/src/portal/src/i18n/lang/zh-tw-lang.json index 318f4adae..666d7e209 100644 --- a/src/portal/src/i18n/lang/zh-tw-lang.json +++ b/src/portal/src/i18n/lang/zh-tw-lang.json @@ -172,7 +172,11 @@ }, "LOGS": "日誌", "TASKS": "任務", - "API_EXPLORER": "API控制中心" + "API_EXPLORER": "API控制中心", + "DISTRIBUTIONS": { + "NAME": "Distributions", + "INSTANCES": "Instances" + } }, "USER":{ "ADD_ACTION": "創建用戶", @@ -1409,5 +1413,71 @@ "HELP_INFO_1": "默認掃描器已安装。獲取掃描變安装幫助,請查看", "HELP_INFO_2": "文檔。", "NO_DEFAULT_SCANNER": "未配置默認掃描器" + }, + "DISTRIBUTION": { + "FILTER_INSTANCE_PLACEHOLDER": "Filter instances", + "FILTER_HISTORIES_PLACEHOLDER": "Filter histories", + "ADD_ACTION": "NEW INSTANCE", + "PREHEAT_ACTION": "Preheat", + "EDIT_ACTION": "Edit", + "ENABLE_ACTION": "Enable", + "DISABLE_ACTION": "Disable", + "DELETE_ACTION": "Delete", + "NOT_FOUND": "Not found", + "NAME": "Name", + "ENDPOINT": "Endpoint", + "STATUS": "Status", + "ENABLED": "Enable", + "SETUP_TIMESTAMP": "Setup Timestamp", + "PROVIDER": "Provider", + "DELETION_TITLE": "Confirm instance deletion", + "DELETION_SUMMARY": "Do you want to delete instance(s) {{param}}?", + "ENABLE_TITLE": "Enable instance(s)", + "ENABLE_SUMMARY": "Do you want to enable instance(s) {{param}}?", + "DISABLE_TITLE": "Disable instance(s)", + "DISABLE_SUMMARY": "Do you want to disable instance(s) {{param}}?", + "IMAGE": "Image", + "START_TIME": "Start time", + "FINISH_TIME": "Finish Time", + "INSTANCE": "Instance", + "HISTORIES": "Histories", + "CREATE_SUCCESS": "Instance created successfully", + "CREATE_FAILED": "Creating instance failed", + "DELETED_SUCCESS": "Instance(s) deleted successfully", + "DELETED_FAILED": "Deleting instance(s) failed", + "ENABLE_SUCCESS": "Instance(s) enabled successfully", + "ENABLE_FAILED": "Enabling instance(s) failed", + "DISABLE_SUCCESS": "Instance(s) disabled successfully", + "DISABLE_FAILED": "Disabling instance(s) failed", + "UPDATE_SUCCESS": "Instance updated successfully", + "UPDATE_FAILED": "Updating instance failed", + "REQUEST_PREHEAT_SUCCESS": "Preheat request successfully", + "REQUEST_PREHEAT_FAILED": "Preheat request failed", + "DESCRIPTION": "description", + "AUTH_MODE": "Auth Mode", + "USERNAME": "Username", + "PASSWORD": "Password", + "TOKEN": "Token", + "SETUP_NEW_INSTANCE": "Setup new instance", + "EDIT_INSTANCE": "Edit instance", + "SETUP": { + "NAME_PLACEHOLDER": "Input instance's name", + "DESCRIPTION_PLACEHOLDER": "Input instance's description", + "ENDPOINT_PLACEHOLDER": "Input instance's endpoint ", + "USERNAME_PLACEHOLDER": "Input username", + "PASSWORD_PLACEHOLDER": "Input password", + "TOKEN_PLACEHOLDER": "Input token" + }, + "MAINTAINER": "Maintainer(s)", + "SOURCE": "Source", + "VERSION": "Version", + "SET_AS_DEFAULT": "Set as default", + "DELETE_INSTANCE": "Delete instance", + "ENABLE_INSTANCE": "Enable instance", + "DISABLE_INSTANCE": "Disable instance", + "SET_DEFAULT_SUCCESS": "Set as default successfully", + "SET_DEFAULT_FAILED": "Setting as default failed", + "UPDATE_INSTANCE": "Update instance", + "CREATE_INSTANCE": "Create instance" } } diff --git a/src/portal/src/images/kraken-logo-color.svg b/src/portal/src/images/kraken-logo-color.svg new file mode 100644 index 000000000..31afafbcf --- /dev/null +++ b/src/portal/src/images/kraken-logo-color.svg @@ -0,0 +1 @@ +kraken-stacked-color \ No newline at end of file