Add P2p preheat distribution instance UI

Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
AllForNothing 2020-06-29 15:45:14 +08:00
parent a78ab897d7
commit d01ff31dc8
24 changed files with 1840 additions and 11 deletions

View File

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

View File

@ -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: [
],

View File

@ -56,6 +56,10 @@
<clr-icon shape="cloud-traffic" clrVerticalNavIcon></clr-icon>
{{'SIDE_NAV.SYSTEM_MGMT.REPLICATION' | translate}}
</a>
<a clrVerticalNavLink routerLink="/harbor/distribution/instances" routerLinkActive="active">
<clr-icon shape="share"></clr-icon>
{{'SIDE_NAV.DISTRIBUTIONS.NAME' | translate}}
</a>
<a *ngIf="!withAdmiral" clrVerticalNavLink routerLink="/harbor/labels"
routerLinkActive="active">
<clr-icon shape="tag" clrVerticalNavIcon></clr-icon>

View File

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

View File

@ -0,0 +1,137 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<h2 class="custom-h2">
{{ 'SIDE_NAV.DISTRIBUTIONS.INSTANCES' | translate }}
</h2>
<div>
<clr-datagrid (clrDgRefresh)="loadData()" [clrDgLoading]="inProgress" [(clrDgSelected)]="selectedRow">
<clr-dg-action-bar>
<div class="clr-row">
<div class="clr-col-7">
<button id="new-instance"
type="button"
class="btn btn-secondary"
(click)="addInstance()"
>
<clr-icon shape="plus" size="16"></clr-icon>&nbsp;{{
'DISTRIBUTION.ADD_ACTION' | translate
}}
</button>
<button id="set-default"
[disabled]="!(selectedRow && selectedRow.length === 1 && !selectedRow[0].default && !selectedRow[0].enabled)"
class="btn btn-secondary"
(click)="setAsDefault()">{{'SCANNER.SET_AS_DEFAULT' | translate}}</button>
<clr-dropdown
[clrCloseMenuOnItemClick]="false"
class="btn btn-link"
clrDropdownTrigger>
<span id="member-action">{{ 'BUTTON.ACTIONS' | translate}}<clr-icon shape="caret down"></clr-icon></span>
<clr-dropdown-menu *clrIfOpen>
<clr-dropdown>
<button type="button" class="btn btn-secondary" (click)="editInstance()"
[disabled]="!(selectedRow && selectedRow.length === 1)">
<clr-icon shape="edit" size="16"></clr-icon>&nbsp;{{'DISTRIBUTION.EDIT_ACTION' | translate}}
</button>
<button type="button" class="btn btn-secondary" (click)="operateInstances('enable', selectedRow)"
[disabled]="!(selectedRow && selectedRow.length === 1 && !selectedRow[0].enabled)">
<clr-icon shape="connect" size="16"></clr-icon>&nbsp;{{'DISTRIBUTION.ENABLE_ACTION' | translate}}
</button>
<button
type="button"
class="btn btn-secondary"
(click)="operateInstances('disable', selectedRow)"
[disabled]="!(selectedRow && selectedRow.length === 1 && selectedRow[0].enabled)">
<clr-icon shape="disconnect" size="16"></clr-icon>&nbsp;{{'DISTRIBUTION.DISABLE_ACTION' | translate}}
</button>
<div class="dropdown-divider"></div>
<button
type="button"
class="btn btn-secondary"
(click)="operateInstances('delete', selectedRow)"
[disabled]="selectedRow.length < 1">
<clr-icon shape="window-close" size="16"></clr-icon>&nbsp;{{'DISTRIBUTION.DELETE_ACTION' | translate}}
</button>
</clr-dropdown>
</clr-dropdown-menu>
</clr-dropdown>
</div>
<div class="clr-col-5">
<div class="action-head-pos">
<hbr-filter [withDivider]="true" filterPlaceholder="{{'DISTRIBUTION.FILTER_INSTANCE_PLACEHOLDER' | translate}}" (filterEvt)="doFilter($event)"></hbr-filter>
<span class="refresh-btn">
<clr-icon shape="refresh" [hidden]="inProgress" ng-disabled="inProgress" (click)="refresh()"></clr-icon>
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
</span>
</div>
</div>
</div>
</clr-dg-action-bar>
<clr-dg-column>{{ 'DISTRIBUTION.NAME' | translate }}</clr-dg-column>
<clr-dg-column>{{ 'DISTRIBUTION.ENDPOINT' | translate }}</clr-dg-column>
<clr-dg-column>{{ 'DISTRIBUTION.PROVIDER' | translate }}</clr-dg-column>
<clr-dg-column>{{ 'DISTRIBUTION.STATUS' | translate }}</clr-dg-column>
<clr-dg-column>{{ 'DISTRIBUTION.ENABLED' | translate }}</clr-dg-column>
<clr-dg-column>{{'SCANNER.AUTH' | translate}}</clr-dg-column>
<clr-dg-column>{{'DISTRIBUTION.SETUP_TIMESTAMP' | translate}}</clr-dg-column>
<clr-dg-column>{{'DISTRIBUTION.DESCRIPTION' | translate}}</clr-dg-column>
<clr-dg-placeholder>{{
'DISTRIBUTION.NOT_FOUND' | translate
}}</clr-dg-placeholder>
<clr-dg-row *ngFor="let instance of instances" [clrDgItem]="instance">
<clr-dg-cell>
<span>{{ instance.name }}</span>
<span *ngIf="instance.default" class="label label-info ml-1">{{'SCANNER.DEFAULT' | translate}}</span>
</clr-dg-cell>
<clr-dg-cell>{{ instance.endpoint }}</clr-dg-cell>
<clr-dg-cell>
<span>{{ instance.vendor }}</span>
<clr-signpost *ngIf="providerMap[instance.vendor]">
<clr-signpost-content *clrIfOpen>
<div>
<span>
<img (error)="showDefaultIcon($event, instance.vendor)" class="height-24" [src]="providerMap[instance.vendor].icon">
</span>
</div>
<div class="margin-top-5px">
<span>{{'DISTRIBUTION.NAME' | translate}}:</span>
<span class="ml-1">{{providerMap[instance.vendor].name}}</span>
</div>
<div class="margin-top-5px">
<span class="no-wrapper">
<span>{{'DISTRIBUTION.MAINTAINER' | translate}}:</span>
<span class="ml-1">{{providerMap[instance.vendor].maintainers?.join(',')}}</span>
</span>
</div>
<div class="margin-top-5px">
<span>{{'DISTRIBUTION.SOURCE' | translate}}:</span>
<a target="_blank" href="{{providerMap[instance.vendor].source}}" class="ml-1">{{providerMap[instance.vendor].source}}</a>
</div>
<div class="margin-top-5px">
<span>{{'DISTRIBUTION.VERSION' | translate}}:</span>
<span class="ml-1">{{providerMap[instance.vendor].version}}</span>
</div>
</clr-signpost-content>
</clr-signpost>
</clr-dg-cell>
<clr-dg-cell [ngSwitch]="instance.status === 'Healthy'">
<span *ngSwitchCase="true" class="label label-success">{{ instance.status }}</span>
<span *ngSwitchDefault class="label label-danger">{{ instance.status }}</span>
</clr-dg-cell>
<clr-dg-cell>{{ instance.enabled || false }}</clr-dg-cell>
<clr-dg-cell>{{ instance.auth_mode }}</clr-dg-cell>
<clr-dg-cell>{{fmtTime(instance.setup_timestamp) | date: 'short'}}</clr-dg-cell>
<clr-dg-cell>{{ instance.description }}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<clr-dg-pagination #pagination [clrDgPageSize]="pageSize" [clrDgTotalItems]="totalCount" [(clrDgPage)]="currentPage">
<span *ngIf="pagination.totalItems">{{ pagination.firstItem + 1 }} - {{ pagination.lastItem + 1 }}{{ 'HELM_CHART.OF' | translate }}</span>
<span>{{ pagination.totalItems }} {{ 'HELM_CHART.ITEMS' | translate }}</span>
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
</div>
</div>
<div>
<dist-setup-modal (refresh)="refresh()" [providers]="providers" #setupModal></dist-setup-modal>
</div>

View File

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

View File

@ -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<DistributionInstancesComponent>;
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<Array<Instance>> = new HttpResponse<Array<Instance>>({
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();
});
});

View File

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

View File

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

View File

@ -0,0 +1,238 @@
<clr-modal
[(clrModalOpen)]="opened"
[clrModalClosable]="false"
[clrModalStaticBackdrop]="true"
>
<h3 class="modal-title">{{ title | translate }}</h3>
<div class="modal-body">
<inline-alert class="modal-title"></inline-alert>
<form #instanceForm="ngForm" class="clr-form clr-form-horizontal">
<!-- 1. provider -->
<clr-select-container>
<label class="required">{{
'DISTRIBUTION.PROVIDER' | translate
}}</label>
<select
clrSelect
name="provider"
id="provider"
[(ngModel)]="model.vendor"
[disabled]="editingMode"
required
>
<option class="display-none" value=""></option>
<option
*ngFor="let provider of providers"
value="{{ provider.id }}"
>{{ provider.name }}</option
>
</select>
<clr-control-error>
{{ 'TOOLTIP.ITEM_REQUIRED' | translate }}
</clr-control-error>
</clr-select-container>
<!-- 2. name -->
<clr-input-container>
<label class="required clr-control-label" for="name">{{
'DISTRIBUTION.NAME' | translate
}}</label>
<input
clrInput
required
type="text"
id="name"
autocomplete="off"
placeholder="{{ 'DISTRIBUTION.SETUP.NAME_PLACEHOLDER' | translate }}"
[(ngModel)]="model.name"
name="name"
[disabled]="editingMode"
/>
<clr-control-error>
{{ 'TOOLTIP.ITEM_REQUIRED' | translate }}
</clr-control-error>
</clr-input-container>
<!-- 3. description -->
<clr-textarea-container>
<label>{{ 'DISTRIBUTION.DESCRIPTION' | translate }}</label>
<textarea
clrTextarea
type="text"
id="description"
class="inputWidth"
row="3"
placeholder="{{
'DISTRIBUTION.SETUP.DESCRIPTION_PLACEHOLDER' | translate
}}"
[(ngModel)]="model.description"
[ngModelOptions]="{ standalone: true }"
></textarea>
</clr-textarea-container>
<!-- 4. endpoint -->
<clr-input-container>
<label class="required clr-control-label" for="endpoint">{{
'DISTRIBUTION.ENDPOINT' | translate
}}</label>
<input
clrInput
required
pattern="^([hH][tT]{2}[pP]:\/\/|[hH][tT]{2}[pP][sS]:\/\/)(.*?)*$"
type="text"
id="endpoint"
placeholder="{{
'DISTRIBUTION.SETUP.ENDPOINT_PLACEHOLDER' | translate
}}"
[(ngModel)]="model.endpoint"
name="endpoint"
autocomplete="off"
/>
<clr-control-error>{{
'TOOLTIP.ENDPOINT_FORMAT' | translate
}}</clr-control-error>
</clr-input-container>
<!-- 5. enabled -->
<clr-checkbox-container *ngIf="!editingMode">
<label for="enabled">
<span>{{ 'DISTRIBUTION.ENABLED' | translate }}</span>
</label>
<clr-checkbox-wrapper>
<input
clrCheckbox
id="enabled"
name="enabled"
type="checkbox"
[(ngModel)]="model.enabled"
/>
</clr-checkbox-wrapper>
</clr-checkbox-container>
<!-- auth mode -->
<clr-radio-container clrInline>
<label>{{ 'DISTRIBUTION.AUTH_MODE' | translate }}</label>
<clr-radio-wrapper>
<input
clrRadio
type="radio"
name="auth_mode"
id="none_mode"
value="NONE"
[(ngModel)]="model.auth_mode"
(change)="authModeChange()"
[ngModelOptions]="{ standalone: true }"
/>
<label for="none_mode">NONE</label>
</clr-radio-wrapper>
<clr-radio-wrapper>
<input
clrRadio
type="radio"
name="auth_mode"
id="basic_mode"
value="BASIC"
[(ngModel)]="model.auth_mode"
(change)="authModeChange()"
[ngModelOptions]="{ standalone: true }"
/>
<label for="basic_mode">Basic</label>
</clr-radio-wrapper>
<clr-radio-wrapper>
<input
clrRadio
type="radio"
name="auth_mode"
id="token_mode"
value="OAUTH"
[(ngModel)]="model.auth_mode"
(change)="authModeChange()"
[ngModelOptions]="{ standalone: true }"
/>
<label for="token_mode">OAuth</label>
</clr-radio-wrapper>
</clr-radio-container>
<!-- auth data -->
<span *ngIf="model.auth_mode == 'BASIC'">
<clr-input-container>
<label class="required clr-control-label" for="auth_data_username">{{
'DISTRIBUTION.USERNAME' | translate
}}</label>
<input
clrInput
required
type="text"
id="auth_data_username"
[(ngModel)]="authData['username']"
placeholder="{{
'DISTRIBUTION.SETUP.USERNAME_PLACEHOLDER' | translate
}}"
name="auth_data_username"
autocomplete="off"
/>
<clr-control-error>
{{ 'TOOLTIP.ITEM_REQUIRED' | translate }}
</clr-control-error>
</clr-input-container>
<clr-input-container>
<label class="required clr-control-label" for="auth_data_password">{{
'DISTRIBUTION.PASSWORD' | translate
}}</label>
<input
clrInput
required
type="password"
id="auth_data_password"
[(ngModel)]="authData['password']"
placeholder="{{
'DISTRIBUTION.SETUP.PASSWORD_PLACEHOLDER' | translate
}}"
name="auth_data_password"
autocomplete="off"
/>
<clr-control-error>
{{ 'TOOLTIP.ITEM_REQUIRED' | translate }}
</clr-control-error>
</clr-input-container>
</span>
<span *ngIf="model.auth_mode == 'OAUTH'">
<clr-input-container>
<label class="required clr-control-label" for="auth_data_token">{{
'DISTRIBUTION.TOKEN' | translate
}}</label>
<input
clrInput
required
type="text"
id="auth_data_token"
[(ngModel)]="authData['token']"
placeholder="{{
'DISTRIBUTION.SETUP.TOKEN_PLACEHOLDER' | translate
}}"
name="auth_data_token"
autocomplete="off"
/>
<clr-control-error>
{{ 'TOOLTIP.ITEM_REQUIRED' | translate }}
</clr-control-error>
</clr-input-container>
</span>
<span *ngIf="model.auth_mode == 'NONE'"></span>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="cancel()">
{{ 'BUTTON.CANCEL' | translate }}
</button>
<button
[clrLoading]="saveBtnState"
type="button"
class="btn btn-primary"
(click)="submit()"
[disabled]="!isValid || !hasChangesForEdit()"
>
{{ 'BUTTON.OK' | translate }}
</button>
</div>
</clr-modal>

View File

@ -0,0 +1,3 @@
.display-none {
display: none;
}

View File

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

View File

@ -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<any> = new EventEmitter<any>();
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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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": "新建实例"
}
}

View File

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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.7 KiB