Commit d01ff31d authored by AllForNothing's avatar AllForNothing
Browse files

Add P2p preheat distribution instance UI


Signed-off-by: default avatarAllForNothing <sshijun@vmware.com>
parent a78ab897
......@@ -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])
}
}
}
}
}
}
......@@ -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: [
],
......
......@@ -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>
......
@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;
}
<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>
@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;
}
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();
});
});
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]));
}
}